Files
baya-monorepo/dev/contracts/domains/identity-profiles.md
T
hamid 39a979b1a7 @
backend phase 3: identity profiles, patients & nurse bank accounts

Add the role-attached identity layer on top of the b2 auth spine: nurse
seller profiles (guarded is_verified, read-only aggregates), thin customer
payer profiles, first-class patients (tenancy-scoped), and nurse payout bank
accounts hardened with an iban_hash uniqueness guard and an automated استعلام
شبا IBAN-ownership inquiry.

- Four usr tables via one migration (1:1 uniques, UNIQUE(iban_hash), filtered
  UNIQUE(nurse_id) WHERE is_primary=1, guarded is_verified, encrypted PII,
  soft-delete on nurse_profiles)
- 15 CQRS slices + 4 role-scoped controllers; reads projected + paginated,
  IBAN masked (last-4); ownership-inquiry endpoints rate-limited
- New IBankAccountOwnershipVerifier seam (mock deterministic شبا match) +
  per-domain repositories on IUnitOfWork + encrypted-PII value converters
- Activate FluentValidation repo-wide (validators were never registered)
- Handler unit tests + WebApplicationFactory integration tests (76 pass);
  contract identity-profiles.md + swagger snapshot; docs, handoff & report

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-07-02 12:03:15 +03:30

6.3 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).