cleanup phases 6
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Frontend Phase 10 — Cancellation & refund status (customer) — report
|
||||
|
||||
**Consumes:** [`dev/contracts/domains/refunds-invoices.md`](../../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 ~7–10-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 ~7–10-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 ~7–10-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
|
||||
2–3 **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 ~7–10-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](./mocks-registry.md) `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`](../frontend/requests/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-021** — `GET 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.ts` — `bookingCancelPath` / `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.
|
||||
Reference in New Issue
Block a user