├── .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 | [](https://github.com/bryceandy/laravel_pesapal/actions)
4 |
5 |
6 |
7 |
8 |
9 | This package enables Laravel developers to easily make use of the [Pesapal](https://www.pesapal.com) API.
10 |
11 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------