Issue #3614422: An abandoned checkout holds its places for ever: locked_expires was written and never read

Locking an order suspends its holds, so nothing but the lock lifting can free them, and the payment outcome that lifts it may never arrive.

What was missing

Most abandonments were already covered: kessai's reaper expires a pending payment past its deadline, which unlocks the order. Two were not.

  • A gateway may give its payment no deadline at all (an offline payment settled by hand days later must not lapse at thirty minutes).
  • A payment that leaves the pending state without resolving the order (authorized in a reserve-first checkout, or captured while the place step never ran) is never swept.

Either way the places stayed held with no deadline and no owner until an operator found the order by hand.

The backstop for exactly this was designed and never finished. locked_expires promises in its own description that "a locked (checkout) order times out and is released", and lockTransaction() takes a $ttl documented as the checkout timeout. Nothing read the field, and the only caller passed no TTL, so the stored value was always NULL.

Both halves wired

  • OrderLockSubscriber derives the TTL from the payment's own deadline plus an hour, so the sweep can never race a payer who could still answer, and falls back to a day when the payment carries no deadline.
  • LockedCheckoutHooks sweeps on cron: a lapsed lock returns the order to an editable basket, whose holds are re-armed with each resource's own TTL so the ordinary hold cron can reap them.
  • An order whose money did arrive is left locked and logged instead. Re-arming the holds of a paid booking would let them lapse, which is worse than leaving an operator something to finish.

Why this before #3614424

[#3614424] wants the checkout amount computed after the lock is taken. The step that brackets payment (PaymentInteraction::respond()) lives in orchestra and cannot know about yoyaku orders, so the lock has to be taken by yoyaku's payable resolver, which runs when the amount is computed: at the landing page rather than at the redirect to the gateway. That widens the abandonment window, so the window needed a floor first.

Testing, and one caveat

LapsedCheckoutReclaimTest covers the deadline being stamped, a payment without a deadline still bounding the lock, cron reclaiming a lapsed checkout (asserting the hold is re-armed, which is what makes the places reclaimable), cron leaving a paid order locked, and cron leaving a live checkout alone.

These tests were not run locally. A concurrent local change to kessai in another working session removed PaymentInterface::KIND_PAYMENT and reordered $kind into the signatures of create(), authorizeToken() and chargeToken(), so every yoyaku_payment test fatals on this machine regardless of this branch. The kessai-free suites pass, and CI resolves kessai from its own dependency, so this pipeline is the first real run of the new test. If it is red, that is mine and I will fix it rather than blame the environment.

Merge request reports

Loading
Loading