frontend phase 9
This commit is contained in:
@@ -12,6 +12,41 @@ for awareness.
|
||||
- **Requests filed:** frontend/requests/for-backend.md (yes/no)
|
||||
-->
|
||||
|
||||
## frontend-phase-9-b10 — Checkout, card payment & invoice — 2026-07-10
|
||||
- **Shipped:** the money moment — a **new `services/payment` domain** (types/keys/constants/
|
||||
apis[client+mock]/invalidations/5 hooks + barrel) and the customer checkout flow: **C6 خلاصه و پرداخت**
|
||||
`/bookings/checkout?request_id=` (acceptance badge, served **reconciling** service-cost/کارمزد/مالیات/مبلغ کل
|
||||
breakdown, verbatim **escrow notice**, payment-window countdown, «ادامه پرداخت ←» with an
|
||||
**idempotency-key-per-attempt**, disabled BNPL seam for f11), the **card states** (initiating → redirect →
|
||||
dev **mock-gateway harness** `/bookings/checkout/gateway` → return `/bookings/checkout/return` with a
|
||||
**backoff pending-callback poll** → succeeded/failed/expired), the **confirmation**
|
||||
`/bookings/checkout/confirmation` («مشاهده رزرو» + «دانلود فاکتور»), and the **invoice**
|
||||
`/bookings/[id]/invoice` (VAT-on-commission line, read-only مودیان state, pdfUrl download or print
|
||||
receipt). Three shared tested composites: `PriceBreakdown`, `EscrowNotice`, `PaymentStatusBadge`. New
|
||||
`payment` i18n namespace (53 keys, both locales).
|
||||
- **Load-bearing rules honored:** money = IRR digit-strings, **BigInt only** (integer parts-per-10000 rate
|
||||
math in the mock — zero floats); the breakdown **must reconcile** (`PriceBreakdown` dev-guards it; rows
|
||||
are served, VAT never derived client-side); **VAT on the commission only** (invoice line labelled
|
||||
accordingly); escrow copy **verbatim** in fa (pinned by test against fa.json), info tone never error;
|
||||
**409 = benign convergence** (re-reads the outcome, never an error toast); poll uses **geometric backoff,
|
||||
stops on terminal + bounded attempts**; success flips the booking **by cache invalidation** (exact keys,
|
||||
no refetch storm).
|
||||
- **Consumes:** dev/contracts/domains/payments.md (b10 — initiate route + `Idempotency-Key` header +
|
||||
`InitiatePaymentResult`; status enum `pending|succeeded|failed`) and the invoice slice of
|
||||
refunds-invoices.md (b11 — `GET invoices/{bookingId}`, `InvoiceDto`), casing verified against
|
||||
swagger.v1.json. **Not served by any contract:** a checkout summary, a client transaction read, and the
|
||||
converted request's booking id → REQ-016/017/018.
|
||||
- **Mocked client-side:** `services/payment` via `paymentMockApi` (**USE_PAYMENT_MOCK=true, primary**) —
|
||||
it is the missing **conversion trigger bridging the f7 ↔ f8 mock stores**: capture converts the request
|
||||
(`converted` + client-augmented `bookingId`), inserts a **confirmed** booking into the f8 store, and
|
||||
auto-issues the b11-shaped invoice, so C5 → C6 → gateway → confirmation → booking detail → invoice runs
|
||||
end-to-end in one session. The dev **mock-gateway page is a test harness, not a product feature**. Real
|
||||
`paymentClientApi`: initiate/invoice = published contract; summary targets the REQ-016 proposed slug;
|
||||
outcome maps `booking_requests/get` (REQ-017).
|
||||
- **Gate:** npm run check green · npm run test:ci green (204 tests, +9) · production build green.
|
||||
- **Requests filed:** frontend/requests/for-backend.md — yes (REQ-016 checkout summary, REQ-017 payment
|
||||
outcome + bookingId, REQ-018 customer invoice availability post-capture).
|
||||
|
||||
## frontend-phase-8-b9 — Booking detail, sessions & nurse EVV — 2026-07-10
|
||||
- **Shipped:** the post-payment engagement — a **new** `services/bookings` domain (the sibling of
|
||||
`bookingRequests`, NOT a rename): types/keys/constants/apis[client(1:1 b9)+mock+serverApi]/8 hooks +
|
||||
|
||||
@@ -214,3 +214,52 @@ delivers fixes in its own change. **Frontend never edits backend code to "fix" a
|
||||
a casing/int drift or a `false`-vs-`null` conflation would mislabel a visit. Low-risk (mock-primary now),
|
||||
but worth locking before f9/f13 consume the same shapes.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-016 — Checkout summary for C6 (served gross/commission/VAT breakdown) — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** A customer-facing read that serves the C6 «خلاصه و پرداخت» money rows for an
|
||||
`accepted_awaiting_payment` request: the three b10 amounts (`grossPriceIrr`,
|
||||
`balinyaarCommissionIrr`, `nursePayoutAmount`) **plus the display decomposition** — service cost,
|
||||
commission **net of VAT**, `vatIrr` + `vatRate` — and the nurse/variant/schedule mini-info +
|
||||
`paymentDeadlineAt`.
|
||||
- **Why:** The b8 `BookingRequestDto` is money-free by design and no checkout-summary endpoint exists,
|
||||
but C6 must show a breakdown that **reconciles to the rial** (service + commission + VAT = total) and
|
||||
the client is forbidden from deriving commission or tax itself (no float math, rates are server
|
||||
config). Today the whole C6 money surface is mocked (`services/payment` mock computes the split from
|
||||
the mock variant price at the configured 12% fee / 10% VAT).
|
||||
- **Proposed shape:** `GET api/v1/booking_requests/checkout_summary/{id}` (owner-scoped) →
|
||||
`{ bookingRequestId, requestStatus, nurseName, patientName, variantLabel, variantPriceUnit,
|
||||
sessionCount, requestedDate, requestedTimeStart, requestedTimeEnd, paymentDeadlineAt,
|
||||
serviceCostIrr, commissionIrr, vatIrr, vatRate, totalIrr, grossPriceIrr, balinyaarCommissionIrr,
|
||||
nursePayoutAmount }` — the client's real `paymentClientApi.getCheckoutSummary` already targets this
|
||||
slug and unwraps this exact shape (`client/src/services/payment/types.ts: CheckoutSummaryDto`).
|
||||
- **Status:** open
|
||||
|
||||
## REQ-017 — Client-readable payment outcome + `bookingId` on a converted request — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** After the gateway redirect returns, the client needs to learn (a) the payment transaction's
|
||||
status (`pending|succeeded|failed`) and (b) **which booking** the capture created. Either a
|
||||
transaction read (e.g. `GET api/v1/payment_transactions/{id}` or
|
||||
`GET api/v1/bookings/{bookingRequestId}/payments/latest`, owner-scoped) or, minimally, a
|
||||
`bookingId` field on `BookingRequestDto` once `status = converted`.
|
||||
- **Why:** b10 confirms captures inside the PSP webhook (correct — the client is never trusted), so the
|
||||
frontend's pending-callback state can only poll `booking_requests/get/{id}` and map
|
||||
`converted → succeeded` / `payment_deadline_expired → failed`. That works, but it cannot distinguish a
|
||||
*declined* payment (still `accepted_awaiting_payment`, retry allowed) from a *slow* callback, and the
|
||||
confirmation screen cannot deep-link «مشاهده رزرو» or «دانلود فاکتور» because the converted request
|
||||
never reveals its booking id. The client DTO already carries a client-augmented
|
||||
`bookingId: number | null` (mock fills it; real path returns null and the UI falls back to the
|
||||
bookings list, hiding the invoice link).
|
||||
- **Proposed shape:** add `bookingId: long?` to `BookingRequestDto` (null until converted) **and/or**
|
||||
`GET api/v1/bookings/{bookingRequestId}/payments/latest` → `{ transactionId, status,
|
||||
gatewayReferenceCode, bookingId? }`.
|
||||
- **Status:** open
|
||||
|
||||
## REQ-018 — Customer invoice availability after capture (auto-issue or owner-issue) — filed by frontend-phase-9-b10 — 2026-07-10
|
||||
- **Need:** Make the b11 invoice reachable by the paying customer right after capture: auto-issue the
|
||||
commission invoice on card capture (idempotent per booking, as `POST admin_invoices` already is), or
|
||||
allow the owning customer to trigger the idempotent issue on first `GET api/v1/invoices/{bookingId}`.
|
||||
- **Why:** The f9 confirmation screen offers «دانلود فاکتور», but b11 issues invoices only via the
|
||||
admin-only `POST api/v1/admin_invoices`, so a customer's `GET invoices/{bookingId}` 404s until an
|
||||
admin acts. The UI handles the 404 as a "فاکتور هنوز صادر نشده است" state (and the mock auto-issues at
|
||||
capture to demo the full flow), but on the real rails every fresh payment would land on that empty
|
||||
state.
|
||||
- **Status:** open
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
# Frontend phase 9 report — Checkout, card payment & invoice (consumes b10 + the invoice slice of b11)
|
||||
|
||||
**Status:** complete · gate green (`npm run check`, `npm run test:ci` — 204 tests, production build) · 2026-07-10
|
||||
**Scope shipped:** the C6 خلاصه و پرداخت checkout, the card-payment state machine (initiate → redirect →
|
||||
pending-callback → succeeded→confirmed / failed→retry), the confirmation screen, the invoice view, the
|
||||
`services/payment` domain, and three shared money composites.
|
||||
|
||||
---
|
||||
|
||||
## 1. What was built
|
||||
|
||||
### `services/payment` (new domain — mirrors the `auth`/`bookings` shape)
|
||||
|
||||
| File | What it is |
|
||||
| --- | --- |
|
||||
| `types.ts` | Contract-derived DTOs + the `PaymentApi` seam. `PaymentTransactionStatus = pending\|succeeded\|failed` (the b10 enum — the phase file's illustrative `initiated/cancelled` states do **not** exist on the wire and were not used). `CheckoutSummaryDto` (REQ-016 shape), `InitiatePaymentResult` (b10 swagger), `PaymentOutcomeDto` (poll target), `InvoiceDto` (b11 swagger, flat totals — **no line-items array exists in the contract**). |
|
||||
| `keys.ts` | `paymentKeys.summary(requestId)` / `.outcome(requestId)` / `.invoice(bookingId)`. |
|
||||
| `constants.ts` | `USE_PAYMENT_MOCK=true` (why documented in-file), `BNPL_ENABLED=false` (the f11 seam gate), poll backoff tuning, checkout query-param names, mock money rates. |
|
||||
| `apis/clientApi.ts` | Real impl: **initiate** = `POST api/v1/bookings/{id}/payments` + **`Idempotency-Key` header** (no body — contract-exact); **invoice** = `GET api/v1/invoices/{bookingId}`; **summary** targets the REQ-016 proposed slug; **outcome** maps `GET booking_requests/get/{id}` (`converted`→succeeded, `payment_deadline_expired`→failed, else pending — REQ-017). |
|
||||
| `apis/mockApi.ts` | Mock-primary state machine — see §3. |
|
||||
| `invalidations.ts` | `invalidateAfterPaymentSuccess` — the one post-capture cache transition (request detail/lists + bookings lists/detail + this request's summary/outcome). Exact keys, never a blanket refetch. |
|
||||
| `hooks/` | `useCheckoutSummary` (short stale), `useInitiatePayment` (caller owns the per-attempt key), `useConfirmGatewayReturn` (primes the outcome key; invalidates on immediate success), `usePaymentOutcome` (**geometric-backoff poll**: 2s → ×1.5 → cap 15s, stops on terminal outcome or 40 attempts; exports `isTerminalPaymentOutcome`), `useInvoice` (immutable → long stale; **404 = "not issued", not retried**). |
|
||||
|
||||
### Screens (customer shell)
|
||||
|
||||
- **C6** `/bookings/checkout?request_id=` — «✓ پرستار تایید کرد» badge (reuses `booking.accepted_badge`),
|
||||
nurse/service/schedule mini-summary, the payment-window `CountdownTimer`, the **served reconciling
|
||||
breakdown** (هزینه خدمت / کارمزد بالینیار / مالیات بر ارزش افزوده / **مبلغ کل**) via `PriceBreakdown`,
|
||||
the verbatim `EscrowNotice`, «ادامه پرداخت ←» (terracotta `secondary`), and the **BNPL seam** — a
|
||||
disabled outlined «یا پرداخت اقساطی» + "coming soon" caption gated by `BNPL_ENABLED` for f11 to wire to D1.
|
||||
Non-payable statuses render convergence cards (already-paid → outcome; window-expired / other terminal → back to C5).
|
||||
- **Gateway harness** `/bookings/checkout/gateway` — **test-only** fake PSP (labelled آزمایشی): success +
|
||||
failure buttons so both return branches are drivable without a gateway. The phase file suggested
|
||||
auto-success; buttons were chosen instead so §7 step 5 (failed attempt → new idempotency key) is testable by a human.
|
||||
- **Return** `/bookings/checkout/return` — fires `useConfirmGatewayReturn` once per mount (ref-guarded; a
|
||||
refresh replays it and converges idempotently), then the pending-callback poll. Succeeded → invalidate +
|
||||
`router.replace` to confirmation (once, ref-guarded, no double invalidation between mutation and poll
|
||||
paths); failed → retry (back to a fresh C6 mount = **new attempt, new key**); window lapsed → back to C5.
|
||||
A manual «بررسی دوباره» covers the bounded poll giving up.
|
||||
- **Confirmation** `/bookings/checkout/confirmation` — success state, amount-paid card, «مشاهده رزرو» →
|
||||
`/bookings/{bookingId}` (falls back to the list without a bookingId — REQ-017), «دانلود فاکتور» →
|
||||
`/bookings/{bookingId}/invoice` (hidden without a bookingId).
|
||||
- **Invoice** `/bookings/[id]/invoice` — `invoiceNumber` + Shamsi issue date, `PriceBreakdown` rows where
|
||||
the **service line is the exact integer remainder** (gross − commission − VAT, via `parseIrr` BigInt) so
|
||||
the lines reconcile by construction, the VAT line labelled **«مالیات بر ارزش افزوده (بر کارمزد
|
||||
بالینیار)»** (product rule: the nurse is never implied to be taxed), read-only **مودیان** state chip
|
||||
(`moadian_*`), and `pdfUrl` download **or** a print receipt (`window.print()` + a print-scoped
|
||||
visibility rule isolating the invoice card). 404 renders «فاکتور هنوز صادر نشده است» (REQ-018).
|
||||
|
||||
### Shared composites (each with a co-located test)
|
||||
|
||||
- **`PriceBreakdown`** — typed rows + total, all IRR digit-strings through `formatIrrToToman`; dev-guard
|
||||
`console.error`s if rows ≠ total (test proves rows render, total = Σ rows, and the guard fires on mismatch).
|
||||
- **`EscrowNotice`** — wraps `AppAlert` (info severity, `--bal-primary` text on `--bal-primary-soft`, lock icon). Its test mocks next-intl to
|
||||
read **the real `fa.json`**, pinning the mandated copy verbatim: a rewording fails the suite.
|
||||
- **`PaymentStatusBadge`** — full `PaymentTransactionStatus` → `StatusChip` kind map (`Record` typed, so an
|
||||
enum change breaks the build); labels from `payment.pstatus_*`.
|
||||
|
||||
### Extensions to prior phases (in place, per operating rules)
|
||||
|
||||
- `services/bookingRequests`: client-augmented **`bookingId: number | null`** on `BookingRequestDto`
|
||||
(REQ-017 twin of REQ-013's `variantPrice`; real client maps it to `null`), and the mock-only
|
||||
`mockMarkBookingRequestConverted(id, bookingId)` capture bridge.
|
||||
- `services/bookings/apis/mockApi.ts`: mock-only `mockInsertConvertedBooking(seed)` — inserts a confirmed
|
||||
single-session booking (ids 6001+/80001+, distinct from the 5001/5002 seeds).
|
||||
- C5 (`bookings/request/[id]`): the `converted` terminal card now deep-links the booking when
|
||||
`bookingId` is present (list fallback otherwise).
|
||||
- `constants/routes.ts`: `CHECKOUT_GATEWAY/RETURN/CONFIRMATION` + `bookingInvoicePath()`.
|
||||
- i18n: new **`payment`** namespace — 53 keys, both locales, inserted textually (no reformat of existing lines).
|
||||
|
||||
## 2. Contract deltas the implementation honors (vs the phase file's illustrative design)
|
||||
|
||||
The phase file sketched `getCheckoutSummary`/`verifyPayment`/`getTransaction` endpoints and an
|
||||
`initiated…cancelled` status enum. The **published contract wins**:
|
||||
|
||||
1. **Status enum is `pending|succeeded|failed`** (b10 `payment_transactions.status`). Client unions match.
|
||||
2. **There is no client verify/transaction endpoint.** The server re-verifies inside the webhook handler;
|
||||
"verify on return" is therefore modelled as `confirmGatewayReturn` (real impl = an outcome *read* — the
|
||||
PSP already hit the webhook before redirecting) + the outcome poll. Filed as REQ-017.
|
||||
3. **There is no checkout-summary endpoint** and the b8 request DTO is money-free — the C6 breakdown
|
||||
cannot be served today. Filed as REQ-016; mocked behind the seam; the real client targets the proposed slug.
|
||||
4. **The invoice is flat totals** (`grossIrr`/`platformCommissionIrr`/`vatRate`/`vatIrr`…, no line-items
|
||||
array) and only exists after the **admin-only** issue action. Filed as REQ-018; the UI has a not-issued state.
|
||||
5. **Idempotency is a header** (`Idempotency-Key`), not a body field — the contract's exact casing.
|
||||
|
||||
## 3. Mocks in this phase (recorded in mocks-registry.md)
|
||||
|
||||
- **`paymentMockApi` (mock-primary)** — the missing **conversion trigger between the f7 and f8 mock
|
||||
stores** (their `converted`/seeded-booking states were previously unconnected): capture flips the f7
|
||||
request to `converted` + stamps `bookingId`, inserts a **confirmed** f8 booking, and auto-issues the
|
||||
b11-shaped invoice. Money split uses integer parts-per-10000 BigInt math (12% fee, 10% VAT,
|
||||
`vat = commission_net × rate` per b11) so `service + commission + vat = total` **exactly** and
|
||||
`gross = balinyaarCommission + payout` holds. Idempotency mirrors b10: same-key retry reuses the
|
||||
attempt, post-capture initiate → `409`, replayed returns converge.
|
||||
- **The mock-gateway page** — test harness only (see registry row for the deletion story).
|
||||
- **Why mock-primary:** upstream ids are mock-primary (f7), REQ-016/017 are unserved, and nothing fires
|
||||
the PSP webhook in dev (b10's "webhook simulator" is a manual server-side POST — after a real initiate,
|
||||
nothing would ever confirm). Swap = deliver REQ-016/017/018 + real upstreams, then `USE_PAYMENT_MOCK=false`.
|
||||
|
||||
## 4. What is now testable, exactly (mock path — `npm run dev`)
|
||||
|
||||
Prereq: an `accepted_awaiting_payment` request — either seed-driven (open `/fa/nurse/requests`, accept a
|
||||
seeded pending request) or full-flow (C4 create → nurse accept). Same browser tab throughout (module-singleton mocks).
|
||||
|
||||
1. **C6:** from C5's «ادامه پرداخت» (or `/fa/bookings/checkout?request_id={id}`) — badge, mini-summary,
|
||||
30-min countdown, breakdown that sums to the rial (2,800,000×1 IRR gross → service 2,436,000 +
|
||||
commission 336,000 − VAT split), escrow notice in info tone. `/en` flips `dir`, translates, still Toman.
|
||||
2. **Pay:** «ادامه پرداخت ←» → spinner → the آزمایشی gateway → «پرداخت موفق» → return surface briefly shows
|
||||
«در حال تایید پرداخت…» → confirmation screen.
|
||||
3. **Booking flips:** «مشاهده رزرو» lands on `/bookings/{id}` showing **confirmed** (React Query Devtools:
|
||||
only request-detail/lists, bookings lists/detail, and this request's payment keys invalidated). C5 now
|
||||
shows the converted card deep-linking the same booking; the bookings list has the new row.
|
||||
4. **Invoice:** «دانلود فاکتور» → number `INV-…`, Shamsi date, service/commission/VAT-on-commission/total
|
||||
reconciling to C6, مودیان «در انتظار ثبت», print button (mock serves no `pdfUrl` so the print path runs).
|
||||
5. **Idempotency/retry:** double-tap pay (one attempt, same key — mock returns the same transaction);
|
||||
re-initiate after success → `409` → converges to confirmation, no error toast. Gateway «شبیهسازی
|
||||
پرداخت ناموفق» → failed card → «تلاش دوباره» → fresh C6 mount issues a **new** key (observable in the
|
||||
mock's `gatewayReferenceCode` suffix).
|
||||
6. **Window expiry:** wait out the 30-min window (or re-enter later) → C6/return show «مهلت پرداخت به
|
||||
پایان رسید» and C5 shows its terminal card; initiate after expiry → `409` handled as state, not error.
|
||||
7. **Invoice not-issued state:** `/fa/bookings/5001/invoice` (a seeded booking that never went through
|
||||
checkout) → «فاکتور هنوز صادر نشده است».
|
||||
|
||||
## 5. Follow-ups for the next phases
|
||||
|
||||
- **f10 (refunds/cancellation):** reuse `PriceBreakdown` (fee disclosure), `EscrowNotice` (identical trust
|
||||
copy), `PaymentStatusBadge`; `RefundStatusDto`/`refunds/{id}/status` is live in b11 and unconsumed;
|
||||
`refundableAmountIrr`/`cancellationRefundPercentage` already ride on `BookingDetailDto`.
|
||||
- **f11 (BNPL):** flip `BNPL_ENABLED` and wire the C6 secondary to D1. The b12 contract
|
||||
(`checkout_bnpl/eligibility|initiate|{id}` + `Idempotency-Key` header) parallels this domain's shapes;
|
||||
`InvoiceDto.bnplCommissionIrr` is already typed. The gateway-harness pattern extends to the BNPL redirect.
|
||||
- **Backend:** REQ-016 (checkout summary), REQ-017 (outcome/bookingId — until then the real poll can't
|
||||
distinguish *declined* from *slow*, and the confirmation can't deep-link), REQ-018 (invoice reachable
|
||||
post-capture). Also note: the PSP's return-URL config must deep-link `/{locale}/bookings/checkout/return`.
|
||||
|
||||
## 6. Post-review hardening (multi-agent adversarial review before close)
|
||||
|
||||
A 26-agent review/verify pass over the diff confirmed and fixed, pre-merge:
|
||||
- **Stale-outcome guard (major):** the return surface now trusts the outcome cache only after *this*
|
||||
mount's return report settles — a previous attempt's cached `failed` outcome can no longer flash a
|
||||
false «پرداخت ناموفق» (with a live retry) while the current attempt's capture is in flight.
|
||||
- **No dead-end retries:** malformed `request_id`/booking-id links render a navigation card instead of a
|
||||
`refetch()` that bypasses `enabled` and would request `checkout_summary/undefined`.
|
||||
- **Unpriced request fails loudly:** the mock throws `409 unpriced_request` instead of silently serving a
|
||||
reconciling 0-rial checkout when `variantPrice` is null (REQ-013 edge).
|
||||
- **i18n/UX:** inline initiate errors always use the localized copy (never raw `ApiError.message`); en
|
||||
`cta_pay` arrow points → (fa keeps ←); fa `error_body` matches the app's «بارگذاری … ممکن نشد» pattern;
|
||||
the C6 service-cost row carries the quantity (`row_service_cost_with_count`); the invoice issuer line
|
||||
uses the product spelling «بالینیار» (note: fa `common.brand` reads «بلینیار» — a pre-existing
|
||||
wordmark/product-spelling divergence worth a product decision).
|
||||
- **Dark scheme:** EscrowNotice text/border use `--bal-primary` (the info token is an alert *background*
|
||||
and is illegible as dark-mode text); the print button temporarily flips `data-mui-color-scheme` to
|
||||
light around `window.print()` (restored on `afterprint`) so a dark-mode user prints paper colors.
|
||||
- **Contract hygiene:** the domain barrel is hooks-only again (`isTerminalPaymentOutcome` moved to
|
||||
`types.ts`, mirroring `isTerminalBookingRequestStatus`); the gateway harness scopes `dir="ltr"` to the
|
||||
reference code, not the Persian label; the invoice VAT percent formats fractional rates
|
||||
(`maximumFractionDigits: 2`).
|
||||
|
||||
## 7. Gate
|
||||
|
||||
`npm run check` green · `npm run test:ci` green (46 suites, 204 tests — +9: PriceBreakdown 4, EscrowNotice 2,
|
||||
PaymentStatusBadge 3) · `npm run build` green. `en.json`/`fa.json` in sync (53-key `payment` namespace).
|
||||
`client/CLAUDE.md` Project Structure updated (checkout subtree, invoice route, `services/payment`, three
|
||||
components, `payment` namespace entry). Gotcha for future phases: **BigInt literals (`0n`) don't compile**
|
||||
(tsconfig target ES2017) — use the `BigInt(...)` constructor like `utils/money.ts`.
|
||||
@@ -71,3 +71,5 @@ 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 | 🟡 |
|
||||
| `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 | 🟡 |
|
||||
| `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 | 🟡 |
|
||||
| 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 | 🟡 |
|
||||
|
||||
Reference in New Issue
Block a user