frontend phase 9

This commit is contained in:
hamid
2026-07-10 11:49:55 +03:30
parent cd6c2591a6
commit 40cc1d163b
49 changed files with 4130 additions and 20 deletions
@@ -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