Nothing lets a domain module refuse a checkout, so a run its rules forbid is priced and charged

A third checkout event, CheckoutEvents::CHECKING, so a domain module can refuse a run the money should never be taken for.

What it changes

CheckoutEvent gains refuse($reason), refusals() and refused(). Propagation continues, so every subscriber gets to speak and a run that breaks two rules is told both at once instead of being sent back twice.

The step fires it at the two moments a payer can be turned away, and a subscriber answers both the same way:

  • Drawing the page. The reasons are listed above what is due and the payment control becomes an inert span (aria-disabled, aria-describedby pointing at the list) instead of a link whose only outcome is this same page with an apology on it.
  • The click, immediately after OPENING. After, so the answer is given about a subject something is already holding still; before routeOutcome() and resolve(), so a refused run is never priced and no payment row is created. The step then dispatches CLOSING, which releases the hold, and renders the page the click came from with the reasons on it.

A subscriber therefore holds nothing while answering: the first firing is a page render, and freezing a subject because somebody opened a page takes it away from a visitor who has not asked to pay.

A run that owes nothing is asked too

no_payment normally never reaches the landing page at all: it is signalled onward and a flow carries it away. A refused one now stops on that page, because it is the only page that visitor would have seen and the reasons have to be read somewhere. Without it, a free event escapes every rule a paid one obeys.

The "nothing to pay" line is suppressed when there are reasons: they are the message, and there is no payment to talk about.

Tests

New CheckoutRefusalTest, 11 cases: every reason listed, announced with role="alert", the control inert and pointing at the reasons, rendering only previews and asks, the click logging opened, checked, closed with nothing priced and nothing charged, the free run held back on the page with no "nothing to pay" line, and the allowed paths still passing through and still reaching the gateway.

CheckoutOrderingTest's three sequence assertions gained checked, which is the honest consequence of the step asking on every path. Its harness moved to a DrivesThePaymentStep trait that both classes use.

Whole orchestra_payment kernel suite green (8/8). phpcs (Drupal, DrupalPractice, warnings included), the CI phpstan ruleset and cspell all clean.

docs/payment.md gains the event row and a Refusing a checkout section; fr.po carries the one new string.

Consumer

yoyaku [#3614531] subscribes and answers from its constraint policies. This is the generic mechanism only.

Merge request reports

Loading
Loading