Retail & POS
Cancellation & Deposit Policies (Hotels)
Indicative price, not an offer: €24/mo; policy enforcement, automatic refund/void reversal, dispute audits
Generated from docs/plan/paid/cancellation-deposit-policies/PLAN.md in the project's planning notes, which are not part of the public repository. Nothing described below is implemented — it is the written plan for it.
An idea, not a commitment. It has a written plan but no place in the build order and no date. What is committed.
Hotel and booking merchants define strict vs flexible cancellation policies with automatic refund enforcement. Policies specify refund windows, deposit treatment, and non-refundable booking rules. When a booking is cancelled, the system calculates the refund due (or void if non-refundable) and logs the dispute trail for chargeback defense.
The problem
Hotel operators run different cancellation policies on different room types—some offer full flexibility, others non-refundable. When a customer cancels after the policy window closes, staff must manually decide whether to refund, and conflicting decisions lead to chargebacks. Without an audit trail, defending a disputed refund is impossible. Payment processors want proof that the refund matched the stated policy.
What it does
- Admin interface to create, edit, and activate cancellation policies (name, terms, refund windows, non-refundable flag, deposit treatment)
- Policy assignment to products (room types, packages) with preview of refund amounts
- Refund calculator that applies policy rules at cancellation time and outputs the refund-eligible amount
- Automatic refund or void trigger when a customer-initiated cancellation API call comes in after the policy window
- Dispute audit log recording policy name, refund amount calculated, reason for refund/void, and decision timestamp
- Admin order screen showing which policy was active at purchase, current cancellation deadline, and any past refunds with policy justification
- Refund SLA enforcement: reject a refund request that violates the policy with a clear reason message
- Webhook event
order.cancellation_policy_appliedfired when policy enforcement runs, for integration with chargeback defense tools - API endpoint
/api/orders/{id}/refundthat requires policy lookup and returns calculated refund amount before executing - CSV export of disputes: order number, original total, refund amount, policy name, cancellation date, customer email
What it deliberately does NOT do
Each boundary carries its reason. A boundary without a reason gets crossed by the next person who reads this.
- Booking-engine integration—this stores policies and calculates refunds; accepting bookings is the storefront’s job (reason: third-party system responsibility)
- Automatic chargeback filing—disputes are logged here; filing them with the payment processor needs that processor’s API, which is out of scope for the policy engine (reason: provider-specific; handled by payments module separately)
- Per-customer policy override—merchants can’t decide to give Alice a refund while denying Bob the same; only admins set policies (reason: audit and compliance; exceptions are noise in the SLA log)
- Mandatory IATA or UNWTO compliance labels—the merchant writes their own terms; this engine enforces them (reason: no single legal standard across jurisdictions; labelling is the merchant’s obligation)
- Automatic reminders to customers before refund window closes (reason: a separate notification workflow; handled by email/SMS layer, not this feature)
- Refund partial-amount negotiation UI—staff can issue a partial refund via the API, but there is no UI to propose a compromise (reason: every partial is a manual decision and should stay audited, not codified as a workflow)
Data model
Schema v8 migration. New collection cancellation_policies with fields: id, name, description, refund_percentage (basis points), refund_window_days, non_refundable, deposit_treatment (enum: ‘keep’|‘refund’), active, created_at, updated_at. New field on Order: policy_id (reference to policy at purchase time). New collection refund_disputes with: id, order_id, policy_id, reason (enum: ‘window_closed’|‘manual’), refund_amount_cents, decision (enum: ‘approved’|‘denied’|‘pending’), decided_by_user_id, decided_at, created_at. Existing table: extend refunds with policy_applied flag and policy_reason text.
API
- POST /api/cancellation-policies
- GET /api/cancellation-policies
- GET /api/cancellation-policies/{id}
- PATCH /api/cancellation-policies/{id}
- DELETE /api/cancellation-policies/{id}
- POST /api/orders/{id}/check-refund-eligible
- POST /api/orders/{id}/refund
- GET /api/refund-disputes
- GET /api/refund-disputes/{id}
- PATCH /api/refund-disputes/{id}
- GET /api/audit?event=refund_policy_*
Every route added here must also appear in src/pages/openapi.json.ts — a test
fails the build if it does not.
Admin
New admin panel section ‘Policies > Cancellation’. Merchants see a list of policies (with active status, refund percentage, window days). Clicking one opens edit form with preview of refund amount for a sample booking. On the order detail screen, a new card shows ‘Cancellation Policy’ with policy name, refund window deadline, and any past refunds with their dispute status. Bulk export button exports disputes as CSV. Staff with ‘manage_commerce’ role can create/edit/delete policies; staff with ‘view_orders’ can see them read-only.
The seam — why this is paid
Core owns: the Order model, refund tracking via refunds[], audit log, payment status lifecycle. Paid pack owns: policy definition (CRUD), enforcement logic (refund calculator), dispute audit, and admin UI. The payment provider integration (Stripe, PayPal refund API calls) stays in core’s payments module; this pack calls it via existing refund endpoints and logs the policy decision alongside.
Support commitment: policy compliance audits, refund SLA, chargeback defense
Dependencies
- orders-api
- audit-log
- payments-refunds
- admin-ui
Acceptance checks
Each of these must be able to fail. Before claiming this is done, break the code deliberately and watch each one go red.
- A policy with refund_window_days=14 and non_refundable=false allows full refund within 14 days of order creation, and zero refund after; system calculates both correctly
- An order carrying a non_refundable policy rejects any refund request with reason ‘non_refundable’ and logs the rejection to the dispute audit
- A merchant creates Policy A with 50% refund, applies it to room type ‘Deluxe’, places order, then deactivates Policy A; the order still shows Policy A and refund_window remains correct
- Cancellation API call after policy window expires returns 403 with message ‘Refund window closed’ and creates a dispute record with decision=‘pending’
- Refund dispute record is created and linked to the order within 100ms of a refund request, audit event is logged with user_id and timestamp
- CSV export includes all disputes from last 90 days with columns: order_number, policy_name, refund_amount_cents, decision, decided_by_email, decided_at
- A payment provider webhook reversing a refund updates the order refunds array and fires webhook event with policy_context included
- Admin user with ‘view_orders’ role can see policy name and refund window on order detail, but cannot edit policies (only ‘manage_commerce’ can)
- System correctly handles edge case: policy window closes on Saturday evening (UTC); refund request on Monday morning is denied; request on Friday is approved
- Refund dispute record cannot be edited after created_at + 7 days unless user has ‘admin’ role; enforced on PATCH endpoint
Risks
Careless builds break: (1) policy arithmetic—off-by-one error in day calculation causes merchants to refuse refunds a day too late; test edge of window. (2) Refund idempotency—replayed webhook or double-click on refund button attempts two refunds; must check existing refund ledger first. (3) Policy deletion—if a policy is hard-deleted while orders carry its id, invoice generation fails; must soft-delete or forbid deletion when in use. (4) Audit log loss—refund decision made but event fails to write; dispute record exists but audit event does not; reconciliation becomes impossible. (5) Timezone handling—policy ‘window closes at 23:59 on day X’ is meaningless without stating timezone; must use merchant’s configured timezone or UTC explicitly.
Commercial context
| Suggested price | €24/mo; policy enforcement, automatic refund/void reversal, dispute audits |
| Rival anchor | Booking.com: built-in; Airbnb: built-in; custom: €1500+ |
The anchor is what the nearest equivalent charges on Shopify or Magento today. It is context for a pricing decision, not the decision.
Generated from the commerce plan. See docs/COMMERCE-PLAN.md for the full
catalogue and CONTRIBUTING.md for how to work on this repository.