diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..5c6751f --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":{"Unicodeveloper\\Paystack\\Test\\HelpersTest::it_returns_instance_of_paystack":0.213,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllCustomersAreReturned":0.089,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllTransactionsAreReturned":0.001,"Unicodeveloper\\Paystack\\Test\\PaystackTest::testAllPlansAreReturned":0.001}} \ No newline at end of file diff --git a/README.md b/README.md index e98d9db..f57e23d 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ return [ Though there are multiple ways to pay an order, most payment gateways expect you to follow the following flow in your checkout process: ### 1. The customer is redirected to the payment provider -After the customer has gone through the checkout process and is ready to pay, the customer must be redirected to site of the payment provider. +After the customer has gone through the checkout process and is ready to pay, the customer must be redirected to the site of the payment provider. The redirection is accomplished by submitting a form with some hidden fields. The form must send a POST request to the site of the payment provider. The hidden fields minimally specify the amount that must be paid, the order id and a hash. @@ -111,7 +111,7 @@ The hash is calculated using the hidden form fields and a non-public secret. The The customer arrives on the site of the payment provider and gets to choose a payment method. All steps necessary to pay the order are taken care of by the payment provider. ### 3. The customer gets redirected back to your site -After having paid the order the customer is redirected back. In the redirection request to the shop-site some values are returned. The values are usually the order id, a paymentresult and a hash. +After having paid the order the customer is redirected back. In the redirection request to the shop-site some values are returned. The values are usually the order id, a payment result and a hash. The hash is calculated out of some of the fields returned and a secret non-public value. This hash is used to verify if the request is valid and comes from the payment provider. It is paramount that this hash is thoroughly checked. @@ -147,6 +147,13 @@ Route::post('/pay', [ 'as' => 'pay' ]); ``` +OR + +```php +// Laravel 8 & 9 +Route::post('/pay', [App\Http\Controllers\PaymentController::class, 'redirectToGateway'])->name('pay'); +``` + ```php Route::get('/payment/callback', 'PaymentController@handleGatewayCallback'); @@ -161,6 +168,13 @@ Route::get('payment/callback', [ ]); ``` +OR + +```php +// Laravel 8 & 9 +Route::get('/payment/callback', [App\Http\Controllers\PaymentController::class, 'handleGatewayCallback']); +``` + ```php redirectNow(); + try{ + return Paystack::getAuthorizationUrl()->redirectNow(); + }catch(\Exception $e) { + return Redirect::back()->withMessage(['msg'=>'The paystack token has expired. Please refresh the page and try again.', 'type'=>'error']); + } } /** @@ -200,6 +219,28 @@ class PaymentController extends Controller } ``` +```php +/** + * In the case where you need to pass the data from your + * controller instead of a form + * Make sure to send: + * required: email, amount, reference, orderID(probably) + * optionally: currency, description, metadata + * e.g: + * + */ +$data = array( + "amount" => 700 * 100, + "reference" => '4g4g5485g8545jg8gj', + "email" => 'user@mail.com', + "currency" => "NGN", + "orderID" => 23456, + ); + +return Paystack::getAuthorizationUrl($data)->redirectNow(); + +``` + Let me explain the fluent methods this package provides a bit here. ```php /** @@ -264,7 +305,7 @@ Paystack::getAllTransactions(); paystack()->getAllTransactions(); /** - * This method generates a unique super secure cryptograhical hash token to use as transaction reference + * This method generates a unique super secure cryptographic hash token to use as transaction reference * @returns string */ Paystack::genTranxRef(); @@ -325,10 +366,27 @@ paystack()->updateSubAccount(); A sample form will look like so: +```php + "percentage", + "currency" => "KES", + "subaccounts" => [ + [ "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 10 ], + [ "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 30 ], + ], + "bearer_type" => "all", + "main_account_share" => 70 +]; +?> +``` + ```html
-
-
+
+

Lagos Eyo Print Tee Shirt @@ -342,18 +400,20 @@ A sample form will look like so: {{-- For other necessary things you want to add to your payload. it is optional though --}} {{-- required --}} + + {{-- to support transaction split. more details https://paystack.com/docs/payments/multi-split-payments/#using-transaction-splits-with-payments --}} + {{-- to support dynamic transaction split. More details https://paystack.com/docs/payments/multi-split-payments/#dynamic-splits --}} {{ csrf_field() }} {{-- works only when using laravel 5.1, 5.2 --}} - {{-- employ this in place of csrf_field only in laravel 5.0 --}} - + {{-- employ this in place of csrf_field only in laravel 5.0 --}}

- +

-
+
``` @@ -367,7 +427,7 @@ We must validate if the redirect to our site is a valid request (we don't want i In the controller that handles the request coming from the payment provider, we have -`Paystack::getPaymentData()` - This function calls the verification methods and ensure it is a valid transction else it throws an exception. +`Paystack::getPaymentData()` - This function calls the verification methods and ensure it is a valid transaction else it throws an exception. You can test with these details diff --git a/composer.json b/composer.json index 0936778..e67e985 100644 --- a/composer.json +++ b/composer.json @@ -1,39 +1,51 @@ { "name": "unicodeveloper/laravel-paystack", "description": "A Laravel Package for Paystack", - "keywords": ["php","github", "laravel","Open Source","payments", "subscription", "paystack", "paystack.co","laravel 5", "laravel 6"], + "keywords": [ + "php", + "github", + "laravel", + "Open Source", + "payments", + "subscription", + "paystack", + "paystack.co", + "laravel 6", + "laravel 7", + "laravel 8" + ], "license": "MIT", "authors": [ - { - "name": "unicodeveloper", - "email": "prosperotemuyiwa@gmail.com" - }, - { - "name": "iamfunsho", - "email": "info@devfunsho.com" - } - ], - "minimum-stability": "stable", - "require": { - "php": "^7.0", - "illuminate/support": "^5.8.15|^6.0|^7.0", - "guzzlehttp/guzzle": "5.*|6.*" - }, - "require-dev": { - "phpunit/phpunit" : "^8.4|^9.0", + { + "name": "unicodeveloper", + "email": "prosperotemuyiwa@gmail.com" + }, + { + "name": "iamfunsho", + "email": "info@devfunsho.com" + } + ], + "minimum-stability": "stable", + "require": { + "php": "^7.2|^8.0|^8.1", + "illuminate/support": "~6|~7|~8|~9|^10.0|^11.0", + "guzzlehttp/guzzle": "~6|~7|~8|~9" + }, + "require-dev": { + "phpunit/phpunit": "^8.4|^9.0|^10.5", "scrutinizer/ocular": "~1.1", "php-coveralls/php-coveralls": "^2.0", "mockery/mockery": "^1.3" }, - "autoload": { + "autoload": { "files": [ - "src/Support/helpers.php" - ], + "src/Support/helpers.php" + ], "psr-4": { - "Unicodeveloper\\Paystack\\": "src/" + "Unicodeveloper\\Paystack\\": "src/" } - }, - "autoload-dev": { + }, + "autoload-dev": { "psr-4": { "Unicodeveloper\\Paystack\\Test\\": "tests" } @@ -41,14 +53,14 @@ "scripts": { "test": "vendor/bin/phpunit" }, - "extra": { - "laravel": { - "providers": [ - "Unicodeveloper\\Paystack\\PaystackServiceProvider" - ], - "aliases": { - "Paystack": "Unicodeveloper\\Paystack\\Facades\\Paystack" + "extra": { + "laravel": { + "providers": [ + "Unicodeveloper\\Paystack\\PaystackServiceProvider" + ], + "aliases": { + "Paystack": "Unicodeveloper\\Paystack\\Facades\\Paystack" + } } } - } } diff --git a/src/Exceptions/isNullException.php b/src/Exceptions/IsNullException.php similarity index 100% rename from src/Exceptions/isNullException.php rename to src/Exceptions/IsNullException.php diff --git a/src/Paystack.php b/src/Paystack.php index 9dc8a81..f4b91dc 100644 --- a/src/Paystack.php +++ b/src/Paystack.php @@ -86,7 +86,7 @@ public function setKey() */ private function setRequestOptions() { - $authBearer = 'Bearer '. $this->secretKey; + $authBearer = 'Bearer ' . $this->secretKey; $this->client = new Client( [ @@ -101,7 +101,7 @@ private function setRequestOptions() } - /** + /** * Initiate a payment request to Paystack * Included the option to pass the payload to this method for situations @@ -109,21 +109,23 @@ private function setRequestOptions() * @return Paystack */ - public function makePaymentRequest( $data = null) + public function makePaymentRequest($data = null) { - if ( $data == null ) { + if ($data == null) { - $quantity = intval(request()->quantity); + $quantity = intval(request()->quantity ?? 1); - $data = [ + $data = array_filter([ "amount" => intval(request()->amount) * $quantity, "reference" => request()->reference, "email" => request()->email, + "channels" => request()->channels, "plan" => request()->plan, "first_name" => request()->first_name, "last_name" => request()->last_name, "callback_url" => request()->callback_url, "currency" => (request()->currency != "" ? request()->currency : "NGN"), + /* Paystack allows for transactions to be split into a subaccount - The following lines trap the subaccount ID - as well as the ammount to charge the subaccount (if overriden in the form) @@ -131,24 +133,46 @@ public function makePaymentRequest( $data = null) */ "subaccount" => request()->subaccount, "transaction_charge" => request()->transaction_charge, + + /** + * Paystack allows for transaction to be split into multi accounts(subaccounts) + * The following lines trap the split ID handling the split + * More details here: https://paystack.com/docs/payments/multi-split-payments/#using-transaction-splits-with-payments + */ + "split_code" => request()->split_code, + + /** + * Paystack allows transaction to be split into multi account(subaccounts) on the fly without predefined split + * form need an input field: + * array must be set up as: + * $split = [ + * "type" => "percentage", + * "currency" => "KES", + * "subaccounts" => [ + * { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 10 }, + * { "subaccount" => "ACCT_li4p6kte2dolodo", "share" => 30 }, + * ], + * "bearer_type" => "all", + * "main_account_share" => 70, + * ] + * More details here: https://paystack.com/docs/payments/multi-split-payments/#dynamic-splits + */ + "split" => request()->split, /* * to allow use of metadata on Paystack dashboard and a means to return additional data back to redirect url * form need an input field: - *array must be set up as: $array = [ 'custom_fields' => [ - * ['display_name' => "Cart Id", "variable_name" => "cart_id", "value" => "2"], - * ['display_name' => "Sex", "variable_name" => "sex", "value" => "female"], - * . - * . - * . - * ] - * - * ] + * array must be set up as: + * $array = [ 'custom_fields' => [ + * ['display_name' => "Cart Id", "variable_name" => "cart_id", "value" => "2"], + * ['display_name' => "Sex", "variable_name" => "sex", "value" => "female"], + * . + * . + * . + * ] + * ] */ 'metadata' => request()->metadata - ]; - - // Remove the fields which were not sent (value would be null) - array_filter($data); + ]); } $this->setHttpResponse('/transaction/initialize', 'POST', $data); @@ -182,16 +206,16 @@ private function setHttpResponse($relativeUrl, $method, $body = []) * Get the authorization url from the callback response * @return Paystack */ - public function getAuthorizationUrl() + public function getAuthorizationUrl($data = null) { - $this->makePaymentRequest(); + $this->makePaymentRequest($data); $this->url = $this->getResponse()['data']['authorization_url']; return $this; } - /** + /** * Get the authorization callback response * In situations where Laravel serves as an backend for a detached UI, the api cannot redirect * and might need to take different actions based on the success or not of the transaction @@ -209,9 +233,9 @@ public function getAuthorizationResponse($data) /** * Hit Paystack Gateway to Verify that the transaction is valid */ - private function verifyTransactionAtGateway() + private function verifyTransactionAtGateway($transaction_id = null) { - $transactionRef = request()->query('trxref'); + $transactionRef = $transaction_id ?? request()->query('trxref'); $relativeUrl = "/transaction/verify/{$transactionRef}"; @@ -222,9 +246,9 @@ private function verifyTransactionAtGateway() * True or false condition whether the transaction is verified * @return boolean */ - public function isTransactionVerificationValid() + public function isTransactionVerificationValid($transaction_id = null) { - $this->verifyTransactionAtGateway(); + $this->verifyTransactionAtGateway($transaction_id); $result = $this->getResponse()['message']; @@ -352,7 +376,6 @@ public function createPlan() $this->setRequestOptions(); return $this->setHttpResponse("/plan", 'POST', $data)->getResponse(); - } /** @@ -390,17 +413,20 @@ public function updatePlan($plan_code) /** * Create a customer */ - public function createCustomer() + public function createCustomer($data = null) { - $data = [ - "email" => request()->email, - "first_name" => request()->fname, - "last_name" => request()->lname, - "phone" => request()->phone, - "metadata" => request()->additional_info /* key => value pairs array */ + if ($data == null) { - ]; + $data = [ + "email" => request()->email, + "first_name" => request()->fname, + "last_name" => request()->lname, + "phone" => request()->phone, + "metadata" => request()->additional_info /* key => value pairs array */ + ]; + } + $this->setRequestOptions(); return $this->setHttpResponse('/customer', 'POST', $data)->getResponse(); } @@ -413,7 +439,7 @@ public function createCustomer() public function fetchCustomer($customer_id) { $this->setRequestOptions(); - return $this->setHttpResponse('/customer/'. $customer_id, 'GET', [])->getResponse(); + return $this->setHttpResponse('/customer/' . $customer_id, 'GET', [])->getResponse(); } /** @@ -433,7 +459,7 @@ public function updateCustomer($customer_id) ]; $this->setRequestOptions(); - return $this->setHttpResponse('/customer/'. $customer_id, 'PUT', $data)->getResponse(); + return $this->setHttpResponse('/customer/' . $customer_id, 'PUT', $data)->getResponse(); } /** @@ -543,7 +569,7 @@ public function disableSubscription() public function fetchSubscription($subscription_id) { $this->setRequestOptions(); - return $this->setHttpResponse('/subscription/'.$subscription_id, 'GET', [])->getResponse(); + return $this->setHttpResponse('/subscription/' . $subscription_id, 'GET', [])->getResponse(); } /** @@ -579,7 +605,7 @@ public function getAllPages() public function fetchPage($page_id) { $this->setRequestOptions(); - return $this->setHttpResponse('/page/'.$page_id, 'GET', [])->getResponse(); + return $this->setHttpResponse('/page/' . $page_id, 'GET', [])->getResponse(); } /** @@ -596,16 +622,17 @@ public function updatePage($page_id) ]; $this->setRequestOptions(); - return $this->setHttpResponse('/page/'.$page_id, 'PUT', $data)->getResponse(); + return $this->setHttpResponse('/page/' . $page_id, 'PUT', $data)->getResponse(); } - /** + /** * Creates a subaccount to be used for split payments . Required params are business_name , settlement_bank , account_number , percentage_charge * * @return array */ - public function createSubAccount(){ + public function createSubAccount() + { $data = [ "business_name" => request()->business_name, "settlement_bank" => request()->settlement_bank, @@ -620,31 +647,30 @@ public function createSubAccount(){ $this->setRequestOptions(); return $this->setHttpResponse('/subaccount', 'POST', array_filter($data))->getResponse(); - } - /** + /** * Fetches details of a subaccount * @param subaccount code * @return array */ - public function fetchSubAccount($subaccount_code){ + public function fetchSubAccount($subaccount_code) + { $this->setRequestOptions(); - return $this->setHttpResponse("/subaccount/{$subaccount_code}","GET",[])->getResponse(); - + return $this->setHttpResponse("/subaccount/{$subaccount_code}", "GET", [])->getResponse(); } - /** + /** * Lists all the subaccounts associated with the account * @param $per_page - Specifies how many records to retrieve per page , $page - SPecifies exactly what page to retrieve * @return array */ - public function listSubAccounts($per_page,$page){ + public function listSubAccounts($per_page, $page) + { $this->setRequestOptions(); - return $this->setHttpResponse("/subaccount/?perPage=".(int) $per_page."&page=".(int) $page,"GET")->getResponse(); - + return $this->setHttpResponse("/subaccount/?perPage=" . (int) $per_page . "&page=" . (int) $page, "GET")->getResponse(); } @@ -654,7 +680,8 @@ public function listSubAccounts($per_page,$page){ * @return array */ - public function updateSubAccount($subaccount_code){ + public function updateSubAccount($subaccount_code) + { $data = [ "business_name" => request()->business_name, "settlement_bank" => request()->settlement_bank, @@ -670,6 +697,33 @@ public function updateSubAccount($subaccount_code){ $this->setRequestOptions(); return $this->setHttpResponse("/subaccount/{$subaccount_code}", "PUT", array_filter($data))->getResponse(); + } + + + /** + * Get a list of all supported banks and their properties + * @param $country - The country from which to obtain the list of supported banks, $per_page - Specifies how many records to retrieve per page , + * $use_cursor - Flag to enable cursor pagination on the endpoint + * @return array + */ + public function getBanks(?string $country, int $per_page = 50, bool $use_cursor = false) + { + if (!$country) + $country = request()->country ?? 'nigeria'; + + $this->setRequestOptions(); + return $this->setHttpResponse("/bank/?country=" . $country . "&use_cursor=" . $use_cursor . "&perPage=" . (int) $per_page, "GET")->getResponse(); + } + /** + * Confirm an account belongs to the right customer + * @param $account_number - Account Number, $bank_code - You can get the list of bank codes by calling the List Banks endpoint + * @return array + */ + public function confirmAccount(string $account_number, string $bank_code) + { + + $this->setRequestOptions(); + return $this->setHttpResponse("/bank/resolve/?account_number=" . $account_number . "&bank_code=" . $bank_code, "GET")->getResponse(); } }