├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── payment-gateways.php ├── resources └── views │ ├── .gitkeep │ ├── checkout │ └── klasha.blade.php │ ├── error.blade.php │ ├── layout.blade.php │ ├── loader.blade.php │ └── status.blade.php ├── routes └── web.php └── src ├── Contracts └── ProviderInterface.php ├── DataObjects ├── SessionData.php └── TransactionData.php ├── Enums └── Provider.php ├── Exceptions ├── HttpException.php ├── InitializationException.php └── VerificationException.php ├── Facades └── PaymentGateway.php ├── Http └── Controllers │ ├── CheckoutController.php │ ├── CompletePaymentController.php │ └── ErrorController.php ├── PaymentGateway.php ├── PaymentGatewayServiceProvider.php └── Providers ├── AbstractProvider.php ├── FlutterwaveProvider.php ├── KlashaProvider.php ├── MonnifyProvider.php ├── PawapayProvider.php ├── Pay4meProvider.php ├── PaystackProvider.php ├── SeerbitProvider.php ├── StartbuttonProvider.php └── StripeProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-payment-gateways` will be documented in this file. 4 | 5 | ## 3.0.10 - 2025-04-29 6 | 7 | - Clean up channel checks for Startbutton [#96010](https://github.com/stephenjude/laravel-payment-gateways/commit/96010a342a5588eb2cbbaeb1d5a1394494fd6ed9) 8 | 9 | **Full Changelog**: https://github.com/stephenjude/laravel-payment-gateways/compare/3.0.9...3.0.10 10 | 11 | ## 3.0.9 - 2025-04-21 12 | 13 | ### What's Changed 14 | 15 | * Add return types to payment provider interface by @stephenjude in https://github.com/stephenjude/laravel-payment-gateways/pull/22 16 | 17 | ### New Contributors 18 | 19 | * @stephenjude made their first contribution in https://github.com/stephenjude/laravel-payment-gateways/pull/22 20 | 21 | **Full Changelog**: https://github.com/stephenjude/laravel-payment-gateways/compare/3.0.8...3.0.9 22 | 23 | ## 3.0.8 - 2025-04-21 24 | 25 | ### What's Changed 26 | 27 | * hotfix: metadata is not always present by @eyounelson in https://github.com/stephenjude/laravel-payment-gateways/pull/21 28 | 29 | **Full Changelog**: https://github.com/stephenjude/laravel-payment-gateways/compare/3.0.7...3.0.8 30 | 31 | ## 3.0.7 - 2025-02-01 32 | 33 | - Removed debug statement by @eyounelson in #20 34 | 35 | ## 3.0.6 - 2025-01-27 36 | 37 | - Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 in #19 38 | 39 | ## 3.0.5 - 2024-09-02 40 | 41 | - Bump deps 42 | 43 | ## 3.0.4 - 2024-06-07 44 | 45 | - Make debug mode Laravel compliant. 46 | - Make Startbutton's USD default payment channel null 47 | 48 | ## 3.0.3 - 2024-06-04 49 | 50 | - Make Startbutton `partner` parameter optional 51 | 52 | ## 3.0.2 - 2024-05-10 53 | 54 | - Fixed Monnify payment status and verification by @stephenjude 55 | - Add partner parameter and default partner for Startbutton integration by @stephenjude 56 | 57 | ## 3.0.1 - 2024-05-08 58 | 59 | - Fixed Klasha initialization error by @stephenjude 60 | 61 | ## 3.0.0 - 2024-04-24 62 | 63 | - Integrate Startbutton APIs 64 | - Laravel 11 compatibility. 65 | 66 | ## 2.2.1 - 2024-01-23 67 | 68 | - Fixed payment confirmation for Seerbit. 69 | 70 | ## 2.2.0 - 2023-11-21 71 | 72 | - Pawapay integration. 73 | 74 | ## 2.1.2 - 2023-10-02 75 | 76 | - Changed transaction metadata to mixed type. 77 | 78 | ## 2.1.1 - 2023-09-22 79 | 80 | - Added metadata to Monnify payment initialization by @Official-David 81 | 82 | ## 2.1.0 - 2023-09-22 83 | 84 | - Added custom routes for successful and failed payments. 85 | - Added transaction find & list method for Pay4Me, Paystack, Flutterwave, Monnify, and Stripe. SEERBIT & Klasha supports only find. 86 | - Fixed Monnify payment confirmation. 87 | - Map error response from supported providers. 88 | - Updated SEERBIT payment integration. 89 | - Update API URL usage. 90 | 91 | ## 2.0.8 - 2023-06-21 92 | 93 | - Fixed API URL path usage 94 | 95 | ## 2.0.7 - 2023-06-20 96 | 97 | - Update API URL usage 98 | 99 | ## 2.0.6 - 2023-05-11 100 | 101 | - Added Seerbit `approved` status code 102 | - Removed debug statement 103 | - Clean up 104 | 105 | ## 2.0.5 - 2023-05-06 106 | 107 | - Switch to Monnify live API 108 | 109 | ## 2.0.4 - 2023-05-06 110 | 111 | - Monnify integration 🔥 112 | 113 | ## 2.0.3 - 2023-04-25 114 | 115 | - SEERBIT Integration 116 | 117 | ## 2.0.2 - 2023-04-13 118 | 119 | - FEATURE: Add custom route configs for successful and failed payments. 120 | 121 | ## 2.0.1 - 2023-04-04 122 | 123 | - Allow html contents on configurable gateway messages 124 | 125 | ## 2.0.0 - 2023-03-07 126 | 127 | - Laravel 10 support 128 | 129 | ## 1.0.18 - 2023-03-07 130 | 131 | - Laravel 10 support 132 | 133 | ## 1.0.17 - 2023-03-05 134 | 135 | - Clean up 136 | 137 | ## 1.0.16 - 2023-03-05 138 | 139 | - Clean up 140 | 141 | ## 1.0.15 - 2023-03-05 142 | 143 | - Stripe payment verification 144 | 145 | ## 1.0.14 - 2023-03-05 146 | 147 | - Fixed stripe amount parameter. 148 | - WIP: Seerbit integration. 149 | 150 | ## 1.0.13 - 2023-01-27 151 | 152 | - Add gateway error to exception. 153 | 154 | ## 1.0.12 - 2023-01-09 155 | 156 | - Fixed dynamic support email - #6 157 | 158 | ## 1.0.11 - 2023-01-05 159 | 160 | - Fixed typographical errors. 161 | - Fixed Pay4Me verification error. 162 | 163 | ## 1.0.10 - 2023-01-05 164 | 165 | - Update: Pay4Me provider default Api URL 166 | 167 | ## 1.0.9 - 2023-01-04 168 | 169 | - UPDATE: Pay4Me provider 170 | 171 | ## 1.0.8 - 2023-01-04 172 | 173 | - Clean up 174 | - Fixed test workflow 175 | 176 | ## 1.0.7 - 2023-01-04 177 | 178 | - Fixed reflection error for Pay4Me provider. 179 | 180 | ## 1.0.6 - 2023-01-04 181 | 182 | - Fixed reflection error for Pay4Me provider. 183 | 184 | ## 1.0.5 - 2023-01-04 185 | 186 | - Pay4Me Pay integration. 187 | 188 | ## 1.0.4 - 2022-11-06 189 | 190 | - Fixed stripe amount conversion error 191 | 192 | ## 1.0.3 - 2022-11-06 193 | 194 | - Fixed decimal value error 195 | 196 | ## 1.0.2 - 2022-11-06 197 | 198 | - Fixed amount calculation error. 199 | 200 | ## 1.0.1 - 2022-11-06 201 | 202 | - Fixed stripe initialization error due to decimal values. 203 | 204 | ## 1.0.0 - 2022-08-27 205 | 206 | - Ready for production use. 207 | - Fixed failing tests. 208 | - Updated docs. 209 | 210 | ## 0.1.8 - 2022-08-25 211 | 212 | - Fix named parameter error. 213 | 214 | ## 0.1.7 - 2022-08-11 215 | 216 | - Klasha payment gateway integration. 217 | - Improved the fallback error page. 218 | 219 | ## 0.1.6 - 2022-08-08 220 | 221 | - Add callback option for custom code execution after customer payment. 222 | - Parse error messages. 223 | - Add processing status message for delayed confirmation. 224 | - Code improvements. 225 | 226 | ## 0.1.5 - 2022-07-04 227 | 228 | - Bump dependabot/fetch-metadata from 1.3.1 to 1.3.3 229 | 230 | ## 0.0.4 - 2022-06-14 231 | 232 | I would have tagged this significant release with lots of breaking changes, but thanks to God, we still don't have a stable release. 233 | 234 | In this release, I reworked the hosted providers to generate the checkout URL from the gateway providers. 235 | 236 | Completed the implementation for the following providers: 237 | 238 | - Paystack 🔥 239 | - Flutterwave 🔥 240 | - Stripe 🔥 241 | 242 | That's all for now, more implementation is coming for Monnify and Paypal 🚀 243 | 244 | ## 0.1.3 - 2022-06-02 245 | 246 | - Increase default timeout to 12 hours 247 | 248 | ## 0.1.2 - 2022-05-31 249 | 250 | - Fix missing debug response 251 | 252 | ## 0.1.1 - 2022-05-31 253 | 254 | - Fix missing debug response 255 | 256 | ## 0.1.0 - 2022-05-31 257 | 258 | - Fix missing debug response 259 | 260 | ## 0.0.9 - 2022-05-31 261 | 262 | - Fix debug mode error 263 | 264 | ## 00.7 - 2022-05-31 265 | 266 | - Updated GitHub Actions 267 | - Updated Readme 268 | 269 | ## 0.0.6 - 2022-05-31 270 | 271 | - Fix config file publishing error 272 | 273 | ## 0.0.5 - 2022-05-31 274 | 275 | Added debug mode for logging HTTP responses. 276 | 277 | ## 0.0.3 - 2022-05-26 278 | 279 | - Allow `null` value for `$channel` property 280 | 281 | ## 0.0.2 - 2022-05-25 282 | 283 | - Clean up 284 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) stephenjude 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Payment Gateways 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/stephenjude/laravel-payment-gateways.svg?style=flat-square)](https://packagist.org/packages/stephenjude/laravel-payment-gateways) 4 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/stephenjude/laravel-payment-gateways/run-tests?label=tests)](https://github.com/stephenjude/laravel-payment-gateways/actions?query=workflow%3Arun-tests+branch%3Amain) 5 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/stephenjude/laravel-payment-gateways/Check%20&%20fix%20styling?label=code%20style)](https://github.com/stephenjude/laravel-payment-gateways/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/stephenjude/laravel-payment-gateways.svg?style=flat-square)](https://packagist.org/packages/stephenjude/laravel-payment-gateways) 7 | 8 | A simple Laravel implementation for all payment providers. This package supports 9 | Paystack, Monnify, Pay4Me Pay, Seerbit Flutterwave, Klasha, and Stripe. 10 | 11 | ## Use Case 12 | Have you had to implement limited SDKs for accepting payments on your mobile app? 13 | That's the problem this package solved. 14 | 15 | With this package, you can generate a payment link and 16 | return it to your mobile app API call and the payment can be completed on the in-app browser. 17 | 18 | When the customer completes their payment, this package verifies the payment and executes the code defined inside your 19 | custom closure. 20 | 21 | The closure should look like this: 22 | 23 | ```php 24 | use Stephenjude\PaymentGateway\DataObjects\TransactionData; 25 | 26 | function (TransactionData $payment){ 27 | $order->update([ 28 | 'status' => $payment->status, 29 | 'amount' => $payment->amount, 30 | 'currency' => $payment->currency 31 | ]); 32 | 33 | $customer->notify(new OrderPaymentNotification($order)); 34 | } 35 | ``` 36 | 37 | If you are using this package on the web this closure is the place where you can return a redirect after updating the customer order or sending a notification. 38 | 39 | ## Installation 40 | 41 | You can install the package via Composer: 42 | 43 | ```bash 44 | composer require stephenjude/laravel-payment-gateways 45 | ``` 46 | 47 | You can publish the config file with: 48 | 49 | ```bash 50 | php artisan vendor:publish --tag="payment-gateways-config" 51 | ``` 52 | 53 | Optionally, you can publish the views using 54 | 55 | ```bash 56 | php artisan vendor:publish --tag="payment-gateways-views" 57 | ``` 58 | 59 | ## Usage 60 | This package currently supports `paystack`, `monnify`, `pay4me`, `seerbit`, `flutterwave`, `klasha` and `stripe`. 61 | 62 | ### How to initialize a payment session 63 | 64 | ```php 65 | use Stephenjude\PaymentGateway\PaymentGateway; 66 | use Stephenjude\PaymentGateway\DataObjects\TransactionData; 67 | 68 | $provider = PaymentGateway::make('paystack') 69 | 70 | $paymentSession = $provider->initializeCheckout([ 71 | 'currency' => 'NGN', // required 72 | 'amount' => 100, // required 73 | 'email' => 'customer@email.com', // required 74 | 'meta' => [ 'name' => 'Stephen Jude', 'phone' => '081xxxxxxxxx'], 75 | 'closure' => function (TransactionData $payment){ 76 | /* 77 | * Payment verification happens immediately after the customer makes payment. 78 | * The payment data obtained from the verification will be injected into this closure. 79 | */ 80 | logger('payment details', [ 81 | 'currency' => $payment->currency, 82 | 'amount' => $payment->amount, 83 | 'status' => $payment->status, 84 | 'reference' => $payment->reference, 85 | 'provider' => $payment->provider, 86 | 'date' => $payment->date, 87 | ]); 88 | }, 89 | ]); 90 | 91 | $paymentSession->provider; 92 | $paymentSession->checkoutUrl; 93 | $paymentSession->expires; 94 | ``` 95 | ### Accessing payment transaction data 96 | ```php 97 | 98 | $provider = PaymentGateway::make('paystack'); 99 | 100 | $transactions = $provider->listTransactions(); // Returns array 101 | 102 | $transaction = $provider->findTransaction(REFERENCE); // Returns Stephenjude\PaymentGateway\DataObjects\TransactionData 103 | $transaction->provider; 104 | $transaction->email; 105 | $transaction->amount; 106 | $transaction->currency; 107 | $transaction->reference; 108 | $transaction->status; 109 | $transaction->date; 110 | ``` 111 | 112 | ### Pawapay Setup 113 | 114 | Required Env Variables 115 | 116 | ``` 117 | PAWAPAY_SECRET= 118 | ``` 119 | Checkout 120 | 121 | ```php 122 | use \Stephenjude\PaymentGateway\Enums\Provider; 123 | use \Stephenjude\PaymentGateway\PaymentGateway; 124 | 125 | $pawapay = PaymentGateway::make(Provider::PAWAPAY())->initializeCheckout([ 126 | "amount" => 15, 127 | "country" => "ZMB", 128 | 'meta' => [ 129 | "description" => "Note of 4 to 22 chars", 130 | "reason" => "Ticket to festival" 131 | ] 132 | ]); 133 | ``` 134 | 135 | ### Pay4Me Setup 136 | ``` 137 | PAY4ME_PUBLIC= 138 | PAY4ME_SECRET= 139 | ``` 140 | 141 | ### Monnify Setup 142 | ``` 143 | MONNIFY_PUBLIC= 144 | MONNIFY_SECRET= 145 | MONNIFY_CONTRACT_CODE= 146 | ``` 147 | 148 | ### Pay4Me Setup 149 | ``` 150 | SEERBIT_PUBLIC= 151 | SEERBIT_SECRET= 152 | ``` 153 | 154 | ### Paystack Setup 155 | ``` 156 | PAYSTACK_PUBLIC= 157 | PAYSTACK_SECRET= 158 | ``` 159 | ### Flutterwave Setup 160 | ``` 161 | FLUTTERWAVE_PUBLIC= 162 | FLUTTERWAVE_SECRET= 163 | ``` 164 | 165 | ### Klasha Setup 166 | ``` 167 | KLASHA_PUBLIC= 168 | KLASHA_SECRET= 169 | ``` 170 | 171 | ### Stripe Setup 172 | ``` 173 | STRIPE_PUBLIC= 174 | STRIPE_SECRET= 175 | ``` 176 | ## Testing 177 | 178 | ```bash 179 | composer test 180 | ``` 181 | 182 | ## Changelog 183 | 184 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 185 | 186 | ## Contributing 187 | 188 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 189 | 190 | ## Security Vulnerabilities 191 | 192 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 193 | 194 | ## Credits 195 | 196 | - [stephenjude](https://github.com/stephenjude) 197 | - [All Contributors](../../contributors) 198 | 199 | ## License 200 | 201 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 202 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stephenjude/laravel-payment-gateways", 3 | "description": "A simple Laravel API implementation for all payment providers like Paystack, Flutterwave, & Paypal etc.", 4 | "keywords": [ 5 | "stephenjude", 6 | "laravel", 7 | "laravel-payment-gateways" 8 | ], 9 | "homepage": "https://github.com/stephenjude/laravel-payment-gateways", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "stephenjude", 14 | "email": "stephenjudesuccess@gmail.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1|^8.2|8.3", 20 | "archtechx/enums": "*", 21 | "illuminate/contracts": "^9.0|^10.0|^11.0", 22 | "illuminate/http": "^9.0|^10.0|^11.0", 23 | "illuminate/support": "^9.0|^10.0|^11.0", 24 | "spatie/laravel-data": "^1.4|^2.0|^3.12", 25 | "spatie/laravel-package-tools": "^1.9.2" 26 | }, 27 | "require-dev": { 28 | "nunomaduro/collision": "^5.0|^6.0|^7.0||^8.0", 29 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0", 30 | "pestphp/pest": "^1.21", 31 | "pestphp/pest-plugin-laravel": "^1.1", 32 | "phpstan/phpstan-phpunit": "^1.0", 33 | "phpunit/phpunit": "^9.5" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Stephenjude\\PaymentGateway\\": "src", 38 | "Stephenjude\\PaymentGateway\\Database\\Factories\\": "database/factories" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "Stephenjude\\PaymentGateway\\Tests\\": "tests" 44 | } 45 | }, 46 | "scripts": { 47 | "analyse": "vendor/bin/phpstan analyse", 48 | "test": "vendor/bin/pest", 49 | "test-coverage": "vendor/bin/pest --coverage" 50 | }, 51 | "config": { 52 | "sort-packages": true, 53 | "allow-plugins": { 54 | "pestphp/pest-plugin": true, 55 | "phpstan/extension-installer": true 56 | } 57 | }, 58 | "extra": { 59 | "laravel": { 60 | "providers": [ 61 | "Stephenjude\\PaymentGateway\\PaymentGatewayServiceProvider" 62 | ], 63 | "aliases": { 64 | "PaymentGateway": "Stephenjude\\PaymentGateway\\Facades\\PaymentGateway" 65 | } 66 | } 67 | }, 68 | "minimum-stability": "dev", 69 | "prefer-stable": true 70 | } 71 | -------------------------------------------------------------------------------- /config/payment-gateways.php: -------------------------------------------------------------------------------- 1 | 'support@company.email', 10 | 11 | /* 12 | * Display messages for successful or failed payments. 13 | */ 14 | 'message' => [ 15 | 'success' => 'Your payment transaction was successful. Please close the tab to continue.', 16 | 'failed' => 'Your payment transaction was not successful. Please close the tab to continue.', 17 | 'pending' => 'Your payment transaction is being processed by our payment partner. Please stay on this page and refresh in 5 minutes.', 18 | ], 19 | 20 | /* 21 | * Debug mode set to true logs all the HTTP response to your application log file 22 | */ 23 | 'debug_mode' => env('APP_DEBUG'), 24 | 25 | /* 26 | * All payment transactions are verified on the callback route. 27 | */ 28 | 'routes' => [ 29 | 'callback' => [ 30 | 'path' => 'payment/gateways/{provider}/callback/{reference}', 31 | 'name' => 'payment.gateway.callback', 32 | ], 33 | 'checkout' => [ 34 | 'path' => 'payment/gateways/{provider}/checkout/{reference}', 35 | 'name' => 'payment.gateway.checkout', 36 | ], 37 | 'error' => [ 38 | 'path' => 'payment-gateway-error', 39 | 'name' => 'payment.gateway.error', 40 | ], 41 | 42 | /* 43 | * Define your custom routes for successful and failed payments. 44 | */ 45 | 'custom' => [ 46 | 'success' => [ 47 | 'path' => null, 48 | 'name' => null, 49 | ], 50 | 'failed' => [ 51 | 'path' => null, 52 | 'name' => null, 53 | ], 54 | ], 55 | ], 56 | 57 | /** 58 | * All check out session and payment references are cached and when the payment have been completed, it gets flushed out. 59 | */ 60 | 'cache' => [ 61 | 'session' => [ 62 | 'key' => '_gateway_session_reference_', 63 | 'expires' => 42300, // 12 hours 64 | ], 65 | 'payment' => [ 66 | 'key' => '_gateway_payment_reference_', 67 | 'expires' => 42300, // 12 hours 68 | ], 69 | ], 70 | 71 | /* 72 | * This is a list of all supported payment gateway providers. 73 | */ 74 | 'providers' => [ 75 | 'pay4me' => [ 76 | 'name' => 'pay4me', 77 | 'channels' => ['bank_transfer'], 78 | 'base_url' => env('PAY4ME_API_URL', 'https://pay.pay4me.app/'), 79 | 'public' => env('PAY4ME_PUBLIC'), 80 | 'secret' => env('PAY4ME_SECRET'), 81 | ], 82 | 'monnify' => [ 83 | 'name' => 'monnify', 84 | 'channels' => ['CARD', 'ACCOUNT_TRANSFER'], 85 | 'base_url' => env('MONNIFY_API_URL', 'https://api.monnify.com/'), 86 | 'public' => env('MONNIFY_PUBLIC'), 87 | 'secret' => env('MONNIFY_SECRET'), 88 | 'contract_code' => env('MONNIFY_CONTRACT_CODE'), 89 | ], 90 | 'pawapay' => [ 91 | 'name' => 'pawapay', 92 | 'channels' => null, 93 | 'base_url' => env('PAWAPAY_API_URL', 'https://api.pawapay.cloud/'), 94 | 'public' => env('PAWAPAY_PUBLIC'), 95 | 'secret' => env('PAWAPAY_SECRET'), 96 | ], 97 | 'seerbit' => [ 98 | 'name' => 'seerbit', 99 | 'channels' => ['card', 'account', 'transfer', 'ussd'], 100 | 'base_url' => env('SEERBIT_API_URL', 'https://seerbitapi.com/'), 101 | 'public' => env('SEERBIT_PUBLIC'), 102 | 'secret' => env('SEERBIT_SECRET'), 103 | ], 104 | 'paystack' => [ 105 | 'name' => 'paystack', 106 | 'channels' => ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer'], 107 | 'base_url' => env('PAYSTACK_API_URL', 'https://api.paystack.co/'), 108 | 'public' => env('PAYSTACK_PUBLIC'), 109 | 'secret' => env('PAYSTACK_SECRET'), 110 | ], 111 | 'startbutton' => [ 112 | 'name' => 'startbutton', 113 | 'channels' => ['card', 'bank', 'ussd', 'qr', 'mobile_money', 'bank_transfer', 'eft', 'payattitude'], 114 | 'base_url' => env('STARTBUTTON_API_URL', 'https://api.startbutton.tech/'), 115 | 'public' => env('STARTBUTTON_PUBLIC'), 116 | 'secret' => env('STARTBUTTON_SECRET'), 117 | ], 118 | 'flutterwave' => [ 119 | 'name' => 'flutterwave', 120 | 'channels' => ['card', 'banktransfer', 'ussd', 'credit', 'mpesa', 'qr'], 121 | 'base_url' => env('FLUTTERWAVE_API_URL', 'https://api.flutterwave.com/'), 122 | 'public' => env('FLUTTERWAVE_PUBLIC'), 123 | 'secret' => env('FLUTTERWAVE_SECRET'), 124 | ], 125 | 'stripe' => [ 126 | 'name' => 'stripe', 127 | 'channels' => ['card', 'acss_debit', 'us_bank_account'], 128 | 'base_url' => env('STRIPE_API_URL', 'https://api.stripe.com/'), 129 | 'public' => env('STRIPE_PUBLIC'), 130 | 'secret' => env('STRIPE_SECRET'), 131 | ], 132 | 'klasha' => [ 133 | 'name' => 'klasha', 134 | 'channels' => null, 135 | 'base_url' => env('KLASHA_API_URL', 'https://gate.klasapps.com/'), 136 | 'public' => env('KLASHA_PUBLIC'), 137 | 'secret' => env('KLASHA_SECRET'), 138 | ], 139 | ], 140 | ]; 141 | -------------------------------------------------------------------------------- /resources/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenjude/laravel-payment-gateways/0003826d7b03ff2d7e19e7eef58e14f5ebef790a/resources/views/.gitkeep -------------------------------------------------------------------------------- /resources/views/checkout/klasha.blade.php: -------------------------------------------------------------------------------- 1 | @extends('payment-gateways::layout') 2 | 3 | @section('title', 'Klasha Checkout') 4 | 5 | @section('content') 6 | @include('payment-gateways::loader') 7 | @endsection 8 | 9 | @push('scripts') 10 | 12 | 14 | 44 | @endpush 45 | -------------------------------------------------------------------------------- /resources/views/error.blade.php: -------------------------------------------------------------------------------- 1 | @extends('payment-gateways::layout') 2 | 3 | @section('title', 'Paystack') 4 | 5 | @section('content') 6 |
7 |
8 | 9 | 12 | 13 |
14 |

15 | {{$title}} 16 |

17 | 18 |

19 | {{$message}} 20 |

21 | 22 | @if(config('payment-gateways.support_email')) 23 |
24 |

If you need further assistance, please contact us at {{ $support_mail }} or reach 27 | out to us via our social media 28 | platforms. 29 |

30 | @endif 31 |
32 |
33 |
34 | @endsection 35 | -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | @stack('styles') 12 | 13 | Pay4Me | @yield('title') 14 | 16 | 17 | 18 | @yield('content') 19 | 20 | 31 | 32 | @stack('scripts') 33 | 34 | 35 | -------------------------------------------------------------------------------- /resources/views/loader.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 |
5 | Loading... 6 |
7 |
8 |

This may take a few seconds, please don't close this page.

9 |
10 | 11 | @push('scripts') 12 | 21 | @endpush 22 | -------------------------------------------------------------------------------- /resources/views/status.blade.php: -------------------------------------------------------------------------------- 1 | @extends('payment-gateways::layout') 2 | 3 | @section('title', 'Paystack') 4 | 5 | @section('content') 6 |
7 |
8 | @if($payment->isSuccessful()) 9 | 10 | 12 | 13 | 14 | @elseif($payment->isProcessing()) 15 | 19 | 21 | 22 | @elseif($payment->failed()) 23 | 24 | 27 | 28 | @endif 29 |
30 |

31 | Payment 32 | @if($payment->isSuccessful()) 33 | Successful 34 | @elseif($payment->failed()) 35 | Failed 36 | @elseif($payment->isProcessing()) 37 | Processing 38 | @endif 39 |

40 | 41 | @if($successful) 42 |

43 | {!! config('payment-gateways.message.success') !!} 44 |

45 | @elseif($payment->failed()) 46 |

47 | {!! config('payment-gateways.message.failed') !!} 48 |

49 | @elseif($payment->isProcessing()) 50 |

51 | {!! config('payment-gateways.message.pending') !!} 52 |

53 | @endif 54 | @if(config('payment-gateways.support_email')) 55 |
56 |

If you need further assistance, please contact us at {{ $support_mail }} or reach 59 | out to us via our social media 60 | platforms. 61 |

62 | @endif 63 |
64 |
65 |
66 | @endsection 67 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name(config('payment-gateways.routes.callback.name')); 10 | 11 | Route::get(config('payment-gateways.routes.checkout.path'), CheckoutController::class) 12 | ->name(config('payment-gateways.routes.checkout.name')); 13 | 14 | Route::get(config('payment-gateways.routes.error.path'), ErrorController::class) 15 | ->name(config('payment-gateways.routes.error.name')); 16 | -------------------------------------------------------------------------------- /src/Contracts/ProviderInterface.php: -------------------------------------------------------------------------------- 1 | status)) { 31 | 'success', 'succeeded', 'successful', 'paid', 'approved', 'completed', 'verified' => true, 32 | default => false 33 | }; 34 | } 35 | 36 | public function isProcessing(): bool 37 | { 38 | // Stripe: processing; 39 | return match (strtolower($this->status)) { 40 | 'processing', 'pending' => true, 41 | default => false 42 | }; 43 | } 44 | 45 | public function failed(): bool 46 | { 47 | /** 48 | * @description Paystack: failed; Flutterwave: failed; Stripe: failed; 49 | */ 50 | return match (strtolower($this->status)) { 51 | 'failed', 'expired' => true, 52 | default => false 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Enums/Provider.php: -------------------------------------------------------------------------------- 1 | new PaystackProvider(), 49 | self::STARTBUTTON() => new StartbuttonProvider(), 50 | self::FLUTTERWAVE() => new FlutterwaveProvider(), 51 | self::PAY4ME() => new Pay4meProvider(), 52 | self::SEERBIT() => new SeerbitProvider(), 53 | self::MONNIFY() => new MonnifyProvider(), 54 | self::STRIPE() => new StripeProvider(), 55 | self::KLASHA() => new KlashaProvider(), 56 | self::PAWAPAY() => new PawapayProvider(), 57 | default => throw new \RuntimeException("Undefined provider [$provider] called.") 58 | }; 59 | } 60 | 61 | public function gateway(): ProviderInterface 62 | { 63 | return self::integration($this->value); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Exceptions/HttpException.php: -------------------------------------------------------------------------------- 1 | getCheckout($reference); 25 | 26 | if (is_null($sessionData)) { 27 | return redirect()->route( 28 | config('payment-gateways.routes.error.name'), 29 | ['message' => 'Your payment session has expired.'] 30 | ); 31 | } 32 | 33 | return view("payment-gateways::checkout.$sessionData->provider", [ 34 | 'sessionData' => $sessionData->toArray(), 35 | ]); 36 | } catch (Exception $exception) { 37 | logger($exception->getMessage(), $exception->getTrace()); 38 | 39 | return redirect()->route(config('payment-gateways.routes.error.name')); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/CompletePaymentController.php: -------------------------------------------------------------------------------- 1 | getCheckout($reference); 25 | 26 | if (is_null($sessionData)) { 27 | return redirect()->route( 28 | route: config('payment-gateways.routes.error.name'), 29 | parameters: ['message' => 'Your payment session has expired.'] 30 | ); 31 | } 32 | 33 | /** 34 | * Session Reference becomes the Payment Reference if the payment session data does 35 | * not contain the reference for the payment OR if the provider doesn't return 36 | * any reference for the transactions via the callback url. 37 | */ 38 | $paymentReference = $request->get('transaction_id') 39 | ?? $sessionData->paymentReference 40 | ?? $sessionData->sessionReference; 41 | 42 | $payment = $paymentProvider->confirmTransaction($paymentReference, $sessionData->closure); 43 | 44 | if ($customSuccessRoute = config('payment-gateways.routes.custom.success.name')) { 45 | return redirect()->route( 46 | route: $customSuccessRoute, 47 | parameters: ['reference' => $paymentReference] 48 | ); 49 | } 50 | 51 | return view('payment-gateways::status', [ 52 | 'payment' => $payment, 53 | 'successful' => $payment->isSuccessful(), 54 | ]); 55 | } catch (Exception $exception) { 56 | logger($exception->getMessage(), $exception->getTrace()); 57 | 58 | if ($customFailedRoute = config('payment-gateways.routes.custom.failed.name')) { 59 | return redirect()->route($customFailedRoute, [ 60 | 'reference' => $paymentReference, 61 | 'message' => $exception->getMessage(), 62 | ]); 63 | } 64 | 65 | return redirect()->route(config('payment-gateways.routes.error.name')); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Http/Controllers/ErrorController.php: -------------------------------------------------------------------------------- 1 | $request->get('status', 400), 21 | 'title' => $request->get('title', 'We have a little problem'), 22 | 'message' => $request->get( 23 | 'message', 24 | 'Something completely went wrong. The issue could be that your payment was not successfully verified or your payment session has expired.' 25 | ), 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PaymentGateway.php: -------------------------------------------------------------------------------- 1 | name('laravel-payment-gateways') 14 | ->hasConfigFile() 15 | ->hasViews() 16 | ->hasRoute('web'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Providers/AbstractProvider.php: -------------------------------------------------------------------------------- 1 | baseUrl = config("payment-gateways.providers.$this->provider.base_url"); 31 | $this->secretKey = config("payment-gateways.providers.$this->provider.secret"); 32 | $this->publicKey = config("payment-gateways.providers.$this->provider.public"); 33 | } 34 | 35 | public function initializePayment(array $parameters = []): SessionData 36 | { 37 | return $this->initializeCheckout($parameters); 38 | } 39 | 40 | public function setChannels(array|null $channels): self 41 | { 42 | $this->channels = $channels; 43 | 44 | return $this; 45 | } 46 | 47 | public function getChannels(): array|null 48 | { 49 | return $this->channels ?? config("payment-gateways.providers.$this->provider.channels"); 50 | } 51 | 52 | /* 53 | * Supported options = [as_form => true] 54 | */ 55 | public function request($method, $path, array $payload = [], array $options = []): array 56 | { 57 | $path = $this->baseUrl.$path; 58 | 59 | $http = Http::withToken($this->secretKey) 60 | ->withOptions(['force_ip_resolve' => 'v4']) 61 | ->acceptJson(); 62 | 63 | $http = Arr::get($options, 'as_form') 64 | ? $http->asForm() 65 | : $http->contentType('application/json'); 66 | 67 | $response = match (strtolower($method)) { 68 | 'post' => $http->post($path, $payload), 69 | default => $http->get($path, $payload), 70 | }; 71 | 72 | $this->logResponse($this->provider, $response); 73 | 74 | if ($response->failed()) { 75 | throw new Exception($response->reason().': '.$this->parseProviderError($response)); 76 | } 77 | 78 | return $response->json(); 79 | } 80 | 81 | public function getCheckout(string $sessionReference): SessionData|null 82 | { 83 | $sessionCacheKey = config('payment-gateways.cache.session.key').$sessionReference; 84 | 85 | return Cache::get($sessionCacheKey); 86 | } 87 | 88 | public function destroyCheckout(string $sessionReference): void 89 | { 90 | $sessionCacheKey = config('payment-gateways.cache.session.key').$sessionReference; 91 | 92 | Cache::forget($sessionCacheKey); 93 | } 94 | 95 | public function getReference(string $sessionReference): string|null 96 | { 97 | $key = config('payment-gateway.cache.payment.key').$sessionReference; 98 | 99 | return Cache::get($key); 100 | } 101 | 102 | public function confirmTransaction(string $reference, ?SerializableClosure $closure = null): TransactionData|null 103 | { 104 | $transaction = $this->findTransaction($reference); 105 | 106 | if ($closure = $closure?->getClosure()) { 107 | $closure($transaction); 108 | } 109 | 110 | return $transaction; 111 | } 112 | 113 | protected function logResponse(string $provider, Response $response): void 114 | { 115 | if (! config('payment-gateways.debug_mode')) { 116 | return; 117 | } 118 | 119 | logger("$provider response: ", [ 120 | 'STATUS' => $response->status(), 121 | 'REASON' => $response->reason(), 122 | 'JSON' => $response->json(), 123 | 'ERROR' => $response->reason(), 124 | 'PSR' => $response->toPsrResponse(), 125 | ]); 126 | } 127 | 128 | public function parseProviderError(Response $response): string 129 | { 130 | return match ($this->provider) { 131 | Provider::PAYSTACK(), 132 | Provider::STARTBUTTON(), 133 | Provider::FLUTTERWAVE() => $response->json('message'), 134 | Provider::STRIPE() => $response->json('error.message'), 135 | Provider::PAY4ME() => $response->json('error.code').'. '.$response->json('error.message'), 136 | Provider::SEERBIT() => $response->json('error').'. '.$response->json('message'), 137 | Provider::MONNIFY() => $response->json('responseMessage') 138 | ?? $response->json('error').'. '.$response->json('error_description'), 139 | Provider::KLASHA() => $response->json('error').'. '.$response->json('message'), 140 | Provider::PAWAPAY() => $response->json('errorMessage') ?? $response->reason(), 141 | default => $response->reason() ?? 'API error.' 142 | }; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Providers/FlutterwaveProvider.php: -------------------------------------------------------------------------------- 1 | request( 26 | method: 'POST', 27 | path: 'v3/payments', 28 | payload: [ 29 | 'amount' => Arr::get($parameters, 'amount'), 30 | 'currency' => Arr::get($parameters, 'currency'), 31 | 'tx_ref' => Arr::get($parameters, 'reference'), 32 | 'payment_options' => implode(', ', $this->getChannels()), 33 | 'customer' => ['email' => Arr::get($parameters, 'email')], 34 | 'meta' => Arr::get($parameters, 'meta'), 35 | 'redirect_url' => Arr::get( 36 | array: $parameters, 37 | key: 'callback_url', 38 | default: route(config('payment-gateways.routes.callback.name'), [ 39 | 'reference' => $parameters['reference'], 40 | 'provider' => $this->provider, 41 | ]) 42 | ), 43 | ] 44 | ); 45 | 46 | return Cache::remember( 47 | $parameters['session_cache_key'], 48 | $parameters['expires'], 49 | fn () => new SessionData( 50 | provider: $this->provider, 51 | sessionReference: $parameters['reference'], 52 | paymentReference: null, 53 | checkoutSecret: null, 54 | checkoutUrl: $flutterwave['data']['link'], 55 | expires: $parameters['expires'], 56 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 57 | ) 58 | ); 59 | } 60 | 61 | public function findTransaction(string $reference): TransactionData 62 | { 63 | $response = $this->request('GET', "v3/transactions/$reference/verify"); 64 | 65 | return $this->transactionDTO($response['data']); 66 | } 67 | 68 | public function listTransactions( 69 | ?string $from = null, 70 | ?string $to = null, 71 | ?string $page = null, 72 | ?string $status = null, 73 | ?string $reference = null, 74 | ?string $amount = null, 75 | ?string $customer = null, 76 | ): array|null { 77 | $queryParameters = array_filter([ 78 | 'from' => $from, 79 | 'to' => $to, 80 | 'page' => $page, 81 | 'customer_email' => $customer, 82 | 'status' => $status, 83 | 'tx_ref' => $reference, 84 | ]); 85 | 86 | $response = $this->request('GET', 'v3/transactions', $queryParameters); 87 | 88 | return [ 89 | 'meta' => [ 90 | 'total' => Arr::get($response, 'meta.page_info.total'), 91 | 'page' => Arr::get($response, 'meta.page_info.current_page'), 92 | 'page_count' => Arr::get($response, 'meta.page_info.total_pages'), 93 | ], 94 | 'data' => collect($response['data']) 95 | ->map(fn ($transaction) => $this->transactionDTO($transaction)) 96 | ->toArray(), 97 | ]; 98 | } 99 | 100 | public function transactionDTO(array $transaction): TransactionData 101 | { 102 | return new TransactionData( 103 | email: $transaction['customer']['email'], 104 | meta: $transaction['meta'] ?? null, 105 | amount: $transaction['amount'], 106 | currency: $transaction['currency'], 107 | reference: $transaction['reference'], 108 | provider: $this->provider, 109 | status: $transaction['status'], 110 | date: Carbon::parse($transaction['created_at'])->toDateTimeString(), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Providers/KlashaProvider.php: -------------------------------------------------------------------------------- 1 | $this->provider, 28 | 'sessionReference' => $parameters['reference'], 29 | 'paymentReference' => $parameters['reference'], 30 | 'checkoutSecret' => null, 31 | 'checkoutUrl' => route(config('payment-gateways.routes.checkout.name'), [ 32 | 'reference' => $parameters['reference'], 33 | 'provider' => $this->provider, 34 | ]), 35 | 'expires' => $parameters['expires'], 36 | 'closure' => Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 37 | 'extra' => [ 38 | 'email' => $parameters['email'], 39 | 'currency' => $parameters['currency'], 40 | 'amount' => $parameters['amount'], 41 | 'channels' => $this->getChannels(), 42 | 'is_test_mode' => false, 43 | 'callback_url' => route(config('payment-gateways.routes.callback.name'), [ 44 | 'reference' => $parameters['reference'], 45 | 'provider' => $this->provider, 46 | ]), 47 | ], 48 | ]; 49 | 50 | return Cache::remember( 51 | key: $parameters['session_cache_key'], 52 | ttl: $parameters['expires'], 53 | callback: fn () => new SessionData(...$sessionData) 54 | ); 55 | } 56 | 57 | public function findTransaction(string $reference): TransactionData 58 | { 59 | $transaction = $this->request( 60 | method: 'POST', 61 | path: 'nucleus/tnx/merchant/status', 62 | payload: ['tnxRef' => $reference] 63 | ); 64 | 65 | if (! isset($transaction['data'])) { 66 | throw new \Exception('Transaction not found'); 67 | } 68 | 69 | $transaction['data']['reference'] = $reference; 70 | 71 | return $this->transactionDTO($transaction['data']); 72 | } 73 | 74 | public function listTransactions( 75 | ?string $from = null, 76 | ?string $to = null, 77 | ?string $page = null, 78 | ?string $status = null, 79 | ?string $reference = null, 80 | ?string $amount = null, 81 | ?string $customer = null, 82 | ): array|null { 83 | throw new Exception("This provider [$this->provider] does not support list transactions"); 84 | } 85 | 86 | public function transactionDTO(array $transaction): TransactionData 87 | { 88 | return new TransactionData( 89 | email: $transaction['customer']['email'], 90 | meta: $transaction['customer'], 91 | amount: $transaction['sourceAmount'], 92 | currency: $transaction['sourceCurrency'], 93 | reference: $transaction['reference'], 94 | provider: $this->provider, 95 | status: $transaction['status'], 96 | date: Carbon::now()->toDateTimeString(), 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Providers/MonnifyProvider.php: -------------------------------------------------------------------------------- 1 | secretKey = $this->getToken(); 23 | } 24 | 25 | public function getToken(): string 26 | { 27 | return Http::acceptJson() 28 | ->withHeaders([ 29 | 'Authorization' => 'Basic '.base64_encode("$this->publicKey:$this->secretKey"), 30 | ]) 31 | ->post($this->baseUrl.'api/v1/auth/login') 32 | ->json('responseBody.accessToken'); 33 | } 34 | 35 | public function initializeCheckout(array $parameters = []): SessionData 36 | { 37 | $parameters['reference'] = 'MNFY_'.Str::random(12); 38 | 39 | $parameters['expires'] = config('payment-gateways.cache.session.expires'); 40 | 41 | $parameters['session_cache_key'] = config('payment-gateways.cache.session.key').$parameters['reference']; 42 | 43 | $monnify = $this->request( 44 | method: 'POST', 45 | path: 'api/v1/merchant/transactions/init-transaction', 46 | payload: [ 47 | 'customerEmail' => $email = Arr::get($parameters, 'email'), 48 | 'customerName' => Arr::get($parameters, 'meta.name', $email), 49 | 'amount' => Arr::get($parameters, 'amount'), 50 | 'currencyCode' => Arr::get($parameters, 'currency'), 51 | 'metaData' => Arr::get($parameters, 'meta'), 52 | 'contractCode' => config('payment-gateways.providers.monnify.contract_code'), 53 | 'paymentReference' => Arr::get($parameters, 'reference'), 54 | 'paymentMethods' => $this->getChannels(), 55 | 'redirectUrl' => $parameters['callback_url'] 56 | ?? route(config('payment-gateways.routes.callback.name'), [ 57 | 'reference' => $parameters['reference'], 58 | 'provider' => $this->provider, 59 | ]), 60 | ] 61 | ); 62 | 63 | return Cache::remember( 64 | key: $parameters['session_cache_key'], 65 | ttl: $parameters['expires'], 66 | callback: fn () => new SessionData( 67 | provider: $this->provider, 68 | sessionReference: $parameters['reference'], 69 | paymentReference: $monnify['responseBody']['transactionReference'], 70 | checkoutSecret: null, 71 | checkoutUrl: $monnify['responseBody']['checkoutUrl'], 72 | expires: $parameters['expires'], 73 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 74 | ) 75 | ); 76 | } 77 | 78 | public function findTransaction(string $reference): TransactionData 79 | { 80 | $response = $this->request('GET', "api/v1/transactions/$reference"); 81 | 82 | return $this->transactionDTO($response['responseBody']); 83 | } 84 | 85 | public function listTransactions( 86 | ?string $from = null, 87 | ?string $to = null, 88 | ?string $page = null, 89 | ?string $status = null, 90 | ?string $reference = null, 91 | ?string $amount = null, 92 | ?string $customer = null, 93 | ): array { 94 | $payload = array_filter([ 95 | 'page' => $page, 96 | 'paymentReference' => $reference, 97 | 'amount' => $amount, 98 | 'customerEmail' => $customer, 99 | 'paymentStatus' => $status, 100 | 'from' => $from, 101 | 'to' => $to, 102 | ]); 103 | 104 | $response = $this->request('GET', 'api/v1/transactions/search', $payload); 105 | 106 | return [ 107 | 'meta' => [ 108 | 'total' => Arr::get($response, 'responseBody.pageable.pageSize'), 109 | 'page' => Arr::get($response, 'responseBody.pageable.pageNumber'), 110 | 'page_count' => Arr::get($response, 'responseBody.pageable.totalPages'), 111 | ], 112 | 'data' => collect(Arr::get($response, 'responseBody.content')) 113 | ->map(fn ($transaction) => $this->transactionDTO($transaction)) 114 | ->toArray(), 115 | ]; 116 | } 117 | 118 | public function transactionDTO(array $transaction): TransactionData 119 | { 120 | return new TransactionData( 121 | email: $transaction['customerDTO']['email'], 122 | meta: $transaction['metaData'] ?? null, 123 | amount: $transaction['amount'], 124 | currency: $transaction['currencyCode'], 125 | reference: $transaction['paymentReference'], 126 | provider: $this->provider, 127 | status: $transaction['paymentStatus'], 128 | date: Carbon::parse(Arr::get($transaction, 'completedOn', now()->toDateTimeString()))->toDateTimeString(), 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Providers/PawapayProvider.php: -------------------------------------------------------------------------------- 1 | request( 31 | method: 'POST', 32 | path: 'v1/widget/sessions', 33 | payload: [ 34 | 'depositId' => Arr::get($parameters, 'reference'), 35 | 'amount' => "$amount", // Pawapay accepts amount as stringpush 36 | "country" => str(Arr::get($parameters, 'country'))->upper()->toString(), 37 | "statementDescription" => Arr::get($parameters, 'meta.description'), 38 | "reason" => Arr::get($parameters, 'meta.reason'), 39 | 'returnUrl' => $parameters['callback_url'] ?? route(config('payment-gateways.routes.callback.name'), [ 40 | 'reference' => $parameters['reference'], 41 | 'provider' => $this->provider, 42 | ]), 43 | ] 44 | ); 45 | 46 | return Cache::remember($parameters['session_cache_key'], $parameters['expires'], fn () => new SessionData( 47 | provider: $this->provider, 48 | sessionReference: $parameters['reference'], 49 | paymentReference: null, 50 | checkoutSecret: null, 51 | checkoutUrl: $pawapay['redirectUrl'], 52 | expires: $parameters['expires'], 53 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 54 | )); 55 | } 56 | 57 | public function findTransaction(string $reference): TransactionData 58 | { 59 | $transaction = $this->request('GET', "deposits/$reference"); 60 | 61 | return $this->transactionDTO($transaction[0]); 62 | } 63 | 64 | public function listTransactions( 65 | ?string $from = null, 66 | ?string $to = null, 67 | ?string $page = null, 68 | ?string $status = null, 69 | ?string $reference = null, 70 | ?string $amount = null, 71 | ?string $customer = null, 72 | ): array|null { 73 | throw new Exception("This provider [$this->provider] does not support list transactions"); 74 | } 75 | 76 | public function transactionDTO(array $transaction): TransactionData 77 | { 78 | $date = Arr::get($transaction, 'respondedByPayer') ?? Arr::get($transaction, 'created'); 79 | 80 | return new TransactionData( 81 | email: null, 82 | meta: [ 83 | 'type' => $transaction['payer']['type'], 84 | 'address' => $transaction['payer']['address']['value'], 85 | ], 86 | amount: (int)$transaction['depositedAmount'], 87 | currency: $transaction['currency'], 88 | reference: $transaction['depositId'], 89 | provider: $this->provider, 90 | status: $transaction['status'], 91 | date: Carbon::parse($date)->toDateTimeString(), 92 | ); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Providers/Pay4meProvider.php: -------------------------------------------------------------------------------- 1 | timestamp; 19 | 20 | $parameters['expires'] = config('payment-gateways.cache.session.expires'); 21 | 22 | $parameters['session_cache_key'] = config('payment-gateways.cache.session.key').$parameters['reference']; 23 | 24 | /* 25 | * Convert and round decimals to the nearest integer because Paystack does not support decimal values. 26 | */ 27 | $amount = Arr::get($parameters, 'amount') * 100; 28 | 29 | $pay4me = $this->request( 30 | method: 'POST', 31 | path: 'api/transactions/initialize', 32 | payload: [ 33 | 'email' => Arr::get($parameters, 'email'), 34 | 'amount' => $amount, 35 | 'currency' => Arr::get($parameters, 'currency'), 36 | 'reference' => Arr::get($parameters, 'reference'), 37 | 'channels' => $this->getChannels(), 38 | 'metadata' => Arr::get($parameters, 'meta'), 39 | 'callback_url' => $parameters['callback_url'] 40 | ?? route(config('payment-gateways.routes.callback.name'), [ 41 | 'reference' => $parameters['reference'], 42 | 'provider' => $this->provider, 43 | ]), 44 | ] 45 | ); 46 | 47 | return Cache::remember($parameters['session_cache_key'], $parameters['expires'], fn () => new SessionData( 48 | provider: $this->provider, 49 | sessionReference: $parameters['reference'], 50 | paymentReference: null, 51 | checkoutSecret: null, 52 | checkoutUrl: $pay4me['data']['authorization_url'], 53 | expires: $parameters['expires'], 54 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 55 | )); 56 | } 57 | 58 | public function findTransaction(string $reference): TransactionData 59 | { 60 | $response = $this->request('GET', "api/transactions/verify/$reference"); 61 | 62 | return $this->transactionDTO($response['data']); 63 | } 64 | 65 | public function listTransactions( 66 | ?string $from = null, 67 | ?string $to = null, 68 | ?string $page = null, 69 | ?string $status = null, 70 | ?string $reference = null, 71 | ?string $amount = null, 72 | ?string $customer = null, 73 | ): array|null { 74 | $payload = array_filter([ 75 | 'from' => $from, 76 | 'to' => $to, 77 | 'page' => $page, 78 | 'customer' => $customer, 79 | 'status' => $status, 80 | 'amount' => $amount, 81 | ]); 82 | 83 | $response = $this->request('GET', 'api/transactions', $payload); 84 | 85 | return [ 86 | 'meta' => [ 87 | 'total' => Arr::get($response, 'meta.total'), 88 | 'page' => Arr::get($response, 'meta.page'), 89 | 'page_count' => Arr::get($response, 'meta.pageCount'), 90 | ], 91 | 'data' => collect($response['data']) 92 | ->map(fn ($transaction) => $this->transactionDTO($transaction)) 93 | ->toArray(), 94 | ]; 95 | } 96 | 97 | public function transactionDTO(array $transaction): TransactionData 98 | { 99 | $date = Arr::get($transaction, 'paid_at') ?? Arr::get($transaction, 'created_at'); 100 | 101 | return new TransactionData( 102 | email: $transaction['customer']['email'], 103 | meta: $transaction['metadata'], 104 | amount: ($transaction['amount'] / 100), 105 | currency: $transaction['currency'], 106 | reference: $transaction['reference'], 107 | provider: $this->provider, 108 | status: $transaction['status'], 109 | date: Carbon::parse($date)->toDateTimeString(), 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Providers/PaystackProvider.php: -------------------------------------------------------------------------------- 1 | request( 31 | method: 'POST', 32 | path: 'transaction/initialize', 33 | payload: [ 34 | 'email' => Arr::get($parameters, 'email'), 35 | 'amount' => $amount, 36 | 'currency' => Arr::get($parameters, 'currency'), 37 | 'reference' => Arr::get($parameters, 'reference'), 38 | 'channels' => $this->getChannels(), 39 | 'metadata' => Arr::get($parameters, 'meta'), 40 | 'callback_url' => $parameters['callback_url'] 41 | ?? route(config('payment-gateways.routes.callback.name'), [ 42 | 'reference' => $parameters['reference'], 43 | 'provider' => $this->provider, 44 | ]), 45 | ] 46 | ); 47 | 48 | return Cache::remember($parameters['session_cache_key'], $parameters['expires'], fn () => new SessionData( 49 | provider: $this->provider, 50 | sessionReference: $parameters['reference'], 51 | paymentReference: null, 52 | checkoutSecret: null, 53 | checkoutUrl: $paystack['data']['authorization_url'], 54 | expires: $parameters['expires'], 55 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 56 | )); 57 | } 58 | 59 | public function findTransaction(string $reference): TransactionData 60 | { 61 | $transaction = $this->request('GET', "transaction/verify/$reference"); 62 | 63 | return $this->transactionDTO($transaction['data']); 64 | } 65 | 66 | public function listTransactions( 67 | ?string $from = null, 68 | ?string $to = null, 69 | ?string $page = null, 70 | ?string $status = null, 71 | ?string $reference = null, 72 | ?string $amount = null, 73 | ?string $customer = null, 74 | ): array|null { 75 | $payload = array_filter([ 76 | 'from' => $from, 77 | 'to' => $to, 78 | 'page' => $page, 79 | 'customer' => $customer, 80 | 'status' => $status, 81 | 'amount' => $amount, 82 | ]); 83 | 84 | $response = $this->request('GET', 'transaction', $payload); 85 | 86 | return [ 87 | 'meta' => [ 88 | 'total' => Arr::get($response, 'meta.total'), 89 | 'page' => Arr::get($response, 'meta.page'), 90 | 'page_count' => Arr::get($response, 'meta.pageCount'), 91 | ], 92 | 'data' => collect($response['data']) 93 | ->map(fn ($transaction) => $this->transactionDTO($transaction)) 94 | ->toArray(), 95 | ]; 96 | } 97 | 98 | public function transactionDTO(array $transaction): TransactionData 99 | { 100 | $date = Arr::get($transaction, 'transaction_date') ?? Arr::get($transaction, 'created_at'); 101 | 102 | return new TransactionData( 103 | email: $transaction['customer']['email'], 104 | meta: $transaction['metadata'], 105 | amount: ($transaction['amount'] / 100), 106 | currency: $transaction['currency'], 107 | reference: $transaction['reference'], 108 | provider: $this->provider, 109 | status: $transaction['status'], 110 | date: Carbon::parse($date)->toDateTimeString(), 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Providers/SeerbitProvider.php: -------------------------------------------------------------------------------- 1 | secretKey = $this->getToken(); 23 | } 24 | 25 | private function getToken() 26 | { 27 | $payload = [ 28 | 'key' => config('payment-gateways.providers.seerbit.secret').'.'.$this->publicKey, 29 | ]; 30 | 31 | return Http::acceptJson() 32 | ->post($this->baseUrl.'api/v2/encrypt/keys', $payload) 33 | ->json('data.EncryptedSecKey.encryptedKey'); 34 | } 35 | 36 | public function initializeCheckout(array $parameters = []): SessionData 37 | { 38 | $parameters['reference'] = 'SEBT_'.Str::random(12); 39 | 40 | $parameters['expires'] = config('payment-gateways.cache.session.expires'); 41 | 42 | $parameters['session_cache_key'] = config('payment-gateways.cache.session.key').$parameters['reference']; 43 | 44 | $seerbit = $this->request( 45 | method: 'POST', 46 | path: 'api/v2/payments', 47 | payload: [ 48 | 'publicKey' => $this->publicKey, 49 | 'email' => Arr::get($parameters, 'email'), 50 | 'amount' => Arr::get($parameters, 'amount'), 51 | 'currency' => Arr::get($parameters, 'currency'), 52 | 'country' => Arr::get($parameters, 'country_code', 'NG'), 53 | 'paymentReference' => Arr::get($parameters, 'reference'), 54 | 'callbackUrl' => $parameters['callback_url'] 55 | ?? route(config('payment-gateways.routes.callback.name'), [ 56 | 'reference' => $parameters['reference'], 57 | 'provider' => $this->provider, 58 | ]), 59 | ], 60 | ); 61 | 62 | return Cache::remember( 63 | key: $parameters['session_cache_key'], 64 | ttl: $parameters['expires'], 65 | callback: fn () => new SessionData( 66 | provider: $this->provider, 67 | sessionReference: $parameters['reference'], 68 | paymentReference: null, 69 | checkoutSecret: null, 70 | checkoutUrl: Arr::get($seerbit, 'data.payments.redirectLink'), 71 | expires: $parameters['expires'], 72 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 73 | ) 74 | ); 75 | } 76 | 77 | public function findTransaction(string $reference): TransactionData 78 | { 79 | $transaction = $this->request('GET', "api/v2/payments/query/$reference"); 80 | 81 | if (isset($transaction['error'])) { 82 | throw new \Exception($transaction['message']); 83 | } 84 | 85 | return $this->transactionDTO($transaction['data']); 86 | } 87 | 88 | public function listTransactions( 89 | ?string $from = null, 90 | ?string $to = null, 91 | ?string $page = null, 92 | ?string $status = null, 93 | ?string $reference = null, 94 | ?string $amount = null, 95 | ?string $customer = null, 96 | ): array|null { 97 | throw new \Exception("This provider [$this->provider] does not support list transactions"); 98 | } 99 | 100 | public function transactionDTO(array $transaction): TransactionData 101 | { 102 | return new TransactionData( 103 | email: $transaction['customers']['customerEmail'], 104 | meta: [ 105 | 'sourceIP' => $transaction['payments']['sourceIP'], 106 | 'deviceType' => $transaction['payments']['deviceType'], 107 | ], 108 | amount: $transaction['payments']['amount'], 109 | currency: $transaction['payments']['currency'], 110 | reference: $transaction['payments']['paymentReference'], 111 | provider: $this->provider, 112 | status: $transaction['payments']['gatewayMessage'], 113 | date: Carbon::parse($transaction['payments']['transactionProcessedTime'])->toDateTimeString(), 114 | ); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Providers/StartbuttonProvider.php: -------------------------------------------------------------------------------- 1 | secretKey = $this->publicKey; 31 | 32 | $currency = Arr::get($parameters, 'currency'); 33 | 34 | $channels = ! in_array(strtoupper($currency), ['USD']) ? $this->getChannels() : null; 35 | 36 | $startbutton = $this->request( 37 | method: 'POST', 38 | path: 'transaction/initialize', 39 | payload: array_filter([ 40 | 'partner' => Arr::get($parameters, 'partner'), 41 | 'email' => Arr::get($parameters, 'email'), 42 | 'amount' => $amount, 43 | 'currency' => $currency, 44 | 'reference' => Arr::get($parameters, 'reference'), 45 | 'paymentMethods' => $channels, 46 | 'metadata' => Arr::get($parameters, 'meta'), 47 | 'redirectUrl' => $parameters['callback_url'] 48 | ?? route(config('payment-gateways.routes.callback.name'), [ 49 | 'reference' => $parameters['reference'], 50 | 'provider' => $this->provider, 51 | ]), 52 | ]) 53 | ); 54 | 55 | return Cache::remember($parameters['session_cache_key'], $parameters['expires'], fn () => new SessionData( 56 | provider: $this->provider, 57 | sessionReference: $parameters['reference'], 58 | paymentReference: null, 59 | checkoutSecret: null, 60 | checkoutUrl: $startbutton['data'], 61 | expires: $parameters['expires'], 62 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 63 | )); 64 | } 65 | 66 | public function findTransaction(string $reference): TransactionData 67 | { 68 | $transaction = $this->request('GET', "transaction/status/$reference"); 69 | 70 | return $this->transactionDTO($transaction['data']['transaction']); 71 | } 72 | 73 | public function listTransactions( 74 | ?string $from = null, 75 | ?string $to = null, 76 | ?string $page = null, 77 | ?string $status = null, 78 | ?string $reference = null, 79 | ?string $amount = null, 80 | ?string $customer = null, 81 | ): ?array { 82 | throw new Exception("This provider [$this->provider] does not support list transactions"); 83 | } 84 | 85 | public function transactionDTO(array $transaction): TransactionData 86 | { 87 | return new TransactionData( 88 | email: $transaction['customerEmail'], 89 | meta: null, 90 | amount: ($transaction['amount'] / 100), 91 | currency: $transaction['currency'], 92 | reference: $transaction['userTransactionReference'], 93 | provider: $this->provider, 94 | status: $transaction['status'], 95 | date: Carbon::parse($transaction['createdAt'])->toDateTimeString(), 96 | ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Providers/StripeProvider.php: -------------------------------------------------------------------------------- 1 | $parameters['reference'], 27 | 'provider' => $this->provider, 28 | ]); 29 | 30 | $stripe = $this->request( 31 | method: 'POST', 32 | path: 'v1/checkout/sessions', 33 | payload: $this->prepareInitializationData($parameters), 34 | options: ['as_form' => true] 35 | ); 36 | 37 | return Cache::remember($parameters['session_cache_key'], $parameters['expires'], fn () => new SessionData( 38 | provider: $this->provider, 39 | sessionReference: $parameters['session_cache_key'], 40 | paymentReference: $stripe['id'], 41 | checkoutSecret: null, 42 | checkoutUrl: $stripe['url'], 43 | expires: $parameters['expires'], 44 | closure: Arr::get($parameters, 'closure') ? new SerializableClosure($parameters['closure']) : null, 45 | )); 46 | } 47 | 48 | public function findTransaction(string $reference): TransactionData 49 | { 50 | $response = $this->request( 51 | method: 'GET', 52 | path: "v1/checkout/sessions/$reference", 53 | options: ['as_form' => true] 54 | ); 55 | 56 | $paymentIntent = $response['payment_intent']; 57 | 58 | $transaction = $this->request( 59 | method: 'POST', 60 | path: "v1/payment_intents/$paymentIntent", 61 | options: ['as_form' => true] 62 | ); 63 | 64 | $transaction['reference'] = $reference; 65 | 66 | return $this->transactionDTO($transaction); 67 | } 68 | 69 | private function prepareInitializationData(array $parameters): array 70 | { 71 | return [ 72 | 'line_items' => [ 73 | [ 74 | 'price_data' => [ 75 | 'unit_amount' => (Arr::get($parameters, 'amount') * 100), 76 | 'currency' => strtolower(Arr::get($parameters, 'currency')), 77 | 'product_data' => [ 78 | 'name' => $parameters['reference'], 79 | ], 80 | ], 81 | 'quantity' => 1, 82 | ], 83 | ], 84 | 'customer_email' => Arr::get($parameters, 'email'), 85 | 'payment_method_types' => $this->getChannels(), 86 | 'metadata' => Arr::get($parameters, 'meta'), 87 | 'mode' => 'payment', 88 | 'success_url' => $parameters['callback_url'], 89 | 'cancel_url' => $parameters['callback_url'], 90 | ]; 91 | } 92 | 93 | public function listTransactions( 94 | ?string $from = null, 95 | ?string $to = null, 96 | ?string $page = null, 97 | ?string $status = null, 98 | ?string $reference = null, 99 | ?string $amount = null, 100 | ?string $customer = null, 101 | ): array|null { 102 | $payload = array_filter([ 103 | 'customer' => $customer, 104 | 'limit' => 100, 105 | 'created' => [ 106 | 'gte' => $from, 107 | 'lte' => $to, 108 | ], 109 | ]); 110 | 111 | $response = $this->request('GET', 'v1/charges', $payload, ['as_form' => true]); 112 | 113 | return [ 114 | 'meta' => [ 115 | 'total' => count($response['data']), 116 | 'page' => null, 117 | 'page_count' => null, 118 | ], 119 | 'data' => collect($response['data']) 120 | ->map(fn ($transaction) => $this->transactionDTO($transaction)) 121 | ->toArray(), 122 | ]; 123 | } 124 | 125 | public function transactionDTO(array $transaction): TransactionData 126 | { 127 | $email = Arr::get($transaction, 'billing_details.email') 128 | ?? Arr::get($transaction, 'charges.data.0.billing_details.email'); 129 | 130 | $reference = Arr::get($transaction, 'reference') 131 | ?? Arr::get($transaction, 'payment_intent') 132 | ?? Arr::get($transaction, 'id'); 133 | 134 | $date = Arr::get($transaction, 'created'); 135 | 136 | return new TransactionData( 137 | email: $email, 138 | meta: $transaction['metadata'], 139 | amount: ($transaction['amount'] / 100), 140 | currency: $transaction['currency'], 141 | reference: $reference, 142 | provider: $this->provider, 143 | status: $transaction['status'], 144 | date: Date::createFromTimestamp($date), 145 | ); 146 | } 147 | } 148 | --------------------------------------------------------------------------------