├── .github ├── FUNDING.yml └── workflows │ └── php.yml ├── .gitignore ├── CHANGELOG.md ├── README.md ├── composer.json ├── composer.lock ├── images ├── callback.png ├── iFrame.png └── register.png ├── phpunit.xml ├── src ├── Exceptions │ └── ConfigurationUnavailableException.php ├── Facades │ └── Pesapal.php ├── Http │ ├── Controllers │ │ └── PaymentController.php │ └── Middleware │ │ └── ValidateConfigMiddleware.php ├── OAuth │ ├── OAuthConsumer.php │ ├── OAuthDataStore.php │ ├── OAuthException.php │ ├── OAuthRequest.php │ ├── OAuthServer.php │ ├── OAuthSignatureMethod.php │ ├── OAuthSignatureMethod_HMAC_SHA1.php │ ├── OAuthSignatureMethod_PLAINTEXT.php │ ├── OAuthSignatureMethod_RSA_SHA1.php │ ├── OAuthToken.php │ └── OAuthUtil.php ├── Payment.php ├── Pesapal.php ├── PesapalServiceProvider.php ├── config │ └── pesapal.php ├── database │ ├── factories │ │ └── PaymentFactory.php │ └── migrations │ │ └── 2020_15_06_000000_create_pesapal_payments_table.php ├── resources │ └── views │ │ └── iframe.blade.php └── routes │ └── web.php └── tests ├── Feature ├── GetIframeSourceTest.php └── PaymentControllerTest.php ├── TestCase.php └── Unit └── PaymentTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: bryceandy -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | phpunit-tests: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Install Dependencies 17 | run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist 18 | - name: Create Database 19 | run: | 20 | mkdir -p database 21 | touch database/database.sqlite 22 | - name: Execute tests via PHPUnit 23 | run: vendor/bin/phpunit 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | .idea 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v2.1.0](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.9...v2.1.0) - October 28, 2021 4 | * Add support for PHP 8 5 | 6 | ## [v2.0.9](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.8...v2.0.9) - July 13, 2021 7 | * Bump [file system](https://github.com/thephpleague/flysystem) from 1.1.3 to 1.1.4 8 | * Update other dependencies 9 | 10 | ## [v2.0.8](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.7...v2.0.8) - May 6, 2021 11 | * Add a custom message for ConfigurationUnavailableException 12 | * Throw ValidationException when validating request data 13 | * Minor fixes 14 | 15 | ## [v2.0.7](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.6...v2.0.7) - April 26, 2021 16 | * Update [hamcrest](https://github.com/hamcrest/hamcrest-php) due to an issue with its generator 17 | * Bump [laravel](https://github.com/laravel/framework) from 7.16.1 to 7.30.* 18 | * Bump [symphony kernel](https://github.com/symphony/http-kernel) from 5.1.2 to 5.1.5 19 | * Update phpunit configuration to the latest valid schema 20 | 21 | ## [v2.0.6](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.5...v2.0.6) - July 7, 2020 22 | * Fix: enable iframe scrolling on smaller devices 23 | 24 | ## [v2.0.5](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.4...v2.0.5) - July 6, 2020 25 | * Fix: properly check request parameters that are missing 26 | 27 | ## [v2.0.4](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.3...v2.0.4) - July 5, 2020 28 | * Add support to post payments with get requests 29 | 30 | ## [v2.0.3](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.2...v2.0.3) - June 18, 2020 31 | * Parameterize post_xml variable 32 | 33 | ## [v2.0.2](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.1...v2.0.2) - June 18, 2020 34 | * Create middleware to validate configurations 35 | * Use request array to create post_xml 36 | 37 | ## [v2.0.1](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.0...v2.0.1) - June 18, 2020 38 | * Add a route name for the route that posts payments 39 | * Remove csrf key in the array that saves a new payment 40 | 41 | ## [v2.0.0](https://github.com/bryceandy/laravel_pesapal/compare/v2.0.0-beta.1...v2.0.0) - June 17, 2020 42 | * Version 2 release 43 | 44 | ## [v2.0.0-beta.1](https://github.com/bryceandy/laravel_pesapal/compare/v1.0.1...v2.0.0-beta.1) - June 17, 2020 45 | * Version support 46 | * PHP 47 | * The minimum PHP version required is from 7.4.* 48 | * Laravel 49 | * The minimum Laravel version supported is 7.* 50 | * Publishing files 51 | * This version would not allow publishing any views, migrations or assets 52 | * The only file that remains published will be the pesapal config file 53 | * New facade to enable the querying of a payment status and order details 54 | * Customizable IPN listener. When a payment status changes, you can choose whether to 55 | * Send an e-mail or notification 56 | * Fire an event or dispatch a job 57 | 58 | ## [v1.0.1](https://github.com/bryceandy/laravel_pesapal/compare/v1.0.0...v1.0.1) - January 11, 2019 59 | * Remove default empty config values for consumer key and consumer secret 60 | * Add important comments 61 | 62 | ## v1.0.0 - January 10, 2019 63 | * Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pesapal package for Laravel apps 2 | 3 | [![Actions Status](https://github.com/bryceandy/laravel_pesapal/workflows/Tests/badge.svg)](https://github.com/bryceandy/laravel_pesapal/actions) 4 | Total Downloads 5 | Latest Stable Version 6 | License 7 | Donate to this project using Patreon 8 | 9 | This package enables Laravel developers to easily make use of the [Pesapal](https://www.pesapal.com) API. 10 | 11 | ![Pesapal iFrame](images/iFrame.png) 12 | 13 | ## Version support 14 | 15 | | Laravel version | Package version | Maintenance | 16 | | --- | --- | --- | 17 | | 5.7 - 6 | 1.0.0 - 1.0.1 | No longer maintained | 18 | | 7 and above | 2.* | Actively maintained | 19 | 20 | ## Installation 21 | 22 | Pre-installation requirements 23 | 24 | * A running or newly installed Laravel 7.* or above 25 | * PHP 7.4 or above 26 | * cURL extension installed 27 | 28 | Now run 29 | 30 | ```bash 31 | composer require bryceandy/laravel_pesapal 32 | ``` 33 | 34 | ## Configuration 35 | 36 | Next we publish the configuration file that comes with the package 37 | 38 | ```bash 39 | php artisan vendor:publish --tag=pesapal-config 40 | ``` 41 | 42 | After publishing, you will find a `pesapal.php` file in your `config` directory 43 | 44 | Head over to [demo](https://demo.pesapal.com) if you want a testing environment or [live](https://www.pesapal.com) for a live integration and create a business account. You will obtain a key-secret pair for your integration 45 | 46 | ![Pesapal Registration](images/register.png) 47 | 48 | Inside your `.env` file, create these environment variables and they will be used to set configuration values available in the published `config/pesapal.php` file 49 | 50 | Use the keys you obtained from Pesapal to fill the key and secret. If you are on a live account, set the **is_live** variable to true. 51 | 52 | ```dotenv 53 | PESAPAL_KEY=yourConsumerKey 54 | PESAPAL_SECRET=yourConsumerSecret 55 | PESAPAL_IS_LIVE=false 56 | PESAPAL_CALLBACK_URL= 57 | ``` 58 | 59 | Thereafter, run the migration command as the package will load a database migration that stores the payment records 60 | 61 | ```bash 62 | php artisan migrate 63 | ``` 64 | 65 | ## Usage 66 | 67 | ### Before making a payment, setup a callback page. 68 | 69 | Create a callback page and register its URL in the `PESAPAL_CALLBACK_URL` environment variable. This can be something like `http://yourwebsite.com/callback` 70 | 71 | Once a payment process has been completed by the user, Pesapal will redirect to your site using the url. 72 | 73 | ### Making a request to Pesapal for a payment. 74 | 75 | Pesapal requires a request sent to their API in order to display the form like the one we [see above](#pesapal-package-for-laravel-apps ) 76 | 77 | This package comes with a route `/pesapal/iframe` where you can post the data as follows: 78 | 79 | ```php 80 | /** 81 | * Create a form and send the appropriate values. You may as 82 | * well send url parameters where a view will be returned. 83 | */ 84 | [ 85 | 'amount' => 'Required, input should be numbers only', 86 | 'currency' => 'Required, values can be TZS,KES,UGX or USD', 87 | 'description' => 'Required, short description of the payment', 88 | 'type' => 'Required, "MERCHANT" or "ORDER"', 89 | 'reference' => 'Required, should be auto-generated and unique for every transaction', 90 | 'first_name' => 'Optional', 91 | 'last_name' => 'Optional', 92 | 'email' => 'Required if there is no phone number', 93 | 'phone_number' => 'Required if there is no email, include the country code. Example 255784999999', 94 | ] 95 | ``` 96 | 97 | For the **type** field, leave the default as MERCHANT. If you use ORDER, be sure to read the Pesapal documentation first. 98 | 99 | When the data is posted successfully, you will have a view of the form to make payments. 100 | 101 | A new payment record will be recorded in your `pesapal_payments` table, now you can choose the payment option you prefer. 102 | 103 | ### Fetching the payment status. 104 | 105 | After making the payment you will be redirected to the callback URL as mentioned above, and Pesapal will redirect with two query parameters: 106 | 107 | * pesapal_merchant_reference – this is the same as `$reference` that you posted to Pesapal 108 | * pesapal_transaction_tracking_id - a unique id for the transaction on Pesapal that you can use to track the status of the transaction later 109 | 110 | With these two we can now: 111 | 112 | A. Use these parameters to query the payment status to display to the user. 113 | 114 | Normally on your callback page you can display whatever you need to your customer to show that the payment is being processed. 115 | 116 | ![Callback page sample](images/callback.png) 117 | 118 | But because Pesapal will send the payment tracking Id which you have not recorded, you can save this unique tracking Id for your payment and also query for the payment status. 119 | 120 | In the controller method where you display the callback page, query the status: 121 | 122 | ```php 123 | namespace App\Http\Controllers; 124 | 125 | use Bryceandy\Laravel_Pesapal\Facades\Pesapal; 126 | use Bryceandy\Laravel_Pesapal\Payment; 127 | 128 | class CallbackController extends Controller 129 | { 130 | public function index() 131 | { 132 | $transaction = Pesapal::getTransactionDetails( 133 | request('pesapal_merchant_reference'), request('pesapal_transaction_tracking_id') 134 | ); 135 | 136 | // Store the paymentMethod, trackingId and status in the database 137 | Payment::modify($transaction); 138 | 139 | $status = $transaction['status']; 140 | // also $status = Pesapal::statusByTrackingIdAndMerchantRef(request('pesapal_merchant_reference'), request('pesapal_transaction_tracking_id')); 141 | // also $status = Pesapal::statusByMerchantRef(request('pesapal_merchant_reference')); 142 | 143 | return view('your_callback_view', compact('status')); // Display this status to the user. Values are (PENDING, COMPLETED, INVALID, or FAILED) 144 | } 145 | } 146 | ``` 147 | 148 | This way requires you to refresh the page because you may not know when the status has changed. 149 | 150 | If this does not have a good user experience, you may setup an 'IPN listener' where Pesapal notifies you when a payment status has changed. 151 | 152 | B. Setting up an IPN (Instant Payment Notifications) listener. 153 | 154 | **This only applies to merchant accounts**. Create a route for your IPN listener, for example a GET request to /pesapal-ipn-listener 155 | 156 | ```php 157 | // For Laravel 7.* 158 | Route::get('pesapal-ipn-listener', 'IpnController'); 159 | // For Laravel 8.* onwards 160 | Route::get('pesapal-ipn-listener', \App\Http\Controllers\IpnController::class); 161 | ``` 162 | 163 | Your IPN Controller could look like this: 164 | 165 | ```php 166 | namespace App\Http\Controllers; 167 | 168 | use Bryceandy\Laravel_Pesapal\Facades\Pesapal; 169 | use Bryceandy\Laravel_Pesapal\Payment; 170 | use Illuminate\Support\Facades\Mail; 171 | 172 | class IpnController extends Controller 173 | { 174 | public function __invoke() 175 | { 176 | $transaction = Pesapal::getTransactionDetails( 177 | request('pesapal_merchant_reference'), request('pesapal_transaction_tracking_id') 178 | ); 179 | 180 | // Store the paymentMethod, trackingId and status in the database 181 | Payment::modify($transaction); 182 | 183 | // If there was a status change and the status is not 'PENDING' 184 | if(request('pesapal_notification_type') == "CHANGE" && request('pesapal_transaction_tracking_id') != ''){ 185 | 186 | //Here you can do multiple things to notify your user that the changed status of their payment 187 | // 1. Send an email or SMS (if your user doesnt have an email)to your user 188 | $payment = Payment::whereReference(request('pesapal_merchant_reference'))->first(); 189 | Mail::to($payment->email)->send(new PaymentProcessed(request('pesapal_transaction_tracking_id'), $transaction['status'])); 190 | // PaymentProcessed is an example of a mailable email, it does not come with the package 191 | 192 | // 2. You may also create a Laravel Event & Listener to process a Notification to the user 193 | // 3. You can also create a Laravel Notification or dispatch a Laravel Job. Possibilities are endless! 194 | 195 | // Finally output a response to Pesapal 196 | $response = 'pesapal_notification_type=' . request('pesapal_notification_type'). 197 | '&pesapal_transaction_tracking_id=' . request('pesapal_transaction_tracking_id'). 198 | '&pesapal_merchant_reference=' . request('pesapal_merchant_reference'); 199 | 200 | ob_start(); 201 | echo $response; 202 | ob_flush(); 203 | exit; // This is mandatory. If you dont exit, Pesapal will not get your response. 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | This controller method will be called every time Pesapal sends you an IPN notification until the payment is completed or has failed. 210 | 211 | ## IMPORTANT 212 | 213 | ### Register IPN settings 214 | 215 | On your Pesapal dashboard find your Account Settings and click IPN Settings. 216 | 217 | Fill in your website domain for example `yourWebsite.com` and IPN listener URL, for example `yourWebsite.co.tz/pesapal-ipn-listener`. 218 | 219 | This is important so that Pesapal can send IPN notifications. 220 | 221 | ## License 222 | 223 | MIT License. 224 | 225 | ## Contributors 226 | 227 | This package is based from the PHP API of [Pesapal](https://pesapal.com) 228 | 229 | - [BryceAndy](http://bryceandy.com) > hello@bryceandy.com 230 | 231 | ## Sponsorship 232 | 233 | If you enjoy using this package, consider contributing to the maintainer 234 | 235 | Donate to this project using Patreon -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bryceandy/laravel_pesapal", 3 | "description": "An unofficial Pesapal API integration for Laravel. Multiple payment options including but not limited to M-Pesa, Tigo Pesa, Visa, Mastercard, American Express in Kenya, Tanzania & Uganda.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "bryceandy", 8 | "email": "hello@bryceandy.com" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "prefer-stable": true, 13 | "require": { 14 | "php": "^7.4|^8.0", 15 | "ext-curl": "*" 16 | }, 17 | "require-dev": { 18 | "orchestra/testbench": "^5.3" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Bryceandy\\Laravel_Pesapal\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Bryceandy\\Laravel_Pesapal\\Tests\\": "tests/" 28 | } 29 | }, 30 | "extra": { 31 | "laravel": { 32 | "providers": [ 33 | "Bryceandy\\Laravel_Pesapal\\PesapalServiceProvider" 34 | ] 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /images/callback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryceandy/laravel_pesapal/0fb05e7693b6c724dac854ba2451d76eb4991737/images/callback.png -------------------------------------------------------------------------------- /images/iFrame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryceandy/laravel_pesapal/0fb05e7693b6c724dac854ba2451d76eb4991737/images/iFrame.png -------------------------------------------------------------------------------- /images/register.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bryceandy/laravel_pesapal/0fb05e7693b6c724dac854ba2451d76eb4991737/images/register.png -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests/Unit 11 | 12 | 13 | ./tests/Feature 14 | 15 | 16 | 17 | 18 | ./app 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Exceptions/ConfigurationUnavailableException.php: -------------------------------------------------------------------------------- 1 | validation = $validation; 24 | } 25 | 26 | /** 27 | * Stores a new payment, post it to Pesapal & 28 | * displays the iframe with payment methods 29 | * 30 | * @param Request $request 31 | * 32 | * @return Factory|View 33 | * 34 | * @throws ValidationException 35 | */ 36 | public function store(Request $request) 37 | { 38 | $this->validation->make($request->all(), [ 39 | 'amount' => 'required|numeric', 40 | 'currency' => 'required|in:TZS,KES,UGX,USD', 41 | 'description' => 'required|min:5', 42 | 'type' => 'required|in:MERCHANT,ORDER', 43 | 'reference' => 'required', 44 | 'first_name' => 'sometimes|required|min:3', 45 | 'last_name' => 'sometimes|required|min:3', 46 | 'email' => 'required_without:phone_number|email', 47 | 'phone_number' => 'required_without:email|numeric', 48 | ])->validate(); 49 | 50 | Payment::create($request->except(['type', '_token'])); 51 | 52 | $iframe_src = Pesapal::getIframeSource($request->all()); 53 | 54 | return view ('laravel_pesapal::iframe', compact('iframe_src')); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Http/Middleware/ValidateConfigMiddleware.php: -------------------------------------------------------------------------------- 1 | key = $key; 15 | $this->secret = $secret; 16 | $this->callback_url = $callback_url; 17 | } 18 | 19 | public function __toString() { 20 | return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/OAuth/OAuthDataStore.php: -------------------------------------------------------------------------------- 1 | parameters = $parameters; 22 | $this->http_method = $http_method; 23 | $this->http_url = $http_url; 24 | } 25 | 26 | /** 27 | * Attempt to build up a request from what was passed to the server 28 | * 29 | * @param null $http_method 30 | * @param null $http_url 31 | * @param null $parameters 32 | * @return OAuthRequest 33 | */ 34 | public static function from_request($http_method = NULL, $http_url = NULL, $parameters = NULL) 35 | { 36 | $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") 37 | ? 'http' 38 | : 'https'; 39 | @$http_url or $http_url = $scheme . 40 | '://' . $_SERVER['HTTP_HOST'] . 41 | ':' . 42 | $_SERVER['SERVER_PORT'] . 43 | $_SERVER['REQUEST_URI']; 44 | @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; 45 | 46 | // We weren't handed any parameters, so let's find the ones relevant to 47 | // this request. 48 | // If you run XML-RPC or similar you should use this to provide your own 49 | // parsed parameter-list 50 | if (!$parameters) { 51 | // Find request headers 52 | $request_headers = OAuthUtil::get_headers(); 53 | 54 | // Parse the query-string to find GET parameters 55 | $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); 56 | 57 | // It's a POST request of the proper content-type, so parse POST 58 | // parameters and add those overriding any duplicates from GET 59 | if ($http_method == "POST" 60 | && @strstr($request_headers["Content-Type"], 61 | "application/x-www-form-urlencoded") 62 | ) { 63 | $post_data = OAuthUtil::parse_parameters( 64 | file_get_contents(self::$POST_INPUT) 65 | ); 66 | $parameters = array_merge($parameters, $post_data); 67 | } 68 | 69 | // We have a Authorization-header with OAuth data. Parse the header 70 | // and add those overriding any duplicates from GET or POST 71 | if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { 72 | $header_parameters = OAuthUtil::split_header( 73 | $request_headers['Authorization'] 74 | ); 75 | $parameters = array_merge($parameters, $header_parameters); 76 | } 77 | } 78 | 79 | return new OAuthRequest($http_method, $http_url, $parameters); 80 | } 81 | 82 | /** 83 | * pretty much a helper function to set up the request 84 | * 85 | * @param $consumer 86 | * @param $token 87 | * @param $http_method 88 | * @param $http_url 89 | * @param null $parameters 90 | * @return OAuthRequest 91 | */ 92 | public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) 93 | { 94 | @$parameters or $parameters = array(); 95 | $defaults = array("oauth_version" => OAuthRequest::$version, 96 | "oauth_nonce" => OAuthRequest::generate_nonce(), 97 | "oauth_timestamp" => OAuthRequest::generate_timestamp(), 98 | "oauth_consumer_key" => $consumer->key); 99 | if ($token) 100 | $defaults['oauth_token'] = $token->key; 101 | 102 | $parameters = array_merge($defaults, $parameters); 103 | 104 | return new OAuthRequest($http_method, $http_url, $parameters); 105 | } 106 | 107 | public function set_parameter($name, $value, $allow_duplicates = true) 108 | { 109 | if ($allow_duplicates && isset($this->parameters[$name])) { 110 | // We have already added parameter(s) with this name, so add to the list 111 | if (is_scalar($this->parameters[$name])) { 112 | // This is the first duplicate, so transform scalar (string) 113 | // into an array so we can add the duplicates 114 | $this->parameters[$name] = array($this->parameters[$name]); 115 | } 116 | 117 | $this->parameters[$name][] = $value; 118 | } else { 119 | $this->parameters[$name] = $value; 120 | } 121 | } 122 | 123 | public function get_parameter($name) 124 | { 125 | return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 126 | } 127 | 128 | public function get_parameters() 129 | { 130 | return $this->parameters; 131 | } 132 | 133 | public function unset_parameter($name) 134 | { 135 | unset($this->parameters[$name]); 136 | } 137 | 138 | /** 139 | * The request parameters, sorted and concatenated into a normalized string. 140 | * 141 | * @return string 142 | */ 143 | public function get_signable_parameters() 144 | { 145 | // Grab all parameters 146 | $params = $this->parameters; 147 | 148 | // Remove oauth_signature if present 149 | // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 150 | if (isset($params['oauth_signature'])) { 151 | unset($params['oauth_signature']); 152 | } 153 | 154 | return OAuthUtil::build_http_query($params); 155 | } 156 | 157 | /** 158 | * Returns the base string of this request 159 | * 160 | * The base string defined as the method, the url 161 | * and the parameters (normalized), each urlencoded 162 | * and the concatenated with &. 163 | */ 164 | public function get_signature_base_string() 165 | { 166 | $parts = array( 167 | $this->get_normalized_http_method(), 168 | $this->get_normalized_http_url(), 169 | $this->get_signable_parameters() 170 | ); 171 | 172 | $parts = OAuthUtil::urlencode_rfc3986($parts); 173 | 174 | return implode('&', $parts); 175 | } 176 | 177 | /** 178 | * Just uppercases the http method 179 | */ 180 | public function get_normalized_http_method() 181 | { 182 | return strtoupper($this->http_method); 183 | } 184 | 185 | /** 186 | * parses the url and rebuilds it to be 187 | * scheme://host/path 188 | */ 189 | public function get_normalized_http_url() 190 | { 191 | $parts = parse_url($this->http_url); 192 | 193 | $port = @$parts['port']; 194 | $scheme = $parts['scheme']; 195 | $host = $parts['host']; 196 | $path = @$parts['path']; 197 | 198 | $port or $port = ($scheme == 'https') ? '443' : '80'; 199 | 200 | if (($scheme == 'https' && $port != '443') || 201 | ($scheme == 'http' && $port != '80')) { 202 | 203 | $host = "$host:$port"; 204 | } 205 | return "$scheme://$host$path"; 206 | } 207 | 208 | /** 209 | * Builds a url usable for a GET request 210 | */ 211 | public function to_url() 212 | { 213 | $post_data = $this->to_postdata(); 214 | $out = $this->get_normalized_http_url(); 215 | if ($post_data) { 216 | $out .= '?'.$post_data; 217 | } 218 | return $out; 219 | } 220 | 221 | /** 222 | * Builds the data one would send in a POST request 223 | */ 224 | public function to_postdata() 225 | { 226 | return OAuthUtil::build_http_query($this->parameters); 227 | } 228 | 229 | /** 230 | * Builds the Authorization: header 231 | * 232 | * @throws OAuthException 233 | */ 234 | public function to_header() 235 | { 236 | $out ='Authorization: OAuth realm=""'; 237 | $total = array(); 238 | 239 | foreach ($this->parameters as $k => $v) { 240 | if (substr($k, 0, 5) != "oauth") continue; 241 | if (is_array($v)) { 242 | throw new OAuthException('Arrays not supported in headers'); 243 | } 244 | $out .= ',' . 245 | OAuthUtil::urlencode_rfc3986($k) . 246 | '="' . 247 | OAuthUtil::urlencode_rfc3986($v) . 248 | '"'; 249 | } 250 | return $out; 251 | } 252 | 253 | public function __toString() 254 | { 255 | return $this->to_url(); 256 | } 257 | 258 | public function sign_request($signature_method, $consumer, $token) 259 | { 260 | $this->set_parameter( 261 | "oauth_signature_method", 262 | $signature_method->get_name(), 263 | false 264 | ); 265 | 266 | $signature = $this->build_signature($signature_method, $consumer, $token); 267 | $this->set_parameter("oauth_signature", $signature, false); 268 | } 269 | 270 | public function build_signature($signature_method, $consumer, $token) 271 | { 272 | return $signature_method->build_signature($this, $consumer, $token); 273 | } 274 | 275 | /** 276 | * util function: current timestamp 277 | */ 278 | private static function generate_timestamp() 279 | { 280 | return time(); 281 | } 282 | 283 | /** 284 | * util function: current nonce 285 | */ 286 | private static function generate_nonce() 287 | { 288 | mt_srand((double)microtime()*10000);//optional for php 4.2.0 and up. 289 | 290 | $charid = strtoupper(md5(uniqid(rand(), true))); 291 | $hyphen = chr(45);// "-" 292 | $uuid = chr(123)// "{" 293 | .substr($charid, 0, 8).$hyphen 294 | .substr($charid, 8, 4).$hyphen 295 | .substr($charid,12, 4).$hyphen 296 | .substr($charid,16, 4).$hyphen 297 | .substr($charid,20,12) 298 | .chr(125);// "}" 299 | 300 | return $uuid; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/OAuth/OAuthServer.php: -------------------------------------------------------------------------------- 1 | data_store = $data_store; 16 | } 17 | 18 | public function add_signature_method($signature_method) 19 | { 20 | $this->signature_methods[$signature_method->get_name()] = $signature_method; 21 | } 22 | 23 | // high level functions 24 | 25 | /** 26 | * process a request_token request & 27 | * returns request token on success 28 | * 29 | * @param $request 30 | * @return string 31 | * @throws OAuthException 32 | */ 33 | public function fetch_request_token(&$request) 34 | { 35 | $this->get_version($request); 36 | 37 | $consumer = $this->get_consumer($request); 38 | 39 | // no token required for the initial token request 40 | $token = NULL; 41 | 42 | $this->check_signature($request, $consumer, $token); 43 | 44 | return $this->data_store->new_request_token($consumer); 45 | } 46 | 47 | /** 48 | * Process an access_token request & 49 | * returns access token on success 50 | * 51 | * @param $request 52 | * @return string 53 | * @throws OAuthException 54 | */ 55 | public function fetch_access_token(&$request) 56 | { 57 | $this->get_version($request); 58 | 59 | $consumer = $this->get_consumer($request); 60 | 61 | // requires authorized request token 62 | $token = $this->get_token($request, $consumer, "request"); 63 | 64 | $this->check_signature($request, $consumer, $token); 65 | 66 | return $this->data_store->new_access_token($token, $consumer); 67 | } 68 | 69 | /** 70 | * Verify an api call, checks all the parameters 71 | * 72 | * @param $request 73 | * @return array 74 | * @throws OAuthException 75 | */ 76 | public function verify_request(&$request) 77 | { 78 | $this->get_version($request); 79 | $consumer = $this->get_consumer($request); 80 | $token = $this->get_token($request, $consumer, "access"); 81 | $this->check_signature($request, $consumer, $token); 82 | 83 | return array($consumer, $token); 84 | } 85 | 86 | // Internals from here 87 | 88 | /** 89 | * Version 1 90 | * 91 | * @param $request 92 | * @return float 93 | * @throws OAuthException 94 | */ 95 | private function get_version(&$request) 96 | { 97 | $version = $request->get_parameter("oauth_version"); 98 | if (!$version) { 99 | $version = 1.0; 100 | } 101 | if ($version && $version != $this->version) { 102 | throw new OAuthException("OAuth version '$version' not supported"); 103 | } 104 | return $version; 105 | } 106 | 107 | /** 108 | * Figure out the signature with some defaults 109 | * @param $request 110 | * @return mixed 111 | * @throws OAuthException 112 | */ 113 | private function get_signature_method(&$request) 114 | { 115 | $signature_method = 116 | @$request->get_parameter("oauth_signature_method"); 117 | if (!$signature_method) { 118 | $signature_method = "PLAINTEXT"; 119 | } 120 | if (!in_array($signature_method, 121 | array_keys($this->signature_methods))) { 122 | throw new OAuthException( 123 | "Signature method '$signature_method' not supported " . 124 | "try one of the following: " . 125 | implode(", ", array_keys($this->signature_methods)) 126 | ); 127 | } 128 | 129 | return $this->signature_methods[$signature_method]; 130 | } 131 | 132 | /** 133 | * Try to find the consumer for the provided request's consumer key 134 | * @param $request 135 | * @noinspection PhpDocMissingReturnTagInspection 136 | * @throws OAuthException 137 | */ 138 | private function get_consumer(&$request) 139 | { 140 | $consumer_key = @$request->get_parameter("oauth_consumer_key"); 141 | if (!$consumer_key) { 142 | throw new OAuthException("Invalid consumer key"); 143 | } 144 | 145 | $consumer = $this->data_store->lookup_consumer($consumer_key); 146 | if (!$consumer) { 147 | throw new OAuthException("Invalid consumer"); 148 | } 149 | 150 | return $consumer; 151 | } 152 | 153 | /** 154 | * Try to find the token for the provided request's token key 155 | * 156 | * @param $request 157 | * @param $consumer 158 | * @param string $token_type 159 | * @throws OAuthException 160 | * @noinspection PhpDocMissingReturnTagInspection 161 | */ 162 | private function get_token(&$request, $consumer, $token_type="access") 163 | { 164 | $token_field = @$request->get_parameter('oauth_token'); 165 | $token = $this->data_store->lookup_token( 166 | $consumer, $token_type, $token_field 167 | ); 168 | if (!$token) { 169 | throw new OAuthException("Invalid $token_type token: $token_field"); 170 | } 171 | 172 | return $token; 173 | } 174 | 175 | /** 176 | * All-in-one function to check the signature on a request 177 | * should guess the signature method appropriately 178 | * 179 | * @param $request 180 | * @param $consumer 181 | * @param $token 182 | * @throws OAuthException 183 | */ 184 | private function check_signature(&$request, $consumer, $token) 185 | { 186 | // this should probably be in a different method 187 | $timestamp = @$request->get_parameter('oauth_timestamp'); 188 | $nonce = @$request->get_parameter('oauth_nonce'); 189 | 190 | $this->check_timestamp($timestamp); 191 | $this->check_nonce($consumer, $token, $nonce, $timestamp); 192 | 193 | $signature_method = $this->get_signature_method($request); 194 | 195 | $signature = $request->get_parameter('oauth_signature'); 196 | $valid_sig = $signature_method->check_signature( 197 | $request, 198 | $consumer, 199 | $token, 200 | $signature 201 | ); 202 | 203 | if (!$valid_sig) { 204 | throw new OAuthException("Invalid signature"); 205 | } 206 | } 207 | 208 | /** 209 | * Check that the timestamp is new enough 210 | * 211 | * @param $timestamp 212 | * @throws OAuthException 213 | */ 214 | private function check_timestamp($timestamp) 215 | { 216 | // verify that timestamp is recentish 217 | $now = time(); 218 | if ($now - $timestamp > $this->timestamp_threshold) { 219 | throw new OAuthException( 220 | "Expired timestamp, yours $timestamp, ours $now" 221 | ); 222 | } 223 | } 224 | 225 | /** 226 | * Check that the nonce is not repeated 227 | * 228 | * @param $consumer 229 | * @param $token 230 | * @param $nonce 231 | * @param $timestamp 232 | * @throws OAuthException 233 | */ 234 | private function check_nonce($consumer, $token, $nonce, $timestamp) 235 | { 236 | // verify that the nonce is uniqueish 237 | $found = $this->data_store->lookup_nonce( 238 | $consumer, 239 | $token, 240 | $nonce, 241 | $timestamp 242 | ); 243 | if ($found) { 244 | throw new OAuthException("Nonce already used: $nonce"); 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/OAuth/OAuthSignatureMethod.php: -------------------------------------------------------------------------------- 1 | build_signature($request, $consumer, $token); 17 | 18 | return $built == $signature; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OAuth/OAuthSignatureMethod_HMAC_SHA1.php: -------------------------------------------------------------------------------- 1 | get_signature_base_string(); 15 | $request->base_string = $base_string; 16 | 17 | $key_parts = array( 18 | $consumer->secret, 19 | ($token) ? $token->secret : "" 20 | ); 21 | 22 | $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 23 | $key = implode('&', $key_parts); 24 | 25 | return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OAuth/OAuthSignatureMethod_PLAINTEXT.php: -------------------------------------------------------------------------------- 1 | secret) 16 | ); 17 | 18 | $token ? 19 | array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret)) : 20 | array_push($sig, ''); 21 | 22 | $raw = implode("&", $sig); 23 | // for debug purposes 24 | $request->base_string = $raw; 25 | 26 | return OAuthUtil::urlencode_rfc3986($raw); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/OAuth/OAuthSignatureMethod_RSA_SHA1.php: -------------------------------------------------------------------------------- 1 | get_signature_base_string(); 52 | $request->base_string = $base_string; 53 | 54 | // Fetch the private key cert based on the request 55 | $cert = $this->fetch_private_cert($request); 56 | 57 | // Pull the private key ID from the certificate 58 | $privatekeyid = openssl_get_privatekey($cert); 59 | 60 | // Sign using the key 61 | $ok = openssl_sign($base_string, $signature, $privatekeyid); 62 | 63 | // Release the key resource 64 | openssl_free_key($privatekeyid); 65 | 66 | return base64_encode($signature); 67 | } 68 | 69 | /** 70 | * @param $request 71 | * @param $consumer 72 | * @param $token 73 | * @param $signature 74 | * @return bool 75 | * @throws Exception 76 | */ 77 | public function check_signature(&$request, $consumer, $token, $signature) 78 | { 79 | $decoded_sig = base64_decode($signature); 80 | 81 | $base_string = $request->get_signature_base_string(); 82 | 83 | // Fetch the public key cert based on the request 84 | $cert = $this->fetch_public_cert($request); 85 | 86 | // Pull the public key ID from the certificate 87 | $publickeyid = openssl_get_publickey($cert); 88 | 89 | // Check the computed signature against the one passed in the query 90 | $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 91 | 92 | // Release the key resource 93 | openssl_free_key($publickeyid); 94 | 95 | return $ok == 1; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/OAuth/OAuthToken.php: -------------------------------------------------------------------------------- 1 | key = $key; 22 | $this->secret = $secret; 23 | } 24 | 25 | /** 26 | * generates the basic string serialization of a token that a server 27 | * would respond to request_token and access_token calls with 28 | */ 29 | public function to_string() 30 | { 31 | return "oauth_token=" . 32 | OAuthUtil::urlencode_rfc3986($this->key) . 33 | "&oauth_token_secret=" . 34 | OAuthUtil::urlencode_rfc3986($this->secret); 35 | } 36 | 37 | public function __toString() 38 | { 39 | return $this->to_string(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OAuth/OAuthUtil.php: -------------------------------------------------------------------------------- 1 | 0) { 42 | $match = $matches[0]; 43 | $header_name = $matches[2][0]; 44 | $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; 45 | if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { 46 | $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); 47 | } 48 | $offset = $match[1] + strlen($match[0]); 49 | } 50 | 51 | if (isset($params['realm'])) { 52 | unset($params['realm']); 53 | } 54 | 55 | return $params; 56 | } 57 | 58 | // helper to try to sort out headers for people who aren't running apache 59 | public static function get_headers() 60 | { 61 | if (function_exists('apache_request_headers')) { 62 | // we need this to get the actual Authorization: header 63 | // because apache tends to tell us it doesn't exist 64 | return apache_request_headers(); 65 | } 66 | // otherwise we don't have apache and are just going to have to hope 67 | // that $_SERVER actually contains what we need 68 | $out = array(); 69 | foreach ($_SERVER as $key => $value) { 70 | if (substr($key, 0, 5) == "HTTP_") { 71 | // this is chaos, basically it is just there to capitalize the first 72 | // letter of every word that is not an initial HTTP and strip HTTP 73 | // code from przemek 74 | $key = str_replace( 75 | " ", 76 | "-", 77 | ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) 78 | ); 79 | $out[$key] = $value; 80 | } 81 | } 82 | return $out; 83 | } 84 | 85 | // This function takes a input like a=b&a=c&d=e and returns the parsed 86 | // parameters like this 87 | // array('a' => array('b','c'), 'd' => 'e') 88 | public static function parse_parameters( $input ) 89 | { 90 | if (!isset($input) || !$input) return array(); 91 | 92 | $pairs = explode('&', $input); 93 | $parsed_parameters = array(); 94 | 95 | foreach ($pairs as $pair) { 96 | $split = explode('=', $pair, 2); 97 | $parameter = OAuthUtil::urldecode_rfc3986($split[0]); 98 | $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; 99 | 100 | if (isset($parsed_parameters[$parameter])) { 101 | // We have already received parameter(s) with this name, so add to the list 102 | // of parameters with this name 103 | 104 | if (is_scalar($parsed_parameters[$parameter])) { 105 | // This is the first duplicate, so transform scalar (string) into an array 106 | // so we can add the duplicates 107 | $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); 108 | } 109 | 110 | $parsed_parameters[$parameter][] = $value; 111 | } else { 112 | $parsed_parameters[$parameter] = $value; 113 | } 114 | } 115 | return $parsed_parameters; 116 | } 117 | 118 | public static function build_http_query($params) 119 | { 120 | if (!$params) return ''; 121 | 122 | // Urlencode both keys and values 123 | $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); 124 | $values = OAuthUtil::urlencode_rfc3986(array_values($params)); 125 | $params = array_combine($keys, $values); 126 | 127 | // Parameters are sorted by name, using lexicographical byte value ordering. 128 | // Ref: Spec: 9.1.1 (1) 129 | uksort($params, 'strcmp'); 130 | 131 | $pairs = array(); 132 | 133 | foreach ($params as $parameter => $value) { 134 | if (is_array($value)) { 135 | // If two or more parameters share the same name, they are sorted by their value 136 | // Ref: Spec: 9.1.1 (1) 137 | natsort($value); 138 | foreach ($value as $duplicate_value) { 139 | $pairs[] = $parameter . '=' . $duplicate_value; 140 | } 141 | } else { 142 | $pairs[] = $parameter . '=' . $value; 143 | } 144 | } 145 | // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 146 | // Each name-value pair is separated by an '&' character (ASCII code 38) 147 | return implode('&', $pairs); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Payment.php: -------------------------------------------------------------------------------- 1 | $payment->amount = number_format($payment->amount, 2)); 21 | } 22 | 23 | /** 24 | * Modify payment 25 | * 26 | * @param $transaction 27 | * @return mixed 28 | */ 29 | public static function modify($transaction) 30 | { 31 | // Status will always detect a change and send notifications by IPN 32 | $payment = Payment::whereReference($transaction['pesapal_merchant_reference'])->first(); 33 | 34 | return $payment->update([ 35 | 'status' => $transaction['status'], 36 | 'payment_method' => $transaction['payment_method'], 37 | 'tracking_id' => $transaction['pesapal_transaction_tracking_id'], 38 | ]); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Pesapal.php: -------------------------------------------------------------------------------- 1 | token = NULL; 59 | $this->params = NULL; 60 | $this->consumerKey = config('pesapal.consumer_key'); 61 | $this->consumerSecret = config('pesapal.consumer_secret'); 62 | $this->consumer = new OAuthConsumer($this->consumerKey, $this->consumerSecret); 63 | $this->signatureMethod = $signature; 64 | $this->serverURL = config('pesapal.is_live') 65 | ? 'https://www.pesapal.com' 66 | : 'https://demo.pesapal.com'; 67 | $this->iframeLink = $this->serverURL . '/api/PostPesapalDirectOrderV4'; 68 | $this->callbackUrl = config('pesapal.callback_url'); 69 | } 70 | 71 | /** 72 | * Fetches the iframe source after passing payment parameters 73 | * 74 | * @param $request 75 | * @return OAuthRequest 76 | */ 77 | public function getIframeSource($request) 78 | { 79 | $parameterizedValue = ""; 80 | // Pesapal params 81 | isset($request['first_name']) ? $parameterizedValue .= "\" FirstName=\"".$request['first_name'] : null; 82 | isset($request['last_name']) ? $parameterizedValue .= "\" LastName=\"".$request['last_name'] : null; 83 | isset($request['email']) ? $parameterizedValue .= "\" Email=\"".$request['email'] : null; 84 | isset($request['phone_number']) ? $parameterizedValue .= "\" PhoneNumber=\"".$request['phone_number'] : null; 85 | 86 | $postXml = ""; 87 | $postXml = htmlentities($postXml); 88 | 89 | // Post transaction to PesaPal 90 | $iframeSrc = OAuthRequest::from_consumer_and_token($this->consumer, $this->token, "GET", $this->iframeLink, $this->params); 91 | $iframeSrc->set_parameter("oauth_callback", $this->callbackUrl); 92 | $iframeSrc->set_parameter("pesapal_request_data", $postXml); 93 | $iframeSrc->sign_request($this->signatureMethod, $this->consumer, $this->token); 94 | // Retrieve iframe source 95 | return $iframeSrc; 96 | } 97 | 98 | public function getTransactionDetails($merchantRef, $trackingId) 99 | { 100 | $url = $this->serverURL . '/API/QueryPaymentDetails'; 101 | 102 | $responseData = $this->getResponseData($merchantRef, $trackingId, $url); 103 | 104 | $pesapalResponse = explode(",", $responseData); 105 | 106 | return [ 107 | 'pesapal_transaction_tracking_id' => $pesapalResponse[0], 108 | 'payment_method' => $pesapalResponse[1], 109 | 'status' => $pesapalResponse[2], 110 | 'pesapal_merchant_reference' => $pesapalResponse[3], 111 | ]; 112 | } 113 | 114 | /** 115 | * Get payment status by merchant reference and tracking id 116 | * 117 | * @param $merchantRef 118 | * @param $trackingId 119 | * @return mixed|string 120 | */ 121 | public function statusByTrackingIdAndMerchantRef($merchantRef, $trackingId) 122 | { 123 | $url = $this->serverURL . '/API/QueryPaymentStatus'; 124 | 125 | return $this->getResponseData($merchantRef, $trackingId, $url); 126 | } 127 | 128 | /** 129 | * Get payment status by merchant reference 130 | * 131 | * @param $merchantReference 132 | * @return mixed|string 133 | */ 134 | public function statusByMerchantRef($merchantReference){ 135 | 136 | $url = $this->serverURL.'/API/QueryPaymentStatusByMerchantRef'; 137 | 138 | $requestStatus = $this->initRequestStatus($url); 139 | $requestStatus->set_parameter("pesapal_merchant_reference", $merchantReference); 140 | $requestStatus->sign_request($this->signatureMethod, $this->consumer, $this->token); 141 | 142 | return $this->curlRequest($requestStatus); 143 | } 144 | 145 | /** 146 | * Returns the response data when checking status or fetching payment details 147 | * 148 | * @param string $merchantReference 149 | * @param $trackingId 150 | * @param string $url 151 | * @return mixed|string 152 | */ 153 | private function getResponseData(string $merchantReference, $trackingId, string $url) 154 | { 155 | $requestStatus = $this->initRequestStatus($url); 156 | 157 | $requestStatus->set_parameter("pesapal_merchant_reference", $merchantReference); 158 | $requestStatus->set_parameter("pesapal_transaction_tracking_id",$trackingId); 159 | $requestStatus->sign_request($this->signatureMethod, $this->consumer, $this->token); 160 | 161 | return $this->curlRequest($requestStatus); 162 | } 163 | 164 | /** 165 | * Initialize request status 166 | * 167 | * @param $url 168 | * @return OAuthRequest 169 | */ 170 | private function initRequestStatus($url) 171 | { 172 | return OAuthRequest::from_consumer_and_token( 173 | $this->consumer, 174 | $this->token, 175 | 'GET', 176 | $url, 177 | $this->params 178 | ); 179 | } 180 | 181 | /** 182 | * Perform curl request to get the payment status 183 | * 184 | * @param $request_status 185 | * @return mixed|string 186 | */ 187 | private function curlRequest($request_status) 188 | { 189 | $ch = curl_init(); 190 | curl_setopt($ch, CURLOPT_URL, $request_status); 191 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 192 | curl_setopt($ch, CURLOPT_HEADER, 1); 193 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); 194 | 195 | if(defined('CURL_PROXY_REQUIRED')) if (CURL_PROXY_REQUIRED == 'True'){ 196 | $proxy_tunnel_flag = ( 197 | defined('CURL_PROXY_TUNNEL_FLAG') 198 | && strtoupper(CURL_PROXY_TUNNEL_FLAG) == 'FALSE' 199 | ) ? false : true; 200 | curl_setopt ($ch, CURLOPT_HTTPPROXYTUNNEL, $proxy_tunnel_flag); 201 | curl_setopt ($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); 202 | curl_setopt ($ch, CURLOPT_PROXY, CURL_PROXY_SERVER_DETAILS); 203 | } 204 | 205 | $response = curl_exec($ch); 206 | $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); 207 | $raw_header = substr($response, 0, $header_size - 4); 208 | $headerArray = explode("\r\n\r\n", $raw_header); 209 | $header = $headerArray[count($headerArray) - 1]; 210 | 211 | // Payment status 212 | $elements = preg_split("/=/",substr($response, $header_size)); 213 | 214 | curl_close($ch); 215 | return $elements[1]; 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/PesapalServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 22 | 23 | $this->publishes([ 24 | __DIR__.'/config/pesapal.php' => config_path('pesapal.php'), 25 | ], 'pesapal-config'); 26 | } 27 | 28 | // Load middleware alias 29 | $router = $this->app->make(Router::class); 30 | $router->aliasMiddleware('config', ValidateConfigMiddleware::class); 31 | // Resources 32 | $this->loadResources(); 33 | } 34 | 35 | /** 36 | * Register any application services. 37 | * 38 | * @return void 39 | */ 40 | public function register() 41 | { 42 | // 43 | } 44 | 45 | /** 46 | * Loads that are needed automatically 47 | */ 48 | private function loadResources() 49 | { 50 | // Boot facade 51 | $this->app->singleton('Pesapal', fn($app) => 52 | new \Bryceandy\Laravel_Pesapal\Pesapal(new OAuthSignatureMethod_HMAC_SHA1()) 53 | ); 54 | 55 | $this->loadRoutesFrom(__DIR__.'/routes/web.php'); 56 | $this->loadViewsFrom(__DIR__.'/resources/views', 'laravel_pesapal'); 57 | $this->loadMigrationsFrom(__DIR__ . '/database/migrations'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/config/pesapal.php: -------------------------------------------------------------------------------- 1 | env('PESAPAL_KEY'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Pesapal Consumer Secret 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The secret key obtained after creating your PesaPal demo or live account 23 | | When committing this to a repository, remove the default value and 24 | | put it into your online PESAPAL_SECRET configuration variable 25 | | 26 | */ 27 | 'consumer_secret' => env('PESAPAL_SECRET'), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Pesapal Account Type 32 | |-------------------------------------------------------------------------- 33 | | 34 | | true if your account was obtained from https://www.pesapal.com and 35 | | false if your account was obtained from https://demo.pesapal.com 36 | | 37 | */ 38 | 'is_live' => env('PESAPAL_IS_LIVE', false), 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Callback URL 43 | |-------------------------------------------------------------------------- 44 | | 45 | | This is the full url pointing to the page that the iframe 46 | | redirects to after processing the order on pesapal.com 47 | | 48 | */ 49 | 'callback_url' => env('PESAPAL_CALLBACK_URL'), 50 | ]; 51 | -------------------------------------------------------------------------------- /src/database/factories/PaymentFactory.php: -------------------------------------------------------------------------------- 1 | define(Payment::class, fn (Generator $faker) => [ 10 | 'first_name' => $faker->firstName, 11 | 'last_name' => $faker->lastName, 12 | 'phone_number' => $faker->numberBetween(254000000000, 256000000000), 13 | 'email' => $faker->safeEmail, 14 | 'amount' => $faker->randomNumber(null, true), 15 | 'currency' => $faker->randomElement(['TZS', 'KES', 'UGX', 'USD']), 16 | 'reference' => Str::random(7), 17 | 'description' => $faker->sentence, 18 | ]); 19 | -------------------------------------------------------------------------------- /src/database/migrations/2020_15_06_000000_create_pesapal_payments_table.php: -------------------------------------------------------------------------------- 1 | id(); 18 | $table->string('first_name')->nullable(); 19 | $table->string('last_name')->nullable(); 20 | $table->unsignedBigInteger('phone_number')->nullable(); 21 | $table->text('email')->nullable(); 22 | 23 | $table->text('amount'); 24 | $table->string('currency'); 25 | 26 | //the reference and description will also be recorded on your PesaPal dashboard 27 | $table->string('reference'); 28 | $table->text('description'); 29 | 30 | // Payment status i.e PENDING or COMPLETED etc... 31 | $table->text('status')->nullable(); 32 | 33 | //This is necessary when receiving IPN notifications 34 | $table->text('tracking_id')->nullable(); 35 | 36 | // Methods include Mpesa, TigoPesa, Visa, Mastercard, American Express etc... 37 | $table->string('payment_method')->nullable(); 38 | $table->timestamps(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | * 45 | * @return void 46 | */ 47 | public function down() 48 | { 49 | Schema::dropIfExists('pesapal_payments'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/resources/views/iframe.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Make Payment 9 | 10 | 11 | 12 | {{--This will load the payment module from PesaPal--}} 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | 'Bryceandy\Laravel_Pesapal\Http\Controllers'], function(){ 6 | 7 | Route::post('pesapal/iframe', 'PaymentController@store') 8 | ->name('payment.store') 9 | ->middleware('config'); 10 | 11 | Route::get('pesapal/iframe', 'PaymentController@store') 12 | ->name('payment.store.get') 13 | ->middleware('config'); 14 | }); 15 | -------------------------------------------------------------------------------- /tests/Feature/GetIframeSourceTest.php: -------------------------------------------------------------------------------- 1 | withoutExceptionHandling(); 19 | 20 | $payment = factory(Payment::class)->make(); 21 | 22 | $data = array_merge(['type' => Arr::random(['MERCHANT', 'ORDER'])], $payment->toArray()); 23 | 24 | $this->expectException(ConfigurationUnavailableException::class); 25 | 26 | $this->post('pesapal/iframe', $data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Feature/PaymentControllerTest.php: -------------------------------------------------------------------------------- 1 | set('pesapal.consumer_key', 'key'); 19 | $app['config']->set('pesapal.consumer_secret', 'secret'); 20 | $app['config']->set('pesapal.callback_url', 'http://testurl.com'); 21 | } 22 | 23 | /** @test */ 24 | public function required_attributes_should_be_validated_when_posting_payments() 25 | { 26 | $payment = factory(Payment::class)->make([ 27 | 'amount' => null, 28 | 'currency' => null, 29 | 'description' => null, 30 | 'type' => null, 31 | 'reference' => null, 32 | ]); 33 | 34 | $response = $this->json('POST','pesapal/iframe', $payment->toArray()); 35 | 36 | $response->assertStatus(422); 37 | 38 | $this->assertEquals($response['errors']["amount"][0], "The amount field is required."); 39 | 40 | $this->assertEquals($response['errors']["reference"][0], "The reference field is required."); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withFactories(__DIR__ . '/../src/database/factories'); 18 | } 19 | 20 | /** 21 | * @param Application $app 22 | */ 23 | protected function getEnvironmentSetUp($app) 24 | { 25 | $app['config']->set('database.default', 'testDb'); 26 | $app['config']->set('database.connections.testDb', [ 27 | 'driver' => 'sqlite', 28 | 'database' => ':memory:', 29 | ]); 30 | } 31 | 32 | /** 33 | * @param Application $app 34 | * @return array 35 | */ 36 | protected function getPackageProviders($app) 37 | { 38 | return [ 39 | PesapalServiceProvider::class, 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Unit/PaymentTest.php: -------------------------------------------------------------------------------- 1 | payment = factory(Payment::class)->create([ 34 | 'reference' => $reference, 35 | ]); 36 | 37 | $this->transaction = [ 38 | 'pesapal_merchant_reference' =>$reference, 39 | 'status' => Arr::random(['PENDING', 'COMPLETED', 'FAILED', 'CANCELLED']), 40 | 'payment_method' => Arr::random(['MPESA', 'VISA', 'MASTERCARD', 'TIGOPESA']), 41 | 'pesapal_transaction_tracking_id' => Str::random(), 42 | ]; 43 | } 44 | 45 | /** @test */ 46 | public function a_payment_can_be_created_with_a_factory() 47 | { 48 | $this->payment; 49 | 50 | $this->assertCount(1, Payment::all()); 51 | } 52 | 53 | /** @test */ 54 | public function payments_can_be_updated() 55 | { 56 | $payment = $this->payment; 57 | 58 | $payment->modify($this->transaction); 59 | 60 | $this->assertDatabaseHas('pesapal_payments', [ 61 | 'id' => $payment->id, 62 | 'status' => $this->transaction['status'], 63 | 'tracking_id' => $this->transaction['pesapal_transaction_tracking_id'], 64 | ]); 65 | } 66 | 67 | /** @test */ 68 | public function payment_amounts_should_be_formatted_and_in_two_decimal_points() 69 | { 70 | $payment = factory(Payment::class)->create([ 71 | 'amount' => 12345, 72 | ]); 73 | 74 | $this->assertEquals("12,345.00", $payment->amount); 75 | } 76 | } 77 | --------------------------------------------------------------------------------