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_value fell back to empty when the stored id was absent, making a configured account read as "None (manual)". It now lists getAvailableGateways().
  • BookingPaymentHooks already 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.
  • PaymentGatewayResolverInterface said "gateway plugin id"; it says account id, and states that the manual fallback 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 chained disable() 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 answers EntityInterface and the method is undefined. Narrowed before the call.
  • yoyaku_placement/src/Controller/PinnedPlaceController.php: $place?->getRow() ?? '' trips nullsafe.neverNull, because loadAll() is typed array with untyped elements so the value is mixed. This line is pre-existing on 1.x and 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. Typing loadAll()'s elements is the deeper fix and is not this branch's — the entities come back as EntityInterface, which has no getRow(), label() or getSectionId(), 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.)

Edited by Frank Mably

Merge request reports

Loading
Loading