Deposits and payment¶
The engine is payment-agnostic. It only knows held vs confirmed;
who calls confirm() and when is a consumer concern. This cleanly models
the common "pay a deposit to confirm" flow - including the two-phase
window (a short window to fill the form, then a longer one for the card
deposit) the legacy reservation system used.
The mapping¶
| Deposit concept | Booking model |
|---|---|
| Cart hold / awaiting deposit | held + hold_expires (TTL) |
| Form-filling window | first hold(ttl = SHORT) |
| Payment window | extendHold(ttl = LONGER) |
| Paid / issued | confirm() |
| Abandoned / failed / timed out | release() or TTL → expired |
Two-phase deposit flow¶
sequenceDiagram
actor User
participant App as Booking flow (consumer)
participant BM as BookingManager
participant Pay as Payment provider
User->>App: pick slot + category
App->>BM: hold(category, qty, ttl = SHORT)
BM-->>App: held (form window)
User->>App: submit registration form
App->>BM: extendHold(booking, ttl = LONGER)
BM-->>App: held (deposit window)
User->>Pay: pay deposit
alt payment ok
Pay-->>App: success callback
App->>BM: confirm(booking)
BM-->>App: confirmed ✅
else timeout / failure
App->>BM: release(booking)
Note over BM: or TTL lapses → expired
end
Because the TTL is per-call, any number of phases works - add another
extendHold() for a third step. The engine stays phase-agnostic; the phase
durations and sequence live in the consumer (often an
Orchestra workflow with a timer per phase).
Pay first, then operator validation¶
A common requirement: an operator manually validates a booking, but only once it has been paid. This is the two-phase hold again - the second phase is "awaiting operator" instead of "awaiting payment":
hold(category, qty, ttl = PAYMENT_WINDOW)→held; expires if unpaid.- Payment confirmed →
extendHold(booking, ttl = 0)→ the hold becomes non-expiring: it is paid and now waits for an operator. The payment layer records the payment and marks the booking awaiting validation. - The operator reviews only paid bookings - the "awaiting validation"
queue is a payment-layer query (held bookings whose payment is confirmed) -
and either
confirm()s (approve) orrelease()/cancel()s (reject). - A rejection emits
BookingReleased/Cancelled, which the payment layer turns into a refund.
sequenceDiagram
actor U as Customer
participant App as Booking flow
participant BM as BookingManager
participant Pay as Payment layer
actor Op as Operator
U->>App: book
App->>BM: hold(ttl = PAYMENT_WINDOW)
BM-->>App: held
U->>Pay: pay deposit
Pay-->>App: payment confirmed
App->>BM: extendHold(ttl = 0)
Note over BM,Pay: held + paid = awaiting validation
Op->>App: review paid bookings only
alt approve
App->>BM: confirm()
BM-->>App: confirmed ✅
else reject
App->>BM: release() / cancel()
BM-->>Pay: Released/Cancelled event → refund
end
The booking stays held throughout (capacity reserved from the first click
until the operator decides, so it is never oversold). "Paid" is payment-layer
state, never in the core - the operator's confirm() is the single commit;
payment confirmation only flips the hold to non-expiring.
Whether a resource auto-confirms or waits for an operator is a consumer-layer
policy - an auto_confirm_on_payment field that yoyaku_payment adds to the
resource, not a core concern: when on, yoyaku_payment calls confirm()
straight after payment; when off, it extendHold(0)s and routes to the operator
queue. The engine exposes only confirm()/release() - it never decides
when to call them.
Where the deposit amount lives¶
Two distinct values, neither in the capacity core:
flowchart TB
subgraph core[booking core]
CAT[Category] --- ST[Booking.state held→confirmed]
end
subgraph pay[yoyaku_payment]
DEP[deposit_required + deposit_amount<br/>field on Category, Resource default]
TXN[Payment record<br/>snapshots charged amount]
end
DEP -. configures .- CAT
TXN -. references .- ST
- Configured deposit (how much this category requires) - a field the
yoyaku_paymentsubmodule attaches to the Category (with a Resource default). Replicates the legacycaution_statut/caution_montant, and can now differ per tier. - Charged amount for a specific booking - snapshotted on the payment transaction record, not derived from config, so historical bookings stay accurate when prices change later.
- Is it paid? - the booking's
state.
This keeps the engine currency-, tax- and provider-agnostic; a
booking_commerce/payment bridge or your own integration owns the money.