Files
baya-monorepo/archive/build-chain/contracts/domains/identity-profiles.md
T
2026-08-02 18:48:32 +03:30

7.1 KiB

Contract — Identity profiles, patients & nurse bank accounts (backend phase b3)

Role-attached identity data on top of the b2 auth spine: the nurse seller profile, the customer payer profile, the customer's patients, and the nurse's payout bank accounts. Assumes ../conventions/api-conventions.md + ../conventions/money-and-types.md. Machine schema: ../openapi/swagger.v1.json.

Status: live as of backend-phase-b3 · Frontend consumer: frontend-phase-f2-b3

All endpoints require a Bearer access token ([Authorize]); unauthenticated calls return 401. Role scoping is enforced in the handler and returns 403 when the caller lacks the required role — and role claims are baked into the access token at mint time, so a client must refresh (or re-login) after me/select_role before these endpoints see the new role. Request bodies are camelCase JSON; URL segments are snake_case; responses use the standard OperationResultApiResult envelope (payload in data).

Enums used

  • gender: male | female — load-bearing for same-gender caregiver matching; required on a patient.
  • blood_type: free-form short string (e.g. O+, AB-), nullable — not a fixed enum at MVP.

Shared shapes

  • NurseProfileDto: id (int64), bio (string), yearsOfExperience (int), educationLevel (string), educationField (string), specializationsJson (string — raw JSON array), isVerified (bool, read-only — always false until b6 verification), isAcceptingBookings (bool), averageRating (decimal), totalReviews (int), totalCompletedBookings (int) — the last three are read-only aggregates, 0 until reviews/bookings phases.
  • CustomerProfileDto: id (int64), defaultEmergencyContactName (string), defaultEmergencyContactPhone (string) — decrypted and returned in full to the owning customer (self).
  • PatientDto: id (int64), displayName, firstName, lastName (strings), birthDate (date YYYY-MM-DD), gender (male/female), bloodType (string, nullable), initialMedicalNotes (string — decrypted, owner-only), isActive (bool).
  • NurseBankAccountDto: id (int64), bankName (string), ibanMasked (string — last 4 only, e.g. ••••3456; the full IBAN is never returned), isPrimary (bool), isVerified (bool), matchedNationalId (bool nullable — null until the ownership inquiry runs).

Endpoints

Nurse profile — role nurse

  • POST api/v1/nurse_profiles/upsert — create/update own profile. Body: { bio, yearsOfExperience, educationLevel, educationField, specializationsJson }. Returns NurseProfileDto. Never accepts isVerified or the aggregates. 400 if yearsOfExperience ∉ [0,80]; 403 non-nurse.
  • POST api/v1/nurse_profiles/set_accepting_bookings — body { accepting: bool }. Empty 200 on success; 404 if no profile yet. Never touches isVerified.
  • GET api/v1/nurse_profiles/me — returns NurseProfileDto; 404 if none.

Customer profile — role customer

  • POST api/v1/customer_profiles/upsert — body { defaultEmergencyContactName, defaultEmergencyContactPhone } (phone stored encrypted). Returns CustomerProfileDto. 400 invalid phone / empty name; 403 non-customer.
  • GET api/v1/customer_profiles/me — returns CustomerProfileDto; 404 if none.

Patients — role customer (tenancy-scoped to the caller)

  • POST api/v1/patients/create — body { displayName, firstName, lastName, birthDate, gender, bloodType, initialMedicalNotes }. customerId is derived from the caller (a thin customer profile is auto-provisioned on first patient) — never taken from the body. Returns PatientDto. 400 missing/invalid gender or future birthDate.
  • GET api/v1/patients/list?page=&pageSize= — paginated (page 1-based, pageSize ≤100, default 50). Returns PagedResult<PatientDto> (items, total, page, pageSize) of the caller's own patients only.
  • GET api/v1/patients/get/{id} — returns PatientDto; 404 if not owned (existence not leaked).
  • POST api/v1/patients/update/{id} — body as create (id from the route). Returns PatientDto; 404 if not owned.
  • POST api/v1/patients/archive/{id} — soft-archive (isActive=false, not a delete). Empty 200; 404 if not owned.

Nurse bank accounts — role nurse (tenancy-scoped)

  • POST api/v1/nurse_bank_accounts/addrate-limited. Body { bankName, accountHolderName, iban } (IBAN IR+24 digits; stored encrypted). Runs the استعلام شبا ownership inquiry and returns NurseBankAccountDto with matchedNationalId set. Becomes primary if it is the nurse's first account. 400 invalid IBAN, duplicate IBAN (via iban_hash uniqueness — a clean failure, not a 500), or no nurse profile.
  • POST api/v1/nurse_bank_accounts/set_primary/{id} — makes the account primary and clears the prior primary atomically (the filtered single-primary index never trips). Empty 200; 404 if not owned.
  • GET api/v1/nurse_bank_accounts/list — returns NurseBankAccountDto[] with masked IBANs.
  • POST api/v1/nurse_bank_accounts/verify_ownership/{id}rate-limited. Re-runs the ownership inquiry (idempotent: same input → same vendor ref). Returns the updated NurseBankAccountDto; 404 if not owned.

Side effects & rules the API enforces

  • Guarded isVerified — there is no field or endpoint to set it; a nurse profile is created unverified and stays so until the b6 verification-confirm transaction.
  • Tenancy — a customer only ever sees/mutates their own patients; a nurse only their own bank accounts. Cross-tenant access returns 404 (never leaks existence).
  • IBAN masking — the full IBAN is never returned; lists/DTOs carry last-4 only. The full value is encrypted at rest.
  • matchedNationalId gates the first payout (b13) — set here by the (mocked) IBankAccountOwnershipVerifier, not by admin eyeballing; null until the inquiry has run.
  • Deferred: saved service addresses & nurse coverage areas (b4); customer national-ID KYC (not collected, never gates browsing/booking).

Changelog

  • b3 — initial contract (nurse/customer profiles, patients, nurse bank accounts + ownership inquiry).

Refinement phase 3 additions (REQ-005/006/007)

  • PatientDto + create/update gain relation (parent|spouse|child|self, nullable) and conditions (string[] of stable codes; empty, never null). Stored as a nullable code + a JSON array column.
  • NurseProfileDto and CustomerProfileDto gain avatarUrl (nullable). CustomerProfileDto also gains preferredLanguage (nullable); the customer upsert body now accepts firstName/lastName (persisted on the base users row) and preferredLanguage.
  • Avatar upload (multipart): POST api/v1/nurse_profiles/avatar and POST api/v1/customer_profiles/avatarmultipart/form-data field file (JPEG/PNG/WebP, ≤ 5 MB), stored via IObjectStorage, returns { url } and persists it on the profile.