diff --git a/src/Pay/Adapter.php b/src/Pay/Adapter.php index e8cf196..f9685dc 100644 --- a/src/Pay/Adapter.php +++ b/src/Pay/Adapter.php @@ -80,6 +80,16 @@ public function getCurrency(): string */ abstract public function purchase(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array; + /** + * Retry a purchase for a payment intent + * + * @param string $paymentId The payment intent ID to retry + * @param string|null $paymentMethodId The payment method to use (optional) + * @param array $additionalParams Additional parameters for the retry (optional) + * @return array The result of the retry attempt + */ + abstract public function retryPurchase(string $paymentId, ?string $paymentMethodId = null, array $additionalParams = []): array; + /** * Refund payment * diff --git a/src/Pay/Adapter/Stripe.php b/src/Pay/Adapter/Stripe.php index 4930e7e..c42d936 100644 --- a/src/Pay/Adapter/Stripe.php +++ b/src/Pay/Adapter/Stripe.php @@ -47,6 +47,30 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod return $result; } + /** + * Retry a purchase for a payment intent + * + * @param string $paymentId The payment intent ID to retry + * @param string|null $paymentMethodId The payment method to use (optional) + * @param array $additionalParams Additional parameters for the retry (optional) + * @return array The result of the retry attempt + */ + public function retryPurchase(string $paymentId, ?string $paymentMethodId = null, array $additionalParams = []): array + { + $path = '/payment_intents/'.$paymentId.'/confirm'; + $requestBody = []; + if (! empty($paymentMethodId)) { + $requestBody = [ + 'payment_method' => $paymentMethodId, + ]; + } + + $requestBody = array_merge($requestBody, $additionalParams); + $result = $this->execute(self::METHOD_POST, $path, $requestBody); + + return $result; + } + /** * Refund payment */ diff --git a/src/Pay/Pay.php b/src/Pay/Pay.php index 29ce45a..333b072 100644 --- a/src/Pay/Pay.php +++ b/src/Pay/Pay.php @@ -86,6 +86,19 @@ public function purchase(int $amount, string $customerId, string $paymentMethodI return $this->adapter->purchase($amount, $customerId, $paymentMethodId, $additionalParams); } + /** + * Retry a purchase for a payment intent + * + * @param string $paymentId The payment intent ID to retry + * @param string|null $paymentMethodId The payment method to use (optional) + * @param array $additionalParams Additional parameters for the retry (optional) + * @return array The result of the retry attempt + */ + public function retryPurchase(string $paymentId, ?string $paymentMethodId = null, array $additionalParams = []): array + { + return $this->adapter->retryPurchase($paymentId, $paymentMethodId, $additionalParams); + } + /** * Refund Payment * diff --git a/tests/Pay/Adapter/StripeTest.php b/tests/Pay/Adapter/StripeTest.php index edb6bf8..9cfb2df 100644 --- a/tests/Pay/Adapter/StripeTest.php +++ b/tests/Pay/Adapter/StripeTest.php @@ -292,6 +292,65 @@ public function testPurchase(array $data): array return $data; } + /** + * Test retryPurchase: create a payment with a failing payment method, then retry with a succeeding one. + * + * @depends testCreateCustomer + * + * @param array $data + * @return array + */ + public function testRetryPurchase(array $data): array + { + $customerId = $data['customerId']; + // Create a payment method that will fail (card_declined) + $failingPm = $this->stripe->createPaymentMethod($customerId, 'card', [ + 'number' => '4000000000000341', + 'exp_month' => 8, + 'exp_year' => 2030, + 'cvc' => 123, + ]); + $this->assertNotEmpty($failingPm['id']); + $failingPmId = $failingPm['id']; + + // Create a payment intent with the failing payment method + $paymentIntentId = null; + try { + $this->stripe->purchase(5000, $customerId, $failingPmId); + $this->fail('Expected payment to fail'); + } catch (Exception $e) { + $this->assertEquals(Exception::GENERIC_DECLINE, $e->getType()); + $this->assertEquals(402, $e->getCode()); + $paymentIntentMeta = $e->getMetadata()['payment_intent'] ?? null; + $paymentIntentId = is_array($paymentIntentMeta) && isset($paymentIntentMeta['id']) ? $paymentIntentMeta['id'] : $paymentIntentMeta; + $this->assertNotEmpty($paymentIntentId); + } + + // Create a succeeding payment method + $succeedingPm = $this->stripe->createPaymentMethod($customerId, 'card', [ + 'number' => '4242424242424242', // Stripe test card: always succeeds + 'exp_month' => 8, + 'exp_year' => 2030, + 'cvc' => 123, + ]); + $this->assertNotEmpty($succeedingPm['id']); + $succeedingPmId = $succeedingPm['id']; + + // Retry the payment intent with the succeeding payment method + $result = $this->stripe->retryPurchase((string) $paymentIntentId, $succeedingPmId); + $this->assertNotEmpty($result['id']); + $this->assertEquals($paymentIntentId, $result['id']); + $this->assertEquals('payment_intent', $result['object']); + $this->assertArrayHasKey('status', $result); + $this->assertEquals('succeeded', $result['status']); + + // Save for further tests if needed + $data['paymentId'] = $paymentIntentId; + $data['paymentMethodId'] = $succeedingPmId; + + return $data; + } + /** * @depends testPurchase */