├── .gitignore ├── utils ├── helpers │ └── bani.php └── config │ └── bani.php ├── src ├── IsNullException.php ├── Facades │ └── Bani.php ├── Misc.php ├── Verification.php ├── BaniServiceProvider.php ├── Customer.php ├── Bani.php └── Collection.php ├── LICENSE.md ├── composer.json ├── views └── bani.blade.php └── README.md /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PrevailExcel/laravel-bani/HEAD/.gitignore -------------------------------------------------------------------------------- /utils/helpers/bani.php: -------------------------------------------------------------------------------- 1 | make('laravel-bani'); 8 | } 9 | } -------------------------------------------------------------------------------- /src/IsNullException.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | */ 15 | 16 | class IsNullException extends Exception 17 | { 18 | public static function make(): self 19 | { 20 | return new static('The signature is invalid.'); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Facades/Bani.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | */ 15 | 16 | class Bani extends Facade 17 | { 18 | /** 19 | * Get the registered name of the component 20 | * @return string 21 | */ 22 | protected static function getFacadeAccessor() 23 | { 24 | return 'laravel-bani'; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Chimeremeze Prevail Ejimadu 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. -------------------------------------------------------------------------------- /src/Misc.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | trait Misc 16 | { 17 | /** 18 | * This endpoint can be used to carry out a lookup on customer account 19 | * @param mixed $wallet 20 | * @param mixed $tag - This can username, email or phone number 21 | * @param null|string $type 22 | * @return array 23 | */ 24 | 25 | public function confirmEwallet($wallet, $tag, ?string $type = null): array 26 | { 27 | if (!$wallet) 28 | $wallet = request()->$wallet ?? null; 29 | if (!$tag) 30 | $tag = request()->tag ?? request()->username ?? request()->phone ?? null; 31 | if (!$type) 32 | $type = request()->$type ?? null; 33 | 34 | $data = array_filter([ 35 | "wallet_name" => $wallet, 36 | "verify_tag" => $tag, 37 | "verify_type" => $type 38 | ]); 39 | 40 | return $this->setHttpResponse('/partner/info/ewallet/', 'POST', $data)->getResponse(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Verification.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | trait Verification 15 | { 16 | /** 17 | * The fastest and most secure way to verify mobile numbers using USSD instead of SMS OTPs. 18 | * 19 | * @param array $data 20 | * @return array 21 | */ 22 | 23 | public function verifyPhone(?string $phone): array 24 | { 25 | if (!$phone) 26 | $phone = request()->phone; 27 | 28 | $data = array_filter([ 29 | "holder_phone" => $phone 30 | ]); 31 | 32 | return $this->setHttpResponse('/partner/verification/phone_ussd/', 'POST', $data)->getResponse(); 33 | } 34 | 35 | /** 36 | * This endpoint is used to confirmed if the customer phone number has been verified 37 | * 38 | * @param array $data 39 | * @return array 40 | */ 41 | 42 | public function checkUssdStatus(?string $verification_reference): array 43 | { 44 | if (!$verification_reference) 45 | $verification_reference = request()->verification_reference; 46 | 47 | return $this->setHttpResponse('/partner/verification/phone_ussd/'. $verification_reference , 'GET')->getResponse(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prevailexcel/laravel-bani", 3 | "description": "A Laravel Package for Bani", 4 | "keywords": [ 5 | "Bani", 6 | "Fiat Payments", 7 | "paystack alternative", 8 | "crypto payments", 9 | "BANI payments", 10 | "fiat payments", 11 | "crypto payments", 12 | "payment gateway", 13 | "online payments", 14 | "digital currency", 15 | "cryptocurrency", 16 | "financial transactions", 17 | "payment processing", 18 | "blockchain payments", 19 | "digital payments", 20 | "financial technology", 21 | "crypto integration", 22 | "payment solutions", 23 | "php", 24 | "github", 25 | "laravel", 26 | "Open Source", 27 | "payments", 28 | "crypto", 29 | "laravel 6", 30 | "laravel 7", 31 | "laravel 8", 32 | "laravel 9", 33 | "laravel 10" 34 | ], 35 | "license": "MIT", 36 | "minimum-stability": "stable", 37 | "autoload": { 38 | "files": [ 39 | "utils/helpers/bani.php" 40 | ], 41 | "psr-4": { 42 | "PrevailExcel\\Bani\\": "src/" 43 | } 44 | }, 45 | "authors": [ 46 | { 47 | "name": "Chimeremeze Prevail Ejimadu", 48 | "email": "prevailexcellent@gmail.com" 49 | } 50 | ], 51 | "require": { 52 | "guzzlehttp/guzzle": "^7.0.1" 53 | }, 54 | "extra": { 55 | "laravel": { 56 | "providers": [ 57 | "PrevailExcel\\Bani\\BaniServiceProvider" 58 | ], 59 | "aliases": { 60 | "Bani": "PrevailExcel\\Bani\\Facades\\Bani" 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /utils/config/bani.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | 13 | return [ 14 | 15 | /** 16 | * Tribe Account Ref Key From Bani Dashboard 17 | * 18 | */ 19 | 'tribeAccountRef' => getenv('BANI_TRIBE_ACCOUNT_REF'), 20 | 21 | /** 22 | * Public Key From Bani Dashboard 23 | * 24 | */ 25 | 'publicKey' => getenv('BANI_PUBLIC_KEY'), 26 | 27 | /** 28 | * Merchant Key From Bani Dashboard 29 | * 30 | */ 31 | 'merchantKey' => getenv('BANI_PRIVATE_KEY'), 32 | 33 | /** 34 | * Merchant Key From Bani Dashboard 35 | * 36 | */ 37 | 'accessToken' => getenv('BANI_ACCESS_TOKEN'), 38 | 39 | /** 40 | * You enviroment can either be live or stage. 41 | * Make sure to add the appropriate API key after changing the enviroment in .env 42 | * 43 | */ 44 | 'env' => env('BANI_ENV', 'stage'), 45 | 46 | /** 47 | * BANI Live URL 48 | * 49 | */ 50 | 'liveUrl' => env('BANI_LIVE_URL', "https://live.getbani.com/api/v1"), 51 | 52 | /** 53 | * BANI Stage URL 54 | * 55 | */ 56 | 'stageUrl' => env('BANI_STAGE_URL', "https://stage.getbani.com/api/v1"), 57 | 58 | /** 59 | * Your callback URL 60 | * 61 | */ 62 | 'callbackUrl' => getenv('BANI_CALLBACK_URL'), 63 | 64 | ]; -------------------------------------------------------------------------------- /src/BaniServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * For the full copyright and license information, please view the LICENSE 15 | * file that was distributed with this source code. 16 | */ 17 | 18 | class BaniServiceProvider extends ServiceProvider 19 | { 20 | 21 | /* 22 | * Indicates if loading of the provider is deferred. 23 | * 24 | * @var bool 25 | */ 26 | protected $defer = false; 27 | 28 | /** 29 | * Publishes all the config file this package needs to function 30 | */ 31 | public function boot() 32 | { 33 | $config = realpath(__DIR__ . '/../utils/config/bani.php'); 34 | 35 | $this->publishes([ 36 | $config => config_path('bani.php') 37 | ]); 38 | $this->mergeConfigFrom( 39 | __DIR__ . '/../utils/config/bani.php', 40 | 'bani' 41 | ); 42 | if (File::exists(__DIR__ . '/../utils/helpers/bani.php')) { 43 | require __DIR__ . '/../utils/helpers/bani.php'; 44 | } 45 | 46 | $this->loadViewsFrom(__DIR__ . '/../views/', 'bani'); 47 | 48 | /** 49 | * @param array|string $controller 50 | * @param string|null $class 51 | * */ 52 | Route::macro('callback', function ($controller, string $class = 'handleGatewayCallback') { 53 | return Route::any('bani/callback', [$controller, $class])->name("bani.lara.callback"); 54 | }); 55 | Route::macro('webhook', function ($controller, string $class = 'handleWebhook') { 56 | return Route::post('bani/webhook', [$controller, $class])->name("bani.lara.webhook"); 57 | }); 58 | } 59 | 60 | /** 61 | * Register the application services. 62 | */ 63 | public function register() 64 | { 65 | $this->app->bind('laravel-bani', function () { 66 | return new Bani; 67 | }); 68 | } 69 | 70 | /** 71 | * Get the services provided by the provider 72 | * @return array 73 | */ 74 | public function provides() 75 | { 76 | return ['laravel-bani']; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /views/bani.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | Bani Laravel 19 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/Customer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | trait Customer 15 | { 16 | /** 17 | * Check if the customer already exist 18 | * For this, you either pass the customer_phone or customer_ref 19 | * @return array 20 | * @param string|null $phone 21 | * @param string|null $ref 22 | * @throws isNullException 23 | */ 24 | public function customer(string $phone = null, string $ref = null): array 25 | { 26 | if (!$phone) 27 | $phone = request()->phone ?? null; 28 | if (!$ref) 29 | $ref = request()->ref ?? null; 30 | 31 | $customer = array_filter([ 32 | "customer_phone" => $phone, 33 | "customer_ref" => $ref 34 | ]); 35 | return $this->setHttpResponse("/comhub/check_customer/", 'POST', $customer)->getResponse(); 36 | } 37 | 38 | /** 39 | * Create a customer object 40 | * 41 | * @param array $data 42 | * @return array 43 | */ 44 | 45 | public function createCustomer($data = null): array 46 | { 47 | if ($data == null) { 48 | $data = [ 49 | "customer_first_name" => request()->first_name, 50 | "customer_last_name" => request()->last_name, 51 | "customer_phone" => request()->phone, 52 | "customer_email" => request()->email, 53 | "customer_address" => request()->address, 54 | "customer_state" => request()->state, 55 | "customer_city" => request()->city, 56 | ]; 57 | } 58 | 59 | return $this->setHttpResponse('/comhub/add_my_customer/', 'POST', array_filter($data))->getResponse(); 60 | } 61 | 62 | /** 63 | * update a customer object 64 | * 65 | * @param array $data 66 | * @return array 67 | */ 68 | 69 | public function updateCustomer($data = null): array 70 | { 71 | if ($data == null) { 72 | $data = [ 73 | "customer_ref" => request()->ref, 74 | "customer_first_name" => request()->first_name, 75 | "customer_last_name" => request()->last_name, 76 | "customer_phone" => request()->phone, 77 | "customer_email" => request()->email, 78 | "customer_address" => request()->address, 79 | "customer_state" => request()->state, 80 | "customer_city" => request()->city, 81 | ]; 82 | } 83 | 84 | return $this->setHttpResponse('/comhub/edit_my_customer/', 'POST', array_filter($data))->getResponse(); 85 | } 86 | 87 | /** 88 | * With this endpoint, you can fetch your customer delivery/billing address(es). 89 | * NG is the default delivery country code 90 | * @return array 91 | * @param string|null $ref 92 | * @param string|null $country_code 93 | * @throws isNullException 94 | */ 95 | public function listBillingAddress(string $ref = null, ?string $country_code): array 96 | { 97 | if (!$country_code) 98 | $country_code = request()->country_code ?? "NG"; 99 | if (!$ref) 100 | $ref = request()->ref ?? null; 101 | 102 | $data = array_filter([ 103 | "delivery_country_code" => $country_code, 104 | "customer_ref" => $ref 105 | ]); 106 | return $this->setHttpResponse("/comhub/list_customer_billing/", 'POST', $data)->getResponse(); 107 | } 108 | 109 | /** 110 | * This endpoint can be used to add customer delivery/billing address(es) 111 | * 112 | * @param array $data 113 | * @return array 114 | */ 115 | 116 | public function addBillingAddress($data = null): array 117 | { 118 | if ($data == null) { 119 | $data = [ 120 | "delivery_country" => request()->country_code, 121 | "customer_ref" => request()->ref, 122 | "delivery_address" => request()->address, 123 | "delivery_city" => request()->city, 124 | "delivery_state" => request()->state, 125 | "delivery_zip_code" => request()->zip, 126 | "delivery_note" => request()->note 127 | ]; 128 | } 129 | 130 | return $this->setHttpResponse('/comhub/add_customer_billing/', 'POST', array_filter($data))->getResponse(); 131 | } 132 | 133 | /** 134 | * This endpoint can be used to update customer's delivery/billing address 135 | * 136 | * @param array $data 137 | * @return array 138 | */ 139 | 140 | public function updateBillingAddress($data = null): array 141 | { 142 | if ($data == null) { 143 | $data = [ 144 | "delivery_id" => request()->id, //required 145 | "customer_ref" => request()->ref, //required 146 | "delivery_country" => request()->country_code, 147 | "delivery_address" => request()->address, 148 | "delivery_city" => request()->city, 149 | "delivery_state" => request()->state, 150 | "delivery_zip_code" => request()->zip, 151 | "delivery_note" => request()->note 152 | ]; 153 | } 154 | 155 | return $this->setHttpResponse('/comhub/edit_customer_billing/', 'POST', array_filter($data))->getResponse(); 156 | } 157 | 158 | /** 159 | * This endpoint can be used to delete customer's delivery/billing address 160 | * 161 | * @param array $data 162 | * @return array 163 | */ 164 | 165 | public function deleteBillingAddress(?string $ref, ?string $id): array 166 | { 167 | if (!$id) 168 | $id = request()->id; 169 | if (!$ref) 170 | $ref = request()->ref; 171 | 172 | $data = array_filter([ 173 | "delivery_id" => $id, 174 | "customer_ref" => $ref, 175 | "is_delete" => true 176 | ]); 177 | 178 | return $this->setHttpResponse('/comhub/edit_customer_billing/', 'POST', $data)->getResponse(); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Bani.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * For the full copyright and license information, please view the LICENSE 16 | * file that was distributed with this source code. 17 | */ 18 | 19 | class Bani 20 | { 21 | use Customer, Collection, Verification, Misc; 22 | 23 | 24 | /** 25 | * Issue Merchant Key from your Bani Dashboard 26 | * @var string 27 | */ 28 | protected $merchantKey; 29 | 30 | /** 31 | * Issue Public Key from your Bani Dashboard 32 | * @var string 33 | */ 34 | protected $publicKey; 35 | 36 | /** 37 | * Issue Tribe Account Ref Key from your Bani Dashboard 38 | * @var string 39 | */ 40 | protected $tribeAccRef; 41 | 42 | /** 43 | * Access Token from your Bani Dashboard 44 | * @var string 45 | */ 46 | protected $accessToken; 47 | 48 | /** 49 | * Instance of Client 50 | * @var Client 51 | */ 52 | protected $client; 53 | 54 | /** 55 | * Response from requests made to Bani 56 | * @var mixed 57 | */ 58 | protected $response; 59 | 60 | /** 61 | * Bani API base Url 62 | * @var string 63 | */ 64 | protected $baseUrl; 65 | 66 | /** 67 | * Bani API Enviroment 68 | * @var string 69 | */ 70 | protected $env; 71 | 72 | /** 73 | * Verified Data from Webhook 74 | */ 75 | protected $webhookData; 76 | 77 | /** 78 | * Your callback Url. You can set this in your .env or you can add 79 | * it to the $data in the methods that require that option. 80 | * @var string 81 | */ 82 | protected $callbackUrl; 83 | 84 | public function __construct() 85 | { 86 | $this->authorize(); 87 | $this->setBaseUrl(); 88 | $this->setRequestOptions(); 89 | } 90 | 91 | /** 92 | * Set properties from Bani config file 93 | */ 94 | private function authorize() 95 | { 96 | $this->tribeAccRef = Config::get('bani.tribeAccountRef'); 97 | $this->publicKey = Config::get('bani.publicKey'); 98 | $this->merchantKey = Config::get('bani.merchantKey'); 99 | $this->accessToken = Config::get('bani.accessToken'); 100 | $this->env = Config::get('bani.env'); 101 | $this->callbackUrl = Config::get('bani.callbackUrl'); 102 | } 103 | 104 | /** 105 | * Get Base Url from NOWPayment config file 106 | */ 107 | private function setBaseUrl() 108 | { 109 | if ($this->env == "stage") 110 | $this->baseUrl = Config::get('bani.stageUrl'); 111 | else 112 | $this->baseUrl = Config::get('bani.liveUrl'); 113 | } 114 | 115 | /** 116 | * The value of MONI-SIGNATURE will be a combination of the 117 | * tribe_account_ref, public_key, and merchant_private_key. 118 | * This is to verify the source sending the request. 119 | */ 120 | private function generateSignature() 121 | { 122 | $digest = $this->tribeAccRef . $this->publicKey; 123 | $signature = hash_hmac('sha256', $digest, $this->merchantKey); 124 | return $signature; 125 | } 126 | 127 | /** 128 | * Set options for making the Client request 129 | */ 130 | private function setRequestOptions() 131 | { 132 | $headers = [ 133 | 'Authorization' => 'Bearer ' . $this->accessToken, 134 | 'moni-signature' => $this->generateSignature(), 135 | 'Content-Type' => 'application/json', 136 | 'Accept' => 'application/json' 137 | ]; 138 | $this->client = new Client( 139 | [ 140 | 'base_uri' => $this->baseUrl, 141 | 'headers' => $headers 142 | ] 143 | ); 144 | } 145 | 146 | /** 147 | * @param string $relativeUrl 148 | * @param string $method 149 | * @param array $body 150 | * @return Bani 151 | * @throws IsNullException 152 | */ 153 | private function setHttpResponse($relativeUrl, $method, $body = []) 154 | { 155 | if (is_null($method)) { 156 | throw new IsNullException("Empty method not allowed"); 157 | } 158 | 159 | $this->response = $this->client->{strtolower($method)}( 160 | $this->baseUrl . $relativeUrl, 161 | ["body" => json_encode($body)] 162 | ); 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Get the whole response from a get operation 169 | * @return array 170 | */ 171 | private function getResponse() 172 | { 173 | return json_decode($this->response->getBody(), true); 174 | } 175 | 176 | /** 177 | * Get payment widget for payment 178 | * @return \Illuminate\Contracts\View\View|\Illuminate\Contracts\View\Factory 179 | */ 180 | public function payWithWidget($data = null) 181 | { 182 | if ($data == null) { 183 | $data = [ 184 | "amount" => request()->amount, 185 | "phoneNumber" => request()->phone, 186 | "email" => request()->email, 187 | "firstName" => request()->firstName, 188 | "lastName" => request()->lastName, 189 | "metadata" => request()->metadata, 190 | "merchantRef" => Random::generate(), 191 | "merchantKey" => $this->publicKey, 192 | "country_code" => "NG" 193 | ]; 194 | } 195 | 196 | return view('bani::bani', compact('data')); 197 | } 198 | 199 | /** 200 | * Get payment data 201 | * @return array 202 | */ 203 | public function getPaymentData() 204 | { 205 | $ref = request()->paymentRef; 206 | $type = request()->paymentType; 207 | $paymentdata = $this->verifyTransaction($ref, $type); 208 | return $paymentdata; 209 | } 210 | 211 | /** 212 | * Verify Transaction with reference 213 | * 214 | * @param string $ref 215 | * @param string $type 216 | * @return array 217 | */ 218 | public function verifyTransaction($ref, $type) 219 | { 220 | $data = array_filter([ 221 | "pay_ref" => $ref 222 | ]); 223 | 224 | // FOR FIAT OR CRYPTO 225 | if ($type == "fiat") 226 | dd($this->setHttpResponse('/partner/collection/pay_status_check/', 'POST', $data)->getResponse()); 227 | else 228 | return $this->setHttpResponse('/partner/collection/coin_payment_status/' . $ref, 'GET')->getResponse(); 229 | } 230 | 231 | /** 232 | * Verify webhook data 233 | * 234 | * @return Bani 235 | * @throws IsNullException 236 | */ 237 | public function getWebhookData() 238 | { 239 | $computedSignature = hash_hmac('sha256', request()->getContent(), $this->merchantKey); 240 | $signature = request()->header('bani-hook-signature'); 241 | $verified = hash_equals($signature, $computedSignature); 242 | if ($verified) { 243 | $this->webhookData = request()->getContent(); 244 | return $this; 245 | } else 246 | throw IsNullException::make(); 247 | } 248 | 249 | /** 250 | * Handle webhook data 251 | * @return array 252 | */ 253 | public function proccessData(callable|Closure $callback) 254 | { 255 | call_user_func($callback, $this->webhookData); 256 | return true; 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Collection.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * For the full copyright and license information, please view the LICENSE 13 | * file that was distributed with this source code. 14 | */ 15 | 16 | trait Collection 17 | { 18 | /** 19 | * Accept payments from customers via bank transfer 20 | * 21 | * @param array $data 22 | * @return array 23 | */ 24 | 25 | public function bankTransfer($data = null): array 26 | { 27 | $def = [ 28 | "pay_va_step" => "direct", 29 | "country_code" => "NG", 30 | "pay_currency" => "NGN", 31 | "pay_ext_ref" => Random::generate(), 32 | ]; 33 | 34 | if ($data == null) { 35 | $data = [ 36 | "pay_amount" => request()->amount, 37 | "holder_account_type" => request()->account_type ?? "temporary", 38 | "customer_ref" => request()->ref, 39 | "custom_data" => request()->custom_data ?? [] 40 | ]; 41 | } 42 | $data = array_merge($def, $data); 43 | 44 | return $this->setHttpResponse('/partner/collection/bank_transfer/', 'POST', array_filter($data))->getResponse(); 45 | } 46 | 47 | /** 48 | * Accept payments via mobile money from customers 49 | * 50 | * @param array $data 51 | * @return array 52 | */ 53 | 54 | public function mobileMoney($data = null): array 55 | { 56 | $def = [ 57 | "pay_va_step" => "direct", 58 | "country_code" => "NG", 59 | "pay_currency" => "NGN", 60 | "pay_ext_ref" => Random::generate(), 61 | ]; 62 | 63 | if ($data == null) { 64 | $data = [ 65 | "holder_phone" => request()->phone, 66 | "holder_ip_address" => request()->ip_address, 67 | "holder_device_tag" => request()->device_tag, 68 | "pay_amount" => request()->amount, 69 | "customer_ref" => request()->ref, 70 | "custom_data" => request()->custom_data ?? [] 71 | ]; 72 | } 73 | $data = array_merge($def, $data); 74 | 75 | return $this->setHttpResponse('/partner/collection/mobile_money/', 'POST', array_filter($data))->getResponse(); 76 | } 77 | 78 | /** 79 | * Accept payments from customers via crypto and get settled in fiat 80 | * 81 | * @param array $data 82 | * @return array 83 | */ 84 | 85 | public function payWithCrypto($data = null): array 86 | { 87 | if ($data == null) { 88 | $data = [ 89 | "coin_type" => request()->coin, 90 | "fiat_deposit_amount" => request()->fiat_amount, 91 | "fiat_deposit_currency" => request()->fiat_currency ?? "NGN", 92 | "coin_deposit_amount" => request()->coin_amount, 93 | "customer_ref" => request()->ref, 94 | "coin_deposit_ref" => Random::generate() . rand(11111, 99999), 95 | "coin_chain_network" => request()->chain_network, 96 | "custom_data" => request()->custom_data ?? [] 97 | ]; 98 | } 99 | 100 | return $this->setHttpResponse('/partner/collection/crypto/', 'POST', array_filter($data))->getResponse(); 101 | } 102 | 103 | /** 104 | * This endpoint can be used to generate a deposit address to accept 105 | * strictly DLT payments and get settled with the same currency. 106 | * No fiat conversion. 107 | * 108 | * @param array $data 109 | * @return array 110 | */ 111 | 112 | public function getWalletAddress($data = null): array 113 | { 114 | if ($data == null) { 115 | $data = [ 116 | "coin_type" => request()->coin, 117 | "customer_ref" => request()->ref, 118 | "coin_deposit_ref" => Random::generate() . rand(11111, 99999), 119 | "coin_chain_network" => request()->chain_network, 120 | "custom_data" => request()->custom_data ?? [] 121 | ]; 122 | } 123 | 124 | return $this->setHttpResponse('/partner/collection/fund_with_crypto/', 'POST', array_filter($data))->getResponse(); 125 | } 126 | 127 | /** 128 | * Accept one-click payment from customers via bani shopper wallet 129 | * 130 | * @param array $data 131 | * @return array 132 | */ 133 | 134 | public function startPayWithBaniShopper($data = null): array 135 | { 136 | $def = [ 137 | "estep" => "initialize", 138 | "ewallet_name" => "bani", 139 | "country_code" => "NG", 140 | "pay_currency" => "NGN", 141 | "pay_ext_ref" => Random::generate(), 142 | ]; 143 | 144 | if ($data == null) { 145 | $data = [ 146 | "customer_ref" => request()->ref, 147 | "custom_data" => request()->custom_data ?? [], 148 | "pay_amount" => request()->amount, 149 | "account_identifier" => request()->account_identifier 150 | 151 | ]; 152 | } 153 | 154 | $data = array_merge($def, $data); 155 | 156 | // Check username first 157 | $check = $this->confirmEwallet("bani", $data["account_identifier"] ?? request()->account_identifier); 158 | if ($check["status"]) 159 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 160 | } 161 | 162 | /** 163 | * Complete 2nd step in payment from customers via bani shopper wallet 164 | * 165 | * @param array $data 166 | * @return array 167 | */ 168 | 169 | public function completePayWithBaniShopper($data = null): array 170 | { 171 | $def = [ 172 | "estep" => "finalize_payment", 173 | "ewallet_name" => "bani", 174 | ]; 175 | 176 | if ($data == null) { 177 | $data = [ 178 | "eotp" => request()->eotp, 179 | "epass_ref" => request()->epass_ref 180 | ]; 181 | } 182 | 183 | $data = array_merge($def, $data); 184 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 185 | } 186 | 187 | /** 188 | * Accept one-click payment from customers via opay 189 | * 190 | * @param array $data 191 | * @return array 192 | */ 193 | 194 | public function startPayWithOpay($data = null): array 195 | { 196 | $def = [ 197 | "estep" => "initialize", 198 | "ewallet_name" => "opay", 199 | "country_code" => "NG", 200 | "pay_currency" => "NGN", 201 | "holder_ip_address" => request()->ip(), 202 | "pay_ext_ref" => Random::generate(), 203 | ]; 204 | 205 | if ($data == null) { 206 | $data = [ 207 | "customer_ref" => request()->ref, 208 | "custom_data" => request()->custom_data ?? [], 209 | "pay_amount" => request()->amount, 210 | "account_type" => request()->account_type, 211 | "holder_phone" => request()->phone, 212 | "epin" => request()->epin, 213 | "holder_first_name" => request()->firstName, 214 | "holder_last_name" => request()->lastName 215 | ]; 216 | } 217 | 218 | $data = array_merge($def, $data); 219 | 220 | // Check username first 221 | $check = $this->confirmEwallet("opay", $data["holder_phone"], $data["account_type"] ?? request()->account_type ); 222 | if ($check["status"]) 223 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 224 | } 225 | 226 | /** 227 | * Send OTP to Opay 228 | * 229 | * @param array $data 230 | * @return array 231 | */ 232 | 233 | public function sendOtpToOpay(?string $epass_ref): array 234 | { 235 | $def = [ 236 | "estep" => "send_otp", 237 | "ewallet_name" => "opay" 238 | ]; 239 | 240 | $data = [ 241 | "epass_ref" => $epass_ref ?? request()->epass_ref, 242 | ]; 243 | 244 | $data = array_merge($def, $data); 245 | 246 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 247 | } 248 | 249 | /** 250 | * Complete 2nd step in payment from customers via Opay 251 | * 252 | * @param array $data 253 | * @return array 254 | */ 255 | 256 | public function completePayWithOpay($data = null): array 257 | { 258 | $def = [ 259 | "estep" => "finalize_payment", 260 | "ewallet_name" => "opay", 261 | ]; 262 | 263 | if ($data == null) { 264 | $data = [ 265 | "eotp" => request()->eotp, 266 | "epass_ref" => request()->epass_ref 267 | ]; 268 | } 269 | 270 | $data = array_merge($def, $data); 271 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 272 | } 273 | 274 | /** 275 | * Accept one-click payment from customers via bani shopper wallet 276 | * 277 | * @param array $data 278 | * @return array 279 | */ 280 | 281 | public function startPayWithPocketApp($data = null): array 282 | { 283 | $def = [ 284 | "estep" => "initialize", 285 | "ewallet_name" => "pocketapp", 286 | "country_code" => "NG", 287 | "pay_currency" => "NGN", 288 | "pay_ext_ref" => Random::generate(), 289 | ]; 290 | 291 | if ($data == null) { 292 | $data = [ 293 | "customer_ref" => request()->ref, 294 | "custom_data" => request()->custom_data ?? [], 295 | "pay_amount" => request()->amount, 296 | "account_identifier" => request()->account_identifier 297 | 298 | ]; 299 | } 300 | 301 | $data = array_merge($def, $data); 302 | 303 | // Check username first 304 | $check = $this->confirmEwallet("pocketapp", $data["account_identifier"] ?? request()->account_identifier); 305 | if ($check["status"]) 306 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 307 | } 308 | 309 | /** 310 | * Complete 2nd step in payment from customers via pocketapp shopper wallet 311 | * 312 | * @param array $data 313 | * @return array 314 | */ 315 | 316 | public function completePayWithPocketApp($data = null): array 317 | { 318 | $def = [ 319 | "estep" => "finalize_payment", 320 | "ewallet_name" => "pocketapp", 321 | ]; 322 | 323 | if ($data == null) { 324 | $data = [ 325 | "eotp" => request()->eotp, 326 | "epass_ref" => request()->epass_ref 327 | ]; 328 | } 329 | 330 | $data = array_merge($def, $data); 331 | return $this->setHttpResponse('/partner/collection/ewallet/', 'POST', array_filter($data))->getResponse(); 332 | } 333 | 334 | 335 | } 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-bani 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/prevailexcel/laravel-bani/v/stable.svg)](https://packagist.org/packages/prevailexcel/laravel-bani) 4 | [![License](https://poser.pugx.org/prevailexcel/laravel-bani/license.svg)](LICENSE.md) 5 | > A Laravel Package for working with Bani Payments seamlessly. 6 | ## 7 | This package also allows you to receive webhooks from [Bani](https://bani.africa) which it verifies for you and processes the payloads. 8 | It also implements The Bani Pop Payment Widget and handles the callback for laravel. You can start collecting payment in fiat and crypto payments in minutes. 9 | ## Installation 10 | 11 | [PHP](https://php.net) 5.4+ or [HHVM](http://hhvm.com) 3.3+, and [Composer](https://getcomposer.org) are required. 12 | 13 | To get the latest version of Laravel Bani, simply require it 14 | 15 | ```bash 16 | composer require prevailexcel/laravel-bani 17 | ``` 18 | 19 | Or add the following line to the require block of your `composer.json` file. 20 | 21 | ``` 22 | "prevailexcel/laravel-bani": "1.0.*" 23 | ``` 24 | 25 | You'll then need to run `composer install` or `composer update` to download it and have the autoloader updated. 26 | 27 | 28 | 29 | Once Laravel Bani is installed, you need to register the service provider. Open up `config/app.php` and add the following to the `providers` key. 30 | > If you use **Laravel >= 5.5** you can skip this step and go to [**`configuration`**](https://github.com/PrevailExcel/laravel-bani#configuration) 31 | 32 | ```php 33 | 'providers' => [ 34 | ... 35 | PrevailExcel\Bani\BaniServiceProvider::class, 36 | ... 37 | ] 38 | ``` 39 | 40 | Also, register the Facade like so: 41 | 42 | ```php 43 | 'aliases' => [ 44 | ... 45 | 'Bani' => PrevailExcel\Bani\Facades\Bani::class, 46 | ... 47 | ] 48 | ``` 49 | 50 | ## Configuration 51 | 52 | You can publish the configuration file using this command: 53 | 54 | ```bash 55 | php artisan vendor:publish --provider="PrevailExcel\Bani\BaniServiceProvider" 56 | ``` 57 | 58 | A configuration-file named `bani.php` with some sensible defaults will be placed in your `config` directory: 59 | 60 | ```php 61 | getenv('BANI_TRIBE_ACCOUNT_REF'), 70 | 71 | /** 72 | * Public Key From Bani Dashboard 73 | * 74 | */ 75 | 'publicKey' => getenv('BANI_PUBLIC_KEY'), 76 | 77 | /** 78 | * Merchant Key From Bani Dashboard 79 | * 80 | */ 81 | 'merchantKey' => getenv('BANI_PRIVATE_KEY'), 82 | 83 | /** 84 | * Merchant Key From Bani Dashboard 85 | * 86 | */ 87 | 'accessToken' => getenv('BANI_ACCESS_TOKEN'), 88 | 89 | /** 90 | * You enviroment can either be live or stage. 91 | * Make sure to add the appropriate API key after changing the enviroment in .env 92 | * 93 | */ 94 | 'env' => env('BANI_ENV', 'stage'), 95 | 96 | /** 97 | * BANI Live URL 98 | * 99 | */ 100 | 'liveUrl' => env('BANI_LIVE_URL', "https://live.getbani.com/api/v1"), 101 | 102 | /** 103 | * BANI Stage URL 104 | * 105 | */ 106 | 'stageUrl' => env('BANI_STAGE_URL', "https://stage.getbani.com/api/v1"), 107 | ]; 108 | ``` 109 | ## General payment flow 110 | This is how the payment flow should be like: 111 | 112 | ### 1. Collect Customer Order Data 113 | You have to collect necessary information from the user about what they are paying for and the amount. These usually include Email, Amount, Phone Number, First name and last name. 114 | You can do this from your blade form directly (if you're building a website) or from the client app via a request. 115 | 116 | ### 2. Get the Bani Pop Widget or call the methods you want. 117 | Bani allows users to pay without leaving your website or app. So you can call the `payWithWidget()` to get the Bani Pop Widget and your user can complete payment. 118 | 119 | If you want to implement your own UI or you're building an API, you can use a variety of fluent methods you can use to start and complete the payment. You can use `bankTransfer()`,`mobileMoney()`,`payWithCrypto()`,`startPayWithOpay()` and even more. 120 | 121 | ### 3. You service the customer after payment 122 | After having completed payment, you have to confirm the payment. This package helps handle webhook in a breeze. It also helps you handle callback if you use the pop widget. 123 | 124 | As recommended, use the webhook response which this package verifies for you and then mark their order as paid and send them to a thank you page, send an email or whatever you want to do. 125 | 126 | ## Usage 127 | 128 | Open your .env file and add all the necessary keys like so: 129 | 130 | ```php 131 | BANI_TRIBE_ACCOUNT_REF=**-************************** 132 | BANI_PUBLIC_KEY=***_****_********************* 133 | BANI_PRIVATE_KEY=********************** 134 | BANI_ACCESS_TOKEN=**************************************************** 135 | BANI_ENV=stage 136 | ``` 137 | *If you are using a hosting service like heroku, ensure to add the above details to your configuration variables.* 138 | *Remember to change BANI_ENV to 'live' and update the keys when you are in production* 139 | 140 | #### Next, you have to setup your routes. 141 | There are 3 routes you should have to get started. 142 | 1. To initiate payment 143 | 2. To setup callback (if you want to use the `payWithWidget()`) - Route::callback. 144 | 3. To setup webhook and handle the event responses - Route::webhook. 145 | 146 | ```php 147 | // Laravel 5.1.17 and above 148 | Route::post('/pay', 'PaymentController@createPayment')->name('pay'); 149 | Route::callback(PaymentController::class, 'handleGatewayCallback'); 150 | Route::webhook(WebhookController::class, 'handleWebhook'); 151 | ``` 152 | OR 153 | 154 | ```php 155 | // Laravel 8 & 9 156 | Route::post('/pay', [PaymentController::class, 'createPayment'])->name('pay'); 157 | Route::callback(PaymentController::class, 'handleGatewayCallback'); 158 | Route::webhook(WebhookController::class, 'handleWebhook'); 159 | ``` 160 | 161 | ### Lets pay with widget now. 162 | ```php 163 | back()->withMessage(['msg' => $e->getMessage(), 'type' => 'error']); 182 | } 183 | } 184 | 185 | public function handleGatewayCallback() 186 | { 187 | // verify transaction and get data 188 | $data = bani()->getPaymentData(); 189 | 190 | // Do anything you want 191 | dd($data); 192 | } 193 | } 194 | ``` 195 | This opens the widget and your user completes payment. The packages redirects to the `handleGatewayCallback()` which veifies the payment and then you can use the payment data. 196 | 197 | > To test with bank transfer, after selecting bank, copy the account and head to https://demo-checkout.getbani.com/test_bank/ to make test payment and then come back to your site and click on "I've paid {amount}" button. 198 | 199 | 200 | ### Lets pay with bank transfer now. 201 | ```php 202 | 10000, 220 | "holder_account_type" => "temporary", 221 | "customer_ref" => "MC-69941173958782368244", 222 | "custom_data" => ["id" => 1, "color" => "white"], 223 | ]; 224 | 225 | // You can use the global helper bani()>method() or the Facade Bani:: method(). 226 | return bani()->bankTransfer($data); 227 | } catch (\Exception $e) { 228 | return redirect()->back()->withMessage(['msg' => $e->getMessage(), 'type' => 'error']); 229 | } 230 | } 231 | 232 | public function handleWebhook() 233 | { 234 | // verify webhook and get data 235 | bani()->getWebhookData()->proccessData(function ($data) { 236 | // Do something with $data 237 | logger($data); 238 | // If you have heavy operations, dispatch your queued jobs for them here 239 | // OrderJob::dispatch($data); 240 | }); 241 | 242 | // Acknowledge you received the response 243 | return http_response_code(200); 244 | } 245 | } 246 | ``` 247 | > To get customer ref, you can create the `bani()->createCustomer($userdata)` method wth user details or find the user via phone `bani()->customer("+2348011111111")` to get the user details. 248 | 249 | This will return data that includes the account details which you will display or send to your user to make payment. 250 | You can listen to the webhook and service the user. Write the heavy operations inside the `handleWebhook()` method. 251 | 252 | > This package recommends to use a queued job to proccess the webhook data especially if you handle heavy operations like sending mail and more 253 | 254 | ##### How does the webhook routing `Route::webhook(Controller::class, 'methodName')` work? 255 | 256 | Behind the scenes, by default this will register a POST route `'bani/webhook'` to the controller and method you provide. Because the app that sends webhooks to you has no way of getting a csrf-token, you must add that route to the except array of the VerifyCsrfToken middleware: 257 | ```php 258 | protected $except = [ 259 | 'bani/webhook', 260 | ]; 261 | ``` 262 | #### A sample form will look like so: 263 | ```blade 264 |
265 | @csrf 266 |
267 | 268 | 269 |
270 |
271 | 272 | 273 |
274 |
275 | 276 | 277 |
278 |
279 | 280 | 281 |
282 |
283 | 284 | 285 |
286 |
287 | 288 |
289 |
290 | ``` 291 | When clicking the submit button the customer gets redirected to the Pop Widget. 292 | 293 | So now the customer did some actions there (hopefully he or she paid the order) and now the package will redirect the customer to the Callback URL `Route::callback()`. 294 | 295 | We must validate if the redirect to our site is a valid request (we don't want imposters to wrongfully place non-paid order). 296 | 297 | In the controller that handles the request coming from the payment provider, we have 298 | 299 | `Bani::getPaymentData()` - This function calls the `verifyTransaction()` methods and ensure it is a valid transaction else it throws an exception. 300 | 301 | ### Some fluent methods this package provides are listed here. 302 | 303 | #### Customer 304 | 305 | ```php 306 | // For all methods, you can get data from request() 307 | 308 | /** 309 | * Check if the customer already exist 310 | * @returns array 311 | */ 312 | Bani::customer(?string $phone, ?string $ref); 313 | // Or 314 | request()->phone; 315 | bani()->customer(); 316 | 317 | 318 | /** 319 | * Create a customer object 320 | * @returns array 321 | */ 322 | Bani::createCustomer(); 323 | // Or 324 | bani()->createCustomer(); 325 | 326 | 327 | /** 328 | * Update a customer object 329 | * @returns array 330 | */ 331 | Bani::updateCustomer(); 332 | // Or 333 | bani()->updateCustomer(); 334 | 335 | 336 | /** 337 | * Fetch your customer delivery/billing address(es). 338 | */ 339 | Bani::listBillingAddress(); 340 | // Or 341 | bani()->listBillingAddress(); 342 | 343 | 344 | /** 345 | * Add customer delivery/billing address(es) 346 | * @returns array 347 | */ 348 | Bani::addBillingAddress(); 349 | // Or 350 | bani()->addBillingAddress(); 351 | 352 | 353 | /** 354 | * Uupdate customer's delivery/billing address 355 | * @returns array 356 | */ 357 | Bani::updateBillingAddress(); 358 | // Or 359 | bani()->updateBillingAddress(); 360 | 361 | 362 | /** 363 | * Delete customer's delivery/billing address 364 | * @returns array 365 | */ 366 | Bani::deleteBillingAddress(); 367 | // Or 368 | bani()->deleteBillingAddress(); 369 | ``` 370 | #### Collecting Payment 371 | 372 | ```php 373 | /** 374 | * Accept payments from customers via bank transfer 375 | * @returns array 376 | */ 377 | Bani::bankTransfer(); 378 | 379 | /** 380 | * Accept payments via mobile money from customers 381 | * @returns array 382 | */ 383 | Bani::mobileMoney(); 384 | 385 | /** 386 | * Accept payments from customers via crypto and get settled in fiat 387 | * @returns array 388 | */ 389 | Bani::payWithCrypto(); 390 | 391 | /** 392 | * generate a deposit address to accept strictly DLT payments 393 | * and get settled with the same currency 394 | */ 395 | Bani::getWalletAddress(); 396 | 397 | /** 398 | * Accept one-click payment from customers via bani shopper wallet 399 | * @returns array 400 | */ 401 | Bani::startPayWithBaniShopper(); 402 | 403 | /** 404 | * Complete 2nd step in payment from customers via bani shopper wallet 405 | * @returns array 406 | */ 407 | Bani::completePayWithBaniShopper(); 408 | ``` 409 | #### Verification 410 | 411 | ```php 412 | /** 413 | * The fastest and most secure way to verify mobile numbers using USSD instead of SMS OTPs. 414 | * @returns array 415 | */ 416 | Bani::verifyPhone(?string $phone); 417 | 418 | /** 419 | * This endpoint is used to confirmed if the customer phone number has been verified 420 | * @returns array 421 | */ 422 | Bani::checkUssdStatus(?string $verification_reference); 423 | ``` 424 | #### Misc 425 | 426 | ```php 427 | /** 428 | * This endpoint can be used to carry out a lookup on customer account 429 | * @returns array 430 | */ 431 | Bani::confirmEwallet($wallet, $tag, ?string $type); 432 | ``` 433 | 434 | ## Todo 435 | 436 | * Add Comprehensive Tests 437 | * Add Support For Agent Endpoints 438 | * Add Support For More Misc Endpoints 439 | * Add Support For Bill Payment Endpoints 440 | * Add Support For Payoutt Endpoints 441 | 442 | ## Contributing 443 | 444 | Please feel free to fork this package and contribute by submitting a pull request to enhance the functionalities. 445 | 446 | ## How can I thank you? 447 | 448 | Why not star the github repo? I'd love the attention! Why not share the link for this repository on Twitter or HackerNews? Spread the word! 449 | 450 | Don't forget to [follow me on Twitter](https://twitter.com/EjimaduPrevail)! and also [follow me on LinkedIn](https://www.linkedin.com/in/chimeremeze-prevail-ejimadu-3a3535219)! 451 | 452 | Also check out my page on medium to catch articles and tutorials on Laravel [follow me on medium](https://medium.com/@prevailexcellent)! 453 | 454 | Thanks! 455 | Chimeremeze Prevail Ejimadu. 456 | 457 | ## License 458 | 459 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 460 | --------------------------------------------------------------------------------