diff --git a/src/Pay/Adapter.php b/src/Pay/Adapter.php index b048231..e75b6f1 100644 --- a/src/Pay/Adapter.php +++ b/src/Pay/Adapter.php @@ -81,16 +81,22 @@ 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 * @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 @@ -103,6 +109,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..672fe27 100644 --- a/src/Pay/Adapter/Stripe.php +++ b/src/Pay/Adapter/Stripe.php @@ -48,10 +48,15 @@ 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, array $additionalParams = []): array + public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array { $path = '/payment_intents'; $requestBody = [ @@ -59,9 +64,24 @@ 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); + $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..6c393ef 100644 --- a/src/Pay/Pay.php +++ b/src/Pay/Pay.php @@ -88,19 +88,36 @@ 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 * @param string|null $paymentMethodId + * @param string $captureMethod + * @param array $additionalParams + * @return array + */ + public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, string $captureMethod = 'automatic', array $additionalParams = []): array + { + return $this->adapter->authorize($amount, $customerId, $paymentMethodId, $captureMethod, $additionalParams); + } + + /** + * Confirm Authorization + * Confirm a previously created authorization + * + * @param string $paymentId * @param array $additionalParams * @return array */ - public function authorize(int $amount, string $customerId, ?string $paymentMethodId = null, array $additionalParams = []): array + public function confirmAuthorization(string $paymentId, array $additionalParams = []): array { - return $this->adapter->authorize($amount, $customerId, $paymentMethodId, $additionalParams); + return $this->adapter->confirmAuthorization($paymentId, $additionalParams); } /** diff --git a/tests/Pay/Adapter/StripeTest.php b/tests/Pay/Adapter/StripeTest.php index 2d87a61..9febaa3 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 - $authorization = $this->stripe->authorize(10000, $customerId, $paymentMethodId); + // Authorize payment - creates intent without confirming + $authorization = $this->stripe->authorize(10000, $customerId, $paymentMethodId, 'manual'); $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; } @@ -682,10 +688,14 @@ 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_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); @@ -710,12 +720,12 @@ 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_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']); @@ -742,6 +752,7 @@ public function testAuthorizeWithMetadata(array $data): void 12000, $customerId, $paymentMethodId, + 'manual', [ 'metadata' => [ 'domain' => 'example.com', @@ -753,7 +764,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']);