Constraint policies are checked in three places: the registration form handler validates on submit and blocks with a form error, a workflow review step lists the violations, and confirmation validates and refuses. The payment step checks nothing.
So the window between the registration form passing and the order being confirmed is unguarded, and it is the window in which money moves. A booker whose order becomes a violation after the form, most plainly by making another booking in parallel, is sent to the gateway, charged, and only then refused at confirmation. In an automated workflow that refusal cancels the order and notifies the customer that it was rejected, so the money moved for a booking the rules never allowed.
This is not theoretical for the per-booker rules. They can only be answered once the order names a booker, which happens at the registration step, so anything that changes after it lands squarely in the gap.
What is needed
A check before the checkout opens, and a way for the payment step to refuse rather than proceed. The orchestra payment interaction dispatches CheckoutEvents::OPENING before it prices anything, but a subscriber cannot currently refuse it: the event carries the instance, the token and the timeout, and nothing else. It needs a veto, the same shape the booking engine already gives its own pre-hold event.
The domain half then subscribes and asks the policies, exactly as the registration handler does. The generic half must stay generic: orchestra_payment cannot know what a booking is, so the refusal has to travel as a reason string on the event rather than as any knowledge of orders.
The pay control should also be rendered disabled, with the reason beside it, rather than accepting a click that will be refused. A control that looks live and then does nothing is the same defect that was fixed for the seat map in #3614349: Cap what one transaction may take of a tariff, and move the policy system to the engine.
Scope
Two modules, so two merge requests: the veto seam in orchestra_payment, and the subscriber plus the disabled control in yoyaku. The orchestra half is the blocker.
Not in scope
Whether the policies themselves are modelled correctly is a separate question, filed on its own. This issue only closes the window.
Issue fork yoyaku-3614531
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #4
mably commented