Issue #3622781: Resolve a payment account, not a gateway plugin
Closes [#3622781].
kessai now records the account a payment was taken against rather than the provider, so a site holding two contracts at one provider holds two accounts and the gateway plugin id names neither. The argument kept its name and position, so nothing failed to compile and the break was silent.
The defect
PaymentGatewayResolver::isInstalled() validated a resolved id with the gateway plugin manager's hasDefinition(). A real account fails that check, so the tenant override and the site default were both discarded and every booking fell through to the manual fallback: a payment that should have been taken by card was created against the offline gateway, with nothing raising and an operator left expected to collect by hand.
It now asks kessai isGatewayAvailable() — the same question the creation door asks, so a check here and a refusal there cannot disagree.
The rest
- The site-wide default select was built from the gateway plugin list, so it offered ids kessai refuses, could not name any account whose machine name is not its plugin's, and its
#default_valuefell back to empty when the stored id was absent, making a configured account read as "None (manual)". It now listsgetAvailableGateways(). BookingPaymentHooksalready held the payment client, so this drops the gateway plugin manager rather than swapping it.- The step override's help text named a plugin id; it now names an account and links to the accounts list.
PaymentGatewayResolverInterfacesaid "gateway plugin id"; it says account id, and states that themanualfallback is the account kessai ships, so a site that deleted or disabled it gets a refusal rather than a payment taken through an account nobody chose.
Tests
Two kernel tests pin the defect: an account named for itself resolves where it used to fall back to manual, and a retired account is skipped. The existing resolution-chain test also needed installConfig(['kessai', 'kessai_simulator']), because the accounts arrive with those modules' config and without them the site has no account at all.
Depends on kessai
This calls isGatewayAvailable() and getAvailableGateways(), added in kessai 3621924, now merged. CI installs drupal/kessai (dev-1.x 409eb8d), the commit that carries them.
Two further commits, both from reading CI rather than the badge
phpstan: the new test chaineddisable()on a storage load. kessai's own test can do that because kessai ships a phpstan entity mapping naming the class; yoyaku ships none, so the load answersEntityInterfaceand the method is undefined. Narrowed before the call.yoyaku_placement/src/Controller/PinnedPlaceController.php:$place?->getRow() ?? ''tripsnullsafe.neverNull, becauseloadAll()is typedarraywith untyped elements so the value ismixed. This line is pre-existing on1.xand the lane reports it against every branch, so it is fixed here rather than left failing under this MR's badge. Written as the five sibling lines around it are, an explicit NULL test. The''fallback is kept deliberately:getRow()returns?string, so swapping?->for->alone would have put NULL in a table cell. TypingloadAll()'s elements is the deeper fix and is not this branch's — the entities come back asEntityInterface, which has nogetRow(),label()orgetSectionId(), so every use in that method would need narrowing first.
phpcs Drupal,DrupalPractice over the changed modules is clean (0 errors, 0 warnings).
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to write the code and tests on the merge request. I reviewed and ran the work myself before posting it.)