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 AstroBaaS repository. Nothing described below is implemented — it is the written plan for it.
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 ../../AI-GUIDE.md for how to work on this repository.