import { clientFetch } from '@/lib/api/client'; import { unwrap, type ApiEnvelope } from '@/lib/api/types'; import { ApiError } from '@/lib/api/errors'; import type { CancelBookingInput, CancellationPolicyPreview, RefundChannel, RefundStatus, RefundSummary, RefundsApi, } from '../types'; const BOOKINGS = '/api/v1/bookings'; const REFUNDS = '/api/v1/refunds'; /** * The thin b11 customer refund payload (`GET refunds/{id}/status`) — the only refund shape the contract * exposes to a customer. The fee-leg decomposition + policy snapshot live on the admin-only * `RefundListItem`, so this maps into `RefundSummary` with those fields `null` until REQ-021 serves them. */ interface RefundStatusWire { id: number; bookingId: number; status: RefundStatus; refundChannel: RefundChannel; amount: string; expectedCustomerRefundEta: string | null; reference: string | null; } function toSummary(wire: RefundStatusWire): RefundSummary { return { id: wire.id, bookingId: wire.bookingId, refundStatus: wire.status, refundChannel: wire.refundChannel, totalRefundedIrr: wire.amount, expectedCustomerRefundEta: wire.expectedCustomerRefundEta, externalRevertReference: wire.reference, // REQ-021: the customer status carries no decomposition/policy/timestamps yet — the fee-split section // is hidden until these are served (the mock fills them so the transparency split demos end-to-end). refundPercentageApplied: null, cancellationPolicyCode: null, platformFeeRefundedIrr: null, nursePayoutRefundedIrr: null, createdAt: null, completedAt: null, }; } /** * Real HTTP implementation of the `RefundsApi` seam. Only `getRefund` maps a **published** b11 route * (`GET refunds/{id}/status`, tenancy-scoped); the other three target contract gaps the frontend filed * (which is why the domain stays mock-primary — see `constants.ts`): * - `resolveCancellationPolicy` → REQ-020 (`GET bookings/{id}/cancellation_policy`): b9 snapshots the * policy only *after* a cancel; there is no pre-cancel preview resolving the tier by current lead time * + per-session refundability. * - `cancelBooking` → REQ-019 (`POST bookings/{id}/cancel`): b11 refunds are admin-only, no customer path. * - `getRefundByBooking` → REQ-021 (`GET refunds/by_booking/{id}`): the customer cannot obtain a refund * id from the admin-only worklist, so it needs to reach its refund from the booking. `404` = no refund. * * NOT the primary implementation this phase (`USE_REFUNDS_MOCK = true`). */ export const refundsClientApi: RefundsApi = { resolveCancellationPolicy: async (bookingId: number) => unwrap( await clientFetch>( `${BOOKINGS}/${bookingId}/cancellation_policy`, ), ), cancelBooking: async ({ bookingId, sessionIds, reasonCategory, reasonNotes }: CancelBookingInput) => toSummary( unwrap( await clientFetch>(`${BOOKINGS}/${bookingId}/cancel`, { method: 'POST', body: JSON.stringify({ sessionIds, reasonCategory, reasonNotes }), }), ), ), getRefundByBooking: async (bookingId: number) => { try { return toSummary( unwrap(await clientFetch>(`${REFUNDS}/by_booking/${bookingId}`)), ); } catch (error) { // No refund for this booking (e.g. not cancelled) is a clean empty state, not a failure. if (error instanceof ApiError && error.status === 404) return null; throw error; } }, getRefund: async (refundId: number) => toSummary(unwrap(await clientFetch>(`${REFUNDS}/${refundId}/status`))), };