diff --git a/config/payment.php b/config/payment.php index cad10bf..153eb39 100644 --- a/config/payment.php +++ b/config/payment.php @@ -1,6 +1,6 @@ [ - 'gateway' => Payment::ORANGE, - 'country' => 'ci', + 'gateway' => Processor::ORANGE, + 'country' => 'ivory_coast', ], /** @@ -23,32 +23,60 @@ 'orange' => [ 'client_key' => '', 'client_secret' => '', - 'webhook_secret' => '' + 'webhook_secret' => '', + 'options' => [ + 'notif_url' => '', // Notification URL + 'return_url' => '', // Return URL after payment + 'cancel_url' => '', // Cancel URL if payment failed + ], ], 'mtn' => [ - 'subscription_key' => '', 'api_user' => '', 'api_key' => '', - 'environment' => 'sandbox', // or 'production' - 'webhook_secret' => '' + 'webhook_secret' => '', + 'subscription_key' => '', + 'options' => [ + 'notif_url' => '', // Notification URL + 'return_url' => '', // Return URL after payment + 'cancel_url' => '', // Cancel URL if payment failed + ], ], 'moov' => [ 'client_key' => '', 'client_secret' => '', - 'webhook_secret' => '' + 'webhook_secret' => '', + 'options' => [ + 'merchant_id' => '', + 'notif_url' => '', // Notification URL + 'return_url' => '', // Return URL after payment + 'cancel_url' => '', // Cancel URL if payment failed + ], ], 'wave' => [ 'api_key' => '', // Your Wave API key (starts with wave_sn_prod_ or wave_sn_sandbox_) - 'webhook_secret' => '' + 'webhook_secret' => '', + 'aggregated_merchant_id' => '', + 'options' => [ + 'restrict_payer_mobile' => false, + 'aggregated_merchant_id' => '', // Aggregated Merchant ID for Senegal to override default + 'notif_url' => '', // Notification URL + 'success_url' => '', // Success URL after payment + 'error_url' => '', // Error URL if payment failed + ], ], 'djamo' => [ 'client_key' => '', 'client_secret' => '', - 'webhook_secret' => '' + 'webhook_secret' => '', + 'options' => [ + 'notif_url' => '', // Notification URL + 'return_url' => '', // Return URL after payment + 'cancel_url' => '', // Cancel URL if payment failed + ], ] ], @@ -59,12 +87,25 @@ 'orange' => [ 'client_key' => '', 'client_secret' => '', - 'webhook_secret' => '' + 'webhook_secret' => '', + 'options' => [ + 'notif_url' => '', // Notification URL + 'return_url' => '', // Return URL after payment + 'cancel_url' => '', // Cancel URL if payment failed + ], ], 'wave' => [ 'api_key' => '', // Your Wave API key (starts with wave_sn_prod_ or wave_sn_sandbox_) - 'webhook_secret' => '' + 'webhook_secret' => '', + 'aggregated_merchant_id' => '', + 'options' => [ + 'restrict_payer_mobile' => false, + 'aggregated_merchant_id' => '', // Aggregated Merchant ID for Senegal to override default + 'notif_url' => '', // Notification URL + 'success_url' => '', // Success URL after payment + 'error_url' => '', // Error URL if payment failed + ], ], ], ]; diff --git a/docs/en.md b/docs/en.md index 3f13b55..7d12a36 100644 --- a/docs/en.md +++ b/docs/en.md @@ -298,20 +298,20 @@ For advanced use cases, you can use providers directly: ### Orange Money Direct Usage ```php -use Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyGateway; -use Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyTokenGenerator; +use Bow\Payment\Gateway\IvoryCost\Orange\OrangeGateway; +use Bow\Payment\Gateway\IvoryCost\Orange\OrangeTokenGenerator; $config = [ 'client_key' => 'YOUR_CLIENT_KEY', 'client_secret' => 'YOUR_CLIENT_SECRET', ]; -$tokenGenerator = new OrangeMoneyTokenGenerator( +$tokenGenerator = new OrangeTokenGenerator( $config['client_key'], $config['client_secret'] ); -$gateway = new OrangeMoneyGateway($tokenGenerator, $config); +$gateway = new OrangeGateway($tokenGenerator, $config); $result = $gateway->payment([ 'amount' => 1000, diff --git a/docs/fr.md b/docs/fr.md index 5761061..421a79a 100644 --- a/docs/fr.md +++ b/docs/fr.md @@ -298,20 +298,20 @@ Pour des cas d'utilisation avancés, vous pouvez utiliser les fournisseurs direc ### Utilisation Directe d'Orange Money ```php -use Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyGateway; -use Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyTokenGenerator; +use Bow\Payment\Gateway\IvoryCost\Orange\OrangeGateway; +use Bow\Payment\Gateway\IvoryCost\Orange\OrangeTokenGenerator; $config = [ 'client_key' => 'VOTRE_CLIENT_KEY', 'client_secret' => 'VOTRE_CLIENT_SECRET', ]; -$tokenGenerator = new OrangeMoneyTokenGenerator( +$tokenGenerator = new OrangeTokenGenerator( $config['client_key'], $config['client_secret'] ); -$gateway = new OrangeMoneyGateway($tokenGenerator, $config); +$gateway = new OrangeGateway($tokenGenerator, $config); $result = $gateway->payment([ 'amount' => 1000, diff --git a/readme.md b/readme.md index 168640d..10f7af0 100644 --- a/readme.md +++ b/readme.md @@ -39,7 +39,7 @@ use Bow\Payment\Payment; return [ 'default' => [ 'gateway' => Payment::ORANGE, - 'country' => 'ci', + 'country' => 'ivory_coast', ], 'ivory_coast' => [ @@ -66,22 +66,26 @@ return [ use Bow\Payment\Payment; // Configure the payment gateway -Payment::configure($config); +$gateway = Payment::configure($config); // Make a payment -Payment::payment([ +$gateway->payment([ 'amount' => 1000, + 'phone_number' => '+225070000001', 'reference' => 'ORDER-123', - 'notif_url' => 'https://your-app.com/webhook', - 'return_url' => 'https://your-app.com/success', - 'cancel_url' => 'https://your-app.com/cancel', + 'options' => [ + 'notif_url' => 'https://your-app.com/webhook', + 'return_url' => 'https://your-app.com/success', + 'cancel_url' => 'https://your-app.com/cancel', + ] ]); // Verify a transaction -$status = Payment::verify([ - 'amount' => 1000, - 'order_id' => 'ORDER-123', - 'pay_token' => 'TOKEN', +$status = $gateway->verify([ + 'reference' => 'ORDER-123', + 'options' => [ + // + ] ]); if ($status->isSuccess()) { @@ -224,9 +228,11 @@ Payment::configure([ $result = Payment::payment([ 'amount' => 1000, 'reference' => 'ORDER-123', - 'notif_url' => 'https://your-app.com/webhook', - 'return_url' => 'https://your-app.com/success', - 'cancel_url' => 'https://your-app.com/cancel', + 'options' => [ + 'notif_url' => 'https://your-app.com/webhook', + 'return_url' => 'https://your-app.com/success', + 'cancel_url' => 'https://your-app.com/cancel', + ], ]); ``` @@ -250,12 +256,12 @@ Payment::configure([ $result = Payment::payment([ 'amount' => 1000, - 'phone' => '0707070707', 'reference' => 'ORDER-123', + 'phone_number' => '0707070707', ]); // Verify transaction -$status = Payment::verify(['reference_id' => $result['reference_id']]); +$status = Payment::verify(['reference' => $result['reference']]); // Check balance $balance = Payment::balance(); diff --git a/src/Common/ProcessorGatewayInterface.php b/src/Common/ProcessorGatewayInterface.php index bcff06b..e5eec62 100644 --- a/src/Common/ProcessorGatewayInterface.php +++ b/src/Common/ProcessorGatewayInterface.php @@ -2,33 +2,48 @@ namespace Bow\Payment\Common; +use Bow\Payment\Exceptions\InputValidationException; + interface ProcessorGatewayInterface { /** * Make payment * + * @param array $params * @return mixed */ - public function payment(...$args); + public function payment(array $params); /** * Make transfer * + * @param array $params * @return mixed */ - public function transfer(...$args); + public function transfer(array $params); /** * Get balance * + * @param array $params * @return mixed */ - public function balance(...$args); + public function balance(array $params = []); /** * Verify payment * + * @param array $params + * @return void + */ + public function verify(array $params); + + /** + * Validate payment data + * + * @param array $params + * @throws InputValidationException * @return void */ - public function verify(); + public function validatePaymentData(array $params): void; } diff --git a/src/Exceptions/InputValidationException.php b/src/Exceptions/InputValidationException.php new file mode 100644 index 0000000..405dbd1 --- /dev/null +++ b/src/Exceptions/InputValidationException.php @@ -0,0 +1,18 @@ +tokenGenerator->getToken(); $payment = new MomoPayment($token, $this->environment); - $amount = $args['amount'] ?? $args[0]; - $phone = $args['phone'] ?? $args[1]; - $reference = $args['reference'] ?? $args[2] ?? uniqid('momo_'); - $currency = $args['currency'] ?? 'XOF'; + $this->validatePaymentData($params); + + $amount = $params['amount']; + $phone = $params['phone_number']; + $reference = $params['reference'] ?? uniqid('momo_'); + $currency = $params['currency'] ?? 'XOF'; return $payment->requestToPay([ 'amount' => $amount, @@ -75,10 +77,10 @@ public function payment(...$args) /** * Make transfer * - * @param mixed ...$args + * @param array $params * @return mixed */ - public function transfer(...$args) + public function transfer(array $params) { // MTN Mobile Money CI uses Collection API for payments // Transfer functionality would require Disbursement API @@ -88,10 +90,10 @@ public function transfer(...$args) /** * Get balance * - * @param mixed ...$args + * @param array $params * @return mixed */ - public function balance(...$args) + public function balance(array $params = []) { $token = $this->tokenGenerator->getToken(); $transaction = new MomoTransaction($token, $this->environment); @@ -104,13 +106,25 @@ public function balance(...$args) * * @return mixed */ - public function verify(...$args) + public function verify(array $params) { $token = $this->tokenGenerator->getToken(); + $transaction = new MomoTransaction($token, $this->environment); - $referenceId = $args['reference_id'] ?? $args[0]; + $referenceId = $params['reference']; return $transaction->getTransactionStatus($referenceId); } + + /** + * Validate payment data + * + * @param array $params + * @return void + */ + public function validatePaymentData(array $params): void + { + // Validation logic can be implemented here as needed + } } diff --git a/src/Gateway/IvoryCost/MoovFlooz/MoovFloozGateway.php b/src/Gateway/IvoryCost/Moov/MoovGateway.php similarity index 70% rename from src/Gateway/IvoryCost/MoovFlooz/MoovFloozGateway.php rename to src/Gateway/IvoryCost/Moov/MoovGateway.php index 6a0d672..87008ec 100644 --- a/src/Gateway/IvoryCost/MoovFlooz/MoovFloozGateway.php +++ b/src/Gateway/IvoryCost/Moov/MoovGateway.php @@ -1,6 +1,6 @@ getTokenGenerator(); - $payment = new OrangeMoneyPayment( + $payment = new OrangePayment( $token_generator->getToken(), $this->config['client_secret'], ); @@ -37,20 +36,20 @@ public function payment(...$args) // Set the right production endpoint $payment->setPaymentEndpoint('/orange-money-webpay/v1/webpayment'); - if (isset($args['notif_url'])) { - $payment->setNotifyUrl($args['notif_url']); + if (isset($params['notif_url'])) { + $payment->setNotifyUrl($params['notif_url']); } - if (isset($args['cancel_url'])) { - $payment->setCancelUrl($args['cancel_url']); + if (isset($params['cancel_url'])) { + $payment->setCancelUrl($params['cancel_url']); } - if (isset($args['return_url'])) { - $payment->setReturnUrl($args['return_url']); + if (isset($params['return_url'])) { + $payment->setReturnUrl($params['return_url']); } - $amount = $args['amount']; - $reference = $args['reference']; + $amount = $params['amount']; + $reference = $params['reference']; $orange = $payment->prepare($amount, $reference); @@ -63,22 +62,22 @@ public function payment(...$args) /** * Verify payment * - * @param array ...$args + * @param array $params * @return ProcessorStatusInterface */ - public function verify(...$args) + public function verify(array $params) { $token_generator = $this->getTokenGenerator(); // Transaction status - $transaction = new OrangeMoneyTransaction($token_generator->getToken()); + $transaction = new OrangeTransaction($token_generator->getToken()); // Set the production url $transaction->setTransactionStatusEndpoint('/orange-money-webpay/v1/transactionstatus'); - $amount = $args['amount']; - $order_id = $args['order_id']; - $pay_token = $args['pay_token']; + $amount = $params['amount']; + $order_id = $params['order_id']; + $pay_token = $params['pay_token']; // Check the transaction status return $transaction->check($amount, $order_id, $pay_token); @@ -87,10 +86,10 @@ public function verify(...$args) /** * Transfer money * - * @param array ...$args + * @param array $params * @return mixed */ - public function transfer(...$args) + public function transfer(array $params) { throw new PaymentRequestException( 'Orange Money payment gateway is not yet implemented. Implementation pending official API documentation.' @@ -100,24 +99,35 @@ public function transfer(...$args) /** * Get balance * - * @param array ...$args + * @param array $params * @return mixed */ - public function balance(...$args) + public function balance(array $params = []) { throw new PaymentRequestException( 'Orange Money balance inquiry is not yet implemented.' ); } + /** + * Validate payment data + * + * @param array $params + * @return void + */ + public function validatePaymentData(array $params): void + { + // Validation logic can be implemented here as needed + } + /** * Create the Token Generator instance * - * @return OrangeMoneyTokenGenerator + * @return OrangeTokenGenerator */ private function getTokenGenerator() { - $token_generator = new OrangeMoneyTokenGenerator($this->config['client_key']); + $token_generator = new OrangeTokenGenerator($this->config['client_key']); // Set the right production endpoint $token_generator->setTokenGeneratorEndpoint('/oauth/v2/token'); diff --git a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyPayment.php b/src/Gateway/IvoryCost/Orange/OrangePayment.php similarity index 89% rename from src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyPayment.php rename to src/Gateway/IvoryCost/Orange/OrangePayment.php index b2b3304..0a542a9 100644 --- a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyPayment.php +++ b/src/Gateway/IvoryCost/Orange/OrangePayment.php @@ -1,11 +1,11 @@ http = new HttpClient(['base_uri' => 'https://api.orange.com']); } @@ -60,7 +60,7 @@ public function __construct(private OrangeMoneyToken $token, private string $mer * * @param int|double $amount * @param string $reference - * @return OrangeMoney + * @return Orange */ public function prepare($amount, string $reference) { @@ -76,7 +76,7 @@ public function prepare($amount, string $reference) // Parse Json data $data = json_decode($response->getBody()->getContents(), true); - return new OrangeMoneyResponse( + return new OrangeResponse( $data['payment_url'], $data['pay_token'], $data['notif_token'] diff --git a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyResponse.php b/src/Gateway/IvoryCost/Orange/OrangeResponse.php similarity index 88% rename from src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyResponse.php rename to src/Gateway/IvoryCost/Orange/OrangeResponse.php index 464bc23..a001912 100644 --- a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyResponse.php +++ b/src/Gateway/IvoryCost/Orange/OrangeResponse.php @@ -1,11 +1,11 @@ access_token, $token->token_type, $token->expires_in diff --git a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransaction.php b/src/Gateway/IvoryCost/Orange/OrangeTransaction.php similarity index 88% rename from src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransaction.php rename to src/Gateway/IvoryCost/Orange/OrangeTransaction.php index 1e21cb3..d245c62 100644 --- a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransaction.php +++ b/src/Gateway/IvoryCost/Orange/OrangeTransaction.php @@ -1,16 +1,16 @@ token = $token; @@ -73,7 +73,7 @@ public function check($amount, string $order_id, string $pay_token) // Cast the request response $status = json_decode($response->getBody()->getContents()); - return new OrangeMoneyTransactionStatus( + return new OrangeTransactionStatus( $status->status, $status->notif_token ); diff --git a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransactionStatus.php b/src/Gateway/IvoryCost/Orange/OrangeTransactionStatus.php similarity index 87% rename from src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransactionStatus.php rename to src/Gateway/IvoryCost/Orange/OrangeTransactionStatus.php index bac51c1..8be2f7d 100644 --- a/src/Gateway/IvoryCost/OrangeMoney/OrangeMoneyTransactionStatus.php +++ b/src/Gateway/IvoryCost/Orange/OrangeTransactionStatus.php @@ -1,10 +1,10 @@ apiKey = $apiKey; $this->http = new HttpClient([ 'base_uri' => self::BASE_URL, 'headers' => [ @@ -60,6 +52,7 @@ public function createCheckoutSession(array $data, ?string $idempotencyKey = nul { try { $headers = []; + if ($idempotencyKey !== null) { $headers['Idempotency-Key'] = $idempotencyKey; } diff --git a/src/Gateway/IvoryCost/Wave/WaveGateway.php b/src/Gateway/IvoryCost/Wave/WaveGateway.php index eb4b876..93a16b9 100644 --- a/src/Gateway/IvoryCost/Wave/WaveGateway.php +++ b/src/Gateway/IvoryCost/Wave/WaveGateway.php @@ -5,6 +5,7 @@ use Bow\Payment\Common\ProcessorGatewayInterface; use Bow\Payment\Exceptions\PaymentRequestException; use Bow\Payment\Exceptions\ConfigurationException; +use Bow\Payment\Exceptions\InputValidationException; /** * Wave Gateway @@ -43,57 +44,76 @@ public function __construct(array $config) /** * Make payment - Create a Wave checkout session * - * @param mixed ...$args + * @param array $params * @return array * @throws PaymentRequestException * * Expected parameters: * - amount: (required) Amount to collect - * - notify_url: (required) Redirect URL on notification - * - success_url: (required) Redirect URL on success - * - error_url: (required) Redirect URL on error + * - reference: (optional) Your unique reference * - currency: (optional) Currency code (default: XOF) - * - client_reference: (optional) Your unique reference (max 255 chars) - * - restrict_payer_mobile: (optional) Phone number (E.164 format) - * - aggregated_merchant_id: (optional) For aggregators only - * - idempotency_key: (optional) Unique key to prevent duplicate payments (auto-generated if not provided) + * - optoons: (optional) Additional payment options + * - notify_url: (required) Redirect URL on notification + * - success_url: (required) Redirect URL on success + * - error_url: (required) Redirect URL on error + * - restrict_payer_mobile: (optional) Phone number (E.164 format) + * - aggregated_merchant_id: (optional) For aggregators only + * - idempotency_key: (optional) Unique key to prevent duplicate payments (auto-generated if not provided) */ - public function payment(...$args) + public function payment(array $params): array { + if (!isset($params['options']) || !is_array($params['options'])) { + $params['options'] = []; + } + + // Merge default options from config + $params['options'] = array_merge( + $this->config['options'] ?? [], + $params['options'] + ); + // Validate required fields - $this->validatePaymentData($args); + $this->validatePaymentData($params); // Generate idempotency key if not provided (prevents duplicate payments) - $idempotencyKey = $args['idempotency_key'] ?? $this->generateIdempotencyKey(); + $idempotencyKey = $params['options']['idempotency_key'] ?? $this->generateIdempotencyKey(); // Create checkout session $session = $this->client->createCheckoutSession( [ - 'amount' => $args['amount'], - 'currency' => $args['currency'] ?? 'XOF', - 'notify_url' => $args['notify_url'], - 'success_url' => $args['success_url'], - 'error_url' => $args['error_url'], - 'client_reference' => $args['client_reference'] ?? null, - 'restrict_payer_mobile' => $args['restrict_payer_mobile'] ?? null, - 'aggregated_merchant_id' => $args['aggregated_merchant_id'] ?? null, + 'amount' => $params['amount'], + 'currency' => $params['currency'] ?? 'XOF', + 'client_reference' => $params['reference'] ?? null, + 'notify_url' => $params['options']['notify_url'] ?? null, + 'success_url' => $params['options']['success_url'] ?? null, + 'error_url' => $params['options']['error_url'] ?? null, + 'restrict_payer_mobile' => $params['options']['restrict_payer_mobile'] ?? false, + 'aggregated_merchant_id' => $params['options']['aggregated_merchant_id'] ?? null, ], $idempotencyKey ); return [ - 'success' => true, - 'session_id' => $session->getId(), - 'wave_launch_url' => $session->getWaveLaunchUrl(), - 'amount' => $session->getAmount(), - 'currency' => $args['currency'] ?? 'XOF', - 'checkout_status' => $session->getCheckoutStatus(), - 'payment_status' => $session->getPaymentStatus(), - 'transaction_id' => $session->getTransactionId(), - 'client_reference' => $session->getClientReference(), - 'when_expires' => $session->toArray()['when_expires'], - 'idempotency_key' => $idempotencyKey, - 'session' => $session, + 'status' => 'success', + 'reference' => $params['reference'], + 'payment_url' => $session->getWaveLaunchUrl(), + 'provider' => 'wave', + 'provider_transaction_id' => $session->getTransactionId(), + 'provider_status' => $session->getPaymentStatus(), + 'provider_data' => [ + 'success' => true, + 'session_id' => $session->getId(), + 'wave_launch_url' => $session->getWaveLaunchUrl(), + 'amount' => $session->getAmount(), + 'currency' => $args['currency'] ?? 'XOF', + 'checkout_status' => $session->getCheckoutStatus(), + 'payment_status' => $session->getPaymentStatus(), + 'transaction_id' => $session->getTransactionId(), + 'client_reference' => $session->getClientReference(), + 'when_expires' => $session->toArray()['when_expires'], + 'idempotency_key' => $idempotencyKey, + 'session' => $session, + ], ]; } @@ -107,25 +127,25 @@ public function payment(...$args) * Expected parameters: * - session_id: (optional) Checkout session ID * - transaction_id: (optional) Transaction ID - * - client_reference: (optional) Your unique reference + * - reference: (optional) Your unique reference * * Note: Provide at least one of the above identifiers */ - public function verify(...$args) + public function verify(array $params) { $session = null; // Try to retrieve by session ID - if (isset($args['session_id'])) { - $session = $this->client->retrieveCheckoutSession($args['session_id']); + if (isset($params['session_id'])) { + $session = $this->client->retrieveCheckoutSession($params['session_id']); } // Try to retrieve by transaction ID - elseif (isset($args['transaction_id'])) { - $session = $this->client->retrieveCheckoutByTransactionId($args['transaction_id']); + elseif (isset($params['transaction_id'])) { + $session = $this->client->retrieveCheckoutByTransactionId($params['transaction_id']); } // Try to search by client reference - elseif (isset($args['client_reference'])) { - $sessions = $this->client->searchCheckoutSessions($args['client_reference']); + elseif (isset($params['client_reference'])) { + $sessions = $this->client->searchCheckoutSessions($params['client_reference']); if (empty($sessions)) { throw new PaymentRequestException('No checkout session found with the provided client reference'); } @@ -244,34 +264,38 @@ private function validateConfig(): void * Validate payment data * * @param array $data - * @throws PaymentRequestException + * @throws InputValidationException */ - private function validatePaymentData(array $data): void + public function validatePaymentData(array $data): void { if (!isset($data['amount']) || empty($data['amount'])) { - throw new PaymentRequestException('Amount is required for Wave payment'); + throw new InputValidationException('Amount is required for Wave payment'); } - if (!isset($data['success_url']) || empty($data['success_url'])) { - throw new PaymentRequestException('Success URL is required for Wave payment'); + if (!isset($data['reference']) || empty($data['reference'])) { + throw new InputValidationException('Reference is required for Wave payment'); } - if (!isset($data['error_url']) || empty($data['error_url'])) { - throw new PaymentRequestException('Error URL is required for Wave payment'); + if (!isset($data['currency']) || empty($data['currency'])) { + throw new InputValidationException('Currency is required for Wave payment'); } // Validate URLs are HTTPS - if (!str_starts_with($data['success_url'], 'https://')) { - throw new PaymentRequestException('Success URL must use HTTPS protocol'); + if (isset($data['options']['success_url']) && !str_starts_with($data['options']['success_url'], 'https://')) { + throw new InputValidationException('Success URL must use HTTPS protocol'); + } + + if (isset($data['options']['error_url']) && !str_starts_with($data['options']['error_url'], 'https://')) { + throw new InputValidationException('Error URL must use HTTPS protocol'); } - if (!str_starts_with($data['error_url'], 'https://')) { - throw new PaymentRequestException('Error URL must use HTTPS protocol'); + if (isset($data['options']['cancel_url']) && !str_starts_with($data['options']['cancel_url'], 'https://')) { + throw new InputValidationException('Cancel URL must use HTTPS protocol'); } // Validate amount is positive if (floatval($data['amount']) <= 0) { - throw new PaymentRequestException('Amount must be greater than zero'); + throw new InputValidationException('Amount must be greater than zero'); } // Validate currency if provided @@ -280,7 +304,7 @@ private function validatePaymentData(array $data): void if ($currency === 'XOF') { // XOF doesn't allow decimals if (strpos((string) $data['amount'], '.') !== false) { - throw new PaymentRequestException( + throw new InputValidationException( 'XOF currency does not allow decimal places. Amount must be a whole number.' ); } diff --git a/src/Gateway/Senegal/OrangeMoney/OrangeMoneyGateway.php b/src/Gateway/Senegal/Orange/OrangeGateway.php similarity index 68% rename from src/Gateway/Senegal/OrangeMoney/OrangeMoneyGateway.php rename to src/Gateway/Senegal/Orange/OrangeGateway.php index ef35e94..e838189 100644 --- a/src/Gateway/Senegal/OrangeMoney/OrangeMoneyGateway.php +++ b/src/Gateway/Senegal/Orange/OrangeGateway.php @@ -1,8 +1,8 @@ - */ - public const CI_PROVIDER = [ - Payment::ORANGE => \Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyGateway::class, - Payment::MTN => \Bow\Payment\Gateway\IvoryCost\Mono\MonoGateway::class, - Payment::MOOV => \Bow\Payment\Gateway\IvoryCost\MoovFlooz\MoovFloozGateway::class, - Payment::WAVE => \Bow\Payment\Gateway\IvoryCost\Wave\WaveGateway::class, - Payment::DJAMO => \Bow\Payment\Gateway\IvoryCost\Djamo\DjamoGateway::class, - ]; - - /** - * Senegal payment provider mapping - * Maps payment provider identifiers to their respective service classes - * for payment processing in Senegal (SN) - * - * @var array - */ - public const SN_PROVIDER = [ - Payment::ORANGE => \Bow\Payment\Gateway\Senegal\OrangeMoney\OrangeMoneyGateway::class, - Payment::WAVE => \Bow\Payment\Gateway\Senegal\Wave\WaveGateway::class, - ]; - - /** - * The payment manager instance - * - * @var ProcessorGatewayInterface - */ - private static $providerGateway; - - /** - * The payment instance - * - * @var Payment - */ - private static $instance; - - /** - * ForPayment constructor - * - * @param array $config - * @return mixed - */ - public function __construct(private array $config) - { - $default = $this->config['default']; - $country = $default['country'] ?? 'ci'; - $defaultProvider = $default['gateway'] ?? Payment::ORANGE; - - $this->resolveGateway($country, $defaultProvider); - } - - /** - * Resolve the payment gateway - * - * @param string $country - * @param string $provider - * @return void - */ - private function resolveGateway(string $country, string $provider) - { - switch ($country) { - case self::CI: - $provider = self::CI_PROVIDER[$provider] ?? null; - if ($provider === null) { - throw new \InvalidArgumentException("The payment gateway [{$provider}] is not supported in country [{$country}]."); - } - $config = $this->resolveConfig('ivory_coast', $provider); - static::$providerGateway = new $provider($config); - break; - case self::SN: - $provider = self::SN_PROVIDER[$provider] ?? null; - if ($provider === null) { - throw new \InvalidArgumentException("The payment gateway [{$provider}] is not supported in country [{$country}]."); - } - $config = $this->resolveConfig('senegal', $provider); - static::$providerGateway = new $provider($config); - break; - // Other gateways can be added here - default: - throw new \InvalidArgumentException("The payment gateway [{$provider}] is not supported."); - } - } - - /** - * Resolve configuration for a specific country and provider - * - * @param string $country - * @param string $provider - * @return array|null - */ - public function resolveConfig(string $country, string $provider) - { - return $this->config[$country][$provider] ?? []; - } - - /** - * Make configuration - * - * @return PaymentManagerContract - */ - public static function configure(array $configuration) - { - static::$instance = new Payment($configuration); - - return static::$instance; - } - - /** - * Switch payment provider - * - * @param string $country - * @param string $provider - * @return void - */ - public function withProvider(string $country, string $provider): void + public function withPaymentProvider(string $country, string $provider): self { - $this->resolveGateway($country, $provider); - } + Processor::withProvider($country, $provider); - /** - * Make payment - * - * @return mixed - */ - public function payment(...$args) - { - return static::$providerGateway->payment(...$args); + return $this; } /** - * Make transfer + * Make user payment * * @return mixed */ - public function transfer(...$args) - { - return static::$providerGateway->transfer(...$args); - } - - /** - * Get balance - * - * @return mixed - */ - public function balance(...$args) - { - return static::$providerGateway->balance(...$args); - } - - /** - * Verify payment - * - * @return void - */ - public function verify() + public function payment(float $amount, string $reference, array $options = []) { - return static::$providerGateway->verify(); + return Processor::payment([ + 'amount' => $amount, + 'reference' => $reference, + 'options' => $options, + ]); } /** - * __callStatic + * Make user payment * - * @param string $methodName - * @param array $methodArguments * @return mixed */ - public static function __callStatic($methodName, $methodArguments) + public function transfer($amount, $reference, array $options = []) { - if (method_exists(static::$instance, $methodName)) { - return call_user_func_array([static::$instance, $methodName], $methodArguments); - } + return Processor::transfer([ + 'amount' => $amount, + 'reference' => $reference, + 'options' => $options, + ]); } } diff --git a/src/Processor.php b/src/Processor.php new file mode 100644 index 0000000..8eedad1 --- /dev/null +++ b/src/Processor.php @@ -0,0 +1,249 @@ + + */ + public const IVORY_COAST_PROVIDER = [ + Processor::ORANGE => \Bow\Payment\Gateway\IvoryCost\Orange\OrangeGateway::class, + Processor::MTN => \Bow\Payment\Gateway\IvoryCost\Mono\MonoGateway::class, + Processor::MOOV => \Bow\Payment\Gateway\IvoryCost\Moov\MoovGateway::class, + Processor::WAVE => \Bow\Payment\Gateway\IvoryCost\Wave\WaveGateway::class, + Processor::DJAMO => \Bow\Payment\Gateway\IvoryCost\Djamo\DjamoGateway::class, + ]; + + /** + * Senegal payment provider mapping + * Maps payment provider identifiers to their respective service classes + * for payment processing in Senegal (SN) + * + * @var array + */ + public const SENEGAL_PROVIDER = [ + Processor::ORANGE => \Bow\Payment\Gateway\Senegal\Orange\OrangeGateway::class, + Processor::WAVE => \Bow\Payment\Gateway\Senegal\Wave\WaveGateway::class, + ]; + + /** + * The payment manager instance + * + * @var ProcessorGatewayInterface + */ + private static $providerGateway; + + /** + * The payment instance + * + * @var Payment + */ + private static $instance; + + /** + * ForPayment constructor + * + * @param array $config + * @return mixed + */ + public function __construct(private array $config) + { + $default = $this->config['default']; + $country = $default['country'] ?? 'ivory_coast'; + $defaultProvider = $default['gateway'] ?? Processor::ORANGE; + + $this->resolveGateway($country, $defaultProvider); + } + + /** + * Resolve the payment gateway + * + * @param string $country + * @param string $provider + * @return void + */ + private function resolveGateway(string $country, string $provider) + { + match($country) { + self::CI => $this->providerFactory('ivory_coast', $provider), + self::SN => $this->providerFactory('senegal', $provider), + default => throw new \InvalidArgumentException("The payment gateway [{$provider}] is not supported."), + }; + } + + /** + * Resolve configuration for a specific country and provider + * + * @param string $country + * @param string $provider + * @return array|null + */ + private function resolveConfig(string $country, string $provider) + { + return $this->config[$country][$provider] ?? []; + } + + /** + * Provider factory + * + * @param string $country + * @param string $provider + * @return void + */ + private function providerFactory(string $country, string $provider): void + { + $provider = self::${strtoupper($country) . '_PROVIDER'}[$provider] ?? null; + + if ($provider === null) { + throw new \InvalidArgumentException("The payment gateway [{$provider}] is not supported in country [{$country}]."); + } + + $config = $this->resolveConfig($country, $provider); + + static::$providerGateway = new $provider($config); + } + + /** + * Make configuration + * + * @return PaymentManagerContract + */ + public static function configure(array $configuration) + { + static::$instance = new Payment($configuration); + + return static::$instance; + } + + /** + * Switch payment provider + * + * @param string $country + * @param string $provider + * @return void + */ + public function withProvider(string $country, string $provider): void + { + $this->resolveGateway($country, $provider); + } + + /** + * Make payment + * + * @param array $params + * @return mixed + */ + public function payment(array $params) + { + return static::$providerGateway->payment($params); + } + + /** + * Make transfer + * + * @param array $params + * @return mixed + */ + public function transfer(array $params) + { + return static::$providerGateway->transfer($params); + } + + /** + * Get balance + * + * @param array $params + * @return mixed + */ + public function balance(array $params = []) + { + return static::$providerGateway->balance($params); + } + + /** + * Verify payment + * + * @param array $params + * @return mixed + */ + public function verify(array $params) + { + return static::$providerGateway->verify($params); + } + + /** + * Validate payment data + * + * @param array $params + * @throws InputValidationException + * @return void + */ + public function validatePaymentData(array $params): void + { + static::$providerGateway->validatePaymentData($params); + } + + /** + * __callStatic + * + * @param string $methodName + * @param array $methodArguments + * @return mixed + */ + public static function __callStatic($methodName, $methodArguments) + { + if (method_exists(static::$instance, $methodName)) { + return call_user_func_array([static::$instance, $methodName], $methodArguments); + } + } +} diff --git a/src/PaymentConfiguration.php b/src/ProcessorConfiguration.php similarity index 93% rename from src/PaymentConfiguration.php rename to src/ProcessorConfiguration.php index 2ddf62d..17a1efc 100644 --- a/src/PaymentConfiguration.php +++ b/src/ProcessorConfiguration.php @@ -5,7 +5,7 @@ use Bow\Configuration\Configuration; use Bow\Configuration\Loader as Config; -class PaymentConfiguration extends Configuration +class ProcessorConfiguration extends Configuration { /** * Create payment configuration diff --git a/src/UserPayment.php b/src/UserPayment.php deleted file mode 100644 index 60bbd73..0000000 --- a/src/UserPayment.php +++ /dev/null @@ -1,33 +0,0 @@ -createMock(OrangeMoneyTokenGenerator::class); + $stub = $this->createMock(OrangeTokenGenerator::class); $stub->expects($this->once())->method('getToken') - ->willReturn($this->getMockBuilder(OrangeMoneyToken::class) + ->willReturn($this->getMockBuilder(OrangeToken::class) ->disableOriginalConstructor()->getMock()); - $this->assertInstanceOf(OrangeMoneyToken::class, $stub->getToken()); + $this->assertInstanceOf(OrangeToken::class, $stub->getToken()); } public function testPreparePayment() { - $token = $this->getMockBuilder(OrangeMoneyToken::class) + $token = $this->getMockBuilder(OrangeToken::class) ->disableOriginalConstructor()->getMock(); - $payment = $this->getMockBuilder(OrangeMoneyPayment::class) + $payment = $this->getMockBuilder(OrangePayment::class) ->setConstructorArgs([$token, 123456]) ->setMethods(['prepare'])->getMock(); - $payment_status = $this->createMock(OrangeMoneyPayment::class); + $payment_status = $this->createMock(OrangePayment::class); $payment->method('prepare')->willReturn($payment_status); - $this->assertInstanceOf(OrangeMoneyPayment::class, $payment->prepare(500, 'reference', 1)); + $this->assertInstanceOf(OrangePayment::class, $payment->prepare(500, 'reference', 1)); } public function testMakePayment() { - $orange = $this->getMockBuilder(OrangeMoneyPayment::class) + $orange = $this->getMockBuilder(OrangePayment::class) ->disableOriginalConstructor()->setMethods(['pay'])->getMock(); $orange->method('pay')->willReturn(true); diff --git a/tests/PaymentTest.php b/tests/PaymentTest.php index dd8c203..01bb004 100644 --- a/tests/PaymentTest.php +++ b/tests/PaymentTest.php @@ -29,7 +29,7 @@ public function testProviderMapping() $this->assertArrayHasKey(Payment::DJAMO, $providers); $this->assertEquals( - \Bow\Payment\Gateway\IvoryCost\OrangeMoney\OrangeMoneyGateway::class, + \Bow\Payment\Gateway\IvoryCost\Orange\OrangeGateway::class, $providers[Payment::ORANGE] );