Orchestra integration¶
Orchestra is a lightweight,
plugin-based workflow engine. Booking pairs with it naturally: Booking owns
capacity, Orchestra owns the process (the steps, timers and approvals
around a reservation). The booking_orchestra submodule provides the glue.
Task types¶
booking_orchestra ships Orchestra #[TaskType] plugins that call the
booking API as workflow steps:
| Task type | Calls | Use |
|---|---|---|
| Hold | hold() |
Reserve capacity when the process starts. |
| Extend | extendHold() |
Move from a form window to a deposit window. |
| Confirm | confirm() |
Commit on approval / payment. |
| Release | release() |
Free capacity on rejection / timeout. |
Example: deposit-gated reservation workflow¶
flowchart TD
START([Start]) --> HOLD[Hold capacity<br/>ttl = form window]
HOLD --> FORM[User task: fill form<br/>⏱ timer]
FORM -->|submitted| EXT[Extend hold<br/>ttl = deposit window]
FORM -->|timeout| REL1[Release]
EXT --> PAY[Payment task<br/>⏱ timer]
PAY -->|paid| CONF[Confirm booking]
PAY -->|timeout / fail| REL2[Release]
CONF --> END([Confirmed])
REL1 --> ENDX([Cancelled])
REL2 --> ENDX
Each timed step uses an Orchestra timer; on timeout the workflow routes to a Release task, which frees the capacity. This is the two-phase deposit flow expressed as a workflow - the booking engine stays phase-agnostic while Orchestra sequences the phases.
Payment workflow actions¶
yoyaku_payment_orchestra exposes the deposit and no-show operations as Drupal
Action plugins, so each is a step you drop into an Orchestra action node
(the orchestra_action task type) and configure in the modeler. The action
resolves the order from the running instance (which carries the transaction id
as the yoyaku_transaction_id process variable) and delegates the money
operation to the payment manager, so the gateway stays the engine's concern.
| Action | Does | Key settings |
|---|---|---|
| Authorize deposit | Holds the deposit, writing yoyaku_deposit_result (held / declined / none) for branching. |
gateway |
| Capture deposit | Claims the held deposit, in full or a set amount (damage). | gateway, amount |
| Release deposit | Voids the held deposit (no claim). | gateway |
| Charge no-show fee | Charges the resource's no-show fee against the stored card. | gateway |
Every action takes a gateway setting (the gateway plugin id, e.g.
worldline or manual); capture adds an amount. The no-show fee amount is
not on the node: it is the resource's noshow_fee payment-block field
(resolved per booking, summed across the order), so the fee is a pricing
decision that lives with price and the deposit, not in the workflow. The
attendance decision itself is an ordinary user task (outcomes
attended,damaged,no_show), and these actions hang off its branches.
Deferring the hold with an Orchestra timer¶
A deposit is a damage hold that only needs to be valid during the stay, and
a card authorization expires in roughly 7 to 30 days, so for a booking made
months ahead a hold placed now would lapse before arrival. The hold is therefore
deferred with a Wait node + an Orchestra "until" timer, not a separate cron:
the booking starter seeds a yoyaku_arrival process variable (the earliest slot
start), and the wait's timeout is configured until: yoyaku_arrival with
until_offset: -P3D (three days before). Orchestra's own timer resumes the wait
near arrival and the flow runs Authorize deposit; a short-lead booking whose
date has already passed simply fires at once. What bridges the long gap between
booking and that hold is the card token captured at checkout. See
the deposit and gateway design for the full
rationale (tokenization, deferred holds, no-show, re-authorization).
The shipped example¶
The yoyaku_orchestra_example yoyaku_booking workflow wires these in after an
order is confirmed:
flowchart TD
CONF[Confirm order] --> PREP[Compute refund deadline]
PREP --> FORK{{Fork}}
FORK --> CW[Await cancellation<br/>timer: until slot start<br/>booking_cancel interaction]
FORK --> WAIT[Await deposit window<br/>timer: until arrival -P3D]
WAIT --> AUTH[Authorize deposit]
AUTH -->|declined| MGR[Notify managers]
AUTH -->|held / none| DONE[Deposit pre-authorized]
MGR --> DONE
CW -->|cancelled / timeout| JOIN{{Quorum join}}
DONE --> JOIN
JOIN -->|not cancelled| ATT[Record attendance]
ATT -->|attended| MC[Mark order completed] --> REL[Release deposit]
ATT -->|damaged| MCD[Mark order completed] --> CAP[Capture deposit]
ATT -->|no-show| MU[Mark order unused] --> FEE[Charge no-show fee]
FEE --> REL
REL --> OK([End: confirmed])
CAP --> OK
JOIN -->|cancelled| RH[Release hold] --> CO[Cancel order] --> RG{Refund due?}
RG -->|yes| RV[Operator: approve refund]
RG -->|no| NC[Notify: cancelled]
RV -->|approve| RF[Refund] --> NC
RV -->|deny| NC
NC --> KO([End: cancelled])
Once confirmed, the run forks into two concurrent branches that rejoin at a value-aware (quorum) join:
- the deposit branch: a timer holds until near arrival, then pre-authorizes
the deposit; a declined hold routes to a manager-notification task. With the
manualgateway this is a safe no-op; with Worldline it places a real hold; - the cancellation branch: an interaction the booker reaches from a link in their confirmation email (see below). Cancellation is always allowed, right up to the slot start.
Fulfillment (recording attendance, then releasing, capturing or charging the deposit) happens after the join, on the not-cancelled path. Because the join resolves only once the cancellation window has closed at the slot start, a booking can never be both attended and still cancellable: the point of no return strictly follows the window, and cancelling an already-attended booking is impossible by construction.
Recording attendance also sets the order's terminal state: attended and damaged mark it completed, a no-show marks it unused. These are order and booking states in their own right (alongside pending, confirmed, partial and cancelled), so a fulfilled booking is told apart from a no-show long after the slot. Dropped lines never count against them: an order stays completed even when the holder cancelled some of its lines.
If the booker cancels, the join fires early and tears down the deposit branch (voiding any authorized hold via a release-hold step), then cancels the order and runs an operator refund review only when a refund is due (a real payment was captured and the cancellation is within the refund deadline). If the window instead times out at the slot start without a cancellation, the run continues down the deposit branch to its confirmed end.
Customer notifications¶
Customer emails go out at three milestones: confirmed, released (a hold
abandoned before confirmation) and cancelled (a confirmed booking undone).
They are built by the yoyaku_orchestra_order OrderNotifier, which does not
send mail itself: it dispatches Orchestra's channel-neutral notification for the
running instance, to the order's customer_email, with the order attached as
[yoyaku-order:*] token data and the instance's process variables in the event
context, so whichever channel a site enables delivers it. orchestra_mail sends
a plain message; orchestra_easy_email renders the rich booking templates the
submodule ships (orchestra_booking_confirmed / _released / _cancelled).
Staff steps (operator validation, deposit-hold-failed, attendance, refund
review) are separate: they notify the resource managers, not the booker. Each
is an interaction_task carrying the order_review interaction, so the
operator is shown the order (its summary, and any constraint-policy violations)
before choosing an outcome, rather than deciding blind. Because the review is an
interaction, the same screen works whether the step is reached from the inbox,
a pull operation, or a signed link.
Cancelling a confirmed booking¶
A confirmed booking is cancelled two ways, both running the graceful workflow
path (deposit release, refund gate, cancellation notice) rather than a blunt
state change. They are the two arms of a cancel fork feeding one quorum join:
the first arm to signal cancelled wins, and if all arms time out the booking
stays confirmed, so neither can double-cancel.
Customer self-cancel. The confirmation email carries the cancel link.
Because the link is a signed, token-scoped URL that only exists once the cancel
step has parked, the confirmation is sent when that step opens (a
TokenParkedEvent subscriber), and it renders a "Cancel booking" button plus
the clickable link. Clicking lands the booker on a confirmation screen (with the
order summary, and a notice when the cancellation is past the refund deadline)
rather than cancelling on arrival, so an email client prefetching the link
cannot cancel a booking; only the explicit confirm signals cancelled. The
customer may self-cancel until the resource's cancellation_deadline (+ unit):
the workflow resolves it (earliest slot start minus the strictest resource
cutoff) into a yoyaku_cancel_deadline variable and the customer wait times out
then, so a link replayed after the cutoff hits a token that is no longer parked
and is a no-op. A zero cutoff keeps self-cancel open to the slot start.
Operator graceful cancel. An operator assigned to the resource
(the resource_managers audience) can cancel up to the slot start through the
n_cancel_window_operator node, a comment_operation that requires a
cancellation comment before it records cancelled. This is the graceful,
workflow-driven cancel; it is distinct from the admin full/forced cancel in
the orders operations column, which hard-cancels outside the workflow.
The refund gate is separate. refund_deadline (+ unit) decides only whether
a cancellation is refunded (resolved into a yoyaku_refund_deadline
variable): cancelling before it is refunded, later is allowed but not refunded.
cancellation_deadline bounds whether the customer may still self-cancel;
refund_deadline bounds whether the cancellation is refunded. They are
independent, so a resource can, say, refund up to two days out yet stop online
self-cancellation five days out.
Surfacing the operator window. The orders overview shows a Workflow
column listing the current operator's assigned operations for each order (the
graceful "Cancel booking" when assigned and before the slot starts), read from
orchestra_ui's pending-actions surface and kept separate from the standard
lifecycle operations column so the graceful cancel is never confused with the
admin full cancel. The column is contributed by yoyaku_orchestra_order through
the hook_yoyaku_order_overview_columns / hook_yoyaku_order_overview_cell
extension point, which any module may use to add its own order column.
Overriding an email template¶
orchestra_easy_email resolves a template with a Drupal-suggestion-style
cascade, most specific first, over the instance tenant, the workflow,
the notification type, and an optional suffix:
orchestra_<tenant>_<workflow>_<type>_<suffix>
orchestra_<tenant>_<workflow>_<type>
orchestra_<tenant>_<type>_<suffix>
orchestra_<tenant>_<type>
orchestra_<workflow>_<type>_<suffix>
orchestra_<workflow>_<type>
orchestra_<type>_<suffix>
orchestra_<type>
orchestra_generic
Empty dimensions are skipped and the first existing template wins. The tenant
comes from the instance, so per-tenant (for example per-domain) templates need
no wiring: ship orchestra_<tenant>_booking_confirmed and it is used for that
tenant's bookings. The suffix comes from an
orchestra_notification_template_suffix process variable for any other
per-context variant (orchestra_booking_confirmed_vip). Everything falls back
to the plain orchestra_<type> default with no code.
Division of responsibility¶
flowchart LR
subgraph O[Orchestra]
P[Process / steps / timers / approvals]
end
subgraph B[Booking]
CAP[Capacity / holds / availability]
end
P -->|hold / extend / confirm / release| CAP
CAP -->|lifecycle events| P
Booking can also react to nothing in Orchestra and simply be driven by it;
or a booking event (e.g. CONFIRMED) can in turn signal an
Orchestra token. The two compose in either direction.