Deposits & cancellation
Charge guests at booking time and decide who keeps the money when they don't show.
Reservation policies decide whether a booking can hold the table without paying anything (free), needs a refundable card hold (guarantee), or pre-pays a non-refundable amount that counts towards the bill (deposit). The matching policy is picked server-side from a ranked rule set, so a six-top on a Saturday at 20:00 can land on a different policy than a two-top at lunchtime — both through the same booking widget.
Policy shape
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| kind | 'free' | 'deposit' | 'guarantee' | Required | — | Deposit charges immediately; guarantee places a card hold; free does nothing. |
| party_size_min / party_size_max | number | — | — | Inclusive bounds. Both NULL means "any party size". |
| applies_to_weekdays | number[] | — | — | ISO weekdays 1–7 (1=Mon). NULL means every day. |
| applies_from_time / applies_to_time | HH:MM | — | — | Time window inside the day. Both NULL means "any time". |
| deposit_amount_cents | integer | — | — | Flat amount charged once per reservation. Combine with deposit_per_seat_cents for hybrid pricing. |
| deposit_per_seat_cents | integer | — | — | Charged on top of the flat amount for every guest. |
| no_show_charge_cents | integer | — | — | For guarantee policies, the amount captured when status flips to no_show. |
| free_cancellation_hours | integer | — | — | Up to this many hours before reserved_at, cancelling refunds the full deposit. Inside that window the deposit is forfeit. |
| priority | integer | — | — | When multiple policies match, the highest priority wins. Use to layer "weekend specials" on top of the default. |
How the policy is matched
Every reservation that hits the database calls the best_policy_for_reservationfunction, which scans every active policy in the restaurant and returns the highest-priority row whose party-size, weekday, and time windows accept the booking. The selected policy_id is stored on the reservation row, so subsequent edits don't silently re-match if you tweak a rule later.
Payment states
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| pending | state | — | — | Deposit required but not yet charged. Stripe Checkout URL is generated on demand. |
| paid | state | — | — | Stripe payment_intent.succeeded webhook landed; deposit_paid_at is set. |
| failed | state | — | — | Payment attempted but declined. The reservation stays in pending until either the guest retries or staff manually overrides. |
| refunded | state | — | — | Either auto-refunded (cancellation inside free_cancellation_hours) or manually refunded from Stripe. deposit_refunded_at is set. |
| cancelled | state | — | — | PaymentIntent cancelled before capture. |
payment_intent.succeeded, payment_intent.payment_failed,payment_intent.canceled and charge.refunded are all routed on metadata.kind = "reservation_deposit". Make sure those event types are enabled in the Stripe Dashboard.Cancellation rules
When a guest cancels via the link in their confirmation email, the server readsfree_cancellation_hours from the matched policy and refunds the deposit automatically if the cancellation is inside that window. Outside the window the deposit is forfeit and the row flips to cancelled with the deposit still paid; the host can refund manually from Stripe if they choose.
policy_id on existing reservations. A booking made under the "Summer weekend 30€" rule stays on that policy even after you raise the deposit to 50€ for next year — the captured amount and refund rules travel with the original policy.