@
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,87 @@
|
||||
# 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/api-conventions.md) +
|
||||
> [`../conventions/money-and-types.md`](../conventions/money-and-types.md). Machine schema:
|
||||
> [`../openapi/swagger.v1.json`](../openapi/README.md).
|
||||
|
||||
**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 `OperationResult`→`ApiResult` 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/add` — **rate-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).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,28 @@ One block per completed backend phase. Newest at the top. Backend lane writes he
|
||||
- **Notes for frontend:** <anything load-bearing>
|
||||
-->
|
||||
|
||||
## backend-phase-3 — Identity: profiles, patients & nurse bank accounts — 2026-07-02
|
||||
- **Shipped:** four `usr` tables via one migration (`IdentityProfilesPatientsBankAccounts`) —
|
||||
`NurseProfiles` (1:1 `Users`; guarded `is_verified` **no public setter**; read-only aggregates;
|
||||
soft-delete), `CustomerProfiles` (thin payer; enc emergency contact), `Patients` (care recipient,
|
||||
tenancy-scoped; `is_active` archive; enc `initial_medical_notes`), `NurseBankAccounts` (enc `iban` +
|
||||
`UNIQUE(iban_hash)` + filtered `UNIQUE(nurse_id) WHERE is_primary=1`; استعلام شبا inquiry fields);
|
||||
15 CQRS slices across 4 controllers (`nurse_profiles`, `customer_profiles`, `patients`,
|
||||
`nurse_bank_accounts`); new **`IBankAccountOwnershipVerifier`** seam (mock = deterministic شبا match);
|
||||
per-domain repositories on `IUnitOfWork`; enc value converters for the new PII columns. Also
|
||||
**activated FluentValidation** repo-wide (`AddApplicationServices` now registers every
|
||||
`AbstractValidator<T>` — the `ValidateCommandBehavior`/model-state filter were previously starved).
|
||||
- **Contracts:** dev/contracts/domains/identity-profiles.md + openapi snapshot refreshed (yes — new
|
||||
nurse/customer/patient/bank paths).
|
||||
- **Mocked:** `IBankAccountOwnershipVerifier` → 🟡 (see reports/mocks-registry.md).
|
||||
- **Gate:** build clean (0 new code warnings) / tests green (75 pass: +13 `Baya.Test.Api` integration,
|
||||
+15 handler unit tests). Migration applied to the dev DB on startup; swagger exposes all b3 paths.
|
||||
- **Handoff:** backend/handoff/after-backend-phase-3.md
|
||||
- **Notes for frontend:** role scoping needs the **role claim in the token** — refresh after
|
||||
`select_role` before calling these. IBAN comes back **masked** (last-4). `isVerified`/aggregates are
|
||||
read-only. Patient `get/update` of another customer's id → **404**. Addresses/service-areas are
|
||||
**deferred to b4**.
|
||||
|
||||
## backend-phase-2 — Identity: phone-OTP auth, sessions & roles (REST) — 2026-07-02
|
||||
- **Shipped:** the six-endpoint REST auth surface (`auth/request_otp`, `auth/verify_otp`,
|
||||
`auth/refresh`, `auth/logout`, `me`, `me/select_role`) wrapping the existing JWE/TOTP/RBAC engine;
|
||||
|
||||
@@ -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).
|
||||
@@ -0,0 +1,69 @@
|
||||
# Backend phase 3 report — Identity: profiles, patients & nurse bank accounts
|
||||
|
||||
## What was built
|
||||
- **Four domain entities** (`Baya.Domain/Entities/Identity/`): `NurseProfile`, `CustomerProfile`,
|
||||
`Patient`, `NurseBankAccount`. `NurseProfile.is_verified` is write-guarded (private setter +
|
||||
`MarkVerified()`/`MarkUnverified()` — only b6 calls it); `is_accepting_bookings` toggled via a domain
|
||||
method; the search aggregates are read-only.
|
||||
- **One EF migration** `IdentityProfilesPatientsBankAccounts` (schema `usr`): 1:1 uniques on
|
||||
`user_id`, `UNIQUE(iban_hash)`, filtered `UNIQUE(nurse_id) WHERE is_primary=1`, soft-delete on
|
||||
`NurseProfiles`, encrypted PII columns, audit fields. No CUT columns.
|
||||
- **15 CQRS slices** under `Features/Identity/{Commands|Queries}/` + 4 `sealed : BaseController`
|
||||
controllers (`NurseProfilesController`, `CustomerProfilesController`, `PatientsController`,
|
||||
`NurseBankAccountsController`). Reads project to DTOs; lists paginate; the ownership-inquiry endpoints
|
||||
are rate-limited (`sensitive` policy).
|
||||
- **New seam `IBankAccountOwnershipVerifier`** (Application `Contracts/Common`) + mock
|
||||
`MockBankAccountOwnershipVerifier` (CrossCutting), registered in `AddCrossCuttingSeams`, config-selected.
|
||||
- **Persistence:** four per-domain repositories on `IUnitOfWork` (`NurseProfileRepository`,
|
||||
`CustomerProfileRepository`, `PatientRepository`, `NurseBankAccountRepository`); encrypted-PII value
|
||||
converters for the new columns wired in `ApplicationDbContext.OnModelCreating`; an atomic
|
||||
`SetPrimaryAsync` (clear-then-set in one transaction) so the single-primary index never trips.
|
||||
- **Infra fix:** `AddApplicationServices` now registers every `AbstractValidator<T>` as `IValidator<T>`
|
||||
so the pre-existing `ValidateCommandBehavior` and `ModelStateValidationAttribute` filter actually
|
||||
validate (they had **no** validators registered before this phase — validation was silently inert).
|
||||
|
||||
## What is now testable, and exactly how (mirrors the phase §7)
|
||||
Log in as a nurse and a customer (b2 OTP flow), **refreshing the token after `select_role`** so the
|
||||
role claim is present. Then:
|
||||
1. **Nurse profile** — `POST api/v1/nurse_profiles/upsert` → row created `is_verified=0`,
|
||||
`is_accepting_bookings=0`; `GET …/me` shows aggregates at 0. No path sets `is_verified`.
|
||||
2. **Accepting-bookings** — `POST …/set_accepting_bookings` flips it; verified untouched.
|
||||
3. **Customer profile** — `POST api/v1/customer_profiles/upsert` with emergency contact → `GET …/me`
|
||||
round-trips it through the encrypted column.
|
||||
4. **Patient CRUD** — `create` (gender required) / `list` / `get/{id}` / `update/{id}` / `archive/{id}`.
|
||||
5. **Tenancy** — customer B calling `get`/`update` on customer A's patient id → **404**.
|
||||
6. **Bank account + inquiry** — `POST api/v1/nurse_bank_accounts/add` (normal IBAN) → `matched_national_id=true`,
|
||||
vendor ref recorded; `list` shows the IBAN **masked**.
|
||||
7. **Mismatch** — add the mismatch IBAN → `matched_national_id=false`.
|
||||
8. **Duplicate IBAN** — re-add the same IBAN → clean `400` via `iban_hash` uniqueness.
|
||||
9. **Primary flip** — add a 2nd account, `set_primary/{id2}` → account 2 primary, account 1 not; never two primaries.
|
||||
|
||||
Automated coverage: 15 handler unit tests (NSubstitute) covering profile upsert, role forbidden, patient
|
||||
CRUD + cross-customer 404, bank add match/mismatch/duplicate + set-primary flip/not-owned; 13
|
||||
`WebApplicationFactory` integration tests (one+ per controller: happy path, 401, validation 400, tenancy
|
||||
404, mask, duplicate, primary flip, mismatch). `dotnet build` clean (0 new code warnings); `dotnet test`
|
||||
green (75 pass).
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
- **`IBankAccountOwnershipVerifier` (🟡)** — deterministic fake استعلام شبا. Make-it-real steps in
|
||||
`reports/mocks-registry.md`. Reused seams: `IFieldEncryptor`, `ICurrentUser`, `IDateTimeProvider`.
|
||||
|
||||
## Contracts produced / consumed
|
||||
- **Produced:** `dev/contracts/domains/identity-profiles.md`; `dev/contracts/openapi/swagger.v1.json`
|
||||
refreshed (adds all nurse-profile / customer-profile / patient / bank-account paths).
|
||||
- **Consumed:** b2 auth (login/roles), b0 seams (`IFieldEncryptor`/`ICurrentUser`/`IDateTimeProvider`),
|
||||
b1 `IPlatformConfig` (available; not needed this phase).
|
||||
|
||||
## Follow-ups for later phases
|
||||
- **b4:** `customer_addresses` + `nurse_service_areas` (need geography + geocoder) — deferred here.
|
||||
- **b6:** the `is_verified` flip (verification-confirm transaction); Shahkar/KYC populate `national_id`;
|
||||
the `bank_account_verification` step couples to `NurseBankAccounts`.
|
||||
- **b13:** first-payout gate on `matched_national_id = true`.
|
||||
- **b9/b14:** recompute `average_rating`/`total_reviews`/`total_completed_bookings` (read-only here).
|
||||
- **Chain-wide:** validators are now active — new phases must ensure route-supplied ids aren't validated
|
||||
in the body command, and can rely on FluentValidation for input rejection.
|
||||
|
||||
## Decisions taken (flagged for confirmation)
|
||||
- A thin `customer_profiles` row is **auto-provisioned** on a customer's first patient (so patient
|
||||
registration needs no separate profile step). Recorded in `product/data-model/01-identity-and-access.md`.
|
||||
- IBAN is returned **masked (last-4)** on every read; first account added is primary by default.
|
||||
@@ -26,7 +26,7 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
| `IShahkarVerifier` | backend-phase-6 | Phone↔national-id match — fake pass | _tbd_ | Real Shahkar/KYC vendor; persist `external_response_json` | 🔴 |
|
||||
| `IIdentityKycProvider` | backend-phase-6 | National-ID + liveness — fake pass | _tbd_ | Finnotech/U-ID/Jibbit/Verify liveness+OCR | 🔴 |
|
||||
| `ICredentialVerifier` | backend-phase-6 | MoH/INO/criminal-record — manual/fake | _tbd_ | Manual admin today; API when a portal appears (`verification_method=api`) | 🔴 |
|
||||
| `IBankAccountOwnershipVerifier` | backend-phase-3/6 | استعلام شبا IBAN↔national-id — fake match | _tbd_ | Real KYC vendor; store `ownership_vendor_ref` | 🔴 |
|
||||
| `IBankAccountOwnershipVerifier` | backend-phase-3 | استعلام شبا IBAN-owner ↔ national-id inquiry — `MockBankAccountOwnershipVerifier` (`Baya.Infrastructure.CrossCutting/Seams/`) returns a deterministic fake: every IBAN matches (`matched_national_id=true`, echoes a holder name + `MOCK-SHEBA-{sha}` vendor ref) except the configured mismatch IBAN which returns `false`; registered singleton in `AddCrossCuttingSeams`. No real bank/KYC call, no money moves | `Seams:BankOwnership:MismatchIban` (default `IR000000000000000000000000`), `Seams:BankOwnership:MatchedHolderName`, `Seams:BankOwnership:MismatchHolderName` | 1) pick a Finnotech / banking-bridge استعلام شبا provider, add its client package to `Directory.Packages.props`; 2) add `Seams:BankOwnership:{ApiKey,BaseUrl}` options; 3) implement `VerifyOwnershipAsync(iban, nurseNationalId)` against the real Sheba-owner inquiry, mapping to `OwnershipInquiryResult`; 4) persist the real `ownership_vendor_ref` (+ raw response if a column is added); 5) swap the registration in `AddCrossCuttingSeams` (config-selected) — handlers unchanged; 6) test match/mismatch + that the b13 first-payout gate honours `matched_national_id=true` | 🟡 |
|
||||
| `IGeocoder` | backend-phase-4 | Address→lat/lng — echo/static | _tbd_ | Neshan/Google geocoding | 🔴 |
|
||||
| `IMoadianClient` | backend-phase-11 | سامانه مودیان e-invoice — leaves ref pending | _tbd_ | Real مودیان submission → 22-digit ref | 🔴 |
|
||||
| `IReviewModerationService` | backend-phase-14 | AI moderation — keyword/pass-through | _tbd_ | Real classifier/LLM endpoint | 🔴 |
|
||||
@@ -36,3 +36,13 @@ Status legend: 🔴 not built · 🟡 mocked (seam + fake impl in place) · 🟢
|
||||
|
||||
> Exact config keys and file paths get filled in by the phase that builds each seam. Keep the
|
||||
> "Make it real →" column actionable enough that a developer can pick up any single row and ship it.
|
||||
|
||||
## Frontend client-side mocks (not backend DI seams)
|
||||
|
||||
These are in-browser mocks behind a `services/{domain}` interface, selected by a config flag. They exist so
|
||||
the frontend can build before the backend phase merges, and swap to the real HTTP client in one line.
|
||||
|
||||
| Seam (interface) | File | What it fakes | Config flag | Make it real → | Status |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `PatientsApi` | `client/src/services/patients/apis/mockApi.ts` | In-memory patient list/create | `USE_PATIENTS_MOCK` (`services/patients/constants.ts`) | Publish `/patients` endpoints, set flag `false` | 🟡 |
|
||||
| `AuthApi` | `client/src/services/auth/apis/mockApi.ts` (`authMockApi`) | Phone-OTP login offline: `requestOtp`→`{otpSent,resendAvailableInSeconds:120}`; `verifyOtp` accepts dev code **`123456`** and locks after 3 wrong tries (`otp_locked`); `getMe`/`selectRole`/`refresh` from a `MOCK_SCENARIO` toggle (`customer`/`nurse_unverified`/`no_role`) to exercise all router branches | `USE_AUTH_MOCK` (`services/auth/constants.ts`, default **false** — b2 is live) + `MOCK_SCENARIO` in `mockApi.ts` | The real `authClientApi` is already wired to the live b2 routes; set `USE_AUTH_MOCK = false` (already the default) — no hook/screen change | 🟢 real by default, 🟡 mock available |
|
||||
|
||||
Reference in New Issue
Block a user