@
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> @
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
# After backend-phase-3 — profiles, patients & nurse bank accounts are live
|
||||
|
||||
On top of the b2 auth spine, the *people behind the accounts* now exist. A nurse has a seller profile
|
||||
(that b6 will verify and b5 will hang service variants off), a customer has a payer profile and their
|
||||
patients, and a nurse has payout bank accounts with an automated IBAN-ownership inquiry. Contract:
|
||||
[`dev/contracts/domains/identity-profiles.md`](../../../contracts/domains/identity-profiles.md); machine
|
||||
schema: `dev/contracts/openapi/swagger.v1.json` (refreshed).
|
||||
|
||||
## What the frontend (f2-b3) can now build
|
||||
- **Nurse profile bootstrap:** `POST api/v1/nurse_profiles/upsert` (bio, experience, education,
|
||||
`specializationsJson`), `GET api/v1/nurse_profiles/me`, and the pause/resume toggle
|
||||
`POST api/v1/nurse_profiles/set_accepting_bookings`. `isVerified` and the rating/booking aggregates are
|
||||
**read-only** (verification is b6) — render them, never send them.
|
||||
- **Customer profile:** `POST api/v1/customer_profiles/upsert` (emergency contact) + `GET …/me`.
|
||||
- **"Who is care for" (patients):** `create` / `list` (paginated) / `get/{id}` / `update/{id}` /
|
||||
`archive/{id}` under `api/v1/patients`. `gender` (`male`/`female`) is **required**. A customer only ever
|
||||
sees/edits their own patients — someone else's id returns **404**.
|
||||
- **Nurse bank-account settings:** `add` (returns the account with `matchedNationalId` set by the شبا
|
||||
inquiry), `list` (IBAN **masked**, last-4), `set_primary/{id}`, and `verify_ownership/{id}` to re-run
|
||||
the inquiry.
|
||||
|
||||
## Rules baked into the API (don't fight them client-side)
|
||||
- **Refresh after `select_role`.** These endpoints authorize on the **role claim in the access token**;
|
||||
the token minted before role selection lacks it. Login → `select_role` → **refresh** (or re-login) →
|
||||
then call profile/patient/bank endpoints. Otherwise you get `403`.
|
||||
- **Guarded verification** — no field/endpoint sets `isVerified`; a nurse is not bookable until b6.
|
||||
- **Tenancy** — patients and bank accounts are strictly owner-scoped; cross-tenant reads/writes are `404`.
|
||||
- **IBAN is masked on the wire** (last-4 only); the full value is encrypted at rest.
|
||||
- **`matchedNationalId`** is the money-mule-prevention gate for the first payout (enforced in b13). It is
|
||||
`null` until the inquiry runs; `add` runs it automatically. The bank rail is **mocked** at MVP.
|
||||
- Duplicate IBAN → a clean `400` (via `iban_hash` uniqueness), not a server error.
|
||||
|
||||
## What's mocked
|
||||
- **IBAN ownership (`IBankAccountOwnershipVerifier` → 🟡).** Deterministic fake استعلام شبا: every IBAN
|
||||
matches except the configured mismatch IBAN (`Seams:BankOwnership:MismatchIban`, default
|
||||
`IR000000000000000000000000`) which returns `matchedNationalId=false`. No real bank/KYC call.
|
||||
|
||||
## Schema / migration
|
||||
Migration **`20260702042131_IdentityProfilesPatientsBankAccounts`** (applied to the dev DB on startup):
|
||||
`usr.NurseProfiles`, `usr.CustomerProfiles`, `usr.Patients`, `usr.NurseBankAccounts` — with the 1:1
|
||||
uniques, `UNIQUE(iban_hash)`, filtered single-primary index, guarded `is_verified`, encrypted PII columns
|
||||
(`iban`, `account_holder_name`, emergency contacts, `initial_medical_notes`), and soft-delete on
|
||||
`NurseProfiles`. None of the CUT columns (`verification_status`, `response_rate`, … ,
|
||||
`customer_profiles.national_id_verified_at`) exist.
|
||||
|
||||
## Deferred to later phases (do not build against these yet)
|
||||
- **Addresses & nurse service areas → b4** (need province/city/district + geocoder).
|
||||
- **`is_verified` flip → b6** (verification pipeline).
|
||||
- **Payout gating on `matched_national_id` → b13.**
|
||||
- **Aggregate recompute (rating/reviews/completed) → b9/b14.**
|
||||
- **Customer national-ID KYC** — intentionally not collected; never gate browsing/booking on it.
|
||||
|
||||
## Note for the whole backend chain
|
||||
FluentValidation was previously inert (no validators registered). b3 activates it in
|
||||
`AddApplicationServices` — every `AbstractValidator<T>` now runs via `ValidateCommandBehavior` and the
|
||||
`ModelStateValidationAttribute` controller filter. **Consequence:** for route-supplied ids, don't add a
|
||||
body validator rule on that id (e.g. `patients/update/{id}` validates the body, whose `Id` is 0).
|
||||
Reference in New Issue
Block a user