From 3f5b3039f1328f2a263b25f4e93c671dc93ecf5e Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 8 Mar 2026 19:14:23 +0530 Subject: [PATCH 1/3] Feat: separate authorize and confirm into two-step flow Split the authorize method into two steps: authorize (creates intent without confirming) and confirmAuthorization (confirms with off_session). This allows callers to perform validation checks between creation and confirmation, and cancel if needed before funds are held. --- src/Pay/Adapter.php | 10 ++++++++++ src/Pay/Adapter/Stripe.php | 17 ++++++++++++++++- src/Pay/Pay.php | 13 +++++++++++++ tests/Pay/Adapter/StripeTest.php | 24 +++++++++++++++++------- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/Pay/Adapter.php b/src/Pay/Adapter.php index b048231..365065f 100644 --- a/src/Pay/Adapter.php +++ b/src/Pay/Adapter.php @@ -103,6 +103,16 @@ abstract public function authorize(int $amount, string $customerId, ?string $pay */ abstract public function capture(string $paymentId, ?int $amount = null, array $additionalParams = []): array; + /** + * Confirm a previously created authorization + * Sends confirmation to process the authorization (e.g., off_session for saved cards) + * + * @param string $paymentId The payment/authorization ID to confirm + * @param array $additionalParams Additional parameters (optional) + * @return array Result of the confirmation + */ + abstract public function confirmAuthorization(string $paymentId, array $additionalParams = []): array; + /** * Cancel/void a payment authorization * Releases the hold on funds without capturing diff --git a/src/Pay/Adapter/Stripe.php b/src/Pay/Adapter/Stripe.php index 72e8365..d3d2569 100644 --- a/src/Pay/Adapter/Stripe.php +++ b/src/Pay/Adapter/Stripe.php @@ -60,8 +60,23 @@ public function authorize(int $amount, string $customerId, ?string $paymentMetho 'customer' => $customerId, 'payment_method' => $paymentMethodId, 'capture_method' => 'manual', + ]; + + $requestBody = array_merge($requestBody, $additionalParams); + $result = $this->execute(self::METHOD_POST, $path, $requestBody); + + return $result; + } + + /** + * Confirm a previously created authorization + * Sends off_session: true to confirm without customer interaction (for saved cards) + */ + public function confirmAuthorization(string $paymentId, array $additionalParams = []): array + { + $path = '/payment_intents/'.$paymentId.'/confirm'; + $requestBody = [ 'off_session' => 'true', - 'confirm' => 'true', ]; $requestBody = array_merge($requestBody, $additionalParams); diff --git a/src/Pay/Pay.php b/src/Pay/Pay.php index fdea6c0..26f4d37 100644 --- a/src/Pay/Pay.php +++ b/src/Pay/Pay.php @@ -103,6 +103,19 @@ public function authorize(int $amount, string $customerId, ?string $paymentMetho return $this->adapter->authorize($amount, $customerId, $paymentMethodId, $additionalParams); } + /** + * Confirm Authorization + * Confirm a previously created authorization + * + * @param string $paymentId + * @param array $additionalParams + * @return array + */ + public function confirmAuthorization(string $paymentId, array $additionalParams = []): array + { + return $this->adapter->confirmAuthorization($paymentId, $additionalParams); + } + /** * Capture * Capture a previously authorized payment diff --git a/tests/Pay/Adapter/StripeTest.php b/tests/Pay/Adapter/StripeTest.php index 2d87a61..2b5f2b6 100644 --- a/tests/Pay/Adapter/StripeTest.php +++ b/tests/Pay/Adapter/StripeTest.php @@ -631,16 +631,22 @@ public function testAuthorize(array $data): array $customerId = $data['customerId']; $paymentMethodId = $data['paymentMethodId']; - // Authorize payment - hold funds without capturing + // Authorize payment - creates intent without confirming $authorization = $this->stripe->authorize(10000, $customerId, $paymentMethodId); $this->assertNotEmpty($authorization['id']); $this->assertEquals('payment_intent', $authorization['object']); $this->assertEquals(10000, $authorization['amount']); - $this->assertEquals('requires_capture', $authorization['status']); + $this->assertEquals('requires_confirmation', $authorization['status']); $this->assertEquals('manual', $authorization['capture_method']); - $data['authorizationId'] = $authorization['id']; + // Confirm the authorization + $confirmed = $this->stripe->confirmAuthorization($authorization['id']); + + $this->assertEquals($authorization['id'], $confirmed['id']); + $this->assertEquals('requires_capture', $confirmed['status']); + + $data['authorizationId'] = $confirmed['id']; return $data; } @@ -685,7 +691,11 @@ public function testPartialCapture(array $data): array $authorization = $this->stripe->authorize(15000, $customerId, $paymentMethodId); $authorizationId = $authorization['id']; - $this->assertEquals('requires_capture', $authorization['status']); + $this->assertEquals('requires_confirmation', $authorization['status']); + + // Confirm the authorization + $confirmed = $this->stripe->confirmAuthorization($authorizationId); + $this->assertEquals('requires_capture', $confirmed['status']); // Capture partial amount (only 10000 of 15000) $captured = $this->stripe->capture($authorizationId, 10000); @@ -713,9 +723,9 @@ public function testCancelAuthorization(array $data): array $authorization = $this->stripe->authorize(8000, $customerId, $paymentMethodId); $authorizationId = $authorization['id']; - $this->assertEquals('requires_capture', $authorization['status']); + $this->assertEquals('requires_confirmation', $authorization['status']); - // Cancel the authorization - release the hold + // Cancel the authorization - release the hold (can cancel before confirming) $cancelled = $this->stripe->cancelAuthorization($authorizationId); $this->assertNotEmpty($cancelled['id']); @@ -753,7 +763,7 @@ public function testAuthorizeWithMetadata(array $data): void ); $this->assertNotEmpty($authorization['id']); - $this->assertEquals('requires_capture', $authorization['status']); + $this->assertEquals('requires_confirmation', $authorization['status']); $this->assertEquals('example.com', $authorization['metadata']['domain']); $this->assertEquals('ORD-12345', $authorization['metadata']['order_id']); $this->assertEquals('domain_registration', $authorization['metadata']['resource_type']); From 87f856df742c2e6ca498b2533aaf1d922cf80a18 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 8 Mar 2026 19:18:44 +0530 Subject: [PATCH 2/3] Feat: add captureMethod parameter to authorize with default 'automatic' Allow callers to specify capture_method ('automatic' or 'manual') as a method parameter instead of hardcoding it. Defaults to 'automatic'. --- src/Pay/Adapter.php | 3 ++- src/Pay/Adapter/Stripe.php | 4 ++-- src/Pay/Pay.php | 5 +++-- tests/Pay/Adapter/StripeTest.php | 7 ++++--- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Pay/Adapter.php b/src/Pay/Adapter.php index 365065f..a6f41b8 100644 --- a/src/Pay/Adapter.php +++ b/src/Pay/Adapter.php @@ -87,10 +87,11 @@ abstract public function purchase(int $amount, string $customerId, ?string $paym * @param int $amount Amount to authorize * @param string $customerId Customer ID * @param string|null $paymentMethodId Payment method ID (optional) + * @param string $captureMethod Capture method: 'automatic' (default) or 'manual' * @param array $additionalParams Additional parameters (optional) * @return array Result of the authorization including authorization ID */ - abstract public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array; + abstract public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array; /** * Capture a previously authorized payment diff --git a/src/Pay/Adapter/Stripe.php b/src/Pay/Adapter/Stripe.php index d3d2569..72d4a38 100644 --- a/src/Pay/Adapter/Stripe.php +++ b/src/Pay/Adapter/Stripe.php @@ -51,7 +51,7 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod * Authorize a payment (hold funds without capturing) * Creates a payment intent with capture_method set to manual */ - public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array + public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array { $path = '/payment_intents'; $requestBody = [ @@ -59,7 +59,7 @@ public function authorize(int $amount, string $customerId, ?string $paymentMetho 'currency' => $this->currency, 'customer' => $customerId, 'payment_method' => $paymentMethodId, - 'capture_method' => 'manual', + 'capture_method' => $captureMethod, ]; $requestBody = array_merge($requestBody, $additionalParams); diff --git a/src/Pay/Pay.php b/src/Pay/Pay.php index 26f4d37..eaaa776 100644 --- a/src/Pay/Pay.php +++ b/src/Pay/Pay.php @@ -95,12 +95,13 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod * @param int $amount * @param string $customerId * @param string|null $paymentMethodId + * @param string $captureMethod * @param array $additionalParams * @return array */ - public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array + public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array { - return $this->adapter->authorize($amount, $customerId, $paymentMethodId, $additionalParams); + return $this->adapter->authorize($amount, $customerId, $paymentMethodId, $captureMethod, $additionalParams); } /** diff --git a/tests/Pay/Adapter/StripeTest.php b/tests/Pay/Adapter/StripeTest.php index 2b5f2b6..9febaa3 100644 --- a/tests/Pay/Adapter/StripeTest.php +++ b/tests/Pay/Adapter/StripeTest.php @@ -632,7 +632,7 @@ public function testAuthorize(array $data): array $paymentMethodId = $data['paymentMethodId']; // Authorize payment - creates intent without confirming - $authorization = $this->stripe->authorize(10000, $customerId, $paymentMethodId); + $authorization = $this->stripe->authorize(10000, $customerId, $paymentMethodId, 'manual'); $this->assertNotEmpty($authorization['id']); $this->assertEquals('payment_intent', $authorization['object']); @@ -688,7 +688,7 @@ public function testPartialCapture(array $data): array $paymentMethodId = $data['paymentMethodId']; // Authorize payment - $authorization = $this->stripe->authorize(15000, $customerId, $paymentMethodId); + $authorization = $this->stripe->authorize(15000, $customerId, $paymentMethodId, 'manual'); $authorizationId = $authorization['id']; $this->assertEquals('requires_confirmation', $authorization['status']); @@ -720,7 +720,7 @@ public function testCancelAuthorization(array $data): array $paymentMethodId = $data['paymentMethodId']; // Authorize payment - $authorization = $this->stripe->authorize(8000, $customerId, $paymentMethodId); + $authorization = $this->stripe->authorize(8000, $customerId, $paymentMethodId, 'manual'); $authorizationId = $authorization['id']; $this->assertEquals('requires_confirmation', $authorization['status']); @@ -752,6 +752,7 @@ public function testAuthorizeWithMetadata(array $data): void 12000, $customerId, $paymentMethodId, + 'manual', [ 'metadata' => [ 'domain' => 'example.com', From 02e2c66706c468cffbf56a5bd41907d22138c273 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Sun, 8 Mar 2026 19:24:13 +0530 Subject: [PATCH 3/3] Docs: clarify authorize flow requires confirmAuthorization, cancel only before confirm --- src/Pay/Adapter.php | 9 +++++++-- src/Pay/Adapter/Stripe.php | 9 +++++++-- src/Pay/Pay.php | 9 ++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Pay/Adapter.php b/src/Pay/Adapter.php index a6f41b8..e75b6f1 100644 --- a/src/Pay/Adapter.php +++ b/src/Pay/Adapter.php @@ -81,8 +81,13 @@ public function getCurrency(): string abstract public function purchase(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array; /** - * Authorize a payment (hold funds without capturing) - * Useful for scenarios where you need to ensure payment availability before providing service + * Authorize a payment + * Creates a payment intent without confirming it. Always call confirmAuthorization() after this. + * + * Flow with 'automatic' (default): authorize() → confirmAuthorization() → funds captured automatically. + * Flow with 'manual': authorize() → confirmAuthorization() → capture() to collect funds. + * + * You may call cancelAuthorization() before confirmAuthorization() to abort. * * @param int $amount Amount to authorize * @param string $customerId Customer ID diff --git a/src/Pay/Adapter/Stripe.php b/src/Pay/Adapter/Stripe.php index 72d4a38..672fe27 100644 --- a/src/Pay/Adapter/Stripe.php +++ b/src/Pay/Adapter/Stripe.php @@ -48,8 +48,13 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod } /** - * Authorize a payment (hold funds without capturing) - * Creates a payment intent with capture_method set to manual + * Authorize a payment + * Creates a payment intent without confirming it. Always call confirmAuthorization() after this. + * + * Flow with 'automatic' (default): authorize() → confirmAuthorization() → funds captured automatically. + * Flow with 'manual': authorize() → confirmAuthorization() → capture() to collect funds. + * + * You may call cancelAuthorization() before confirmAuthorization() to abort. */ public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array { diff --git a/src/Pay/Pay.php b/src/Pay/Pay.php index eaaa776..6c393ef 100644 --- a/src/Pay/Pay.php +++ b/src/Pay/Pay.php @@ -88,9 +88,12 @@ public function purchase(int $amount, string $customerId, ?string $paymentMethod /** * Authorize - * Authorize a payment (hold funds without capturing) - * Useful for scenarios where you need to ensure payment availability before providing service - * Returns authorization ID on successful authorization + * Creates a payment intent without confirming it. Always call confirmAuthorization() after this. + * + * Flow with 'automatic' (default): authorize() → confirmAuthorization() → funds captured automatically. + * Flow with 'manual': authorize() → confirmAuthorization() → capture() to collect funds. + * + * You may call cancelAuthorization() before confirmAuthorization() to abort. * * @param int $amount * @param string $customerId