Skip to content

Concepts

Yoyaku models a handful of content entities plus a tenant. A quota is carried by a tariff - the resource tariff, or the slot tariff where one slot departs from it - rather than by the Slot itself: this is what lets a single time window carry several independently-counted pools (ticket tariffs, room tariffs, price classes).

Pricing is two-level, and the distinction is the one most worth reading twice. A Tariff belongs to the resource and names something it offers ("adult", "reduced"). A Slot tariff is that tariff made available on one slot, with the quota allocated for that slot alone. The same tariff can therefore be offered at different quotas from one date to the next, and a booking consumes a slot tariff, never the tariff directly.

Four words this documentation is careful with

Slot, never session. A slot is what the engine books, and slot is the word for it in the code, on the admin screens and here. A session in this documentation is the visitor's own web session, the one a basket lives in, and one word for both makes a sentence about holding places ambiguous. Booker-facing wording is a separate matter: a page may call a performance a session where that is what its audience would call it.

Slot, never event either. In a Drupal codebase an event is a Symfony event: this module dispatches plenty of them, and BookingEvents, an EventSubscriber and getSubscribedEvents() all mean that and nothing else. A bookable window is a slot, in the code, on the admin screens and here, so the same word never has to carry both. Booker-facing wording is again a separate matter: what an audience calls an event is what a page may call it.

Booked, never sold. A great deal of what a house offers is free, so nothing here says sold, sale or customer about a place or the person taking it. A place is taken, offered or out of the offer; the person is a booker. Where the word sale does appear it is naming what the engine is not, as the basket window setting does when it says a held seat is not unsold stock.

Attachment, meaning a policy on a host. A constraint policy is attached to something that governs a booking, and one attachment is that policy paired with the host it hangs on: a resource type, a resource, a tariff or a slot tariff. The four nest from the general to the particular and all of them apply, so nothing picks a winner; where two express a ceiling, the tighter refuses first.

The word is busy elsewhere, which is why it is pinned here. Drupal calls a render array's assets its attachments (#attached, hook_page_attachments), and a file sent with an order's email is an attachment too (OrderNotificationEvent::addAttachment(), which is how ticket PDFs go). Neither is what this documentation means. The policy readers say what they hand back: getAttachedPoliciesForBooking() for the policies governing one booking, and getAttachedPoliciesInContext() for every distinct one reaching a whole evaluation.

Entity model

erDiagram
    TENANT        ||--o{ RESOURCE         : scopes
    TENANT        ||--o{ TARIFF_PRESET    : scopes
    TENANT        ||--o{ TARIFF_CLASS     : scopes
    RESOURCE      ||--o{ SLOT             : "offers"
    RESOURCE      ||--o{ RESOURCE_TARIFF  : "offers"
    TARIFF_PRESET ||--o{ TARIFF           : "holds"
    TARIFF        ||..o{ RESOURCE_TARIFF  : "copied onto a resource as"
    TARIFF_CLASS  ||--o{ TARIFF           : "optionally groups"
    TARIFF_CLASS  ||--o{ RESOURCE_TARIFF  : "optionally groups"
    SLOT          ||--o{ SLOT_TARIFF      : "allocates to 0+"
    RESOURCE_TARIFF ||--o{ SLOT_TARIFF    : "allocated per slot as"
    SLOT_TARIFF   ||--o{ BOOKING          : "consumed by"
    TRANSACTION   ||--o{ BOOKING          : "groups"
    SLOT          ||..o{ BOOKING          : "denormalized"
    RESOURCE      ||..o{ BOOKING          : "denormalized"

    RESOURCE {
        int id PK
        string tenant
        int type FK "resource type, a level that may rule fields"
        string label
        bool status
        int capacity "default for its slots; NULL = unlimited"
        string booking_mode "collective|individual|inherit"
        string booking_cutoff "start|end|inherit"
        int lead_time
        string lead_time_unit "hours|days|inherit"
        string allow_self_cancellation "yes|no|inherit"
        int cancellation_deadline
        string cancellation_deadline_unit "hours|days|inherit"
        string settlement "complete|unused|manual|inherit"
        string settlement_grace "seconds or ISO 8601; empty inherits"
        bool tariffs_enabled "offers named tariffs; bookings must name one"
    }
    SLOT {
        int id PK
        string tenant
        int resource FK
        string label
        int start "timestamp"
        int end "timestamp"
        int capacity "optional overall cap; NULL = none"
        int opens_at "timestamp; NULL = open immediately"
        bool status
        bool published
    }
    TARIFF_CLASS {
        int id PK
        string tenant
        string key "unique within the tenant"
        string label
        int weight
    }
    TARIFF_PRESET {
        int id PK
        string tenant
        string key "e.g. grille-a"
        string label
        int weight
        int venue FK "0+, with yoyaku_placement; none = applies anywhere"
    }
    TARIFF {
        int id PK
        string tenant
        int preset FK
        int tariff_class FK "optional heading"
        string key "copied through as the resource tariff's key"
        string label
        int quota "NULL = no limit"
        int weight
        int grade FK "0+ place grades, with yoyaku_placement"
    }
    RESOURCE_TARIFF {
        int id PK
        string tenant
        int resource FK
        int tariff_class FK "optional heading"
        string key "e.g. default, adult, vip"
        string label
        int quota "seeds new slot tariffs; NULL = no limit"
        int weight
    }
    SLOT_TARIFF {
        int id PK
        string tenant
        int slot FK
        int tariff FK "the resource tariff it overrides"
        int quota "NULL = no own limit"
    }
    BOOKING {
        int id PK
        string tenant
        int slot FK
        int tariff FK "the resource tariff; NULL when it offers no tariffs"
        int resource FK
        int transaction FK
        int allotment FK "the allotment it was taken out of, nullable"
        int quantity
        string state "held|confirmed|released|expired|canceled|completed|unused"
        decimal unit_price "what one unit was booked at; stamped when held"
    }
    TRANSACTION {
        int id PK
        string tenant
        string state "empty|pending|locked|placed|confirmed|partial|canceled|completed|unused"
        int hold_expires "timestamp, nullable; the one hold clock"
        int hold_suspended_expires "timestamp, nullable; what a checkout took"
        int hold_started "timestamp, nullable; anchors the renewal ceiling"
        int locked_expires "timestamp, nullable"
        string reference "optional consumer label, not the grouping key"
        string booking_channel "the channel it was opened through, nullable"
        bool disable_constraint_policies
    }

Resource

A bookable thing - a room, a piece of equipment, an event. Holds the defaults its slots inherit and the booking window rule:

  • capacity - the default capacity a new slot starts from, and the limit a slot offers against when it offers no tariffs.
  • lead_time / lead_time_unit - the minimum delay before a slot may be booked (enforced by the booking_window constraint).
  • label is the name a booker reads, and it heads every slot title the engine composes, so it is translatable. Nothing else here is: a capacity and a deadline read the same in every language. See Translation.

The core Resource holds only what the engine itself uses. Policy that lives outside the engine - automatic vs manual confirmation, prices and guarantees, node binding - is added by consumer submodules as extra fields (entities are fieldable), keeping the core lean. See Paying for a booking.

Slot

A concrete time window of a resource (start/end). One level, not two: a full-day booking is one slot, and hourly slots are several sibling slots.

  • capacity here is an optional slot-wide overall cap across all its slot tariffs (e.g. "200 total, however the tariffs split it"). NULL means the slot is governed solely by its slot tariffs. On a slot offering no tariffs it is the only limit.
  • published / status gate public visibility and bookability.
  • opens_at holds booking closed until that moment; empty means open immediately.
  • label is the title a house types when a slot is named something of its own, and it is translatable. Where none is typed, the label is composed from the resource name and the start time, in the slot's own language. See Translation.

Tariff

One word per scope, and the module names each entity after the thing it hangs off:

Entity type What it is
yoyaku_tariff the definition, living in a reusable preset
yoyaku_resource_tariff that tariff as one resource offers it
yoyaku_slot_tariff that tariff on one slot

A resource tariff is a named thing the resource offers: default, adult, child, vip. It belongs to the resource, not to any one slot, so it is declared once and reused by every slot that offers it. It is what a booking names and what every price and quota reader walks; the sections below describe it.

A tariff is the same shape held in a preset, typed once and copied onto a resource. Nothing reads a preset at booking time. See Tariff presets.

Whether a resource offers by tariff at all is declared on it, by tariffs_enabled, rather than inferred from whether any tariff row exists: a resource being set up intends to offer tariffs before it has any, and counting rows cannot tell that apart from a resource that never will. With the mode on, the engine refuses a booking that names no tariff, the resource's own price no longer applies, and the mode cannot be turned off while tariffs remain.

  • key and label identify it; weight orders it on the booking page.
  • quota is the tariff's allocation: how much of the room it may offer. Slot generation copies it onto each new slot tariff, and changing it never moves an allocation a slot tariff already holds; where no slot tariff exists it is the allocation in force. A slot tariff simply overrides it.
  • The allocation is never a promise about the room. The slot's own capacity bounds the total across every tariff, so the effective limit is the lower of the two.
  • Price is carried here when yoyaku_payment is installed, and it is required: the tariff is what is booked, so it prices itself rather than falling back to the resource. Enter 0 for a free tariff. See Paying for a booking.
  • Label is what a booker chooses between on the offer card, so it is translatable, the way the class heading above it is. The machine key, the quota and the weight are not: they are the same allocation in every language. See Translation.

Slot tariff

The "sub-slot" - one tariff made available on one slot, holding the quota allocated for that slot alone. Where one exists, its quota is the one in force; where none does, the resource tariff's quota is. What the engine locks is the anchor row of each scope a hold consumes (see Capacity and holds).

  • A slot tariff carries no label of its own and so is not translatable: what a booker reads is the resource tariff it provisions.
  • A slot may have no tariff at all. Such a slot offers against its own capacity, and its bookings leave tariff empty.
  • A booking names the resource tariff, not the slot tariff: the tariff is what was booked, and it is offered on every slot of the resource. A slot tariff is an override the operator may or may not have created for this slot, so making a booking depend on one would make the booking depend on a row that need not exist.
  • Ticketing and multi-tariff use cases give the slot one row per tariff offered (adult, child, vip), each with its own quota. A quota is an allocation the operator chose, never a count of the room: see Capacity and holds.
  • quota = NULL means the slot tariff sets no limit of its own.
  • Because the quota lives here and not on the tariff, the same adult tariff can be allocated 20 units in January and 200 in July. That is what makes seasonal offers possible without duplicating tariffs.
  • A slot overrides each tariff at most once: (slot, tariff) is unique. A booking names its slot and its tariff, so the pair is the only way back to the override, and a second row would leave which quota and price apply down to whichever row a query happened to return first. A collision is reported on the Tariff field (the UniqueSlotTariff constraint) rather than surfacing as a storage exception.

Tariff class

A resource often prices the same thing for several audiences, which means several tariffs differing only in who may book them: full price, reduced, subscription. A tariff class (yoyaku_tariff_class) is the heading over those, with a translatable label a visitor reads and a weight setting the order they are offered in. It prices nothing itself; the tariffs under it stay the priced things.

A class belongs to the tenant, not to one resource. The same "Full price" heads a preset's tariffs and every production's, so the vocabulary a house reads is one list rather than one per production.

Classes are optional. A tariff naming no class is offered on its own, and a tenant that defines no class at all offers a plain list. The booking page uses them to offer one collapsible panel per class instead of a flat stack of quantity fields.

Allotment

A name places are set aside under: the schools, the guests, a partner. yoyaku_allotment is the name itself, tenant-level like a tariff class and carrying no number; yoyaku_resource_allotment says how many places every slot of a resource holds under it, and yoyaku_slot_allotment says how one slot departs from that.

Places held under an allotment leave the general count the moment they are declared, with no booking behind them, and only a booking drawing on that allotment takes them back. A tariff names the allotment it draws on; an operator may name a different one for a single booking; the booking records what it actually took. See Allotments.

An allotment is not a quota. A quota is one tariff's allocation and withholds nothing until it is used, where an allotment is shared by every tariff drawing on it and withholds its places whether or not anybody does.

Booking

A consumer of a tariff's quota on a slot, or of the slot's own capacity when the slot offers no tariffs. It records the slot and the tariff it was booked at. Carries a quantity (the number of units) and a lifecycle state. held, confirmed, completed and unused consume it; released, expired and canceled do not. Where it came out of an allotment rather than the general capacity, it records that too, stamped as it is held and never derived again. See Capacity and holds.

What one of those units is called is the resource's to say: a hall offers places, a refuge offers nights. See Booking units.

Every booking belongs to a transaction: the atomic group it was placed and is handled with. The transaction's reference is a free-form string the consumer fills with whatever correlation handle it needs (an order id, a request uuid, a user reference); it is also what constraints receive as their subject. The core never interprets it, keeping the engine decoupled from who booked.

Order lifecycle

The yoyaku_order module decorates a booking transaction as an order, whose state is derived from what it holds: pending while any booking is held, then confirmed while any is still confirmed, and once every one has settled canceled, completed, unused, or partial (a terminal mix of completed and unused). Operators drive it from the orders overview, and the lifecycle runs active → settled → archived → deleted:

  • Active (pending/confirmed): confirm or cancel it.
  • Settled (canceled/completed/unused/partial): archive it to set it aside. A used order, partly or wholly, is never canceled. Archived orders leave the active list; a Show archived filter reveals them, and Unarchive returns one to the list.
  • Deletable only once the order is archived or canceled (a void order needs no archive step first), and never while its Orchestra workflow is still running. Archiving is the deliberate "cleared for removal / retained" step, so completed and no-show history is never purged by a stray click.

Settlement after the slot ends

A booking left confirmed once its slot has ended is unsettled: nobody recorded whether it was honored. Rather than let such bookings accumulate, cron settles them a grace period after the slot ends. The policy is set on the resource type and overridable on the resource (the same cascade as booking mode and cancellation), with two parts:

  • A settlement mode:
    • Auto-complete (the default): mark the remaining confirmed bookings completed. Fits events where a booking is honored unless explicitly marked otherwise.
    • Auto-mark unused: mark the remaining confirmed bookings unused (a no-show).
    • Manual: never touch them; attendance is recorded out of band.
  • A grace period before the sweep acts, as a number of seconds or an ISO 8601 duration (default PT1H).

The sweep only ever acts on bookings still confirmed, so it composes with check-in: a scan (or a presence-list check) completes attendees during the grace window, and the sweep settles whatever is left. Run a presence-list check long after the slot instead? Set the resource to manual and settle by hand.

Tenant

Every row is stamped with a tenant. Single-site installs transparently use the shipped default tenant; multi-site / multi-booker deployments partition data per tenant. See Multi-tenancy.