Nothing checks the constraint policies before payment, so a violating order can be charged and only refused afterwards

Closes the gap between the form and the confirmation, which is exactly where the money moves.

Why the gap is real

The policies were checked when the booker registered and are checked again when the order is confirmed. An order can comply at the first and not at the second: a per-booker limit cannot be counted until the order names a booker, so every basket the same booker fills afterwards lands in that gap. The booker paid, was refused at confirmation, and a workflow cancelling the order behind them had taken money for a booking the rules never allowed.

What it adds

  • OrderManager::checkoutViolations(), running the same policies at the PolicyCheckpoint::Checkout checkpoint, which had no caller until now. A dedicated method rather than a checkpoint argument on validateOrder(), whose callers all mean the confirmation and whose contract says so.
  • CheckoutPolicySubscriber, answering Orchestra's CheckoutEvents::CHECKING (added in orchestra [#3614892]) with one reason per violation. Separate from CheckoutLockSubscriber: that one holds the order still, this one judges it. They meet only in the order the step dispatches them, so what is judged is an order nothing can add to.

The booker is stopped twice over, both times before any money moves: on the payment page the reasons are listed above the amount and Go to payment is drawn inert, and on the click the checkout is closed and they land back on that page with the reasons.

Every violation is reported, not the first. A booker fixes what they are told about, so one rule at a time means editing the basket once per rule to buy nothing.

An operator waiver needs no code here: ConstraintPolicyManager::evaluate() already reports nothing for a waived order at every checkpoint but the hold.

What deliberately does not reach the booker

Deferrals. A policy that could not be asked is not a policy that refused, and "this limit could not be checked" is an operator's concern: the Order review step shows it and the payment page does not.

Tests

New CheckoutPolicyTest: an order over a per-booker limit refused in the policy's own words, a compliant one not, two attachments giving two reasons, a waived order not refused, a policy still waiting for a booker refusing nothing, and a non-booking run left alone.

Both refusal cases were run against the code without the subscriber first and seen to fail. yoyaku_order and yoyaku_payment kernel suites green (24/24). phpcs (Drupal, DrupalPractice, warnings included), the CI phpstan ruleset and cspell all clean.

docs/constraint-policies.md gains a The checkout section and its checkpoint table row is corrected: the order is pending while the page is drawn and locked on the click. docs/payment.md gains the subscription. No new interface strings, so no .po change: the sentences the booker reads are the policies' own, already translated.

Merge request reports

Loading
Loading