Skip to content

Doctor patient workflow ​

Patient profiles are persistent through Doctor-owned /api/v1/doctors/patients routes. Every call verifies a current Identity session and active account, a professional Doctor profile, active organization and exact doctor affiliation. Doctor patient operations do not require Authorization roles, grants or permission checks. No license-review approval is added. An ordinary member's doctor role cannot establish professional eligibility. Platform-admin status alone grants no patient access.

MethodRouteSuccess
POST/api/v1/doctors/patients201 resource envelope
GET/api/v1/doctors/patients?organization_id={uuid}200 paginated list envelope
GET/api/v1/doctors/patients/{patientId}200 resource envelope
PATCH/api/v1/doctors/patients/{patientId}200 resource envelope

All responses, including errors, use Cache-Control: no-store. Authentication failures return 401, a missing professional profile returns 403, unavailable dependencies 503, inaccessible profiles/organizations 404, invalid input 422, and stale versions/retry conflicts 409. Errors use application/problem+json. All identifiers use canonical UUIDs.

Creation requires Authorization: Bearer <token> and Idempotency-Key (1–128 printable, nonblank characters):

json
{"organization_id":"11111111-1111-4111-8111-111111111111","name":"Patient Example","age":"25 years","sex":"female"}

Name, age and sex are required. Age accepts any nonblank text, such as "25", "6 months", "2 years 3 months" or "newborn"; the server does not parse a value or unit. Sex must be male, female, other or unknown, with no implicit default. Email, phone, blood group and address are optional. Address is a single text string of up to 1,000 Unicode characters and supports multiple lines. Age and address are trimmed. Birth date, allergies and chronic conditions are no longer profile fields, and requests containing them are rejected.

National phone input uses PATIENT_DEFAULT_PHONE_REGION (default BD); international numbers retain their own country code. The server assigns UUID, MRN, creator, timestamps and version 1. Reusing the same key and canonical body in the same actor/organization scope returns the current authorized profile with 201, without another profile, MRN or audit event. A changed body returns 409. The same key in another organization creates independently. Duplicate names, emails and phone numbers are allowed. Retries require a current authenticated doctor and active affiliation; do not rely on an old response snapshot.

Resource responses use { "message": "Success", "status_code": 201, "data": { ... } } (200 for detail/update). List responses follow the standard list contract:

json
{
  "message": "Patients retrieved successfully.",
  "status_code": 200,
  "data": [],
  "pagination": {
    "next_cursor": "",
    "current_page": 1,
    "per_page": 20,
    "total_pages": 0,
    "total_items": 0
  }
}

Totals count the complete result matching the organization and search before applying the cursor boundary. Empty results use data: [] and zero totals; current_page starts at 1. The last page has an empty next_cursor. The list query owns concrete body, item and pagination DTOs with an explicit mapper between HTTP and shared patient contracts. Transport request/response types are the distinct CreatePatientReqDto/CreatePatientResDto, ListPatientsReqDto/ListPatientsResDto, GetPatientReqDto/GetPatientResDto, and PatchPatientReqDto/PatchPatientResDto.

Each endpoint's route registration, DTOs and validation live together in doctor_create_patient_command.go, doctor_list_patients_query.go, doctor_get_patient_query.go or doctor_patch_patient_command.go under services/doctor/internal/presentation/http. Shared handler wiring and error mapping stay in patients.go; shared request helpers stay in patient_contracts.go.

List accepts optional literal substring search across name, UUID, MRN, display/canonical phone, email and address. The deprecated q alias remains supported; supplying both with different values returns 422 (surrounding whitespace and case are ignored when comparing). % and _ are literal characters. Limit defaults to 20, maximum 100. Ordering is creation timestamp then UUID, descending. Cursors bind organization, search digest, page size and the current actor; changed filters or page size require a fresh first page. Cursors from the previous cursor-only contract must also restart from the first page. Search queries and cursors are omitted from ordinary request logs. Reads remain available after subscription expiry.

Updates require the returned version and at least one editable field:

json
{"expected_version":1,"email":null,"address":"Dhaka, Bangladesh"}

Omission preserves existing values. Null clears optional email, phone, blood group and address. Supplied name, age and sex cannot be null or blank; PATCH does not require resending unchanged required fields. A supplied address must be nonblank or null to clear it. The server sets age_recorded_at on creation and refreshes it only when normalized age changes. Current no-ops preserve version/timestamps/audit; stale no-ops return 409. Successful changes increment version. Contacts and demographic edits never alter another organization's profile or historical prescription snapshots.

Create/update lock Identity account then Organization and recheck affiliation and transaction-bound subscription eligibility. SUBSCRIPTION_ENFORCEMENT=enforce denies ineligible writes with subscription_read_only; shadow mode logs the eligibility restriction. Provider failures fail closed in either mode. Audit append failure rolls back profiles, counters and retry state.

Shared service boundary ​

pkg/types owns the shared patient model, demographics, snapshots, registration and patch inputs, errors, validation and contact normalization. pkg/types/ports.PatientService exposes DoctorCreatePatient, DoctorListPatients, DoctorGetPatient and DoctorPatchPatient.

Doctor use cases verify the session through IIdentityService.Verify, retain an empty authorization section, and pass the verified user ID to this port. The implementation lives in services/doctor/internal/services, with persistence behind Doctor repository ports and PostgreSQL adapters. These paths do not import the Patient service or call its use cases. The Patient module retains compatibility aliases for existing internal consumers.

An active doctor affiliation is sufficient for patient operations in that organization. Patient role provisioning is not required. RBAC grants, denials and ordinary membership changes do not filter doctor patient results or invalidate their cursors. Every query remains constrained to the affiliated organization.

Patient login, verified-contact matching, invitations for patient connections, guardianship, deletion and merging remain deferred. Contact equality alone cannot grant medical-record access.

Documentation follows the source. Planned contracts are marked explicitly.