From adbff61dbe9ec4d45c340c573d9cd9c9cfae80ac Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Mon, 19 Sep 2022 16:52:01 +0200 Subject: [PATCH 1/3] Add new request factory to handle ExpireSession and AllSession --- src/Factory/AllSessionRequestFactory.php | 16 ++++++++++++++++ .../AllSessionRequestFactoryInterface.php | 12 ++++++++++++ src/Factory/ExpireSessionRequestFactory.php | 16 ++++++++++++++++ .../ExpireSessionRequestFactoryInterface.php | 12 ++++++++++++ src/Resources/config/services/factory.yaml | 8 +++++++- 5 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/Factory/AllSessionRequestFactory.php create mode 100644 src/Factory/AllSessionRequestFactoryInterface.php create mode 100644 src/Factory/ExpireSessionRequestFactory.php create mode 100644 src/Factory/ExpireSessionRequestFactoryInterface.php diff --git a/src/Factory/AllSessionRequestFactory.php b/src/Factory/AllSessionRequestFactory.php new file mode 100644 index 0000000..0cb5171 --- /dev/null +++ b/src/Factory/AllSessionRequestFactory.php @@ -0,0 +1,16 @@ + Date: Mon, 19 Sep 2022 16:52:27 +0200 Subject: [PATCH 2/3] Use Expire Session instead of Cancel PaymentIntent --- ...ncelExistingPaymentIntentExtensionSpec.php | 133 ++++++++++++++++-- .../CancelExistingPaymentIntentExtension.php | 60 +++++--- src/Resources/config/services/payum.yaml | 5 +- 3 files changed, 165 insertions(+), 33 deletions(-) diff --git a/spec/Extension/CancelExistingPaymentIntentExtensionSpec.php b/spec/Extension/CancelExistingPaymentIntentExtensionSpec.php index 084c6e7..85a3611 100644 --- a/spec/Extension/CancelExistingPaymentIntentExtensionSpec.php +++ b/spec/Extension/CancelExistingPaymentIntentExtensionSpec.php @@ -4,15 +4,19 @@ namespace spec\FluxSE\SyliusPayumStripePlugin\Extension; +use FluxSE\PayumStripe\Request\Api\Resource\AllInterface; use FluxSE\PayumStripe\Request\Api\Resource\CustomCallInterface; use FluxSE\SyliusPayumStripePlugin\Action\ConvertPaymentActionInterface; -use FluxSE\SyliusPayumStripePlugin\Factory\CancelPaymentIntentRequestFactoryInterface; +use FluxSE\SyliusPayumStripePlugin\Factory\AllSessionRequestFactoryInterface; +use FluxSE\SyliusPayumStripePlugin\Factory\ExpireSessionRequestFactoryInterface; use Payum\Core\Action\ActionInterface; use Payum\Core\Extension\Context; use Payum\Core\Extension\ExtensionInterface; use Payum\Core\GatewayInterface; use Payum\Core\Request\Convert; use PhpSpec\ObjectBehavior; +use Stripe\Checkout\Session; +use Stripe\Collection; use Stripe\PaymentIntent; use Stripe\SetupIntent; use Sylius\Component\Core\Model\PaymentInterface; @@ -20,9 +24,13 @@ final class CancelExistingPaymentIntentExtensionSpec extends ObjectBehavior { public function let( - CancelPaymentIntentRequestFactoryInterface $cancelPaymentIntentRequestFactory + ExpireSessionRequestFactoryInterface $expireSessionRequestFactory, + AllSessionRequestFactoryInterface $allSessionRequestFactory ): void { - $this->beConstructedWith($cancelPaymentIntentRequestFactory); + $this->beConstructedWith( + $expireSessionRequestFactory, + $allSessionRequestFactory + ); } public function it_is_initializable(): void @@ -30,7 +38,7 @@ public function it_is_initializable(): void $this->shouldBeAnInstanceOf(ExtensionInterface::class); } - public function it_do_nothing_when_action_is_not_the_convert_payment_action_targeted( + public function it_does_nothing_when_action_is_not_the_convert_payment_action_targeted( Context $context, ActionInterface $action ): void { @@ -39,7 +47,7 @@ public function it_do_nothing_when_action_is_not_the_convert_payment_action_targ $this->onExecute($context); } - public function it_do_nothing_when_payment_details_are_empty( + public function it_does_nothing_when_payment_details_are_empty( Context $context, ConvertPaymentActionInterface $action, Convert $request, @@ -54,7 +62,7 @@ public function it_do_nothing_when_payment_details_are_empty( $this->onExecute($context); } - public function it_do_nothing_when_payment_details_are_something_else_than_a_payment_intent( + public function it_does_nothing_when_payment_details_are_something_else_than_a_payment_intent( Context $context, ConvertPaymentActionInterface $action, Convert $request, @@ -71,14 +79,14 @@ public function it_do_nothing_when_payment_details_are_something_else_than_a_pay $this->onExecute($context); } - public function it_found_a_previous_payment_intent_and_cancel_it( + public function it_doesnt_found_the_related_session_and_do_nothing( Context $context, ConvertPaymentActionInterface $action, Convert $request, PaymentInterface $payment, GatewayInterface $gateway, - CancelPaymentIntentRequestFactoryInterface $cancelPaymentIntentRequestFactory, - CustomCallInterface $cancelPaymentIntentRequest + AllSessionRequestFactoryInterface $allSessionRequestFactory, + AllInterface $allSessionRequest ): void { $context->getAction()->willReturn($action); $context->getRequest()->willReturn($request); @@ -91,13 +99,110 @@ public function it_found_a_previous_payment_intent_and_cancel_it( 'object' => PaymentIntent::OBJECT_NAME, ]); - $cancelPaymentIntentRequest->beConstructedWith([$piId]); + $allSessionRequestFactory + ->createNew() + ->willReturn($allSessionRequest); - $cancelPaymentIntentRequestFactory - ->createNew($piId) - ->willReturn($cancelPaymentIntentRequest); + $allSessionRequest->setParameters([ + 'payment_intent' => $piId + ])->shouldBeCalled(); + $allSessionRequest->getApiResources()->willReturn(Collection::constructFrom(['data'=>[]])); - $gateway->execute($cancelPaymentIntentRequest)->shouldBeCalled(); + $gateway->execute($allSessionRequest)->shouldBeCalled(); + + $this->onExecute($context); + } + + public function it_founds_a_related_expired_session_and_does_nothing( + Context $context, + ConvertPaymentActionInterface $action, + Convert $request, + PaymentInterface $payment, + GatewayInterface $gateway, + AllSessionRequestFactoryInterface $allSessionRequestFactory, + AllInterface $allSessionRequest + ): void { + $context->getAction()->willReturn($action); + $context->getRequest()->willReturn($request); + $request->getSource()->willReturn($payment); + $context->getGateway()->willReturn($gateway); + + $piId = 'pi_test_0000000000000000000'; + $csId = 'cs_test_0000000000000000000'; + $payment->getDetails()->willReturn([ + 'id' => $piId, + 'object' => PaymentIntent::OBJECT_NAME, + ]); + + $allSessionRequestFactory + ->createNew() + ->willReturn($allSessionRequest); + + $allSessionRequest->setParameters([ + 'payment_intent' => $piId + ])->shouldBeCalled(); + $allSessionRequest->getApiResources()->willReturn(Collection::constructFrom([ + 'data'=>[ + [ + 'id' => $csId, + 'status' => Session::STATUS_EXPIRED, + ] + ] + ])); + + $gateway->execute($allSessionRequest)->shouldBeCalled(); + + $this->onExecute($context); + } + + public function it_founds_a_related_session_and_expires_it( + Context $context, + ConvertPaymentActionInterface $action, + Convert $request, + PaymentInterface $payment, + GatewayInterface $gateway, + AllSessionRequestFactoryInterface $allSessionRequestFactory, + AllInterface $allSessionRequest, + ExpireSessionRequestFactoryInterface $expireSessionRequestFactory, + CustomCallInterface $expireSessionRequest + ): void { + $context->getAction()->willReturn($action); + $context->getRequest()->willReturn($request); + $request->getSource()->willReturn($payment); + $context->getGateway()->willReturn($gateway); + + $piId = 'pi_test_0000000000000000000'; + $csId = 'cs_test_0000000000000000000'; + $payment->getDetails()->willReturn([ + 'id' => $piId, + 'object' => PaymentIntent::OBJECT_NAME, + ]); + + $allSessionRequestFactory + ->createNew() + ->willReturn($allSessionRequest); + + $allSessionRequest->setParameters([ + 'payment_intent' => $piId + ])->shouldBeCalled(); + $allSessionRequest->getApiResources()->willReturn(Collection::constructFrom([ + 'data'=>[ + [ + 'id' => $csId, + 'status' => Session::STATUS_OPEN, + ] + ] + ])); + + $gateway->execute($allSessionRequest)->shouldBeCalled(); + + $expireSessionRequest->beConstructedWith([$csId]); + + $expireSessionRequestFactory + ->createNew($csId) + ->willReturn($expireSessionRequest); + + $gateway->execute($expireSessionRequest)->shouldBeCalled(); $this->onExecute($context); } diff --git a/src/Extension/CancelExistingPaymentIntentExtension.php b/src/Extension/CancelExistingPaymentIntentExtension.php index 51f8ffe..972512b 100644 --- a/src/Extension/CancelExistingPaymentIntentExtension.php +++ b/src/Extension/CancelExistingPaymentIntentExtension.php @@ -5,26 +5,40 @@ namespace FluxSE\SyliusPayumStripePlugin\Extension; use FluxSE\SyliusPayumStripePlugin\Action\ConvertPaymentActionInterface; -use FluxSE\SyliusPayumStripePlugin\Factory\CancelPaymentIntentRequestFactoryInterface; +use FluxSE\SyliusPayumStripePlugin\Factory\AllSessionRequestFactoryInterface; +use FluxSE\SyliusPayumStripePlugin\Factory\ExpireSessionRequestFactoryInterface; use Payum\Core\Extension\Context; use Payum\Core\Extension\ExtensionInterface; use Payum\Core\Request\Convert; -use Stripe\Exception\ApiErrorException; +use Stripe\Checkout\Session; +use Stripe\Collection; use Stripe\PaymentIntent; use Sylius\Component\Core\Model\PaymentInterface; /** * This extension will cancel a PaymentIntent if there is an existant one * inside the payment details + * + * UPDATE [09/2022] : Instead of canceling the PaymentIntent now it will Expire the related session + * + * @see https://stripe.com/docs/api/payment_intents/cancel + * You cannot cancel the PaymentIntent for a Checkout Session. Expire the Checkout Session instead + * @see https://github.com/FLUX-SE/SyliusPayumStripePlugin/issues/32 */ final class CancelExistingPaymentIntentExtension implements ExtensionInterface { - /** @var CancelPaymentIntentRequestFactoryInterface */ - private $cancelPaymentIntentRequestFactory; + /** @var ExpireSessionRequestFactoryInterface */ + private $expireSessionRequestFactory; - public function __construct(CancelPaymentIntentRequestFactoryInterface $cancelPaymentIntentRequestFactory) - { - $this->cancelPaymentIntentRequestFactory = $cancelPaymentIntentRequestFactory; + /** @var AllSessionRequestFactoryInterface */ + private $allSessionRequestFactory; + + public function __construct( + ExpireSessionRequestFactoryInterface $expireSessionRequestFactory, + AllSessionRequestFactoryInterface $allSessionRequestFactory + ) { + $this->expireSessionRequestFactory = $expireSessionRequestFactory; + $this->allSessionRequestFactory = $allSessionRequestFactory; } public function onPreExecute(Context $context): void @@ -63,17 +77,29 @@ public function onExecute(Context $context): void } $gateway = $context->getGateway(); - $cancelPaymentIntentRequest = $this->cancelPaymentIntentRequestFactory->createNew($id); - - try { - $gateway->execute($cancelPaymentIntentRequest); - } catch (ApiErrorException $e) { - // Avoid error message like : - // "You cannot cancel this PaymentIntent because it has a status of canceled. - // Only a PaymentIntent with one of the following statuses may be canceled: - // requires_payment_method, requires_capture, requires_confirmation, - // requires_action, processing." + + //Retrieve the corresponding Session + $allSessionRequest = $this->allSessionRequestFactory->createNew(); + $allSessionRequest->setParameters([ + 'payment_intent' => $id, + ]); + + $gateway->execute($allSessionRequest); + + /** @var Collection $sessions */ + $sessions = $allSessionRequest->getApiResources(); + /** @var Session|null $session */ + $session = $sessions->first(); + if (null === $session) { + return; } + + if (Session::STATUS_OPEN !== $session->status) { + return; + } + + $expireSessionRequest = $this->expireSessionRequestFactory->createNew($session->id); + $gateway->execute($expireSessionRequest); } public function onPostExecute(Context $context): void diff --git a/src/Resources/config/services/payum.yaml b/src/Resources/config/services/payum.yaml index 0306ca3..ffeeba7 100644 --- a/src/Resources/config/services/payum.yaml +++ b/src/Resources/config/services/payum.yaml @@ -29,8 +29,9 @@ services: public: true class: FluxSE\SyliusPayumStripePlugin\Extension\CancelExistingPaymentIntentExtension arguments: - $cancelPaymentIntentRequestFactory: '@flux_se.sylius_payum_stripe.factory.cancel_payment_intent_request' + $expireSessionRequestFactory: '@flux_se.sylius_payum_stripe.factory.expire_session_request' + $allSessionRequestFactory: '@flux_se.sylius_payum_stripe.factory.all_session_request' tags: - name: payum.extension factory: stripe_checkout_session - alias: flux_se.sylius_payum_stripe.extension.cancel_existing_payment_intent \ No newline at end of file + alias: flux_se.sylius_payum_stripe.extension.cancel_existing_payment_intent From ea48106188f06060df5ce3d5e584872d34ad6c3d Mon Sep 17 00:00:00 2001 From: Francis Hilaire Date: Tue, 20 Sep 2022 20:11:31 +0200 Subject: [PATCH 3/3] Add expire session mock --- .../Mocker/StripeCheckoutSessionMocker.php | 83 +++++++++++++++++-- tests/Behat/Resources/services/mocker.xml | 10 +++ 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/tests/Behat/Mocker/StripeCheckoutSessionMocker.php b/tests/Behat/Mocker/StripeCheckoutSessionMocker.php index 57e1148..2e79c87 100644 --- a/tests/Behat/Mocker/StripeCheckoutSessionMocker.php +++ b/tests/Behat/Mocker/StripeCheckoutSessionMocker.php @@ -4,12 +4,16 @@ namespace Tests\FluxSE\SyliusPayumStripePlugin\Behat\Mocker; +use FluxSE\PayumStripe\Action\Api\Resource\AbstractAllAction; use FluxSE\PayumStripe\Action\Api\Resource\AbstractCreateAction; use FluxSE\PayumStripe\Action\Api\Resource\AbstractRetrieveAction; +use FluxSE\PayumStripe\Request\Api\Resource\AllSession; use FluxSE\PayumStripe\Request\Api\Resource\CreateSession; +use FluxSE\PayumStripe\Request\Api\Resource\ExpireSession; use FluxSE\PayumStripe\Request\Api\Resource\RetrievePaymentIntent; use FluxSE\PayumStripe\Request\Api\Resource\RetrieveSession; use Stripe\Checkout\Session; +use Stripe\Collection; use Stripe\PaymentIntent; use Sylius\Behat\Service\Mocker\MockerInterface; @@ -25,7 +29,7 @@ public function __construct(MockerInterface $mocker) public function mockCreatePayment(callable $action): void { - $this->mockCreateSession(); + $this->mockCreateSessionAction(); $this->mockSessionSync( $action, @@ -38,6 +42,7 @@ public function mockCreatePayment(callable $action): void public function mockGoBackPayment( callable $action ): void { + $this->mockExpireSession(Session::STATUS_OPEN); $this->mockSessionSync( $action, Session::STATUS_OPEN, @@ -96,7 +101,7 @@ public function mockSuccessfulPaymentWithoutWebhookUsingAuthorize( public function mockPaymentIntentSync(callable $action, string $status): void { - $this->mockRetrievePaymentIntent($status); + $this->mockRetrievePaymentIntentAction($status); $action(); @@ -109,11 +114,17 @@ public function mockSessionSync( string $paymentStatus, string $paymentIntentStatus ): void { - $this->mockRetrieveSession($sessionStatus, $paymentStatus); + $this->mockRetrieveSessionAction($sessionStatus, $paymentStatus); $this->mockPaymentIntentSync($action, $paymentIntentStatus); } - private function mockCreateSession(): void + public function mockExpireSession(string $sessionStatus): void + { + $this->mockAllSessionAction($sessionStatus); + $this->mockExpireSessionAction(); + } + + private function mockCreateSessionAction(): void { $mockCreateSession = $this->mocker->mockService( 'tests.flux_se.sylius_payum_stripe_checkout_session_plugin.behat.mocker.action.create_session', @@ -147,7 +158,7 @@ private function mockCreateSession(): void }); } - private function mockRetrievePaymentIntent(string $status): void + private function mockRetrievePaymentIntentAction(string $status): void { $mock = $this->mocker->mockService( 'tests.flux_se.sylius_payum_stripe_checkout_session_plugin.behat.mocker.action.retrieve_payment_intent', @@ -179,7 +190,7 @@ private function mockRetrievePaymentIntent(string $status): void }); } - private function mockRetrieveSession(string $status, string $paymentStatus): void + private function mockRetrieveSessionAction(string $status, string $paymentStatus): void { $mock = $this->mocker->mockService( 'tests.flux_se.sylius_payum_stripe_checkout_session_plugin.behat.mocker.action.retrieve_session', @@ -212,4 +223,64 @@ private function mockRetrieveSession(string $status, string $paymentStatus): voi ])); }); } + + private function mockAllSessionAction(string $status): void + { + $mock = $this->mocker->mockService( + 'tests.flux_se.sylius_payum_stripe_checkout_session_plugin.behat.mocker.action.all_session', + AbstractAllAction::class + ); + + $mock + ->shouldReceive('setApi') + ->once(); + $mock + ->shouldReceive('setGateway') + ->once(); + + $mock + ->shouldReceive('supports') + ->andReturnUsing(function ($request) { + return $request instanceof AllSession; + }); + + $mock + ->shouldReceive('execute') + ->once() + ->andReturnUsing(function (AllSession $request) use ($status) { + $request->setApiResources( + Collection::constructFrom(['data' => [ + [ + 'id' => 'cs_1', + 'object' => Session::OBJECT_NAME, + 'status' => $status, + ], + ]])); + }); + } + + private function mockExpireSessionAction(): void + { + $mock = $this->mocker->mockService( + 'tests.flux_se.sylius_payum_stripe_checkout_session_plugin.behat.mocker.action.expire_session', + AbstractRetrieveAction::class + ); + + $mock + ->shouldReceive('setApi') + ->once(); + $mock + ->shouldReceive('setGateway') + ->once(); + + $mock + ->shouldReceive('supports') + ->andReturnUsing(function ($request) { + return $request instanceof ExpireSession; + }); + + $mock + ->shouldReceive('execute') + ->once(); + } } diff --git a/tests/Behat/Resources/services/mocker.xml b/tests/Behat/Resources/services/mocker.xml index 4012cb4..63ca4da 100644 --- a/tests/Behat/Resources/services/mocker.xml +++ b/tests/Behat/Resources/services/mocker.xml @@ -18,6 +18,16 @@ + + + + + + + +