diff --git a/src/Action/StripeCheckoutSession/CancelAction.php b/src/Action/StripeCheckoutSession/CancelAction.php index 894490f..dbee5b1 100644 --- a/src/Action/StripeCheckoutSession/CancelAction.php +++ b/src/Action/StripeCheckoutSession/CancelAction.php @@ -5,8 +5,8 @@ namespace FluxSE\PayumStripe\Action\StripeCheckoutSession; use ArrayAccess; -use FluxSE\PayumStripe\Request\Api\Resource\AllSession; use FluxSE\PayumStripe\Request\Api\Resource\ExpireSession; +use FluxSE\PayumStripe\Request\Api\Resource\RetrieveSession; use Payum\Core\Action\ActionInterface; use Payum\Core\Bridge\Spl\ArrayObject; use Payum\Core\Exception\RequestNotSupportedException; @@ -28,27 +28,13 @@ public function execute($request): void $model = ArrayObject::ensureArrayObject($request->getModel()); - // @link https://stripe.com/docs/api/payment_intents/cancel - // > You cannot cancel the PaymentIntent for a Checkout Session. - // > Expire the Checkout Session instead. - $allSessionRequest = new AllSession( - [ - 'payment_intent' => $model['id'], - ] - ); - $this->gateway->execute($allSessionRequest); - - $sessions = $allSessionRequest->getApiResources(); - $session = $sessions->first(); - if (!$session instanceof Session) { - return; - } - - if ($session->status !== Session::STATUS_OPEN) { + if ($model['status'] !== Session::STATUS_OPEN) { return; } - $this->gateway->execute(new ExpireSession($session->id)); + /** @var string $id */ + $id = $model['id']; + $this->gateway->execute(new ExpireSession($id)); } public function supports($request): bool @@ -62,7 +48,6 @@ public function supports($request): bool return false; } - // if capture_method=automatic it means the payment intent was created from a checkout session without authorization - return $model->offsetExists('capture_method') && $model->offsetGet('capture_method') === 'automatic'; + return $model->offsetExists('object') && $model->offsetGet('object') === Session::OBJECT_NAME; } } diff --git a/src/Action/StripeCheckoutSession/LegacyCancelAction.php b/src/Action/StripeCheckoutSession/LegacyCancelAction.php new file mode 100644 index 0000000..0fa8b84 --- /dev/null +++ b/src/Action/StripeCheckoutSession/LegacyCancelAction.php @@ -0,0 +1,73 @@ +getModel()); + + // @link https://stripe.com/docs/api/payment_intents/cancel + // > You cannot cancel the PaymentIntent for a Checkout Session. + // > Expire the Checkout Session instead. + $allSessionRequest = new AllSession( + [ + 'payment_intent' => $model['id'], + ] + ); + $this->gateway->execute($allSessionRequest); + + $sessions = $allSessionRequest->getApiResources(); + $session = $sessions->first(); + if (!$session instanceof Session) { + return; + } + + if ($session->status !== Session::STATUS_OPEN) { + return; + } + + $this->gateway->execute(new ExpireSession($session->id)); + } + + public function supports($request): bool + { + if (!$request instanceof Cancel) { + return false; + } + + $model = $request->getModel(); + if (!$model instanceof ArrayAccess) { + return false; + } + + if (!$model->offsetExists('object') || $model->offsetGet('object') !== PaymentIntent::OBJECT_NAME) { + return false; + } + + // if capture_method=automatic it means the payment intent was created from a checkout session without authorization + return $model->offsetExists('capture_method') && $model->offsetGet('capture_method') === 'automatic'; + } +} diff --git a/tests/Action/StripeCheckoutSession/CancelActionTest.php b/tests/Action/StripeCheckoutSession/CancelActionTest.php index d589970..561f1e8 100644 --- a/tests/Action/StripeCheckoutSession/CancelActionTest.php +++ b/tests/Action/StripeCheckoutSession/CancelActionTest.php @@ -9,8 +9,8 @@ use Payum\Core\Request\Authorize; use Payum\Core\Request\Cancel; use PHPUnit\Framework\TestCase; -use Stripe\Checkout\Session; use Stripe\Collection; +use Stripe\Checkout\Session; use Tests\FluxSE\PayumStripe\Action\GatewayAwareTestTrait; final class CancelActionTest extends TestCase @@ -28,68 +28,28 @@ public function testShouldSupportOnlyCaptureAndArrayAccessModelWithCaptureMethod { $action = new CancelAction(); - $this->assertTrue($action->supports(new Cancel(['capture_method' => 'automatic']))); - $this->assertFalse($action->supports(new Cancel(['capture_method' => 'manual']))); + $this->assertTrue($action->supports(new Cancel(['object' => Session::OBJECT_NAME]))); $this->assertFalse($action->supports(new Cancel([]))); + $this->assertFalse($action->supports(new Cancel(['object' => '']))); + $this->assertFalse($action->supports(new Cancel(['object' => 'foo']))); $this->assertFalse($action->supports(new Cancel(null))); - $this->assertFalse($action->supports(new Authorize(['capture_method' => 'manual']))); - } - - public function testDoesNothingForNotACheckoutSession(): void - { - $request = new Cancel(['capture_method' => 'automatic', 'id' => 'pi_abc123']); - $action = new CancelAction(); - - $allSessionRequest = new AllSession(['payment_intent' => 'pi_abc123']); - $gatewayMock = $this->createGatewayMock(); - $gatewayMock - ->expects($this->once()) - ->method('execute') - ->with($allSessionRequest) - ->willReturn( - $this->returnCallback( - function (AllSession $request) { - $sessions = Collection::emptyCollection(); - $request->setApiResources($sessions); - return true; - } - ) - ); - - $action->setGateway($gatewayMock); - $action->execute($request); } public function testDoesNothingForNotAnOpenedCheckoutSession(): void { - $request = new Cancel(['capture_method' => 'automatic', 'id' => 'pi_abc123']); + $request = new Cancel([ + 'object' => Session::OBJECT_NAME, + 'id' => 'cs_abc123', + 'status' => Session::STATUS_COMPLETE, + ]); $action = new CancelAction(); - $allSessionRequest = new AllSession(['payment_intent' => 'pi_abc123']); $gatewayMock = $this->createGatewayMock(); + $gatewayMock - ->expects($this->once()) + ->expects($this->never()) ->method('execute') - ->with($allSessionRequest) - ->willReturn( - $this->returnCallback( - function (AllSession $request) { - $sessions = Collection::constructFrom( - [ - 'data' => [ - [ - 'id' => 'cs_1', - 'object' => Session::OBJECT_NAME, - 'status' => Session::STATUS_COMPLETE, - ], - ], - ] - ); - $request->setApiResources($sessions); - return true; - } - ) - ); + ; $action->setGateway($gatewayMock); $action->execute($request); @@ -97,44 +57,21 @@ function (AllSession $request) { public function testExpiresAnOpenedCheckoutSession(): void { - $request = new Cancel(['capture_method' => 'automatic', 'id' => 'pi_abc123']); + $request = new Cancel([ + 'object' => Session::OBJECT_NAME, + 'id' => 'pi_abc123', + 'status' => Session::STATUS_OPEN, + ]); $action = new CancelAction(); $gatewayMock = $this->createGatewayMock(); $gatewayMock - ->expects($this->exactly(2)) + ->expects($this->once()) ->method('execute') - ->withConsecutive( - [ - $this->callback( - function (AllSession $request): bool { - $this->assertSame(['payment_intent' => 'pi_abc123'], $request->getParameters()); - return true; - } - ), - ], - [new ExpireSession('cs_1')] - ) - ->willReturnOnConsecutiveCalls( - $this->returnCallback( - function (AllSession $request) { - $sessions = Collection::constructFrom( - [ - 'data' => [ - [ - 'id' => 'cs_1', - 'object' => Session::OBJECT_NAME, - 'status' => Session::STATUS_OPEN, - ], - ], - ] - ); - $request->setApiResources($sessions); - return true; - } - ), - $this->anything() - ); + ->willReturnCallback(function(ExpireSession $request): void { + return; + }) + ; $action->setGateway($gatewayMock); $action->execute($request); diff --git a/tests/Action/StripeCheckoutSession/LegacyCancelActionTest.php b/tests/Action/StripeCheckoutSession/LegacyCancelActionTest.php new file mode 100644 index 0000000..37fc16b --- /dev/null +++ b/tests/Action/StripeCheckoutSession/LegacyCancelActionTest.php @@ -0,0 +1,157 @@ +assertInstanceOf(ActionInterface::class, $action); + } + + public function testShouldSupportOnlyCaptureAndArrayAccessModelWithCaptureMethodAutomatic(): void + { + $action = new LegacyCancelAction(); + + $this->assertTrue($action->supports(new Cancel(['object' => PaymentIntent::OBJECT_NAME, 'capture_method' => 'automatic']))); + $this->assertFalse($action->supports(new Cancel(['object' => PaymentIntent::OBJECT_NAME, 'capture_method' => 'manual']))); + $this->assertFalse($action->supports(new Cancel([]))); + $this->assertFalse($action->supports(new Cancel(['object' => '']))); + $this->assertFalse($action->supports(new Cancel(['object' => 'foo']))); + $this->assertFalse($action->supports(new Cancel(null))); + $this->assertFalse($action->supports(new Authorize(['capture_method' => 'manual']))); + } + + public function testDoesNothingForNotACheckoutSession(): void + { + $request = new Cancel([ + 'object' => PaymentIntent::OBJECT_NAME, + 'capture_method' => 'automatic', + 'id' => 'pi_abc123' + ]); + $action = new LegacyCancelAction(); + + $allSessionRequest = new AllSession(['payment_intent' => 'pi_abc123']); + $gatewayMock = $this->createGatewayMock(); + $gatewayMock + ->expects($this->once()) + ->method('execute') + ->with($allSessionRequest) + ->willReturn( + $this->returnCallback( + function (AllSession $request) { + $sessions = Collection::emptyCollection(); + $request->setApiResources($sessions); + return true; + } + ) + ); + + $action->setGateway($gatewayMock); + $action->execute($request); + } + + public function testDoesNothingForNotAnOpenedCheckoutSession(): void + { + $request = new Cancel([ + 'object' => PaymentIntent::OBJECT_NAME, + 'capture_method' => 'automatic', + 'id' => 'pi_abc123' + ]); + $action = new LegacyCancelAction(); + + $allSessionRequest = new AllSession(['payment_intent' => 'pi_abc123']); + $gatewayMock = $this->createGatewayMock(); + $gatewayMock + ->expects($this->once()) + ->method('execute') + ->with($allSessionRequest) + ->willReturn( + $this->returnCallback( + function (AllSession $request) { + $sessions = Collection::constructFrom( + [ + 'data' => [ + [ + 'id' => 'cs_1', + 'object' => Session::OBJECT_NAME, + 'status' => Session::STATUS_COMPLETE, + ], + ], + ] + ); + $request->setApiResources($sessions); + return true; + } + ) + ); + + $action->setGateway($gatewayMock); + $action->execute($request); + } + + public function testExpiresAnOpenedCheckoutSession(): void + { + $request = new Cancel([ + 'object' => PaymentIntent::OBJECT_NAME, + 'capture_method' => 'automatic', + 'id' => 'pi_abc123' + ]); + $action = new LegacyCancelAction(); + + $gatewayMock = $this->createGatewayMock(); + $gatewayMock + ->expects($this->exactly(2)) + ->method('execute') + ->withConsecutive( + [ + $this->callback( + function (AllSession $request): bool { + $this->assertSame(['payment_intent' => 'pi_abc123'], $request->getParameters()); + return true; + } + ), + ], + [new ExpireSession('cs_1')] + ) + ->willReturnOnConsecutiveCalls( + $this->returnCallback( + function (AllSession $request) { + $sessions = Collection::constructFrom( + [ + 'data' => [ + [ + 'id' => 'cs_1', + 'object' => Session::OBJECT_NAME, + 'status' => Session::STATUS_OPEN, + ], + ], + ] + ); + $request->setApiResources($sessions); + return true; + } + ), + $this->anything() + ); + + $action->setGateway($gatewayMock); + $action->execute($request); + } +}