ui phase 6
This commit is contained in:
@@ -710,3 +710,69 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
produces the JSON blob today.
|
||||
- **Status:** open — the client keeps the defensive multi-key parse until this lands; no user-facing defect
|
||||
today (the address still renders when any recognizable key is present), just an unenforced contract.
|
||||
|
||||
## REQ-046 — Checkout/receipt enrichment: nurse identity on the summary + a client-readable payment reference — filed by ui-phase-6 — 2026-07-18
|
||||
- **Need:** Two additions to the checkout/payment surfaces:
|
||||
1. `nurseAvatarUrl` (string, nullable) and `nurseVerified` (bool) on `CheckoutSummaryDto` — the C6 identity
|
||||
moment ("who am I paying for").
|
||||
2. A client-readable **tracking code** and **paid-at timestamp** for a captured payment — either on the
|
||||
pending-callback poll target or a dedicated confirm-read: `trackingCode` (string, nullable — a
|
||||
customer-quotable reference) and `paidAt` (UTC ISO, nullable) on `PaymentOutcomeDto`
|
||||
(`GET booking_requests/get/{id}`-derived today per REQ-017).
|
||||
- **Why:** C6 now shows the nurse's avatar + a verified badge next to the payable total, and the
|
||||
confirmation screen is rebuilt as a screenshot-worthy receipt (کد پیگیری in a copyable row + Shamsi
|
||||
payment date-time) — Iranian users screenshot payment receipts and expect a reference number to quote in
|
||||
a dispute. Neither field exists on the wire today; the mock supplies both (nurse is always seed-verified,
|
||||
no avatar in the f7 mock store; `trackingCode` = the PSP `gatewayReferenceCode`, `paidAt` = the capture
|
||||
timestamp) and the real `paymentClientApi` leaves them `null` — the receipt hides the row rather than
|
||||
render a fabricated code (extends REQ-016/017).
|
||||
- **Proposed shape:** `CheckoutSummaryDto { …, nurseAvatarUrl: string|null, nurseVerified: boolean }`;
|
||||
`PaymentOutcomeDto { …, trackingCode: string|null, paidAt: string|null }`.
|
||||
- **Status:** open — mock-only until served; real path hides the identity avatar/badge and the tracking
|
||||
code/paid-at rows gracefully.
|
||||
|
||||
## REQ-047 — Customer payment-transactions list (card + BNPL) — filed by ui-phase-6 — 2026-07-18
|
||||
- **Need:** A customer-scoped read of every payment transaction the caller made — proposed
|
||||
`GET api/v1/bookings/payment_history` (owner-scoped) →
|
||||
`[{ transactionId, bookingRequestId, bookingId, status, amountIrr, createdAt }]`.
|
||||
- **Why:** The wallet's «پرداختها» tab is the fix for the audit's top finding — the Wallet tab was a
|
||||
permanently empty installments-only shell for every card-paying customer (the default path; BNPL is
|
||||
mock-gated). No endpoint serves a card payment history today. The client added `getPaymentHistory` to the
|
||||
`PaymentApi` seam; the mock returns the in-memory transaction list, the real `paymentClientApi` targets
|
||||
this proposed route (404s until delivered — the tab renders its empty state until then). BNPL rows are
|
||||
sourced separately from `services/bnpl`'s wallet installments (REQ-024) and merged client-side for display
|
||||
— this REQ covers the card half only.
|
||||
- **Proposed shape:** as above; `status` reuses the existing `PaymentTransactionStatus` enum
|
||||
(`pending|succeeded|failed`).
|
||||
- **Status:** open — wallet «پرداختها» renders the empty state on the real path until delivered.
|
||||
|
||||
## REQ-048 — Customer "all my refunds" list — filed by ui-phase-6 — 2026-07-18
|
||||
- **Need:** A customer-scoped read of every refund the caller owns — proposed `GET api/v1/refunds/my`
|
||||
(owner-scoped) → the same thin shape `GET refunds/{id}/status` already returns, as an array.
|
||||
- **Why:** The wallet's «استردادها» tab renders every refund via the shared `RefundStatusCard`. b11's only
|
||||
customer-facing refund reads are by-booking (REQ-021) and by-id — there's no "list everything I've been
|
||||
refunded" read, and REQ-021's by-booking route requires already knowing which booking to ask about. The
|
||||
client added `getMyRefunds` to the `RefundsApi` seam (mock-primary per `USE_REFUNDS_MOCK`, extends
|
||||
REQ-019/020/021); the real `refundsClientApi` targets this proposed route.
|
||||
- **Proposed shape:** `GET api/v1/refunds/my` → `RefundStatusWire[]` (the same per-item shape as
|
||||
`refunds/{id}/status`).
|
||||
- **Status:** open — wallet «استردادها» renders the empty state on the real path until delivered.
|
||||
|
||||
## REQ-049 — Invoice fiscal fields: payment method, transaction reference, seller fiscal identity — filed by ui-phase-6 — 2026-07-18
|
||||
- **Need:** Three additions to `InvoiceDto` so the invoice is a document a family can file for
|
||||
reimbursement/dispute:
|
||||
1. `paymentMethod: 'card' | 'bnpl' | null`.
|
||||
2. `transactionReference: string | null` (opaque, the settling payment/BNPL reference).
|
||||
3. `sellerFiscalIdentity: { legalName, economicCode: string|null, address: string|null } | null` — a
|
||||
platform-level fact (not per-invoice data), read from wherever the مودیان enrollment config lands.
|
||||
- **Why:** The invoice page was print-capable but not audit-worthy (no payment method, no transaction
|
||||
reference, no seller tax/economic identity — the things a real Iranian VAT invoice carries). Buyer name
|
||||
and the service/visit-date recap are composed client-side today (a UI join over the customer's own
|
||||
profile + the booking detail read — no money math, per phase §3.5), but payment method/reference and the
|
||||
seller identity have no client-side source and must be served. The mock populates all three for card
|
||||
payments (`paymentMethod: 'card'`, `transactionReference` = the gateway reference, a placeholder
|
||||
`sellerFiscalIdentity`); BNPL-settled invoices and the real path leave them `null` — the invoice hides
|
||||
those rows rather than render fabricated fiscal data. Extends REQ-018.
|
||||
- **Proposed shape:** as above.
|
||||
- **Status:** open — invoice renders the reconciling money breakdown + مودیان status unconditionally; the
|
||||
three new rows render only when served.
|
||||
|
||||
@@ -121,11 +121,11 @@ the frontend can build before the backend phase merges, and swap to the real HTT
|
||||
| `VerificationApi` | `client/src/services/verification/apis/mockApi.ts` | The whole nurse trust journey (b6). Seeds the six required steps on `start` (idempotent); `runIdentityKyc` passes any well-formed 10-digit id **except** `0000000000` (→ `failed`/`kyc_no_match`, matches backend `MockIdentityKycProvider`); `runShahkarMatch` requires identity passed, fails **shared-SIM** when the bound national id is `1111111111` (→ `failed`/`shared_sim`); `runBankVerification` passes (assumes a primary bank account); `uploadStepDocument` simulates signed-URL PUT progress then moves the step to `in_review` (metadata only); `submitCredentialDetails` validates the INO number. Re-aggregates like the server (`approved` only when every step passes). **Dev-only** `__mockApproveAll()`/`__mockRejectStep(code,reason)` stand in for the deferred (f15) admin review queue so a human can watch `is_verified`/the trust badge/the publish gate flip — reachable from B3/B6 only while the flag is true | `USE_VERIFICATION_MOCK` (`services/verification/constants.ts`, default `true`) | b6 `nurse_verification/*` + `nurses/{id}/trust_badge` are live; set flag `false` — `verificationClientApi` is wired (action-style routes, camelCase, XHR signed-URL PUT for upload progress + SHA-256 integrity hash). **Caveat:** the real `submitCredentialDetails` no-ops pending REQ-011 (no nurse-facing endpoint for the structured INO/specialties fields yet) — the document uploads it accompanies are contract-backed. No hook/component change | 🟡 |
|
||||
| `BookingsApi` | `client/src/services/bookings/apis/mockApi.ts` | The post-payment engagement (b9). Seeds **2 confirmed bookings** (one 3-session multi-day, one single-visit) + `booking_care_instructions` + a per-session **EVV state machine** — `checkInVisit` flips the session→`in_progress`/`checked_in` (booking→`in_progress`) and computes the **advisory** `checkInAddressMatch` (haversine vs the seeded address ± `MOCK_EVV_TOLERANCE_METERS`, `null` when GPS was absent); `checkOutVisit` requires an open check-in (**`400 no_open_check_in`** otherwise), completes the session (stamps `payoutEligibleAt`), and completes the booking + opens the dispute window once **all** sessions settle. `getCareInstructions` **404s any viewer but the assigned nurse** (the two-stage-disclosure boundary; the UI `enabled` gate means the customer never even calls it). Money stays IRR digit-strings with `gross = commission + payout` and `Σ visitPayout = payout` | `USE_BOOKINGS_MOCK` (`services/bookings/constants.ts`, default `true`) | b9 `bookings/*` + `booking_sessions/*` are live, but a booking only exists after `bookings/convert` runs on a **paid** request — both upstreams (`bookingRequests` mock, card capture b10) aren't real client-side yet. Once conversion is live, set flag `false` — `bookingsClientApi` maps the routes 1:1 (+ `bookingsServerApi` for the RSC prefetch). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `ILocationProvider` | `client/src/services/bookings/evv/locationProvider.ts` | **EVV GPS capture** — the only client seam f8 introduces. `getCurrentPosition()` never rejects (denied/unavailable → `null`, so a GPS problem is **advisory, never a block**). The **real** provider wraps `navigator.geolocation.getCurrentPosition`; the **mock** returns canned coordinates per mode so the in-range / advisory-out-of-range / denied paths are all demoable without a device (the mock `BookingsApi` computes the match against the same seeded reference point) | `NEXT_PUBLIC_EVV_MOCK_GPS` = `in_range` \| `out_of_range` \| `denied` \| `off` (default `in_range` while `USE_BOOKINGS_MOCK`, else `off`) | Set `NEXT_PUBLIC_EVV_MOCK_GPS=off` (or flip `USE_BOOKINGS_MOCK`) → the real `navigator.geolocation` provider is selected. Real **address-match math** stays server-side (backend geocoding seam), not here — this seam only *captures* the position | 🟢 (real, refinement-phase-4) |
|
||||
| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `PaymentApi` | `client/src/services/payment/apis/mockApi.ts` | **The f9 checkout money path** — plays the PSP + webhook roles the client can't reach: `getCheckoutSummary` serves the unserved C6 breakdown (REQ-016; commission-net/VAT/service split via **integer parts-per-10000 BigInt math**, 12% fee / 10% VAT, reconciles to the rial); `initiatePayment` enforces b10 idempotency (same `Idempotency-Key` → same attempt; repeat after capture / lapsed window → **`409`**) and returns a `redirectUrl` into the local mock-gateway harness; `confirmGatewayReturn` on success is the **webhook-confirm stand-in and the missing f7↔f8 bridge** — flips the request `converted` (+ client-augmented `bookingId`, via `mockMarkBookingRequestConverted` in the f7 mock), inserts a **confirmed** booking into the f8 store (`mockInsertConvertedBooking`), and auto-issues the b11-shaped invoice (`moadianStatus: pending`, `pdfUrl: null` so the print path exercises); replayed returns converge idempotently; `getInvoice` 404s until issued. **ui-phase-6:** `initiatePayment`'s `redirectUrl` is now `null` (was a stale pointer to the deleted card-gateway harness page — a latent bug, since the harness itself was already removed in refinement-phase-4; the checkout page's `!redirectUrl` branch already reads the outcome directly, no behavior change); `getCheckoutSummary` adds mock `nurseAvatarUrl: null`/`nurseVerified: true` (REQ-046, the C6 identity moment); the capture path stamps `capturedAt`/`createdAt` on the transaction so `PaymentOutcomeDto` serves `trackingCode`/`paidAt` (REQ-046, the confirmation receipt) and the new `getPaymentHistory` reads the same transaction list (REQ-047, wallet «پرداختها»); invoice creation adds mock `paymentMethod: 'card'`/`transactionReference`/`sellerFiscalIdentity` (REQ-049, fiscal-grade invoice) | `USE_PAYMENT_MOCK` (`services/payment/constants.ts`, default `true`) | b10 initiate + b11 invoice are live and `paymentClientApi` maps them 1:1 (`Idempotency-Key` header, `GET invoices/{bookingId}`); deliver **REQ-016** (checkout summary — the real client already targets the proposed `booking_requests/checkout_summary/{id}` slug) + **REQ-017** (transaction status / `bookingId`; until then the real outcome poll maps `booking_requests/get` statuses and can't distinguish declined from slow) + **REQ-018** (invoice reachable post-capture) + **REQ-046** (nurse identity + tracking code/paid-at) + **REQ-047** (payment history) + **REQ-049** (invoice fiscal fields), make the upstream `bookingRequests` flow real, then set flag `false`. No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| Mock-gateway page (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the PSP's hosted payment page so the initiate → redirect → return round-trip is exercisable without a gateway: the mock `redirectUrl` points here, and its success/failure buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «درگاه پرداخت آزمایشی», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b10's `redirectUrl` is the PSP's **absolute** URL (the checkout does a full `window.location.assign` for `http(s)` URLs), so this page is simply never linked; delete it when `USE_PAYMENT_MOCK` retires. The PSP's return deep-link into `/bookings/checkout/return` is backend/PSP config | 🗑 removed in refinement-phase-4 (payment flipped real) |
|
||||
| `RefundsApi` | `client/src/services/refunds/apis/mockApi.ts` | **The f10 customer cancel + refund surface** b11 doesn't serve (refunds are admin-only; no customer cancel command, no policy preview, no refund-by-booking, no fee-leg decomposition on the customer status → REQ-019/020/021). Reads the shared **f8 bookings store** (`mockGetBookingForRefund`) to resolve the tier by lead time (`free_24h` >24h / `partial_under_24h` <24h / `customer_no_show` started — client-invented codes → i18n keys) and the per-session refundable(un-started)/locked(completed-and-verified) breakdown, decomposing the refund across the two fee legs via **integer parts-per-10000 BigInt math** (`refundAmount + fee = refundableGross` to the rial). `cancelBooking` flips the booking → `cancelled` (`mockMarkBookingCancelled` stamps the b9 snapshot + cancels only un-started sessions) and creates a refund: **card → `succeeded`** immediately (no ETA); **BNPL → `approved`→`processing`→`succeeded`** over status polls with a `expected_customer_refund_eta` ~10 business days out (Fridays skipped) so the ~7–10-day banner renders. Enforces the outside-policy **`409`** (already-cancelled / nothing-refundable / non-refundable session). Seeds a **`failed`** refund on the cancelled booking 5004 so the contact-support state demos; booking 5002 is pinned to the BNPL channel; booking 5003 (new, mid-engagement) demos the mixed refundable/locked breakdown. Also adds bookings-store seeds 5003/5004 + the two non-seam exports | `USE_REFUNDS_MOCK` (`services/refunds/constants.ts`, default `true`) | Deliver **REQ-019** (customer cancel command — the real `refundsClientApi.cancelBooking` already targets `POST bookings/{id}/cancel`) + **REQ-020** (cancellation-policy preview → `GET bookings/{id}/cancellation_policy`, incl. the canonical `cancellation_policy_code` set) + **REQ-021** (`GET refunds/by_booking/{id}` + the decomposition fields on the customer `refunds/{id}/status`), then set flag `false` — the real client maps the published `refunds/{id}/status` 1:1 and targets the proposed slugs for the rest. No hook/component change | 🟡 |
|
||||
| `RefundsApi` | `client/src/services/refunds/apis/mockApi.ts` | **The f10 customer cancel + refund surface** b11 doesn't serve (refunds are admin-only; no customer cancel command, no policy preview, no refund-by-booking, no fee-leg decomposition on the customer status → REQ-019/020/021). Reads the shared **f8 bookings store** (`mockGetBookingForRefund`) to resolve the tier by lead time (`free_24h` >24h / `partial_under_24h` <24h / `customer_no_show` started — client-invented codes → i18n keys) and the per-session refundable(un-started)/locked(completed-and-verified) breakdown, decomposing the refund across the two fee legs via **integer parts-per-10000 BigInt math** (`refundAmount + fee = refundableGross` to the rial). `cancelBooking` flips the booking → `cancelled` (`mockMarkBookingCancelled` stamps the b9 snapshot + cancels only un-started sessions) and creates a refund: **card → `succeeded`** immediately (no ETA); **BNPL → `approved`→`processing`→`succeeded`** over status polls with a `expected_customer_refund_eta` ~10 business days out (Fridays skipped) so the ~7–10-day banner renders. Enforces the outside-policy **`409`** (already-cancelled / nothing-refundable / non-refundable session). Seeds a **`failed`** refund on the cancelled booking 5004 so the contact-support state demos; booking 5002 is pinned to the BNPL channel; booking 5003 (new, mid-engagement) demos the mixed refundable/locked breakdown. Also adds bookings-store seeds 5003/5004 + the two non-seam exports. **ui-phase-6:** `getMyRefunds` (REQ-048) returns every in-memory refund newest-first — the wallet «استردادها» tab | `USE_REFUNDS_MOCK` (`services/refunds/constants.ts`, default `true`) | Deliver **REQ-019** (customer cancel command — the real `refundsClientApi.cancelBooking` already targets `POST bookings/{id}/cancel`) + **REQ-020** (cancellation-policy preview → `GET bookings/{id}/cancellation_policy`, incl. the canonical `cancellation_policy_code` set) + **REQ-021** (`GET refunds/by_booking/{id}` + the decomposition fields on the customer `refunds/{id}/status`) + **REQ-048** (`GET refunds/my`), then set flag `false` — the real client maps the published `refunds/{id}/status` 1:1 and targets the proposed slugs for the rest. No hook/component change | 🟡 |
|
||||
| `BnplApi` | `client/src/services/bnpl/apis/mockApi.ts` | **The f11 BNPL installment checkout (D1–D5)** b12 doesn't serve client-side (b12 is order-centric — eligibility/initiate/status/webhook — and **explicitly does not model the repayment schedule**; no provider/plan options, no wallet installment status → REQ-022/023/024). Reads the frozen request gross from the shared **f7 store** and plays the provider: `getBnplOptions` builds the provider set as **data** (دیجیپی 3/6/12 · اسنپپی ۴ · اقساط بالینیار; per-plan monthly/down-payment/total via **integer parts-per-10000 BigInt math**, never a hardcoded fee in the UI); `checkEligibility` returns `eligible` unless the national-id last digit is `0` (→`not_eligible`) or the order exceeds `MOCK_CREDIT_CEILING_IRR` (→`ceiling_exceeded`) so both declined paths demo; `getBnplSchedule` serves the down-payment + N-installment rows (last absorbs the remainder → rows sum to total); `issueBnplToken` enforces b12 idempotency (same key → same token; repeat after settle / lapsed window → **`409`**) + a `redirectUrl` into the local provider-handoff harness; `acceptBnplSchedule` on success is the **settle stand-in and reuses the f9 conversion bridge** — flips the request `converted` (`mockMarkBookingRequestConverted`), inserts a **confirmed** booking (`mockInsertConvertedBooking`; a settled BNPL order = a card payment net-of-fee, payout invariant to method), and **seeds a provider-reported Wallet plan**; `getWalletInstallments` serves D5 (seeded active دیجیپی ۶-ماهه with paid/due-soon/upcoming rows + each settled checkout's plan). Money = served IRR digit-strings end-to-end (components only format) | `USE_BNPL_MOCK` (`services/bnpl/constants.ts`, default `true`) | Deliver **REQ-022** (options + schedule — real `bnplClientApi` targets `checkout_bnpl/options/{id}` + `checkout_bnpl/schedule/{id}`), **REQ-023** (eligibility accepts the D3 national-id/mobile/consent), **REQ-024** (`checkout_bnpl/wallet_installments` provider-reported status + a customer `bookingId` on the settled order), and make the upstream `bookingRequests` flow real, then set flag `false` — `checkEligibility`/`issueBnplToken`(`Idempotency-Key`)/`getBnplOrder` already map the live b12 routes 1:1; the settle-on-return reads the order (the real settle is the provider webhook). No hook/component change | 🟡 |
|
||||
| BNPL provider-handoff harness (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the provider's hosted BNPL page so the initiate → redirect → return round-trip is exercisable without a provider: the mock `redirectUrl` points here, and its pay/cancel buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «در حال انتقال به ارائهدهنده», dashed border | _none — only reachable via the mock's `redirectUrl`_ | On the real path b12's `redirectUrl` is the provider's **absolute** URL (the wizard does a full `window.location.assign` for `http(s)`), so this page is never linked; delete it when `USE_BNPL_MOCK` retires. The provider's return deep-link into `/bookings/checkout/bnpl/return` is backend/provider config | 🟡 |
|
||||
| BNPL provider-handoff harness (test harness) | `client/src/app/[locale]/(private-routes)/(customer)/bookings/checkout/bnpl/gateway/page.tsx` | **Not a product feature** — a dev stand-in for the provider's hosted BNPL page so the initiate → redirect → return round-trip is exercisable without a provider: the mock `redirectUrl` points here, and its pay/cancel buttons drive both branches of the return surface (`?outcome=success\|failure`). Clearly labelled «در حال انتقال به ارائهدهنده», dashed border. **ui-phase-6:** was reachable by direct URL in a production build (the equivalent card-gateway harness was already deleted, this one never got the same guard since BNPL stays mock-primary) — now `notFound()`-gated outside `NODE_ENV=development`, still fully reachable in `next dev` | _none — only reachable via the mock's `redirectUrl`_ | On the real path b12's `redirectUrl` is the provider's **absolute** URL (the wizard does a full `window.location.assign` for `http(s)`), so this page is never linked; delete it when `USE_BNPL_MOCK` retires. The provider's return deep-link into `/bookings/checkout/bnpl/return` is backend/provider config | 🟡 |
|
||||
| `PayoutsApi` | `client/src/services/payouts/apis/mockApi.ts` | **The f12 nurse earnings surface** b13 doesn't serve read-side for a nurse (b13's only nurse route is `GET nurse_payouts/history`; the four-bucket **earnings summary**, the per-booking **earnings list + money-state**, and a **nurse-readable payout detail** with batch context + booking links are gaps → **REQ-025**). Self-contained, money-correct fixtures exercising **every** UI state: all four earnings states (`pending`/`eligible`/`paid`/`clawback_applied`; booking ids 5001–5004 align with the f8 bookings-store seeds so "view booking" deep-links land), all four `PayoutStatus` values in history (`pending`/`submitted`/`paid`/`failed`, incl. a `failed` payout with `failureReason: 'invalid_sheba'` for the read-only failure banner), payout **details that reconcile** (`gross − clawback = net = amount`, Σ booking-link amounts = `grossEarnings`), and a **signed net balance** computed with BigInt via a `MOCK_SCENARIO` toggle (`standard` = positive; **`clawback_heavy` = negative "owed back"** for phase §7 step 3). Timestamps are relative to `now` so the pending dispute-window countdown always ticks; money stays IRR digit-strings end-to-end (components only format). `getNurseEarnings` filters by `state` + paginates | `USE_PAYOUTS_MOCK` (`services/payouts/constants.ts`, default `true`) + `MOCK_SCENARIO` in `constants.ts` | Deliver **REQ-025** (earnings_balance + earnings list + nurse `nurse_payouts/{id}` detail + `failureReason` on the history DTO), then set flag `false` — `payoutsClientApi` already maps the live `GET nurse_payouts/history` 1:1 and targets the proposed slugs for the other three. No hook/component change | 🟡 |
|
||||
| `ReviewsApi` | `client/src/services/reviews/apis/mockApi.ts` | **The f13 moderated-review trust loop.** b14 serves the review **submit** (`POST bookings/{id}/review`), the public **nurse reviews** page (`GET nurses/{id}/reviews`), and the tag rollup — those are mapped 1:1 in `reviewsClientApi`. But there is **no review-eligibility read** and **no my-review-for-booking read** (**REQ-026**), and the whole moderation transition (`pending_moderation → published`) is **admin-only (f15)**. The mock reads a booking from the shared **f8 bookings store** (`mockGetBookingForReview`) to gate eligibility on a **completed/closed** booking (aligns with the new completed seed 5005 / nurse 1 / patient 905), tracks the customer's submission as `pending_moderation` so eligibility flips `already_reviewed` + `getMyReviewForBooking` returns the persistent "under review" state, and seeds a **published list per nurse** (nurse 1 has 7 → the profile tab paginates; nurses 5/6 empty → empty state). The aggregate is **recomputed from the published list** (never a stored sum). A submitted review **never** enters any public list. Dev-only `__mockPublishSubmittedReview(bookingId)` stands in for the deferred (f15) admin queue so a human can watch a review appear on the profile. Money-free | `USE_REVIEWS_MOCK` (`services/reviews/constants.ts`, default `true`) | Deliver **REQ-026** (`review_eligibility` + `my_review` reads; confirm masked-author omission), then set flag `false` — `reviewsClientApi.getNurseReviews`/`createReview` already map the live b14 routes 1:1 and target the two proposed slugs for the gaps. Moderation UI itself is **f15** (admin). No hook/component change | 🟢 (real, refinement-phase-4) |
|
||||
| `PatientRecordsApi` | `client/src/services/patientRecords/apis/mockApi.ts` | **The f13 continuity-of-care surface.** Two very different things: (1) the **nurse-authored visit-note history** (`getPatientHistory`/`createVisitNote`) is **REAL b14** (`GET`/`POST patients/{id}/care_records`), mapped 1:1 in `patientRecordsClientApi` (the append composes the ticked task checklist into the note `body` since the wire has no structured task field); (2) the **family-owned editable record** (medications/routine/tasks — the داروها/روتین/وظایف tabs) and the **access check** have **NO backend at all** (neither the b14 contract nor `data-model/10-reviews-and-records.md` model them → **REQ-027**). The mock is **patient-scoped** and lazily seeds a coherent default per patient: a default family record (customer edits it), a **multi-nurse continuity history** (two prior notes from *different* nurses, proving the history persists across nurse changes; a nurse append prepends to the same patient's history), and a **foreign-patient access-denied** path (`MOCK_FOREIGN_PATIENT_ID = 8888` → `canView:false` + a `403` on every read) so the non-leaking access-denied card is demoable. Clinical text is fixture data (never logged) | `USE_PATIENT_RECORDS_MOCK` (`services/patientRecords/constants.ts`, default `true`) | Deliver **REQ-027** (family-owned `care_record` GET/PUT + `record_access` + structured `taskResults`), then set flag `false` — the history/append methods already map the real b14 routes; only the family-record/access methods flip. Confirm whether the family-owned record is a real MVP entity | 🟡 |
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# UI Phase 6 — Checkout & Money — Report (2026-07-18)
|
||||
|
||||
## What was built
|
||||
|
||||
**Three new shared components** (`src/components/`, each tested):
|
||||
- **`PaymentStateCard/`** — the one terminal/wait-state card (icon/tone/title/body + an actions slot)
|
||||
replacing the four copy-pasted private `MessageCard` (checkout/page.tsx, bnpl/page.tsx) and `StateCard`
|
||||
(checkout/return/page.tsx, bnpl/return/page.tsx) functions. All eight call sites now render through it;
|
||||
the card and BNPL flows can no longer visually drift.
|
||||
- **`BnplProviderLogo/`** — `providerCode` → a bundled SVG when a real asset exists (none licensed yet,
|
||||
registry left empty on purpose) falling back to a *designed* tinted-monogram roundel, replacing the old
|
||||
two-letter text-glyph stand-in (`DG`/`SP`/…) in D1's `MethodStep`. Asset-tolerant: dropping in a real logo
|
||||
later touches only the internal registry map, no call-site changes.
|
||||
- **`EscrowExplainer/`** — wraps the product-mandated `EscrowNotice` (never edited) with an optional
|
||||
«چطور کار میکند؟» expander: a 3-step visual (پرداخت ← امانت نزد بالینیار ← آزادسازی پس از تایید پایان
|
||||
ویزیت, grounded in `product/payments/escrow-ledger.md`) plus the cancellation/refund implication. Used on
|
||||
checkout and the confirmation receipt.
|
||||
|
||||
**Minimal foundation extension**: `Money` gained a fourth size, `xl` → MUI `h4`, for the checkout/
|
||||
confirmation "prominent total" hero figure that phase 1 didn't need. Test added.
|
||||
|
||||
**C6 checkout** (`bookings/checkout/page.tsx`) — real hierarchy:
|
||||
- Identity moment: `EngagementSummary` now shows the nurse's avatar + a `TrustBadge` (verified/unverified
|
||||
off the new `nurseVerified` field) next to the service/patient/time details.
|
||||
- A prominent `<Money size="xl">` total sits above the countdown/breakdown — the same served `totalIrr`
|
||||
`PriceBreakdown` reconciles below, never recomputed.
|
||||
- A safe-area-aware sticky pay bar (`StickyActionBar`, reusing phase-4's shell-composed safe-area padding —
|
||||
`layout/` untouched): total + pay CTA co-located, a lock-icon «پرداخت امن از طریق درگاه بانکی» trust line,
|
||||
and the BNPL branch button — **both** CTAs now `disabled={busy}` during initiate (the race the audit
|
||||
flagged: only the card button used to disable).
|
||||
- An explicit «بازگشت به درخواست» text link above the identity card (beyond the shell's header chrome).
|
||||
- Un-baked the arrow: `cta_pay` no longer bakes «←»/«→» into the translated string; the button now uses
|
||||
`endIcon="forward"` (already a registered directional icon — mirrors under RTL automatically).
|
||||
|
||||
**Confirmation rebuilt as a receipt** (`bookings/checkout/confirmation/page.tsx`):
|
||||
- A copyable, `dir="ltr"` کد پیگیری row with a copy button (clipboard + a "copied" toast) — sourced from
|
||||
`usePaymentOutcome`'s new `trackingCode` (card path) or the settled `BnplOrderStatus.id` (BNPL path, no
|
||||
new field needed).
|
||||
- Shamsi paid-at (`formatShamsiDateTime`), payment method («کارت بانکی» / «اقساطی — {provider}»), and the
|
||||
booking reference — every row conditionally rendered so an unserved real-path field just disappears,
|
||||
never a fabricated value.
|
||||
- `EscrowExplainer` (was a bare `EscrowNotice`) and a "what happens next" 2-step `StatusTimeline`
|
||||
(اطلاعرسانی به پرستار ← ویزیت و ثبت ورود).
|
||||
- **Real loading/error states**: the old `{summary ? (...) : null}` (a failed fetch silently erased the
|
||||
paid amount) is now a skeleton → `ErrorState` (with retry) → receipt sequence.
|
||||
|
||||
**checkout/return staged wait state** — the pending-callback branch's spinner+`PaymentStatusBadge`+title
|
||||
triple is replaced with a 2-node `StatusTimeline` («بازگشت از درگاه ✓» → «در انتظار تایید بانک», reusing the
|
||||
timeline's existing `current`-state animated pulse for the "calm animated indicator" ask) plus an
|
||||
expected-duration caption. The manual «بررسی دوباره» escape hatch and the bounded backoff poll are
|
||||
untouched. The failed/window-expired/invalid-id branches now render through `PaymentStateCard` and no
|
||||
longer double an icon with a redundant status chip (the failed state's `PaymentStatusBadge` was dropped —
|
||||
audit's own complaint about the doubled signal).
|
||||
|
||||
**Wallet rebuilt as the money hub** (`wallet/`) — `page.tsx` → new `WalletScreen.tsx` (MUI `Tabs`, one
|
||||
shared `CONTENT_MAX_WIDTH`, no local width override — the old `maxWidth: 560` is gone):
|
||||
- **«پرداختها»** (`WalletPaymentHistory.tsx`) — every card + BNPL payment, newest first, `PaymentStatusBadge`
|
||||
+ deep-link to the booking. Card rows come from the new `usePaymentHistory()` (REQ-047); BNPL rows are
|
||||
derived from each settled wallet plan's own down-payment leg. A co-located `useWalletHistoryRows.ts` hook
|
||||
merges the two independent seams once, reused by the receipts tab too. Degrades gracefully: either source
|
||||
failing alone still renders the other's rows.
|
||||
- **«اقساط»** — the unchanged f11 D5 `WalletInstallments` body (own heading/width stripped; it's a section
|
||||
now, not a page).
|
||||
- **«استردادها»** (`WalletRefunds.tsx`) — every refund via the new `useMyRefunds()` (REQ-048), each rendered
|
||||
through the existing `RefundStatusCard` with a link to `bookingRefundStatusPath`.
|
||||
- **«رسیدها»** (`WalletReceipts.tsx`) — invoice deep-links derived client-side from the same merged history
|
||||
rows (succeeded + a known `bookingId`) — no endpoint, no money math, just a filter + `bookingInvoicePath`.
|
||||
- A card-paying customer (the default path) now sees payment history instead of a permanently-empty
|
||||
installments-only tab — the audit's top-severity finding.
|
||||
|
||||
**BNPL honesty + polish** (`checkout/bnpl/`):
|
||||
- `BnplPlanCard` no longer shows a percent + `LinearProgress` bar for the down payment (a static fact
|
||||
styled as a loading indicator). It now shows three plain Toman rows: پیشپرداخت (امروز), قسط ماهانه, and
|
||||
**مجموع بازپرداخت** with the fee delta spelled out («+۴۵۰٬۰۰۰ تومان کارمزد» in `--bal-money-emphasis`) or
|
||||
«بدون سود» for interest-free plans. The delta is `totalIrr − orderAmountIrr` — the exact BigInt difference
|
||||
of two already-served amounts (the same "exact remainder of served amounts" pattern the invoice page uses
|
||||
for its service line), never a computed rate.
|
||||
- `PlanStep`'s «مبلغ کل» header no longer defaults to `plans[0]` before any selection (the silently-morphing
|
||||
number the audit flagged) — it renders only once a plan is selected, **names** the plan
|
||||
(«مبلغ کل با طرح {plan}»), and shows the same fee delta.
|
||||
- `EligibilityStep`: the credit-check button now swaps to a spinner + «در حال استعلام اعتبار…» while
|
||||
pending (mirrors C6's `state_initiating` pattern); the prefilled mobile field is `readOnly` (normal
|
||||
contrast, screen-reader-reachable) instead of `disabled`.
|
||||
- `bnpl/return`'s invalid-link state CTA was promising a card payment it couldn't perform (labelled
|
||||
«پرداخت با کارت», navigated to the bookings list) — relabelled «رزروهای من» (`bd_my_bookings`, C6's own
|
||||
invalid-link pattern) so the label matches the destination; no recoverable request id exists at that
|
||||
point to route to an actual card checkout instead.
|
||||
- `checkout/bnpl/gateway/page.tsx` (the dev provider-handoff harness) is now `notFound()`-gated outside
|
||||
`NODE_ENV=development` — it was reachable by direct URL in a production build. The equivalent card-gateway
|
||||
harness was already deleted in refinement-phase-4; this one stays (BNPL is still mock-primary) but is no
|
||||
longer reachable in prod. The dead `ROUTES.CHECKOUT_GATEWAY` constant (pointed at that already-deleted card
|
||||
harness page) is removed, and the one place still referencing it — the payment mock's
|
||||
`initiatePayment` — now returns `redirectUrl: null` (a latent bug: it was building a URL to a page that no
|
||||
longer exists; the checkout page's `!redirectUrl` branch already reads the outcome directly, so behavior
|
||||
is unaffected).
|
||||
|
||||
**Invoice: fiscal-grade** (`bookings/[id]/invoice/page.tsx`):
|
||||
- Buyer name and a service+visit-date recap are composed **client-side** (a UI join, not money math) from
|
||||
`useCustomerProfile()` and `useBookingDetail(bookingId, 'customer')` — no new fields needed.
|
||||
- Payment method, transaction reference, and a seller fiscal-identity block (legal name / economic code /
|
||||
address) render when `InvoiceDto` serves them (new `paymentMethod`/`transactionReference`/
|
||||
`sellerFiscalIdentity` fields, REQ-049) — `null` on the real path hides the row rather than fake it.
|
||||
- An A4 print pass: a `@page { size: A4; margin: 16mm }` rule alongside the existing print-visibility rule,
|
||||
and a print-only footer (invoice number + issue date + مودیان reference when present) shown only inside
|
||||
`@media print`. The existing print mechanics (visibility-scoped area, `insetInlineStart` anchoring,
|
||||
dark→light token flip) are untouched.
|
||||
- Deleted the stale comment claiming fa `common.brand` reads «بلینیار» — it actually reads «بالین یار»
|
||||
(plain space); the brand-spelling unification itself stays phase 12's, per this phase's scope note.
|
||||
|
||||
**Money-display sweep**: `PriceBreakdown` rows now render through `<Money>` (every row carries «تومان», not
|
||||
just the total — the exact Toman/Rial ambiguity the audit flagged); `InstallmentScheduleRow`'s `hideUnit`
|
||||
was dropped so every installment amount carries the unit too.
|
||||
|
||||
## What is now testable (and exactly how)
|
||||
|
||||
1. Login as a seeded customer with an accepted request → `/fa/bookings/checkout?request_id=…` at 375px: the
|
||||
nurse's avatar + verified badge sit above a large total figure; the sticky bar (total + «پرداخت») pins
|
||||
above the bottom nav. Tap «پرداخت» → both CTAs (card + BNPL) disable, label swaps to «در حال شروع…».
|
||||
2. Complete the mock capture round-trip → `checkout/return` shows «بازگشت از درگاه ✓» → «در انتظار تایید
|
||||
بانک» with an animated current-node pulse and a duration hint — no bare spinner+chip stack.
|
||||
3. Land on the confirmation: کد پیگیری renders LTR with a working copy-to-clipboard (toast confirms);
|
||||
Shamsi paid-at, method, booking reference, and the escrow line are present; tap «چطور کار میکند؟» → the
|
||||
3-step explainer expands. Block the network and reload → skeleton then a retryable error, never a
|
||||
silently-missing amount.
|
||||
4. Open «کیفپول» → four tabs. «پرداختها» lists the just-made payment linking to the booking; «اقساط» is
|
||||
the unchanged installment tracker; «استردادها» shows a `RefundStatusCard` after cancelling a paid
|
||||
booking; «رسیدها» links to the invoice. All four read at the same content width as checkout/invoice.
|
||||
5. Back on checkout, tap «پرداخت اقساطی» → provider rows show a tinted monogram (no two-letter glyph); plan
|
||||
cards show پیشپرداخت/قسط ماهانه/مجموع بازپرداخت in Toman with the fee delta on fee plans and no
|
||||
`LinearProgress`; selecting a plan names the header total; the eligibility check shows a spinner + label
|
||||
change while pending; an invalid BNPL return link offers «رزروهای من», not a dead "pay with card" promise.
|
||||
6. `/fa/bookings/checkout/bnpl/gateway` in a production build (`npm run build && npm start`) → 404; `npm run
|
||||
dev` → still reachable. Open a paid booking's invoice → buyer/service/visit-date/reference rows present
|
||||
(mock); print preview shows an A4 page with a footer; from dark mode, the print dialog shows paper colors.
|
||||
7. `npm run check` is green (`tsc` + `eslint`, zero errors) and `npm run test:ci` is green — **104/104 test
|
||||
suites, 447/447 tests** (3 new suites: `PaymentStateCard`, `BnplProviderLogo`, `EscrowExplainer`; existing
|
||||
`Money`/`PriceBreakdown`/`BnplPlanCard` suites extended for the new size/behavior).
|
||||
8. Repeat 1–5 on `/en` (LTR) and in dark mode: no clipped RTL/LTR islands, tokens resolve in both schemes,
|
||||
«تومان» (fa) / "Toman" (en) on every amount including breakdown rows and installment amounts.
|
||||
|
||||
## What is mocked / waiting on a real service
|
||||
|
||||
No new seams — everything stays behind the existing `PaymentApi`/`BnplApi`/`RefundsApi` seams
|
||||
(`USE_PAYMENT_MOCK=false`/`USE_BNPL_MOCK=true`/`USE_REFUNDS_MOCK=true`, all unchanged). Extended, not
|
||||
replaced:
|
||||
- `PaymentApi.getCheckoutSummary`/`getPaymentOutcome` gained mock-only `nurseAvatarUrl`/`nurseVerified`/
|
||||
`trackingCode`/`paidAt`; a new `PaymentApi.getPaymentHistory` reads the mock's transaction list.
|
||||
- `PaymentApi.getInvoice`'s mock-issued invoices gained `paymentMethod`/`transactionReference`/
|
||||
`sellerFiscalIdentity`.
|
||||
- `RefundsApi` gained `getMyRefunds`, reading the same in-memory refund store `getRefundByBooking` uses.
|
||||
- The BNPL provider-handoff harness page is now env-gated (see above) — not a seam change.
|
||||
|
||||
See `mocks-registry.md`'s updated `PaymentApi`/`RefundsApi`/BNPL-harness rows for the exact deltas and the
|
||||
"make it real" steps (REQ-046/047/048/049).
|
||||
|
||||
## Contracts
|
||||
|
||||
- Consumed: `services/payment` (b10/b11), `services/bnpl` (b12), `services/refunds` (b11) — all unchanged
|
||||
contract-wise; the mock/real seam split is untouched.
|
||||
- Requested (`dev/shared-working-context/frontend/requests/for-backend.md`, next free numbers 046–049):
|
||||
- **REQ-046** — `nurseAvatarUrl`/`nurseVerified` on `CheckoutSummaryDto` (the C6 identity moment) +
|
||||
`trackingCode`/`paidAt` on `PaymentOutcomeDto` (the confirmation receipt). Extends REQ-016/017.
|
||||
- **REQ-047** — a customer payment-transactions list (`GET bookings/payment_history` proposed) for the
|
||||
wallet «پرداختها» tab.
|
||||
- **REQ-048** — a customer "all my refunds" list (`GET refunds/my` proposed) for the wallet «استردادها»
|
||||
tab — confirmed b11 truly has only by-booking/by-id customer reads. Extends REQ-021.
|
||||
- **REQ-049** — `paymentMethod`/`transactionReference`/`sellerFiscalIdentity` on `InvoiceDto` for the
|
||||
fiscal-grade invoice. Extends REQ-018.
|
||||
|
||||
## Docs updated
|
||||
|
||||
- `client/CLAUDE.md` "Project Structure": the checkout/return/confirmation/bnpl subtree (identity moment,
|
||||
sticky bar, staged wait state, receipt fields, provider-logo/eligibility/plan-naming polish, the gateway
|
||||
harness env-gate), the invoice line (fiscal fields + A4 print), the wallet line (4-tab hub replacing the
|
||||
installments-only shell), the `Money`/`PriceBreakdown`/`EscrowNotice`/`InstallmentScheduleRow`/
|
||||
`BnplPlanCard` component lines, and three new component entries (`PaymentStateCard`, `BnplProviderLogo`,
|
||||
`EscrowExplainer`). The `services/payment`/`services/refunds`/`services/bnpl` domain-summary lines note
|
||||
the new hooks/DTO fields.
|
||||
|
||||
## Follow-ups for later phases
|
||||
|
||||
- **Buyer name on the invoice is a live client-side join** (`useCustomerProfile()`), not a snapshot on
|
||||
`InvoiceDto` — correct for MVP but not strictly immutable (a later profile-name change would reflect on
|
||||
an old invoice). If invoice immutability becomes a real concern, a future REQ should ask for a
|
||||
server-snapshotted `buyerName` at issue time.
|
||||
- **`variantName()`'s best-effort `variantSnapshotJson` parse is now duplicated a third time**
|
||||
(`BookingDetailView.tsx`, the review page, and this phase's invoice page) — REQ-045 (typed
|
||||
`variantSnapshot`) would let all three collapse to one read; not extracted to a shared helper this phase
|
||||
to avoid touching files outside this phase's lane speculatively.
|
||||
- **Wallet «پرداختها»/«رسیدها» merge two independent seams client-side** (card `PaymentApi` history + BNPL
|
||||
`WalletInstallmentPlan` down-payment rows) rather than one unified "customer transactions" read — kept
|
||||
the domains separate per existing architecture; if a future phase adds a true unified ledger read, the
|
||||
merge in `useWalletHistoryRows.ts` can retire.
|
||||
- The BNPL card's fee-delta math (`totalIrr − orderAmountIrr`) needs the order amount threaded down through
|
||||
`PlanStep`/`BnplPlanCard` — fine today (both wizard steps already have it in scope) but worth folding into
|
||||
a single served `feeIrr` field per plan if REQ-022 (options/schedule) ever lands, so the client stops
|
||||
doing even this exact-remainder subtraction.
|
||||
- Real gateway/Shaparak logos near the pay CTA are still DEFERRED (no licensed assets) — `secure_gateway_notice`
|
||||
(lock icon + text) is the interim trust signal, per the phase's own scope note.
|
||||
|
||||
## Memory
|
||||
|
||||
Saved a `project`-type memory (`ui_phase_6_checkout_and_money.md`, indexed in `MEMORY.md`) covering: the
|
||||
`PaymentStateCard`/`BnplProviderLogo`/`EscrowExplainer` extraction decisions, the provider-logo (monogram
|
||||
roundel, no brand colors) and money-width (one `CONTENT_MAX_WIDTH`) decisions, the fee-delta-as-exact-
|
||||
remainder pattern, the wallet history/receipts merge-two-seams-client-side decision, the sticky-bar-vs-
|
||||
bottom-nav layering (reuses `StickyActionBar`'s existing safe-area composition, never re-implemented), the
|
||||
print-stylesheet `@page` addition, and REQ-046..049 for whichever backend phase next touches b10/b11/b12.
|
||||
Reference in New Issue
Block a user