Skip to content

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":

  1. hold(category, qty, ttl = PAYMENT_WINDOW)held; expires if unpaid.
  2. 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.
  3. 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) or release()/cancel()s (reject).
  4. 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_payment submodule attaches to the Category (with a Resource default). Replicates the legacy caution_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.