diff --git a/src/ApiClient/PayPlugApiClientFactory.php b/src/ApiClient/PayPlugApiClientFactory.php index 6b7d8f8a..6e6f641d 100644 --- a/src/ApiClient/PayPlugApiClientFactory.php +++ b/src/ApiClient/PayPlugApiClientFactory.php @@ -21,6 +21,18 @@ public function __construct( ) { } + /** + * Channel-ambiguous: since PRE-3628 several enabled gateway configs may share a factory name — + * one per channel — and findOneBy() then returns an arbitrary one of them, so the client this + * returns may carry another channel's account credentials. + * + * @internal Kept off {@see PayPlugApiClientFactoryInterface} so no application class can reach + * it; the sole remaining callers are the `payplug_sylius_payplug_plugin.api_client.*` + * service-factory definitions in config/services/client.xml, which are #[Autowire]d + * into seven services that have no payment method in scope. Use + * {@see self::createForPaymentMethod()} everywhere else. Removed once those + * singletons are made channel-aware — the open half of PRE-3682. + */ public function create(string $factoryName): PayPlugApiClientInterface { /** @var GatewayConfigInterface|null $gatewayConfig */ diff --git a/src/ApiClient/PayPlugApiClientFactoryInterface.php b/src/ApiClient/PayPlugApiClientFactoryInterface.php index 12b96f2a..70cb1912 100644 --- a/src/ApiClient/PayPlugApiClientFactoryInterface.php +++ b/src/ApiClient/PayPlugApiClientFactoryInterface.php @@ -8,7 +8,12 @@ interface PayPlugApiClientFactoryInterface { - public function create(string $factoryName): PayPlugApiClientInterface; - + /** + * The only way to obtain a client from application code. Resolving one by factory name is + * deliberately absent: since PRE-3628 several enabled gateway configs may share a factory name + * — one per channel — so a name-based lookup returns an arbitrary one of them and can sign a + * request for channel A with channel B's account credentials. Keeping that signature off this + * interface makes the compiler, rather than review, the guard against reintroducing it. + */ public function createForPaymentMethod(PaymentMethodInterface $paymentMethod): PayPlugApiClientInterface; } diff --git a/src/Controller/IntegratedPaymentController.php b/src/Controller/IntegratedPaymentController.php index a5b53925..d8474560 100644 --- a/src/Controller/IntegratedPaymentController.php +++ b/src/Controller/IntegratedPaymentController.php @@ -5,7 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Controller; use Doctrine\ORM\EntityManagerInterface; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Creator\PayPlugPaymentDataCreator; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; @@ -35,7 +35,7 @@ public function __construct( private RepositoryInterface $paymentMethodRepository, private OrderRepositoryInterface $orderRepository, private PayPlugPaymentDataCreator $paymentDataCreator, - private PayPlugApiClientFactory $apiClientFactory, + private PayPlugApiClientFactoryInterface $apiClientFactory, private EntityManagerInterface $entityManager, private LoggerInterface $logger, ) { @@ -74,8 +74,7 @@ public function initPaymentAction(Request $request, int $paymentMethodId): Respo } $payment->setMethod($paymentMethod); - $factoryName = $paymentMethod->getGatewayConfig()?->getFactoryName(); - if (PayPlugGatewayFactory::FACTORY_NAME !== $factoryName) { + if (PayPlugGatewayFactory::FACTORY_NAME !== $paymentMethod->getGatewayConfig()?->getFactoryName()) { throw new BadRequestHttpException('Unsupported payment method of Integrated Payment'); } @@ -84,7 +83,7 @@ public function initPaymentAction(Request $request, int $paymentMethodId): Respo $paymentData['integration'] = PayPlugApiClientInterface::INTEGRATED_PAYMENT_INTEGRATION; $this->logger->debug('Payplug Payment data for creation', $paymentData->getArrayCopy()); - $apiClient = $this->apiClientFactory->create($factoryName); + $apiClient = $this->apiClientFactory->createForPaymentMethod($paymentMethod); $payplugPayment = $apiClient->createPayment($paymentData->getArrayCopy()); $this->logger->debug('PayPlug payment created', (array) $payplugPayment); diff --git a/src/Controller/IpnAction.php b/src/Controller/IpnAction.php index 6f2332ff..ec4709e0 100644 --- a/src/Controller/IpnAction.php +++ b/src/Controller/IpnAction.php @@ -90,7 +90,7 @@ public function __invoke(Request $request): JsonResponse if ( !$paymentMethod->getGatewayConfig() instanceof GatewayConfigInterface || - !\in_array($factoryName = $paymentMethod->getGatewayConfig()->getFactoryName(), [ + !\in_array($paymentMethod->getGatewayConfig()->getFactoryName(), [ PayPlugGatewayFactory::FACTORY_NAME, OneyGatewayFactory::FACTORY_NAME, BancontactGatewayFactory::FACTORY_NAME, @@ -100,7 +100,7 @@ public function __invoke(Request $request): JsonResponse return new JsonResponse(null, Response::HTTP_UNAUTHORIZED); } - $this->payPlugApiClient = $this->apiClientFactory->create($factoryName); + $this->payPlugApiClient = $this->apiClientFactory->createForPaymentMethod($paymentMethod); try { $resource = $this->payPlugApiClient->treat($input); diff --git a/src/Controller/OneClickAction.php b/src/Controller/OneClickAction.php index a1e27ab2..9128e584 100644 --- a/src/Controller/OneClickAction.php +++ b/src/Controller/OneClickAction.php @@ -5,7 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Controller; use PayPlug\SyliusPayPlugPlugin\Action\Api\ApiAwareTrait; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use Payum\Core\ApiAwareInterface; use Payum\Core\GatewayAwareInterface; use Payum\Core\GatewayAwareTrait; @@ -33,7 +33,7 @@ class OneClickAction extends AbstractController implements GatewayAwareInterface public function __construct( private PaymentRepositoryInterface $paymentRepository, private Payum $payum, - private PayPlugApiClientFactory $payPlugApiClientFactory, + private PayPlugApiClientFactoryInterface $payPlugApiClientFactory, ) { } diff --git a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php index 0d889031..7237b12e 100644 --- a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php +++ b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php @@ -5,7 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints; use Payplug\Exception\UnauthorizedException; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\Checker\OneyChecker; use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; @@ -18,7 +18,7 @@ final class IsOneyEnabledValidator extends ConstraintValidator { - public function __construct(private PayPlugApiClientFactory $apiClientFactory) + public function __construct(private PayPlugApiClientFactoryInterface $apiClientFactory) { } diff --git a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php index 328b47e8..eaf8f752 100644 --- a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php +++ b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php @@ -5,7 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Gateway\Validator\Constraints; use Payplug\Exception\UnauthorizedException; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -13,7 +13,7 @@ final class PayplugPermissionValidator extends ConstraintValidator { - public function __construct(private PayPlugApiClientFactory $apiClientFactory) + public function __construct(private PayPlugApiClientFactoryInterface $apiClientFactory) { } diff --git a/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php b/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php index 4b16ee5b..b876a361 100644 --- a/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php +++ b/src/PaymentProcessing/CaptureAuthorizedPaymentProcessor.php @@ -4,7 +4,7 @@ namespace PayPlug\SyliusPayPlugPlugin\PaymentProcessing; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Handler\PaymentNotificationHandler; @@ -18,7 +18,7 @@ final class CaptureAuthorizedPaymentProcessor { public function __construct( - private PayPlugApiClientFactory $apiClientFactory, + private PayPlugApiClientFactoryInterface $apiClientFactory, private PaymentNotificationHandler $paymentNotificationHandler, ) { } diff --git a/src/Provider/SupportedMethodsProvider.php b/src/Provider/SupportedMethodsProvider.php index 5506ed90..f285683b 100644 --- a/src/Provider/SupportedMethodsProvider.php +++ b/src/Provider/SupportedMethodsProvider.php @@ -63,8 +63,8 @@ public function provide( $memoKey = $this->accountMemoKey($gatewayConfig); $account = $accounts[$memoKey] ??= $this->clientFactory->createForPaymentMethod($paymentMethod)->getAccount(); - $authorizedCurrencies = $this->resolveAuthorizedCurrencies($account, $factoryName); - $allowedCountries = $this->resolveAllowedCountries($account, $factoryName); + $authorizedCurrencies = $this->resolveAuthorizedCurrencies($account, $gatewayConfig); + $allowedCountries = $this->resolveAllowedCountries($account, $gatewayConfig); if ($billingCountryCode !== null && $allowedCountries !== [] && !\in_array($billingCountryCode, $allowedCountries, true)) { unset($supportedMethods[$key]); @@ -189,29 +189,30 @@ private function accountMemoKey(GatewayConfigInterface $gatewayConfig): string } /** + * Both resolvers below read the factory name off the gateway config the $account was fetched + * for, rather than off provide()'s $factoryName argument. The loop guard above makes the two + * equal today, but keeping the account payload and the key used to index it sourced from the + * same config is what stops the pair drifting apart if that guard is ever relaxed. + * * @param array $account * * @return array */ - private function resolveAuthorizedCurrencies(array $account, string $factoryName): array + private function resolveAuthorizedCurrencies(array $account, GatewayConfigInterface $gatewayConfig): array { - $underscorePos = strpos($factoryName, '_'); - $paymentMethodKey = false !== $underscorePos ? substr($factoryName, $underscorePos + 1) : null; - - return $this->amountRangeResolver->resolve($account, $paymentMethodKey); + return $this->amountRangeResolver->resolve($account, $this->paymentMethodKey($gatewayConfig)); } /** * @param array $account */ - private function resolveAllowedCountries(array $account, string $factoryName): array + private function resolveAllowedCountries(array $account, GatewayConfigInterface $gatewayConfig): array { - $underscorePos = strpos($factoryName, '_'); - if ($underscorePos === false) { + $pmKey = $this->paymentMethodKey($gatewayConfig); + if (null === $pmKey) { return []; } - $pmKey = substr($factoryName, $underscorePos + 1); $paymentMethods = $account['payment_methods'] ?? []; Assert::isArray($paymentMethods); $pmData = $paymentMethods[$pmKey] ?? []; @@ -226,4 +227,20 @@ private function resolveAllowedCountries(array $account, string $factoryName): a return $allowedCountries; } + + /** + * The `/account` payload keys each PPRO method under the factory name's suffix — `payplug_oney` + * is advertised as `oney`. A suffix-less factory name (`payplug`) is the card gateway, which + * has no such sub-payload. + */ + private function paymentMethodKey(GatewayConfigInterface $gatewayConfig): ?string + { + // provide()'s loop guard has already matched this config against a non-null factory name, + // so the null coalesce is unreachable from there; it keeps the helper total for any later + // caller, and an empty name carries no suffix anyway. + $factoryName = $gatewayConfig->getFactoryName() ?? ''; + $underscorePos = strpos($factoryName, '_'); + + return false !== $underscorePos ? substr($factoryName, $underscorePos + 1) : null; + } } diff --git a/src/Resolver/PaymentStateResolver.php b/src/Resolver/PaymentStateResolver.php index 1c8de161..9d749928 100644 --- a/src/Resolver/PaymentStateResolver.php +++ b/src/Resolver/PaymentStateResolver.php @@ -7,7 +7,7 @@ use Doctrine\ORM\EntityManagerInterface; use Payplug\Resource\Payment; use Payplug\Resource\PaymentAuthorization; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Component\Core\Model\PaymentInterface; @@ -19,7 +19,7 @@ final class PaymentStateResolver implements PaymentStateResolverInterface { public function __construct( private StateMachineInterface $stateMachine, - private PayPlugApiClientFactory $payPlugApiClientFactory, + private PayPlugApiClientFactoryInterface $payPlugApiClientFactory, private EntityManagerInterface $paymentEntityManager, ) { } diff --git a/tests/Behat/Mocker/PayPlugApiClientFactory.php b/tests/Behat/Mocker/PayPlugApiClientFactory.php index a143f64d..913e7313 100644 --- a/tests/Behat/Mocker/PayPlugApiClientFactory.php +++ b/tests/Behat/Mocker/PayPlugApiClientFactory.php @@ -6,6 +6,7 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; +use Sylius\Component\Payment\Model\PaymentMethodInterface; use Symfony\Component\DependencyInjection\ContainerInterface; class PayPlugApiClientFactory implements PayPlugApiClientFactoryInterface @@ -22,7 +23,11 @@ public function __construct(ContainerInterface $container, string $serviceName) $this->serviceName = $serviceName; } - public function create(string $factoryName, ?string $key = null): PayPlugApiClientInterface + /** + * The Behat suites stub one PayPlug account for the whole scenario, so the payment method is + * ignored here — the mocked client is the same whichever one is passed. + */ + public function createForPaymentMethod(PaymentMethodInterface $paymentMethod): PayPlugApiClientInterface { return new PayPlugApiClient($this->container, $this->serviceName); } diff --git a/tests/PHPUnit/ApiClient/PayPlugApiClientFactoryTest.php b/tests/PHPUnit/ApiClient/PayPlugApiClientFactoryTest.php index a072416c..2b49a754 100644 --- a/tests/PHPUnit/ApiClient/PayPlugApiClientFactoryTest.php +++ b/tests/PHPUnit/ApiClient/PayPlugApiClientFactoryTest.php @@ -4,6 +4,7 @@ namespace Tests\PayPlug\SyliusPayPlugPlugin\PHPUnit\ApiClient; +use Doctrine\Common\Collections\ArrayCollection; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException; @@ -13,6 +14,8 @@ use PayplugUnifiedCore\Contracts\ITokenCache; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\PaymentMethodInterface as CorePaymentMethodInterface; use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Payment\Model\PaymentMethodInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; @@ -160,13 +163,76 @@ public function testCreateForPaymentMethod_withCachedToken_doesNotCallTheTokenEn $this->factory->createForPaymentMethod($paymentMethod); } - private function buildGatewayConfig(bool $isLive): GatewayConfigInterface&MockObject + // ------------------------------------------------------------------------- + // createForPaymentMethod() — credentials are scoped to the payment method, not the factory name + // ------------------------------------------------------------------------- + + /** + * Since PRE-3628 several enabled gateway configs may share a factory name — one per channel. + * `findOneBy(['factoryName' => ...])` then resolves to an arbitrary one of them, so a client + * built that way can sign a request for channel A with channel B's account credentials. + * createForPaymentMethod() must read the credentials off the payment method's own gateway + * config and never consult the repository; routing it back through that lookup is the + * production change that makes this test fail. + */ + public function testCreateForPaymentMethod_withTwoChannelsSharingAFactoryName_usesEachChannelsOwnCredentials(): void { + $frConfig = $this->buildGatewayConfig(isLive: false, clientId: 'client_fr', clientSecret: 'secret_fr'); + $deConfig = $this->buildGatewayConfig(isLive: false, clientId: 'client_de', clientSecret: 'secret_de'); + + // Both rows are enabled and carry factoryName 'payplug', so Doctrine is free to return + // either one; the FR row stands in for "whichever one it picked". + $this->gatewayConfigRepository->method('findOneBy')->willReturn($frConfig); + + $this->tokenCache->method('get')->willReturn(null); // cache miss for both client ids + + /** @var list $sentCredentials */ + $sentCredentials = []; + $this->oauthHttpClient->method('post')->willReturnCallback( + function (string $url, array $formParams, array $headers = []) use (&$sentCredentials): array { + $sentCredentials[] = $headers['Authorization']; + + return [ + 'status' => 200, + 'body' => json_encode(['access_token' => 'jwt', 'expires_in' => 300, 'token_type' => 'Bearer']), + ]; + }, + ); + + $this->factory->createForPaymentMethod($this->buildPaymentMethodOnChannel('FR', $frConfig)); + $this->factory->createForPaymentMethod($this->buildPaymentMethodOnChannel('DE', $deConfig)); + + self::assertSame([ + 'Basic ' . base64_encode('client_fr:secret_fr'), + 'Basic ' . base64_encode('client_de:secret_de'), + ], $sentCredentials); + } + + private function buildPaymentMethodOnChannel( + string $channelCode, + GatewayConfigInterface $gatewayConfig, + ): PaymentMethodInterface&MockObject { + $channel = $this->createMock(ChannelInterface::class); + $channel->method('getCode')->willReturn($channelCode); + + $paymentMethod = $this->createMock(CorePaymentMethodInterface::class); + $paymentMethod->method('isEnabled')->willReturn(true); + $paymentMethod->method('getChannels')->willReturn(new ArrayCollection([$channel])); + $paymentMethod->method('getGatewayConfig')->willReturn($gatewayConfig); + + return $paymentMethod; + } + + private function buildGatewayConfig( + bool $isLive, + string $clientId = 'client', + string $clientSecret = 'secret', + ): GatewayConfigInterface&MockObject { $gatewayConfig = $this->createMock(GatewayConfigInterface::class); $gatewayConfig->method('getConfig')->willReturn([ 'live' => $isLive, - 'live_client' => ['client_id' => 'client_live', 'client_secret' => 'secret_live'], - 'test_client' => ['client_id' => 'client_test', 'client_secret' => 'secret_test'], + 'live_client' => ['client_id' => $clientId, 'client_secret' => $clientSecret], + 'test_client' => ['client_id' => $clientId, 'client_secret' => $clientSecret], ]); $gatewayConfig->method('getFactoryName')->willReturn('payplug'); diff --git a/tests/PHPUnit/Controller/IntegratedPaymentControllerTest.php b/tests/PHPUnit/Controller/IntegratedPaymentControllerTest.php new file mode 100644 index 00000000..0f0122ae --- /dev/null +++ b/tests/PHPUnit/Controller/IntegratedPaymentControllerTest.php @@ -0,0 +1,102 @@ +paymentMethodRepository = $this->createMock(RepositoryInterface::class); + $this->cartContext = $this->createMock(CartContextInterface::class); + $this->paymentDataCreator = $this->createMock(PayPlugPaymentDataCreator::class); + $this->apiClientFactory = $this->createMock(PayPlugApiClientFactoryInterface::class); + + $this->controller = new IntegratedPaymentController( + $this->cartContext, + $this->paymentMethodRepository, + $this->createMock(OrderRepositoryInterface::class), + $this->paymentDataCreator, + $this->apiClientFactory, + $this->createMock(EntityManagerInterface::class), + $this->createMock(LoggerInterface::class), + ); + } + + /** + * The iframe posts a payment method id, so the account the payment is created on must be the + * one configured on *that* payment method. Resolving the client by factory name instead would + * pick an arbitrary one of the several gateway configs that may now share it — one per channel + * since PRE-3628 — and create the payment on another channel's PayPlug account. + */ + public function testInitPayment_createsThePaymentOnTheAccountOfTheSubmittedPaymentMethod(): void + { + $paymentMethod = $this->paymentMethodWithFactoryName(PayPlugGatewayFactory::FACTORY_NAME); + $this->paymentMethodRepository->method('find')->with(42)->willReturn($paymentMethod); + + $payment = $this->createMock(PaymentInterface::class); + $order = $this->createMock(OrderInterface::class); + $order->method('getLastPayment')->willReturn($payment); + $this->cartContext->method('getCart')->willReturn($order); + + $this->paymentDataCreator->method('create')->willReturn(new ArrayObject()); + + $payplugPayment = $this->createMock(PayplugPayment::class); + $payplugPayment->id = 'pay_1'; + $payplugPayment->is_live = false; + + $apiClient = $this->createMock(PayPlugApiClientInterface::class); + $apiClient->method('createPayment')->willReturn($payplugPayment); + + $this->apiClientFactory->expects(self::once()) + ->method('createForPaymentMethod') + ->with(self::identicalTo($paymentMethod)) + ->willReturn($apiClient); + + $response = $this->controller->initPaymentAction(Request::create('/payplug/integrated-payment/init/42'), 42); + + self::assertSame(201, $response->getStatusCode()); + } + + private function paymentMethodWithFactoryName(string $factoryName): PaymentMethodInterface&MockObject + { + $gatewayConfig = $this->createMock(GatewayConfigInterface::class); + $gatewayConfig->method('getFactoryName')->willReturn($factoryName); + + $paymentMethod = $this->createMock(PaymentMethodInterface::class); + $paymentMethod->method('getGatewayConfig')->willReturn($gatewayConfig); + + return $paymentMethod; + } +} diff --git a/tests/PHPUnit/Controller/IpnActionTest.php b/tests/PHPUnit/Controller/IpnActionTest.php index 7b50599e..f8ad7e49 100644 --- a/tests/PHPUnit/Controller/IpnActionTest.php +++ b/tests/PHPUnit/Controller/IpnActionTest.php @@ -69,14 +69,21 @@ private function paymentWithGatewayConfig(): PaymentInterface&MockObject return $payment; } - public function testInvoke_forALegacyPayment_goesThroughTheSdk(): void + /** + * The webhook must sign its `treat()` call with the credentials of the account the payment was + * actually created on. Resolving the client by factory name instead would pick an arbitrary one + * of the several gateway configs that may now share it — one per channel since PRE-3628. + */ + public function testInvoke_forALegacyPayment_buildsTheApiClientFromThePaymentsOwnMethod(): void { $payment = $this->paymentWithGatewayConfig(); $this->paymentRepository->method('findOneByPayPlugPaymentId')->willReturn($payment); $request = Request::create('/payplug/ipn', 'POST', content: \json_encode(['id' => 'pay_1'])); - $this->apiClientFactory->expects(self::once())->method('create')->with(PayPlugGatewayFactory::FACTORY_NAME) + $this->apiClientFactory->expects(self::once()) + ->method('createForPaymentMethod') + ->with(self::identicalTo($payment->getMethod())) ->willReturn($this->createMock(PayPlugApiClientInterface::class)); $response = $this->action->__invoke($request);