diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05e7b40..e0e272a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest', 'windows-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.2', '8.3', '8.4'] diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index a857bce..fda7a3d 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -31,4 +31,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.2', '8.3', '8.4'] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index e33eca8..48c3bb8 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -29,4 +29,4 @@ jobs: os: >- ['ubuntu-latest'] php: >- - ['8.1', '8.2', '8.3'] + ['8.2', '8.3', '8.4'] diff --git a/README.md b/README.md index ad24b02..f45a756 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A modern PHP 8.4+ library providing a unified interface for multiple payment gat ## Requirements -- PHP 8.1 or higher. +- PHP 8.2 or higher. ## Installation @@ -155,7 +155,7 @@ $intent = new PaymentIntent( id: 'pi_123', // null for new intents amount: 1000, // $10.00 currency: 'usd', - status: 'requires_payment_method', + status: PaymentIntentStatus::RequiresPaymentMethod, customerId: 'cus_123', paymentMethodId: 'pm_123', metadata: ['order_id' => 'abc123'] @@ -201,7 +201,7 @@ const { paymentMethod, error } = await stripe.createPaymentMethod({ ```php $paymentMethod = $gateway->createPaymentMethod(new PaymentMethod( id: $_POST['payment_method_id'], - type: 'card', + type: PaymentMethodType::Card, customerId: $customer->id )); ``` @@ -318,7 +318,7 @@ $stripe = new StripeGateway( #### PayPal ```php -use PaymentGateway\Gateways\PayPalGateway; +use \Yiisoft\Payments\Gateways\PayPalGateway; $paypal = new PayPalGateway( 'your-paypal-client-id', @@ -334,7 +334,7 @@ $paypal = new PayPalGateway( ```php // Create a customer -$customer = new \PaymentGateway\Models\Customer( +$customer = new \Yiisoft\Payments\Models\Customer( null, // id will be generated by the gateway 'customer@example.com', 'John Doe' @@ -357,9 +357,9 @@ $gateway->deleteCustomer('cus_123'); ```php // Create a payment method (e.g., card) -$paymentMethod = new \PaymentGateway\Models\PaymentMethod( +$paymentMethod = new \Yiisoft\Payments\Models\PaymentMethod( null, // id will be generated by the gateway - 'card', + \Yiisoft\Payments\Models\PaymentMethodType::CARD, [ 'number' => '4242424242424242', 'exp_month' => '12', @@ -451,7 +451,7 @@ use Yiisoft\PaymentGateway\Gateways\AbstractGateway; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\StreamFactoryInterface; -use Psr\Log\LoggerInterface; +use Psr\Log\LoggerInterface;use Yiisoft\Payments\Enums\PaymentIntentStatus; final class AcmePayGateway extends AbstractGateway { @@ -503,7 +503,7 @@ final class AcmePayGateway extends AbstractGateway return new PaymentIntent( id: $response['id'], - status: $response['status'], + status: PaymentIntentStatus::tryFrom($response['status']), amount: $intent->amount, currency: $intent->currency, customerId: $intent->customerId, diff --git a/composer.json b/composer.json index c8838b7..f0999bf 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", diff --git a/src/Enums/PaymentIntentStatus.php b/src/Enums/PaymentIntentStatus.php new file mode 100644 index 0000000..6218e7f --- /dev/null +++ b/src/Enums/PaymentIntentStatus.php @@ -0,0 +1,16 @@ +clientId = $clientId; - $this->clientSecret = $clientSecret; - $this->sandbox = $sandbox; } protected function getBaseUri(): string { - return $this->sandbox + return $this->sandbox ? 'https://api-m.sandbox.paypal.com/v1' : 'https://api-m.paypal.com/v1'; } @@ -68,11 +64,11 @@ private function getAccessToken(): string ); $data = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR); - + if (!isset($data['access_token'])) { throw new \RuntimeException('Failed to get access token: ' . ($data['error'] ?? 'Unknown error')); } - + $this->accessToken = $data['access_token']; $this->tokenExpires = time() + ($data['expires_in'] ?? 3600); @@ -82,12 +78,12 @@ private function getAccessToken(): string protected function createRequest(string $method, string $endpoint, array $data = []): \Psr\Http\Message\RequestInterface { $request = parent::createRequest($method, $endpoint, $data); - + // Skip adding auth header for token requests if (str_contains($endpoint, '/oauth2/token')) { return $request; } - + return $request ->withHeader('Authorization', 'Bearer ' . $this->getAccessToken()) ->withHeader('PayPal-Request-Id', uniqid('', true)); @@ -109,7 +105,7 @@ public function createCustomer(Customer $customer): Customer ]; $response = $this->sendRequest($this->createRequest('POST', '/customer/partner-referrals', $data)); - + // PayPal doesn't have a direct customer creation endpoint in the same way as Stripe // This is a simplified implementation return new Customer( @@ -161,7 +157,7 @@ public function updateCustomer(Customer $customer): Customer ]; $this->sendRequest($this->createRequest('PATCH', "/customer/partner-referrals/{$customer->id}", [$data])); - + return $customer; } @@ -183,14 +179,14 @@ public function createPaymentMethod(PaymentMethod $paymentMethod): PaymentMethod // In PayPal, payment methods are typically associated with orders, not directly with customers // This is a simplified implementation - return new PaymentMethod($paymentMethod->id, 'paypal', [], $paymentMethod->customerId); + return new PaymentMethod($paymentMethod->id, PaymentMethodType::PAYPAL, [], $paymentMethod->customerId); } public function attachPaymentMethod(string $paymentMethodId, string $customerId): PaymentMethod { // In PayPal, payment methods are typically associated with orders, not directly with customers // This is a simplified implementation - return new PaymentMethod($paymentMethodId, 'paypal', [], $customerId); + return new PaymentMethod($paymentMethodId, PaymentMethodType::PAYPAL, [], $customerId); } public function createPaymentIntent(PaymentIntent $intent): PaymentIntent @@ -230,10 +226,18 @@ public function createPaymentIntent(PaymentIntent $intent): PaymentIntent break; } } - + + // Map PayPal state to PaymentIntentStatus + $status = match (strtolower($response['state'] ?? '')) { + 'created' => PaymentIntentStatus::RequiresPaymentMethod, + 'approved' => PaymentIntentStatus::Succeeded, + 'failed' => PaymentIntentStatus::Canceled, + default => PaymentIntentStatus::RequiresPaymentMethod, + }; + return new PaymentIntent( id: $response['id'], - status: strtoupper($response['state'] ?? 'created'), + status: $status, amount: $intent->amount, currency: strtolower($response['transactions'][0]['amount']['currency'] ?? 'usd'), customerId: $intent->customerId, @@ -255,10 +259,10 @@ public function confirmPaymentIntent(string $intentId, array $params = []): Paym public function capturePaymentIntent(string $intentId, array $params = []): PaymentIntent { $response = $this->createRequest('POST', "/checkout/orders/{$intentId}/capture", $params); - + return new PaymentIntent( $response['id'], - strtolower($response['status']), + PaymentIntentStatus::tryFrom(strtolower($response['status'])), (int) ((float) $response['purchase_units'][0]['payments']['captures'][0]['amount']['value'] * 100), strtolower($response['purchase_units'][0]['payments']['captures'][0]['amount']['currency_code']), null, @@ -274,10 +278,10 @@ public function cancelPaymentIntent(string $intentId, array $params = []): Payme { // In PayPal, we void the authorization $response = $this->createRequest('POST', "/checkout/orders/{$intentId}/void-authorization", $params); - + return new PaymentIntent( $response['id'], - 'canceled', + PaymentIntentStatus::Canceled, null, null, null, @@ -301,7 +305,7 @@ public function createRefund(string $captureId, array $params = []): array $response = $this->sendRequest( $this->createRequest('POST', "/v1/payments/capture/{$captureId}/refund", $data) ); - + return [ 'id' => $response['id'], 'state' => $response['state'], @@ -326,7 +330,7 @@ public function retrievePaymentIntent(string $intentId): PaymentIntent return new PaymentIntent( id: $data['id'], - status: strtoupper($data['state']), + status: PaymentIntentStatus::tryFrom(strtolower($data['state'])), amount: (int)($amount['total'] * 100) ?? 0, currency: $amount['currency'] ?? null, customerId: $data['payer']['payer_info']['customer_id'] ?? null, diff --git a/src/Gateways/StripeGateway.php b/src/Gateways/StripeGateway.php index 9fdf9fb..48e625b 100644 --- a/src/Gateways/StripeGateway.php +++ b/src/Gateways/StripeGateway.php @@ -14,18 +14,16 @@ class StripeGateway extends AbstractGateway { - private string $apiKey; private string $apiVersion = '2023-10-16'; public function __construct( - string $apiKey, + private string $apiKey, ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, ?LoggerInterface $logger = null ) { parent::__construct($httpClient, $requestFactory, $streamFactory, $logger); - $this->apiKey = $apiKey; } protected function getBaseUri(): string diff --git a/src/Models/PaymentIntent.php b/src/Models/PaymentIntent.php index c966ae5..8aacdf8 100644 --- a/src/Models/PaymentIntent.php +++ b/src/Models/PaymentIntent.php @@ -4,7 +4,7 @@ namespace Yiisoft\Payments\Models; -use Yiisoft\Payments\Constants\PaymentIntentStatus; +use Yiisoft\Payments\Enums\PaymentIntentStatus; use Yiisoft\Payments\Exceptions\InvalidArgumentException; /** @@ -25,17 +25,9 @@ private function validateCurrency(string $currency): string return strtoupper($currency); } - public const STATUS_REQUIRES_PAYMENT_METHOD = 'requires_payment_method'; - public const STATUS_REQUIRES_CONFIRMATION = 'requires_confirmation'; - public const STATUS_REQUIRES_ACTION = 'requires_action'; - public const STATUS_PROCESSING = 'processing'; - public const STATUS_REQUIRES_CAPTURE = 'requires_capture'; - public const STATUS_CANCELED = 'canceled'; - public const STATUS_SUCCEEDED = 'succeeded'; - /** * @param string|null $id The unique identifier for the payment intent. - * @param string|null $status The status of the payment intent. + * @param PaymentIntentStatus|null $status The status of the payment intent. * @param int|null $amount The amount to be collected by this payment intent. * @param string|null $currency Three-letter ISO currency code. * @param string|null $customerId ID of the customer this payment intent is for. @@ -56,7 +48,7 @@ private function validateCurrency(string $currency): string public function __construct( public ?string $id = null, - public ?string $status = null, + public ?PaymentIntentStatus $status = null, public ?int $amount = null, ?string $currency = null, public ?string $customerId = null, @@ -84,7 +76,7 @@ public function toArray(): array { return [ 'id' => $this->id, - 'status' => $this->status, + 'status' => $this->status->value, 'amount' => $this->amount, 'currency' => $this->currency, 'customer_id' => $this->customerId, @@ -118,7 +110,7 @@ public static function fromArray(array $data): self return new self( id: $data['id'] ?? null, - status: $data['status'] ?? null, + status: PaymentIntentStatus::tryFrom($data['status']), amount: $data['amount'] ?? null, currency: $currency, customerId: $data['customer_id'] ?? $data['customerId'] ?? null, diff --git a/src/PaymentGatewayInterface.php b/src/PaymentGatewayInterface.php index 25ce8e8..e107f16 100644 --- a/src/PaymentGatewayInterface.php +++ b/src/PaymentGatewayInterface.php @@ -10,15 +10,13 @@ /** * Payment Gateway Interface - * + * * This interface defines the standard contract that all payment gateway implementations must follow. * It provides a consistent API for processing payments, managing customers, and handling payment methods * across different payment service providers. * * Implementations of this interface should handle the communication with specific payment gateways * (like Stripe, PayPal, etc.) while providing a unified interface to the application. - * - * @package Yiisoft\\Payments\\Core */ interface PaymentGatewayInterface { @@ -51,7 +49,7 @@ public function createPaymentMethod(PaymentMethod $paymentMethod): PaymentMethod * Attaches a payment method to a customer */ public function attachPaymentMethod( - string $paymentMethodId, + string $paymentMethodId, string $customerId ): PaymentMethod; diff --git a/tests/Gateways/PayPalGatewayTest.php b/tests/Gateways/PayPalGatewayTest.php index 7ffb8e0..ba5a180 100644 --- a/tests/Gateways/PayPalGatewayTest.php +++ b/tests/Gateways/PayPalGatewayTest.php @@ -7,7 +7,9 @@ use Yiisoft\Payments\Models\Customer; use Yiisoft\Payments\Models\PaymentIntent; use Yiisoft\Payments\Models\PaymentMethod; +use Yiisoft\Payments\Enums\PaymentIntentStatus; use Yiisoft\Payments\Gateways\PayPalGateway; +use Yiisoft\Payments\Models\PaymentMethodType; use Yiisoft\Payments\Tests\Support\TestHttpClient; use PHPUnit\Framework\TestCase; @@ -52,7 +54,7 @@ private function getLastRequest(): array public function testCreateCustomer(): void { $this->mockTokenRequest(); - + $testCustomer = new Customer( id: null, email: 'test@example.com', @@ -89,7 +91,7 @@ public function testCreateCustomer(): void $this->assertSame('Test User', $result->name); $this->assertSame('+1234567890', $result->phone); $this->assertSame('Test Customer', $result->description); - + $lastRequest = $this->getLastRequest(); $this->assertSame('POST', $lastRequest['method']); $this->assertStringContainsString('/customer/partner-referrals', $lastRequest['uri']); @@ -98,7 +100,7 @@ public function testCreateCustomer(): void public function testCreatePaymentIntent(): void { $this->mockTokenRequest(); - + $paymentIntent = new PaymentIntent( id: null, amount: 1000, @@ -140,14 +142,14 @@ public function testCreatePaymentIntent(): void $result = $this->gateway->createPaymentIntent($paymentIntent); - $this->assertSame('CREATED', $result->status); + $this->assertSame(PaymentIntentStatus::RequiresPaymentMethod, $result->status); $this->assertSame(1000, $result->amount); $this->assertSame('USD', $result->currency); $this->assertSame('CUST-123', $result->customerId); $this->assertSame('paypal', $result->paymentMethodId); $this->assertSame('12345', $result->metadata['order_id']); $this->assertSame('test@example.com', $result->receiptEmail); - + $lastRequest = $this->getLastRequest(); $this->assertSame('POST', $lastRequest['method']); $this->assertStringContainsString('/payments/payment', $lastRequest['uri']); @@ -156,10 +158,10 @@ public function testCreatePaymentIntent(): void public function testCreatePaymentMethod(): void { $this->mockTokenRequest(); - + $paymentMethod = new PaymentMethod( id: null, - type: 'paypal', + type: PaymentMethodType::PAYPAL, details: ['email' => 'test@example.com'], customerId: 'CUST-123', billingDetails: [ @@ -172,9 +174,9 @@ public function testCreatePaymentMethod(): void // For PayPal, we're just testing that the method returns a PaymentMethod with the same customer ID $result = $this->gateway->createPaymentMethod($paymentMethod); - $this->assertSame('paypal', $result->type); + $this->assertSame(PaymentMethodType::PAYPAL, $result->type); $this->assertSame('CUST-123', $result->customerId); - + // Since PayPal handles payment methods differently, we don't expect an API call here // The method should just return the payment method with the customer ID set } @@ -182,7 +184,7 @@ public function testCreatePaymentMethod(): void public function testCreateRefund(): void { $this->mockTokenRequest(); - + $this->withResponse([ 'id' => 'REF-123', 'state' => 'completed', @@ -207,7 +209,7 @@ public function testCreateRefund(): void $this->assertSame('completed', $result['state']); $this->assertSame('10.00', $result['amount']['total']); $this->assertSame('USD', $result['amount']['currency']); - + $lastRequest = $this->getLastRequest(); $this->assertSame('POST', $lastRequest['method']); $this->assertStringContainsString('/payments/capture/CAPTURE-123/refund', $lastRequest['uri']); diff --git a/tests/Gateways/StripeGatewayTest.php b/tests/Gateways/StripeGatewayTest.php index 2b0cbca..547adfb 100644 --- a/tests/Gateways/StripeGatewayTest.php +++ b/tests/Gateways/StripeGatewayTest.php @@ -8,6 +8,7 @@ use Yiisoft\Payments\Models\Customer; use Yiisoft\Payments\Models\PaymentIntent; use Yiisoft\Payments\Models\PaymentMethod; +use Yiisoft\Payments\Enums\PaymentIntentStatus; use Yiisoft\Payments\Gateways\StripeGateway; use Yiisoft\Payments\Tests\Support\TestHttpClient; use Nyholm\Psr7\Factory\Psr17Factory; @@ -149,7 +150,7 @@ public function testConfirmPaymentIntent(): void $result = $this->gateway->confirmPaymentIntent('pi_test123', ['return_url' => 'https://example.com/return']); $this->assertSame('pi_test123', $result->id); - $this->assertSame('succeeded', $result->status); + $this->assertSame(PaymentIntentStatus::Succeeded, $result->status); $lastRequest = $this->getLastRequest(); $this->assertSame('POST', $lastRequest['method']); diff --git a/tests/Support/TestHttpClient.php b/tests/Support/TestHttpClient.php index 6e4d56e..37591ac 100644 --- a/tests/Support/TestHttpClient.php +++ b/tests/Support/TestHttpClient.php @@ -11,13 +11,11 @@ class TestHttpClient implements ClientInterface { - private Psr17Factory $factory; private ?array $nextResponse = null; public array $lastRequest = []; - public function __construct(Psr17Factory $factory) + public function __construct(private Psr17Factory $factory) { - $this->factory = $factory; } public function setNextResponse(?array $response): void