# refunds — cancellation, reversal, clawbacks, invoices > Client seam `client/src/services/refunds/` · `USE_REFUNDS_MOCK = true` (**mock is primary**) · 8 server ops > Last verified: 2026-07-30 against commit `d3ec723` and swagger.v1.json (2026-07-29). Money going back. The customer half is thin and real; the admin half is largely deferred, which is why the seam is mocked. ## Endpoints | Method | Path | Auth | Verdict | | --- | --- | --- | --- | | GET | `/api/v1/refunds/by_booking/{bookingId}` | `[Authorize]` | wired — the customer's refund for a booking | | GET | `/api/v1/refunds/{id}/status` | `[Authorize]` | wired | | GET | `/api/v1/admin_refunds` | admin · `sensitive` | wired · paginated | | POST | `/api/v1/admin_refunds` | admin · `sensitive` | **unwired** — creates **and executes** in one call; the client wants preview → approve (REQ-035) | | POST | `/api/v1/admin_refunds/{id}/confirm_settlement` | admin · `sensitive` | **unwired** — the manual-channel settlement confirm | | POST | `/api/v1/admin_refunds/{id}/mark_failed` | admin · `sensitive` | **unwired** | | POST | `/api/v1/admin_clawbacks/{id}/write_off` | admin · `sensitive` | **unwired** — no console screen | | POST | `/api/v1/admin_invoices` | admin · `sensitive` | **unwired** — owner-issue an invoice (REQ-018's fallback path) | Also wired by this domain, documented with their owner ([bookings.md](bookings.md)): `POST bookings/{id}/cancel` (cancel **and** refund, REQ-019) and `GET bookings/{id}/cancellation_policy` (the pre-cancel preview, REQ-020). ### Phantom — 4 | Client call | REQ | Note | | --- | --- | --- | | `POST /api/v1/admin_refunds/preview` | REQ-035 | Deferred. Today's single `POST admin_refunds` creates+executes with no preview step | | `POST /api/v1/admin_refunds/{id}/approve` | REQ-035 | Deferred | | `POST /api/v1/admin_refunds/{id}/reject` | REQ-035 | Deferred | | `GET /api/v1/refunds/my` | REQ-048 | The customer's "all my refunds" list. The wallet «استردادها» tab renders empty on the real path | ## The money rules Four, and none of them are the client's to compute: 1. **A refund is a reversal leg, not a deletion.** `ledger_entries` is append-only; every posting group balances. Nothing is ever edited or removed. 2. **Fee-leg decomposition is served, not derived.** `RefundStatusDto` carries `platformFeeRefundedIrr` and `nursePayoutRefundedIrr` separately (REQ-021, delivered) — a partial refund does not necessarily refund the commission and the payout in the same proportion. Never split a total. 3. **Pre-payout and post-payout fork.** If the nurse has not been paid, the payout leg is simply reduced. If they have, the platform raises a **clawback**, which the payout engine nets against the nurse's next batch — whole-clawback greedy netting, never a partial. See [payouts.md](payouts.md). 4. **VAT is on commission only**, so a refund's VAT leg follows the commission leg, never the payout. ## Shape rules the JSON does not express - **`refundChannel` decides the mechanics and is not a display detail**: `psp_card` reverses through the acquirer, `bnpl_revert` calls the BNPL provider's revert (see [bnpl.md](bnpl.md)), `manual` is a bank transfer an admin confirms with `confirm_settlement`. Each has a different ETA, and `expectedCustomerRefundEta` is server-computed per channel — the client displays it verbatim. - **`CancellationPolicyPreviewDto` is the complete pre-cancel answer** (REQ-020, delivered): `cancellable`, `cancellationPolicyCode`, `refundPercentageApplied`, `feePercentage`, `refundAmountIrr`, `feeAmountIrr`, `refundableAmountIrr`, the two fee legs, `appliesTo`, `leadTimeLabel`, `refundChannel`, `expectedCustomerRefundEta`, and a **per-session** breakdown. The client shows it and asks for confirmation; it computes none of it. - **Cancellation tiers are config rows**, not code — `cancellation_tier1/2/3_refund_rate` in [admin.md](admin.md) — and the applied rate is **snapshotted onto the refund** at compute time, so a later tier change is not retroactive. - **`refundPercentage` on `CancellationPolicyDto` is a rate, `refundPercentageApplied` on the refund is the snapshot.** They can legitimately differ; that is the point. - The crash-window fix in refinement-phase-6 wired the previously unreachable BNPL and manual settlement paths — a refund created against those channels now actually clears. ## Enums | Vocabulary | Values | | --- | --- | | `RefundStatus` | `requested` `approved` `processing` `succeeded` `failed` `rejected` | | `RefundChannel` | `psp_card` `bnpl_revert` `manual` | | `ClawbackStatus` | `pending` `recovered` `written_off` | | `CancellationPolicyCode` | `free_24h` `partial_under_24h` `customer_no_show` | | `CancellationLeadTime` | `gt_24h` `lt_24h` `started` | | `CancellationScope` | `whole_booking` `remaining_sessions` | | `CancelReasonCategory` *(client)* | `changed_mind` `schedule_conflict` `found_other_care` `other` | The first three are verified identical to `Entities/Refunds/RefundStatus.cs` and `ClawbackStatus.cs`. ## Open REQs | REQ | Status | Effect | | --- | --- | --- | | REQ-035 | deferred | No admin preview / approve / reject → 3 phantom routes. The one live route creates and executes together, which the console will not call | | REQ-048 | open | No customer "all my refunds" list → 1 phantom. The wallet tab renders empty on the real path | | REQ-033 | partially delivered | `totalIrr` is on `InvoiceDto`; the partner per-booking commission invoice list is deferred. See [partner-center.md](partner-center.md) |