Issue #3623039: Pin a step's payment through kessai's contract instead of on its table
The payment step remembers which workflow token asked for a payment, and whether the run wants the payer's stored token forgotten when it ends. Both were base fields this submodule added to kessai_payment, with a storage-schema subclass swapped in to index one of them.
That is a column on another project's table. It works only while orchestra and kessai share a database, it is not orchestra's table to shape, and a kessai settling payments on another host could not carry it at all. Several more places reached into that table by name to find a step's payments and to delete a gone run's.
Depends on kessai !79, which is merged.
The pin is payment metadata
Orchestra's own names and string values, which kessai stores, indexes and never reads. OrchestraPaymentEntityHooks, PaymentStorageSchema and the hook_entity_type_alter() that swapped it are deleted, along with the two base fields and their French.
- It travels with the payment request, so creating a pinned payment is one call rather than two, and one request rather than two against a kessai on another host. Nothing writes a pin afterwards: a reusable pending payment is one this same token's own lookup found, so it already carries that token's pin, and what a payment records is what was asked for when it was taken.
- The token a payment is pinned to is read off the payment, with no lookup at all. That is the direction a provider webhook, the expiry sweep and a back-office capture need, because each arrives carrying a payment and nothing else.
- The run's payments are one indexed lookup for the whole run, asked with a
PaymentQuery, rather than one lookup per token. - The run-end token cleanup is one index read. The retention flag holds the token id rather than a yes or a no, and is absent when the answer is no, so a run that flagged nothing — which is nearly every run — is answered without a payment being loaded at all.
- Deleting a gone run's payments goes through
deletePayments(), which takes their claims, refunds and reversals with them, where deleting the rows here left those behind.
The settle step settles the run's current payment
It takes the most recent of the run's open payments and releases the holds behind it, so the order it reads them in decides which authorization is captured and which is canceled. That order is stated where the payments are produced rather than assumed by the caller.
The dependency is the contract
kessai:kessai, and no production file names Drupal\kessai_engine. Which module settles a site's payments is the site's decision, and naming the engine here made it this module's: it put an engine on every site that installs this one, including a site whose payments are taken by a kessai somewhere else.
A dependency list is checked by nothing that runs — kernel tests name their own modules and resolve none — so DeclaredDependenciesTest installs the declared closure, transitively and nothing else, and asserts both that the payment client is there to inject and that no engine came with it.
Also
PinnedPayment's finders take verb+noun names and answer with payments rather than queries.- The subject is named with
PaymentSubjectand the request withPaymentRequest. - The French for the two removed base fields is dropped, which the project's translation audit reports as entries appearing in no file of their own project.
Checked
phpstan level 5, phpcs, cspell and the translation audit clean; orchestra_payment's kernel tests green. Functional and the next-major lanes run in CI.
AI-Generated: Yes (Claude Code was used to help produce this change.)