refinement phase 3
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
|
||||
@@ -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>`.
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user