The payment step reads its amount from whatever the resolver prices at the moment it is asked, and nothing is holding that subject still. PaymentInteraction::respond() calls routeOutcome() and then resolve() on its way to creating the payment, and the only freeze in the whole sequence happens afterwards, when kessai announces PaymentEvents::INITIATED from the handoff route and a consumer locks whatever it owns. The order is therefore compute, create the payment, then freeze, and a subject the visitor can still edit is editable throughout the first two.

Rendering the landing page has the opposite problem. It prices as well, and a consumer must not freeze on that: a visitor reading the page may still be editing the very thing it prices, and may never come back. So there is currently no moment a consumer can act on that is both after the visitor has committed and before the amount is read.

Proposal: the step brackets the checkout with two events of its own. CheckoutEvents::OPENING fires on the click that commits to paying, before anything is priced, and a consumer holds whatever the run owns. CheckoutEvents::CLOSING fires on every path that opened a checkout and then does not charge, so a run that turns out to owe nothing is not left held. Pricing splits in two to match: preview() answers what to show, from an unfrozen subject, and resolve() answers what to charge and is called only between the two events. A resolver whose subject cannot move under the visitor implements only resolve(), and the base class derives the preview from it.

The figure the landing page displayed travels back with the click, signed with a CSRF token bound to the parked token, and is compared against what the frozen subject then prices. Agreeing, the step charges it. Differing, it charges nothing, closes the checkout, and shows the new total with an alert saying so. The signed figure is only ever compared and never charged, so a payer who edits it in the URL fails the signature and is treated exactly like a total that moved.

The payer window also has to end before the wait node timeout, or a payment can still be completed after that timeout has fired and the run has routed away. The step now stamps the payment with the shorter of its configured payment timeout and what is left of the node timeout. The same figure travels on the opening event, so a consumer holding something sizes its own deadline from the number the payer actually gets, and it reaches the provider unchanged, since kessai_worldline already clamps its hosted checkout session to what is left of the payment deadline. This makes a settlement arriving after the node timeout rare rather than impossible, which is all it can be: a slow provider or a retried callback can always land late, and the bridge already survives that by resuming nothing.

This is the domain-neutral half. The booking module locks its order on these events in #3614424: The checkout amount is computed before the order is locked, so the basket can still change between the two.

Issue fork orchestra-3614465

Command icon 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

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

  • mably committed 09ad8371 on 1.x
    fix: #3614465 Nothing holds the payment step's subject still while it...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.