create mvp path
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# verification — the nurse trust pipeline
|
||||
|
||||
> Client seam `client/src/services/verification/` · `USE_VERIFICATION_MOCK = true` (**mock is primary**) · 17 server ops
|
||||
> Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29).
|
||||
|
||||
The largest domain, and the one the whole marketplace's trust claim rests on. A nurse is not bookable until
|
||||
this pipeline says so.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Nurse-facing
|
||||
|
||||
| Method | Path | Verdict |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/v1/nurse_verification` | wired — **the one cached status query** |
|
||||
| POST | `/api/v1/nurse_verification/submit` | wired |
|
||||
| POST | `/api/v1/nurse_verification/steps/{stepId}/upload_url` | wired — presigned PUT |
|
||||
| POST | `/api/v1/nurse_verification/steps/{stepId}/documents` | wired — confirm the upload |
|
||||
| POST | `/api/v1/nurse_verification/steps/identity_kyc/run` | wired |
|
||||
| POST | `/api/v1/nurse_verification/steps/shahkar_match/run` | wired |
|
||||
| POST | `/api/v1/nurse_verification/steps/bank_account_verification/run` | wired |
|
||||
| POST | `/api/v1/nurse_verification/credential_details` | **unwired** — the write half of REQ-011; the client has no read-back (REQ-056) |
|
||||
|
||||
### Public
|
||||
|
||||
| Method | Path | Auth | Verdict |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/v1/nurses/{nurseId}/trust_badge` | **anonymous** | wired |
|
||||
|
||||
### Admin — all `DynamicPermission` + `sensitive`
|
||||
|
||||
| Method | Path | Verdict |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/v1/admin_verifications` | wired · paginated |
|
||||
| GET | `/api/v1/admin_verifications/{nurseVerificationId}` | wired |
|
||||
| POST | `/api/v1/admin_verifications/steps/{stepId}/decide` | wired — **per-step** decision |
|
||||
| POST | `/api/v1/admin_verifications/{nurseVerificationId}/suspend` | **unwired** |
|
||||
| POST | `/api/v1/admin_verifications/scan_expiring` | **unwired** — an ops one-shot; the scheduler runs it |
|
||||
| GET | `/api/v1/admin_verification_step_types` | **unwired** — the step catalogue is data-driven; no editor |
|
||||
| POST | `/api/v1/admin_verification_step_types` | **unwired** |
|
||||
| DELETE | `/api/v1/admin_verification_step_types/{id}` | **unwired** |
|
||||
|
||||
### Phantom — 3
|
||||
|
||||
| Client call | REQ | Note |
|
||||
| --- | --- | --- |
|
||||
| `GET /api/v1/admin_verifications/documents/{id}/url` | REQ-034 | Deferred. No on-demand signed document URL, so the queue cannot open an uploaded document |
|
||||
| `POST /api/v1/admin_verifications/{id}/approve` | REQ-034 | Deferred. Today approval is **per-step** only — the aggregate flips when the last required step passes |
|
||||
| `POST /api/v1/admin_verifications/{id}/reject` | REQ-034 | Deferred |
|
||||
|
||||
## The two rules that must not be broken
|
||||
|
||||
1. **`status` is the source of truth; `nurse_profiles.is_verified` is derived.** The boolean is written
|
||||
**only** by the finalize transaction when the aggregate reaches `approved`, as one guarded
|
||||
cross-aggregate flip: load both tracked, mutate through one pure domain helper, commit once. Never set
|
||||
`is_verified` from a profile write, a controller, or out of band. See [profiles.md](profiles.md).
|
||||
2. **The step catalogue is data, not code.** `verification_step_types` rows define which steps exist,
|
||||
which are required, and which are automated. Adding a step is a data change. The client must not
|
||||
hardcode the step list — it renders whatever `VerificationStatusDto.steps` contains.
|
||||
|
||||
## Shape rules the JSON does not express
|
||||
|
||||
- **`VerificationStatusDto` is `{ status, isBookable, blockingSteps, steps }`** — the server computes
|
||||
`isBookable` and names the `blockingSteps`. The client does not derive bookability from the step array.
|
||||
This is why the client keeps **one cached status query** and every screen reads it.
|
||||
- **`expiresAt` on a step is real.** MoH competency licences and INO membership lapse; `scan_expiring`
|
||||
reverts a lapsed step to `expired`, which re-gates bookability. `expired` is therefore a normal state,
|
||||
not an error.
|
||||
- **Document upload is a two-step presign flow**: `upload_url` returns a presigned PUT, the client uploads
|
||||
**directly to storage**, then `documents` confirms. The document bytes never transit the API.
|
||||
`Seams:ObjectStorage:PresignExpirySeconds` (900) bounds the window.
|
||||
- **The three `run` steps are seams, each independently switchable** —
|
||||
`Seams:IdentityKyc:Provider`, `Seams:Shahkar:Provider`, `Seams:BankOwnership:Provider`, all `mock` by
|
||||
default, all `finnotech` for the real bridge (sharing `Seams:Finnotech` credentials). Designated test
|
||||
values make each failure path reachable: `SharedSimPhone` `09120000000`,
|
||||
`MismatchNationalId` `1111111111`, `FailNationalId` `0000000000`,
|
||||
`MismatchIban` `IR0000…0000`.
|
||||
- **`shahkar_match` failing as *shared SIM* is a distinct outcome** from a plain phone↔national-id
|
||||
mismatch, and the UI must say which — a shared family SIM is a common, innocent case.
|
||||
- **National id and licence numbers are encrypted at rest**; the admin queue sees them only where the
|
||||
decision requires it.
|
||||
- The nurse's INO membership field is **locked once submitted** (it feeds the public trust badge).
|
||||
|
||||
## Two vocabularies for one thing
|
||||
|
||||
`GET /me` reports a *nurse verification summary* using `not_started` `in_progress` `pending_review`
|
||||
`verified` `rejected`. This domain's aggregate uses `not_started` `pending` `in_review` `approved`
|
||||
`rejected` `suspended`. **They are two read models over the same source of truth, not a drift** — but they
|
||||
are not interchangeable strings. Map deliberately. See [auth.md](auth.md).
|
||||
|
||||
## Enums
|
||||
|
||||
| Vocabulary | Values |
|
||||
| --- | --- |
|
||||
| `VerificationStatus` (aggregate) | `not_started` `pending` `in_review` `approved` `rejected` `suspended` |
|
||||
| `VerificationStepStatus` | `not_started` `pending` `in_review` `passed` `failed` `expired` |
|
||||
| `StepTypeCode` | `identity_kyc` `shahkar_match` `moh_competency_license` `ino_membership` `criminal_record` `bank_account_verification` |
|
||||
| `CredentialType` | `moh_competency_license` `ino_membership` `criminal_record` |
|
||||
| `VerificationMethod` | `manual` `portal` `api` |
|
||||
| `BadgeState` *(client, from `TrustBadgeDto`)* | `verified` `unverified` `expired` |
|
||||
|
||||
The first two are C# enums serialised as snake_case codes (`Entities/Verification/VerificationStatus.cs`,
|
||||
`VerificationStepStatus.cs`); the rest are string constants in `VerificationStepTypeCodes.cs`. All verified
|
||||
identical to the client's unions.
|
||||
|
||||
`TrustBadgeDto` is `{ nurseId, isVerified, approvedAt, credentialTypes }` — a summary, with no per-step
|
||||
detail. That absence is REQ-043.
|
||||
|
||||
## Open REQs
|
||||
|
||||
| REQ | Status | Effect |
|
||||
| --- | --- | --- |
|
||||
| REQ-034 | deferred | No nurse-grouped queue, no on-demand document URL, no whole-verification approve/reject → 3 phantom routes. This is the main reason the seam is mocked |
|
||||
| REQ-043 | open | `TrustBadgeDto` has no per-step detail (step codes + decision dates), so the public verification panel shows a summary only |
|
||||
| REQ-055 | open | No `submittedAt` on `VerificationStatusDto` — the real B6 screen omits the timestamp line |
|
||||
| REQ-056 | open | No nurse-facing read-back of submitted credential details. `credential_details` writes; nothing reads. The real form degrades to blank |
|
||||
| REQ-062 | open | No name/phone search and no per-status counts on the admin queue |
|
||||
Reference in New Issue
Block a user