refinement phase 3

This commit is contained in:
hamid
2026-07-13 11:26:39 +03:30
parent 1ce36f9414
commit 314763f764
194 changed files with 24211 additions and 274 deletions
+2 -2
View File
@@ -35,8 +35,8 @@ server — mirror that, don't invent a new envelope.
the client picks by locale.
## Pagination (mandatory on lists)
- Query params: `page` (1-based) + `page_size` (cap it server-side, e.g. ≤100). Response payload carries
`items` + `total` (+ `page`/`page_size`). Document the default and max `page_size` per endpoint.
- Query params: `page` (1-based) + `pageSize` (cap it server-side, e.g. ≤100). Response payload carries
`items` + `total` (+ `page`/`pageSize`). Document the default and max `pageSize` per endpoint.
## Idempotency (money & side-effecting POSTs)
- Where stated, the client sends an idempotency key (header or body field) and the server dedups. Webhook
+1 -1
View File
@@ -15,7 +15,7 @@
### `<HTTP> api/v1/<controller>/<action>`
- **Purpose:** …
- **Auth:** none | authenticated | policy/role … · **Rate-limited:** yes/no · **Idempotency key:** yes/no
- **Path/query params:** `name` (type) — meaning; pagination `page`/`page_size` (default/max) for lists.
- **Path/query params:** `name` (type) — meaning; pagination `page`/`pageSize` (default/max) for lists.
- **Request body:**
```json
{ "field": "example" }
+14
View File
@@ -148,3 +148,17 @@ customer's repayment schedule — `installment_count` is informational (default
## Changelog
- b12 — initial contract (eligibility, initiate, customer/admin status, webhook, admin verify/settle/revert).
---
## Refinement phase 3 additions (REQ-022/023/024)
- `balinyaar` added to the `provider_code` enum (in-house plan; identical net-of-fee mechanics, resolves to the
same adapter). The set is now `snapppay|digipay|tara|torobpay|balinyaar`.
- `POST checkout_bnpl/eligibility` accepts optional `{ nationalId, mobile, consent }` (consent required when the
KYC inputs are present; a supplied mobile drives the provider inquiry, else the account mobile).
- `GET api/v1/checkout_bnpl/by_request/{bookingRequestId}` (owner-scoped) → `BnplOrderStatusDto`; `bookingId` on
the settled order was already present on the DTO.
- **DEFERRED:** `checkout_bnpl/options/{id}` + `schedule` + `wallet_installments` — b12 deliberately does not model
the customer repayment schedule / per-installment status, and there is no installment ledger to serve them from.
Keep the D1/D2/D4/D5 plan visualization mocked until a provider-schedule integration (or a schedule table) lands.
+18
View File
@@ -146,3 +146,21 @@
## Changelog
- b8 — initial contract (create/accept/reject/cancel + role-scoped list + single get + admin expire).
---
## Refinement phase 3 additions (REQ-013/014/016/017)
- **`BookingRequestDto`** gains `variantPrice` (IRR digit-string — the chosen variant's *display rate*, not
an engagement total; the request stays money-free), `nurseAvatarUrl` (nullable), and `bookingId`
(nullable — the booking created once the request is `converted`, for the confirmation deep-link).
- **`BookingRequestListItemDto`** gains `variantLabel` (self-describing inbox row) and `patientAge`
(nullable coarse triage age).
- **`GET api/v1/booking_requests/checkout_summary/{id}`** (owner-scoped) — the C6 money breakdown:
`{ bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit, sessionCount,
requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt, serviceCostIrr, commissionIrr,
vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr, nursePayoutAmount }`. All IRR
digit-strings, computed server-side. **Canonical rates:** `platform_fee_rate = 0.15`, `vat_rate = 0.10`.
VAT is **carved out of the commission** so `serviceCostIrr + commissionIrr + vatIrr = totalIrr = gross`
(the captured amount); `commissionIrr` is the commission **net of VAT**, and the raw b10 amounts
(`grossPriceIrr = balinyaarCommissionIrr + nursePayoutAmount`) are surfaced alongside.
+3 -3
View File
@@ -38,7 +38,7 @@ ISO-8601. Enums cross as their stable string codes.
(customer own / nurse assigned / admin all). The **nurse** view omits `addressSnapshotJson`. Never includes
care-instruction clinical fields. **Failure:** `401`, `404` (not found / not a party → no leak).
### `GET api/v1/bookings/list?role=customer|nurse|all&status=&page=&page_size=`
### `GET api/v1/bookings/list?role=customer|nurse|all&status=&page=&pageSize=`
- **Purpose:** role-scoped "My bookings" (paginated, projected). `role=all` is admin-only (`403` otherwise).
**Success:** `PagedResult<BookingListItem>`.
@@ -76,7 +76,7 @@ ISO-8601. Enums cross as their stable string codes.
`payout_eligible_at`, and — when all sessions are settled — completes the booking + sets
`dispute_window_ends_at`. **Failure:** `400` no open check-in, `409` not checkout-able.
### `GET api/v1/booking_sessions/today?date=&page=&page_size=`
### `GET api/v1/booking_sessions/today?date=&page=&pageSize=`
- **Purpose:** the nurse's sessions for a day (default all), with check-in/out CTA state. **Auth:** nurse,
tenancy-scoped. **Success:** `PagedResult<BookingSessionListItem>`.
@@ -87,7 +87,7 @@ ISO-8601. Enums cross as their stable string codes.
- **Purpose:** cancel a single un-started session · **Rate-limited:** yes. Snapshots the policy + computes the
session's refundable share. **Failure:** `409` if the session already started.
### `GET api/v1/admin_evv/list?type=mismatch|no_show&page=&page_size=`
### `GET api/v1/admin_evv/list?type=mismatch|no_show&page=&pageSize=`
- **Purpose:** admin EVV-review queue. **Auth:** admin policy · **Rate-limited:** yes. **Success:**
`PagedResult<AdminEvvItem>`.
+3 -3
View File
@@ -38,8 +38,8 @@
## Public catalog browse — `CatalogController` (no auth)
### `GET api/v1/catalog/categories?page=&page_size=`
- Active categories ordered by `sortOrder`, **paginated** (default `page_size` 50, max 100). Cached. `data`:
### `GET api/v1/catalog/categories?page=&pageSize=`
- Active categories ordered by `sortOrder`, **paginated** (default `pageSize` 50, max 100). Cached. `data`:
`PagedResult<ServiceCategoryDto>`.
### `GET api/v1/catalog/option_groups?category_id={id}`
@@ -89,7 +89,7 @@ Every write **invalidates the catalog cache**. Both labels required (`nameFa`/`n
### `POST api/v1/nurse_variants/set_active/{id}`
- **Body:** `{ isActive }`. Deactivate/reactivate — **never hard-delete**. `data`: `true`. `404` if not owned.
### `GET api/v1/nurse_variants/list?page=&page_size=`
### `GET api/v1/nurse_variants/list?page=&pageSize=`
- The nurse's own offerings — **active and inactive**, active-first, paginated. `data`:
`PagedResult<VariantDto>`.
+7 -7
View File
@@ -8,7 +8,7 @@
**Status:** live as of backend-phase-1 · **Frontend consumer:** frontend-phase-f14 (notification center) / frontend-phase-f15 (admin config/holidays/audit/alerts)
All responses are the standard `OperationResult``ApiResult` envelope (camelCase body, snake_case URLs).
Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-based) + `page_size`
Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-based) + `pageSize`
(default 50, max 100) — bound from the query string; derive exact casing from `swagger.v1.json`.
## Enums used
@@ -25,7 +25,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
### `GET api/v1/platform_config/get_platform_configs`
- **Purpose:** list config rows. **Auth:** admin (DynamicPermission). **Rate-limited:** no.
- **Query:** `page`, `page_size`.
- **Query:** `page`, `pageSize`.
- **200 `data`:** `PagedResult<PlatformConfigDto>``{ items:[{ key, value, dataType, description }], total, page, pageSize }`.
### `POST api/v1/platform_config/update_platform_config`
@@ -36,7 +36,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
### `GET api/v1/platform_config/get_config_change_history`
- **Purpose:** the audited change history for one key (from the append-only trail). **Auth:** admin.
- **Query:** `key` (required), `page`, `page_size`.
- **Query:** `key` (required), `page`, `pageSize`.
- **200 `data`:** `PagedResult<ConfigChangeDto>``{ items:[{ id, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first.
---
@@ -44,7 +44,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
## Admin — Holidays (`holidays` controller, `[Authorize(DynamicPermission)]`)
### `GET api/v1/holidays/get_holidays`
- **Query:** `from` (date, optional), `to` (date, optional), `page`, `page_size`.
- **Query:** `from` (date, optional), `to` (date, optional), `page`, `pageSize`.
- **200 `data`:** `PagedResult<HolidayDto>``{ items:[{ id, holidayDate, nameFa, type, isBankClosed }], … }`, by date.
### `POST api/v1/holidays/upsert_holiday`
@@ -60,7 +60,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
### `GET api/v1/audit/get_audit_trail`
- **Purpose:** the immutable trail for one entity. **Auth:** admin.
- **Query:** `entity_type` (e.g. `PlatformConfig`), `entity_id` (string), `page`, `page_size`.
- **Query:** `entity_type` (e.g. `PlatformConfig`), `entity_id` (string), `page`, `pageSize`.
- **200 `data`:** `PagedResult<AuditLogDto>``{ items:[{ id, entityType, entityId, action, changedFieldsJson, actorUserId, occurredAt }], … }`, newest first. **Notes:** read-only; there is no write/update/delete endpoint for audit rows.
---
@@ -68,7 +68,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
## Admin — Support alerts (`support_alerts` controller, `[Authorize(DynamicPermission)]`, never user-facing)
### `GET api/v1/support_alerts/get_support_alerts`
- **Query:** `type?`, `status?`, `owner_user_id?`, `page`, `page_size`.
- **Query:** `type?`, `status?`, `owner_user_id?`, `page`, `pageSize`.
- **200 `data`:** `PagedResult<SupportAlertDto>``{ items:[{ id, type, severity, status, entityType, entityId, bookingId, reviewId, ownerUserId, resolutionNote, resolvedAt, createdAt }], … }`.
### `POST api/v1/support_alerts/assign_support_alert`
@@ -84,7 +84,7 @@ Lists carry `{ items, total, page, pageSize }`. Pagination inputs are `page` (1-
Every endpoint is scoped to the signed-in caller (`ICurrentUser`) — never a body-supplied user id.
### `GET api/v1/notifications/get_notifications`
- **Query:** `page`, `page_size`.
- **Query:** `page`, `pageSize`.
- **200 `data`:** `PagedResult<NotificationDto>``{ items:[{ id, type, title, body, dataJson, isRead, readAt, createdAt }], … }`, **unread-first** then newest-first.
### `GET api/v1/notifications/get_unread_count`
+12 -2
View File
@@ -72,7 +72,7 @@ Every write **invalidates the geo cache**. Names required (`nameFa`/`nameEn`); `
### `DELETE api/v1/nurse_service_areas/remove/{id}`
- Soft-removes the nurse's own area. `data`: `true`. `404` if not owned/absent (existence not leaked).
### `GET api/v1/nurse_service_areas/list?page=&page_size=`
### `GET api/v1/nurse_service_areas/list?page=&pageSize=`
- The nurse's own areas, whole-city first, paginated. `data`: `PagedResult<NurseServiceAreaDto>`.
## Customer addresses — `CustomerAddressesController` (authenticated; customer-scoped in handler)
@@ -97,7 +97,7 @@ Every write **invalidates the geo cache**. Names required (`nameFa`/`nameEn`); `
### `DELETE api/v1/customer_addresses/delete/{id}`
- Soft-deletes the owned address. `data`: `true`. `404` if not owned.
### `GET api/v1/customer_addresses/list?page=&page_size=`
### `GET api/v1/customer_addresses/list?page=&pageSize=`
- The customer's own addresses, **primary first**, paginated, with PII **decrypted for the owner**. `data`:
`PagedResult<CustomerAddressDto>`.
@@ -123,3 +123,13 @@ Tehran city id `101`, Tehran districts `1001…1022`; other cities have no distr
## Changelog
- b4 — initial contract: public geo lookups, admin geo CRUD + set_active, nurse service areas, customer
addresses; `IGeocoder` seam; `409` conflict added to the envelope.
---
## Refinement phase 3 additions (REQ-008/009)
- **`CustomerAddressDto`** gains `provinceId` (joined from `cities.province_id`) so the edit form can
prefill the province → city cascade from a server-loaded address.
- **`customer_addresses/create` + `update/{id}`** now accept optional `latitude`/`longitude` (both or
neither). When present, the user's dropped pin is stored (`geocode_source = user_pin`, preferred for the
EVV distance check); when absent the server geocodes as before (`geocode_source = geocoder`).
+11
View File
@@ -128,3 +128,14 @@
## Changelog
- b2 — initial contract (phone-OTP auth, sessions, `/me`, role selection).
---
## Refinement phase 3 additions (REQ-002/003)
- **`RequestOtpResult`** gains `codeLength` (6) and `expiresInSeconds` (60) so the OTP box count + expiry
hint are contract-driven.
- **`verify_otp` failures** now carry a stable machine `code` on the envelope: `otp_invalid` (wrong **or**
expired — collapsed for anti-enumeration) and `otp_locked` with `data: { retryAfterSeconds }` on lockout.
The coded-error envelope is `{ isSuccess: false, statusCode: 400, message, code, data? }` (the optional
`code` is omitted from every other response).
@@ -85,3 +85,16 @@ segments are snake_case; responses use the standard `OperationResult`→`ApiResu
## 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/avatar``multipart/form-data` field `file` (JPEG/PNG/WebP, ≤ 5 MB),
stored via `IObjectStorage`, returns `{ url }` and persists it on the profile.
@@ -173,3 +173,23 @@ rebuild). All are `[Authorize(DynamicPermission)]` (admin role passes; other sta
| Verification queue / refunds / payouts / moderation / config / holidays | their own phase routes | b6/b11/b13/b14/b1 |
`support_alerts` are internal-only and must never appear in a user-facing response or join.
---
## Refinement phase 3 additions (REQ-029/030/031/032/033/034/035/036/037)
**Delivered:**
- **REQ-029** `PlatformConfigDto` gains `updatedAt` + `updatedBy` (from the entity audit fields).
- **REQ-030** `GET audit/get_audit_trail` filters also by `actorId`, `action`, `from`, `to` (all optional; `entityType`/
`entityId` now optional too). **Query params bind camelCase** (`actorId`/`from`/`to`), not `actor_id`.
- **REQ-037** `tagCodes: string[]` on `ModerationQueueItemDto`. **REQ-033** `totalIrr` on `InvoiceDto`
(= platform commission + BNPL commission + VAT).
- **REQ-032** activate/suspend toggle `POST admin/partner-centers/{id}/set-active { isActive }`. **Route casing
pinned:** the admin partner-center routes are **kebab-case** (`admin/partner-centers`, `.../set-active`) — an
intentional b15 divergence from the `snake_case` convention; the frontend's kebab-case client is CORRECT.
**Deferred (admin-console polish, documented in the tracker):** REQ-031 (RBAC `admin_roles/list|grant|revoke`),
REQ-032 `centers/me` + split portal reads (need the user↔center admin association REQ-038 deferred), REQ-033
center-scoped invoice list, REQ-034 verification nurse-queue/signed-url/whole-approve, REQ-035 refund admin
preview+approve/reject (the customer preview REQ-020 IS delivered), REQ-036 payout admin preview/holidayShifted/
transfer-reference.
+13
View File
@@ -0,0 +1,13 @@
---
## Refinement phase 3 additions (REQ-028)
- **`TicketSummaryDto`** gains `lastMessageAt` (last non-internal activity) + `unreadCount` (the caller's unread
non-internal messages from others; 0 on the admin queue). Unread is computed against the participant's
`last_read_at`, **stamped when the participant fetches the user-facing thread**.
- **`GET /tickets`** gains a `bookingId` query filter (jump to a booking's coordination ticket).
- **`POST /tickets/{id}/messages`** accepts an optional `clientMessageId` — a retried send with the same key is
deduplicated (returns the original) and the key is echoed on `PostMessageResult`.
- **Message author = role label, not a name** (confirmed intentional, privacy): the DTO carries `senderId`; the
client derives the author label from the participant role. No raw identity/name is exposed.
+8
View File
@@ -69,3 +69,11 @@ DEBIT escrow_held gross_price_irr (e.g. 23300000)
- **The checkout shows gross + commission/VAT breakdown only** — never the internal `account_type`s.
- **Payment is idempotent end-to-end**: a retried `initiate` (same `Idempotency-Key`) reuses the attempt; a
replayed webhook is a no-op; a repeat `initiate` after capture is a `409`.
---
## Refinement phase 3 additions (REQ-018)
- **Invoice auto-issue on capture/settle:** the commission invoice is now issued automatically when a card
capture (`ConfirmPaymentAndPostLedger`) or a BNPL settle first creates the booking — idempotent per
booking — so a paying customer's `GET api/v1/invoices/{bookingId}` resolves right away (was admin-only).
+13
View File
@@ -97,3 +97,16 @@ The response envelope is the standard `{ data, … }`; the shapes below are the
## Changelog
- b13 — initial contract.
---
## Refinement phase 3 additions (REQ-025 — nurse earnings)
- **`GET api/v1/nurse_payouts/earnings_balance`** → `{ pendingTotalIrr, eligibleTotalIrr, paidTotalIrr,
clawbackOutstandingIrr, netPayableBalanceIrr }`. `netPayableBalanceIrr` is the **ledger-derived, SIGNED**
nurse_payable balance (may be negative = "owed back"; never clamped); `paidTotalIrr` is lifetime, not in the net.
- **`GET api/v1/nurse_payouts/earnings?state=&page=&pageSize=`** → `PagedResult<NurseEarningsItem>`; `state`
(`pending|eligible|paid|clawback_applied`) is **derived server-side** from `bookings.status` +
`dispute_window_ends_at < now` + the payout link + any clawback. Filterable by `state`.
- **`GET api/v1/nurse_payouts/{id}`** → nurse-scoped payout detail (batch window + covered bookings).
- **`NursePayoutHistoryDto`** gains `failureReason`.
+21
View File
@@ -125,3 +125,24 @@ ISO-8601; `expected_customer_refund_eta` is a **date** (`"2026-08-24"`).
## Changelog
- b11 — initial contract (create refund, list refunds, write-off clawback, issue invoice, refund status, get invoice).
---
## Refinement phase 3 additions (REQ-019/020/021 — customer refunds)
- **`POST api/v1/bookings/{id}/cancel`** (customer) — cancels the booking (freezing the policy snapshot) AND
opens its refund in one call → `RefundStatusDto`. Body `{ reasonCategory, reasonNotes?, sessionIds? }`
(MVP cancels all un-started sessions; `sessionIds` is accepted for forward-compat).
- **`GET api/v1/bookings/{id}/cancellation_policy`** (customer) — pre-cancel disclosure: resolves the
applicable policy by **current** lead time + per-session refundability →
`{ bookingId, cancellable, cancellationPolicyCode, refundPercentageApplied, feePercentage, refundAmountIrr,
feeAmountIrr, refundableAmountIrr, platformFeeRefundedIrr, nursePayoutRefundedIrr, appliesTo, leadTimeLabel,
refundChannel, expectedCustomerRefundEta (null in preview), sessions: [{ bookingSessionId, sessionIndex,
scheduledDate, refundable, reasonCode }] }`. `refundAmountIrr + feeAmountIrr = refundableAmountIrr`.
- **`GET api/v1/refunds/by_booking/{bookingId}`** (customer) — the booking's latest refund status (404 if none).
- **`RefundStatusDto`** gains `platformFeeRefundedIrr`, `nursePayoutRefundedIrr`, `refundPercentageApplied`,
`cancellationPolicyCode`, `createdAt`, `completedAt` (the fee-leg transparency split).
- **Canonical `cancellation_policy_code` set** (seeded, stable — the frontend's `free_24h`/`partial_under_24h`/
`customer_no_show` were invented): **`standard_24h`** (customer ≥24h → full refund), **`standard_inside_24h`**
(customer <24h → partial), **`nurse_no_show`** (nurse-initiated → full refund + penalty), **`admin_cancellation`**
(admin → full refund). Per-session `reasonCode`: **`un_started`** when refundable, else the blocking session status.
+16
View File
@@ -119,3 +119,19 @@ access rule is enforced in the handler, not just the route policy.
- **Failure cases:** `401`; `403` no clinical access; `404` patient not found.
- **Notes:** The record is **patient-scoped, not booking-scoped** — a new nurse taking over reads the whole
history (not just their own booking's notes).
---
## Refinement phase 3 additions (REQ-026/027)
- **`GET api/v1/bookings/{bookingId}/review_eligibility`** → `{ canReview, reason?:
not_completed|already_reviewed|not_owner|not_found }`.
- **`GET api/v1/bookings/{bookingId}/my_review`** → `{ moderationStatus:
pending_moderation|published|hidden|rejected|none, rating?, body?, tagCodes[], createdAt? }`. Masked-author
omission on the public list is **intentional** (privacy).
- **Family-owned care plan (new entity `usr.PatientCarePlans`):** `GET/PUT api/v1/patients/{patientId}/care_record`
→ `{ patientId, medications:[{id,name,dosage?,frequency,timingNote?}], routine:[{id,label,timeOfDay?,note?}],
tasks:[{id,label,done}] }`. Read = owner/nurse-with-booking/admin; write = owning customer only.
- **`GET api/v1/patients/{patientId}/record_access`** → `{ canView, canEdit, canAppendNote, deniedReason? }`
(always 200; non-leaking `not_found`/`not_authorized`).
- **Structured `taskResults`** (`[{ label, done }]`) added to the visit-note write body + the history DTO.
+14 -2
View File
@@ -49,7 +49,7 @@
- `nurse_gender` (`male`|`female`, optional) — the same-gender facet.
- `min_price` / `max_price` (long IRR, optional) — inclusive range over the copied `price`.
- `price_unit` (enum, optional) — compare like-for-like listings (e.g. only `per_day`).
- `page` (int, default 1), `page_size` (int, default 50, max 100).
- `page` (int, default 1), `pageSize` (int, default 50, max 100).
- **Success `200` payload (`data` = `PagedResultOfNurseSearchResultDto`):**
```json
{
@@ -74,7 +74,7 @@
}
```
- **Failure cases:** `400``service_category_id`/`city_id` missing or ≤ 0, `nurse_gender` not `male`/`female`,
`min_price > max_price`, invalid `price_unit`, or `page_size > 100`.
`min_price > max_price`, invalid `price_unit`, or `pageSize > 100`.
- **Notes:** returns only `is_searchable = 1` rows. `districtId = null` in a result row means the nurse covers
the whole city. No entity is hydrated — the read is a projected, paginated, `AsNoTracking` index scan.
@@ -103,3 +103,15 @@
## Changelog
- b7 — initial contract: public `search/nurses`, admin `admin_search/rebuild_index`.
---
## Refinement phase 3 additions (REQ-012)
- **`NurseSearchResultDto`** gains `nurseName` + `avatarUrl` (denormalized onto `nurse_search_index`, so no
per-row join) and `distanceKm` (nullable — the covering index carries no coordinate, so it is null today).
- **`GET api/v1/nurses/{id}/profile`** (public) — the aggregated discovery detail:
`{ nurseId, nurseName, avatarUrl, bio, yearsExperience, averageRating, totalReviews,
totalCompletedBookings, isVerified, inoMembership, attributeChips[], services: [{ variantId, displayName,
priceIrr, priceUnit, sessionCount? }], latestReview?: { rating, body, authorMasked (null by design),
createdAt } }`. No encrypted credential number is ever exposed.
+14 -2
View File
@@ -124,9 +124,9 @@
## Admin review queue — `AdminVerificationsController` (`[Authorize(DynamicPermission)]`, rate-limited `sensitive`)
### `GET api/v1/admin_verifications?status=&page=&page_size=`
### `GET api/v1/admin_verifications?status=&page=&pageSize=`
- **Purpose:** the review queue — one row per step awaiting attention.
- **Params:** `status` (default `in_review`) + pagination `page`/`page_size`.
- **Params:** `status` (default `in_review`) + pagination `page`/`pageSize`.
- **`data`:** `PagedResult<AdminPendingStepDto>`. Documents carry **signed GET URLs**.
### `GET api/v1/admin_verifications/{nurseVerificationId}`
@@ -255,3 +255,15 @@ GET /api/v1/nurses/{nurseId}/trust_badge -> { isVerified: true, approvedAt, cr
`verification_status` / `verification_step_status` / `credential_type` / `verification_method` enums;
transactional `is_verified` flip; encrypted-never-serialized `credential_number`; three new mocked vendor
seams (`IShahkarVerifier`, `IIdentityKycProvider`, `ICredentialVerifier`). Scheduled expiry cron deferred.
---
## Refinement phase 3 additions (REQ-011)
- **`VerificationStepDto`** gains `isRequired` (mirrors the step-type catalog; an optional step never blocks
bookability).
- **`POST api/v1/nurse_verification/credential_details`** (nurse) — captures the structured credential
fields collected with the uploads: `{ inoNumber (required), specialties: string[], licenseNumber?,
issuingAuthority?, holderName?, issuedAt?, expiresAt? }` → `VerificationStatusDto`. Upserts an
`ino_membership` (and, if a license number is sent, `moh_competency_license`) `nurse_credentials` row
(unverified — admin still decides) and persists `specialties` on the profile. The INO number is encrypted.
File diff suppressed because it is too large Load Diff