Files
baya-monorepo/dev/shared-working-context/reports/frontend-phase-10-report.md
T
2026-07-10 12:51:53 +03:30

8.4 KiB
Raw Blame History

Frontend Phase 10 — Cancellation & refund status (customer) — report

Consumes: dev/contracts/domains/refunds-invoices.md (b11). Depends on: f8 (services/bookings, booking detail), f9 (services/payment, checkout/invoice, the PriceBreakdown/EscrowNotice composites, the money util). Gate: npm run check green · npm run test:ci green (214 tests, +10). Mock-primary behind USE_REFUNDS_MOCK (default true).


What was built

A vertical slice — the customer half of the refund story: what cancelling costs (disclosed before confirm) and where the money is (a read-only refund status that tells the truth about the BNPL window).

services/refunds domain (new)

types.ts / constants.ts / keys.ts / invalidations.ts / apis/{clientApi,mockApi,index}.ts / hooks/{useCancellationPolicyPreview,useCancelBooking,useRefundStatus}.ts / index.ts, mirroring the services/payment + services/bookings shape exactly.

  • Contract-derived enums: RefundStatus = requested|approved|processing|succeeded|failed|rejected (the b11 set, forward-only), RefundChannel = psp_card|bnpl_revert|manual. The six statuses collapse onto three customer steps via refundCustomerStep (submitted → on its way → completed, failed/ rejected → a distinct error state). isTerminalRefundStatus gates the poll.
  • Money: the refund is the decomposition of gross = commission + payout. All IRR math is BigInt (integer parts-per-10000), never a float; refundAmount + fee = refundableGross and platformFeeRefunded + nursePayoutRefunded = totalRefunded reconcile to the rial (so PriceBreakdown's dev-guard never trips).
  • Caching: useRefundStatus polls (refetchInterval) only while non-terminal and stops at succeeded/failed/rejected (and never polls the null no-refund empty state); the cancel mutation setQueryData-primes the fresh refund into refundKeys.byBooking and invalidates the booking detail + lists + the policy preview (invalidations.ts) — no refetch storm.

Screens

  • Cancellation flow /bookings/[id]/cancel — step 1 discloses the resolved tier (label off the cancellation_policy_code i18n key, never the raw code), the refund % + fee %, the concrete Toman split (refunded vs kept, via the money util), and the multi-session refundable/locked breakdown; the confirm button is gated behind an explicit acknowledgement. Step 2 restates the amounts and submits via useCancelBooking → routes to the refund status. Copy makes the admin-approved, never-self-issued reality explicit.
  • Refund status /bookings/[id]/refund_status — the three-step stepper, the refunded amount, the per-channel ETA (BNPL's honest ~710-business-day window from expected_customer_refund_eta), the optional fee-leg split, and a failed/rejected contact-support state (no retry — retry is admin-only, DEFERRED to f15). An empty state renders when the booking has no refund.
  • Booking detail /bookings/[id] now composes CustomerBookingActions (page-only glue): the Cancel booking CTA while cancellable, or the refund section once cancelled — reusing the cached booking query key (no extra fetch); the refund read is enabled only after the booking is cancelled.

Shared composites (each with a co-located *.test.tsx)

  • CancellationPolicyDisclosure — the pre-confirm disclosure block (policy tier + %/fee + reconciling PriceBreakdown refund-vs-fee split + per-session refundable/locked + admin-approval explainer + ETA).
  • RefundStatusCard — the 3-step stepper + amount + masked reference + optional fee-leg split + failed/contact-support state. Reused on the refund-status page and the booking-detail refund section.
  • RefundEtaBanner — one branch on refund_channel: bnpl_revert surfaces the ~710-business-day window honestly (never instant), psp_card/manual their wording.

i18n

A new refunds namespace (69 keys) added to both messages/en.json and messages/fa.json in sync (fa brand بالین‌یار, RTL-first). Policy-tier, refund-status, per-channel-ETA, per-session-reason, and failed/contact-support strings are all i18n keys off the enum codes — never hardcoded off a raw code.


What is now testable and exactly how

Run npm run dev (the services/refunds mock is primary). Seeded bookings live in the f8 store:

Booking State Demonstrates
5001 confirmed, 3 sessions (today) multi-session cancel at the partial tier (<24h)
5002 confirmed, single (today) BNPL cancel — processing refund + the ~710-day ETA banner, walks to completed over polls
5003 in_progress, 3 sessions (1 completed, 2 un-started >24h) the mixed refundable/locked breakdown at the free tier
5004 already cancelled the failed refund → contact-support state (no retry)
  1. Disclosure before confirm. Open booking 5003 → Cancel booking → the resolved tier label, refund %
    • fee %, and the Toman refund-vs-fee split render before confirm is enabled (acknowledgement gate).
  2. Multi-session breakdown. 5003 shows session 1 locked (completed) with a reason chip and sessions 23 refundable. 5001 shows all three refundable (partial tier).
  3. Refund progression. Cancel 5002 (BNPL) → refund status walks submitted → on its way → completed; polling stops at completed. Cancel 5001/5003 (card) → completed immediately.
  4. BNPL ETA. 5002's refund shows the ~710-business-day window + a Shamsi ETA + provider-routed wording.
  5. No self-refund. There is no issue/approve/retry control anywhere; 5004 shows contact-support copy.
  6. Locale + RTL. Toggle fa/en → every string flips and is present in both files.
  7. Caching (Devtools). The cancel mutation invalidates bookingKeys.bookingDetail/lists and primes refundKeys.byBooking; the refund poll is active only while non-terminal; re-entry doesn't refetch.

What is mocked (and how to make it real)

The whole customer cancel + refund surface is mocked behind RefundsApi (USE_REFUNDS_MOCK = true) because b11 shipped refunds admin-only — there is no customer cancel command, policy preview, refund-by-booking lookup, or fee-leg decomposition on the customer status. The mock reads the shared f8 bookings store (tier by lead time + per-session refundability), flips the booking to cancelled, and drives card-immediate / BNPL-processing refunds. See the mock registry RefundsApi row. The real refundsClientApi maps the published refunds/{id}/status 1:1 and targets the proposed slugs for the gaps; when REQ-019/020/021 land, flip the flag — no hook/component change.

Contract gaps filed (for-backend.md):

  • REQ-019 — customer-initiated cancellation command (POST bookings/{id}/cancel).
  • REQ-020 — pre-cancel cancellation-policy preview + per-session refundability + the canonical cancellation_policy_code set (the tier codes free_24h/partial_under_24h/customer_no_show are client placeholders mapped to i18n keys).
  • REQ-021GET refunds/by_booking/{id} + the fee-leg decomposition / policy / timestamps on the customer refunds/{id}/status (today admin-only); confirm provider_commission_reversed_amount nullable.

Prior-phase files touched (in place, noted per operating-rules §0.3)

  • services/bookings/apis/mockApi.ts — added two non-seam exports (mockGetBookingForRefund, mockMarkBookingCancelled) alongside mockInsertConvertedBooking, and two additive seeds (5003 mid-engagement, 5004 cancelled). Dated so they don't disturb the f8 today check-in demo; f8 tests build their own fixtures and don't read the store, so unaffected.
  • constants/routes.tsbookingCancelPath / bookingRefundStatusPath helpers (id-keyed, like bookingInvoicePath).
  • app/.../bookings/[id]/page.tsx — composes CustomerBookingActions.

Follow-ups for f15-b15 (admin)

The admin refund console (create/approve, leg-split editor, ticket linkage, the clawback "nurse already paid" banner, refund retry), self-service partial refund UI, and holiday-specific policy overrides remain DEFERRED. This phase is strictly the customer read + cancel-request surface.