.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Paynow Zimbabwe PHP SDK
2 |
3 | PHP SDK for Paynow Zimbabwe's API
4 |
5 | # Prerequisites
6 |
7 | This library has a set of prerequisites that must be met for it to work
8 |
9 | 1. PHP version 5.6 or higher
10 | 2. Curl extension
11 |
12 | # Installation
13 |
14 | Install the library using composer
15 |
16 | ```sh
17 | $ composer require paynow/php-sdk
18 | ```
19 |
20 | and include the composer autoloader
21 |
22 | ```php
23 | createPayment('Invoice 35', 'user@example.com');
61 | ```
62 |
63 | You can then start adding items to the payment
64 |
65 | ```php
66 | // Passing in the name of the item and the price of the item
67 | $payment->add('Bananas', 2.50);
68 | $payment->add('Apples', 3.40);
69 | ```
70 |
71 | When you're finally ready to send your payment to Paynow, you can use the `send` method in the `$paynow` object.
72 |
73 | ```php
74 | // Save the response from paynow in a variable
75 | $response = $paynow->send($payment);
76 | ```
77 |
78 | The response from Paynow will b have some useful information like whether the request was successful or not. If it was, for example, it contains the url to redirect the user so they can make the payment. You can view the full list of data contained in the response in our wiki
79 |
80 | If request was successful, you should consider saving the poll url sent from Paynow in the database
81 |
82 | ```php
83 | if($response->success()) {
84 | // Redirect the user to Paynow
85 | $response->redirect();
86 |
87 | // Or if you prefer more control, get the link to redirect the user to, then use it as you see fit
88 | $link = $response->redirectLink();
89 |
90 | // Get the poll url (used to check the status of a transaction). You might want to save this in your DB
91 | $pollUrl = $response->pollUrl();
92 | }
93 | ```
94 |
95 | ---
96 |
97 | > Mobile Transactions
98 |
99 | If you want to send an express (mobile) checkout request instead, the only thing that differs is the last step. You make a call to the `sendMobile` in the `$paynow` object
100 | instead of the `send` method.
101 |
102 | The `sendMobile` method unlike the `send` method takes in two additional arguments i.e The phone number to send the payment request to and the mobile money method to use for the request. **Note that currently only ecocash and onemoney are supported**
103 |
104 | ```php
105 | // Save the response from paynow in a variable
106 | $response = $paynow->sendMobile($payment, '077777777', 'ecocash');
107 | ```
108 |
109 | The response object is almost identical to the one you get if you send a normal request. With a few differences, firstly, you don't get a url to redirect to. Instead you instructions (which ideally should be shown to the user instructing them how to make payment on their mobile phone)
110 |
111 | ```php
112 | if($response->success()) {
113 | // Get the poll url (used to check the status of a transaction). You might want to save this in your DB
114 | $pollUrl = $response->pollUrl();
115 |
116 | // Get the instructions
117 | $instrutions = $response->instructions();
118 | }
119 | ```
120 |
121 | # Checking transaction status
122 |
123 | The SDK exposes a handy method that you can use to check the status of a transaction. Once you have instantiated the Paynow class.
124 |
125 | ```php
126 | // Check the status of the transaction with the specified pollUrl
127 | // Now you see why you need to save that url ;-)
128 | $status = $paynow->pollTransaction($pollUrl);
129 |
130 | if($status->paid()) {
131 | // Yay! Transaction was paid for
132 | } else {
133 | print("Why you no pay?");
134 | }
135 | ```
136 |
137 | # Full Usage Example
138 |
139 | ```php
140 | require_once('./paynow/vendor/autoload.php');
141 |
142 | $paynow = new Paynow\Payments\Paynow(
143 | 'INTEGRATION_ID',
144 | 'INTEGRATION_KEY',
145 | 'http://example.com/gateways/paynow/update',
146 |
147 | // The return url can be set at later stages. You might want to do this if you want to pass data to the return url (like the reference of the transaction)
148 | 'http://example.com/return?gateway=paynow'
149 | );
150 |
151 | # $paynow->setResultUrl('');
152 | # $paynow->setReturnUrl('');
153 |
154 | $payment = $paynow->createPayment('Invoice 35', 'melmups@outlook.com');
155 |
156 | $payment->add('Sadza and Beans', 1.25);
157 |
158 | $response = $paynow->send($payment);
159 |
160 |
161 | if($response->success()) {
162 | // Redirect the user to Paynow
163 | $response->redirect();
164 |
165 | // Or if you prefer more control, get the link to redirect the user to, then use it as you see fit
166 | $link = $response->redirectLink();
167 |
168 | $pollUrl = $response->pollUrl();
169 |
170 |
171 | // Check the status of the transaction
172 | $status = $paynow->pollTransaction($pollUrl);
173 |
174 | }
175 | ```
176 |
--------------------------------------------------------------------------------
/autoloader.php:
--------------------------------------------------------------------------------
1 | =5.6.3",
21 | "ext-mbstring": "*"
22 | },
23 | "require-dev": {
24 | "phpunit/phpunit": "7.1",
25 | "victorjonsson/markdowndocs": "^1.3"
26 | },
27 | "minimum-stability": "dev",
28 | "prefer-stable": true
29 | }
30 |
--------------------------------------------------------------------------------
/examples/callback.php:
--------------------------------------------------------------------------------
1 | Payment Status: %s || ", $status->status());
16 |
17 | $str .= sprintf("Transaction ID: %s || ", $status->reference());
18 | $str .= sprintf("Paynow Reference: %s \n\n", $status->paynowReference());
19 |
20 | file_put_contents(__DIR__ . '/status.logs', $str);
21 |
22 | }
23 |
24 | $paynow = new Paynow\Payments\Paynow(
25 | 'INTEGRATION_ID',
26 | 'INTEGRATION_KEY',
27 | 'http://d8403290.ngrok.io/paynow-demo-php/examples/index.php?paynow-return=true',
28 | 'http://d8403290.ngrok.io/paynow-demo-php/examples/callback.php'
29 | );
30 |
31 |
32 | $status = $paynow->processStatusUpdate();
33 |
34 |
35 | // Check if the transaction was paid
36 | if($status->paid()) {
37 |
38 | // Update transaction in DB maybe?
39 | $reference = $status->reference();
40 |
41 |
42 | // Get the reference of the Payment in paynow
43 | $paynowReference = $status->paynowReference();
44 |
45 | // Log out the data
46 | dummy_logger($status);
47 | }
--------------------------------------------------------------------------------
/examples/index.php:
--------------------------------------------------------------------------------
1 | createPayment('Order 3');
15 |
16 |
17 | $payment->add('Sadza and Cold Water', 12.2)
18 | ->add('Sadza and Hot Water', 20.5);
19 |
20 | // Optionally set a description for the order.
21 | // By default, a description is generated from the items
22 | // added to a payment
23 | $payment->setDescription("Mr Maposa's lunch order");
24 |
25 |
26 | // Initiate a Payment
27 | $response = $paynow->send($payment);
28 |
29 |
30 | ?>
31 |
32 |
33 | success): ?>
34 |
35 | An error occured while communicating with Paynow
36 | = $response->error ?>
37 |
38 |
39 |
40 | Click here to make payment of $= $payment->total ?>
41 |
42 |
43 |
44 |
45 |
46 |
49 |
--------------------------------------------------------------------------------
/examples/mobile.php:
--------------------------------------------------------------------------------
1 | createPayment('Order 3', 'testmerchant@mailinator.com');
14 |
15 |
16 | $payment->add('Sadza and Cold Water', 0.5)
17 | ->add('Sadza and Hot Water', 0.5);
18 |
19 | // Optionally set a description for the order.
20 | // By default, a description is generated from the items
21 | // added to a payment
22 | $payment->setDescription("Mr Maposa\'s lunch order");
23 |
24 |
25 | // Initiate a Payment
26 | $response = $paynow->sendMobile($payment, '0777832735', 'ecocash');
27 |
28 |
29 | ?>
30 |
31 |
32 | success): ?>
33 |
34 |
35 | An error occured while communicating with Paynow
36 | = $response->error ?>
37 |
38 |
39 |
40 |
41 | = $response->instructions() ?>
42 |
43 |
44 |
45 |
46 |
47 |
50 |
--------------------------------------------------------------------------------
/examples/status.logs:
--------------------------------------------------------------------------------
1 | Recieved updated from Paynow --> Payment Status: paid || Transaction ID: Order 3 || Paynow Reference: 9405
2 |
3 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests
14 |
15 |
16 |
17 |
18 |
19 | ./src
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/Core/CanFail.php:
--------------------------------------------------------------------------------
1 | errors[] = $error;
37 | }
38 | }
39 |
40 |
41 | /**
42 | * Get the errors sent by Paynow
43 | *
44 | * @param bool $pretty Boolean to indicate whether to get errors as string or as an array
45 | *
46 | * @return string|array
47 | */
48 | public function errors($pretty = true)
49 | {
50 | if(!$pretty) {
51 | return $this->errors;
52 | }
53 |
54 | return implode(' ', $this->errors);
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Core/Constants.php:
--------------------------------------------------------------------------------
1 | data = $response;
43 | $this->load();
44 | }
45 |
46 | /**
47 | * Reads through the response data sent from Paynow
48 | *
49 | * @throws InvalidIntegrationException
50 | */
51 | private function load()
52 | {
53 | if(arr_has($this->data,'status')) {
54 | $this->status = strtolower($this->data['status']);
55 | $this->success = $this->status === Constants::RESPONSE_OK;
56 | }
57 |
58 | if(!$this->success()) {
59 | if(arr_has($this->data, 'error')) {
60 | $this->fail(strtolower($this->data['error']));
61 | }
62 | }
63 | }
64 |
65 | public function instructions()
66 | {
67 | return arr_has($this->data, 'instructions') ? $this->data['instructions'] : '';
68 | }
69 |
70 |
71 | /**
72 | * Returns the poll URL sent from Paynow
73 | *
74 | * @return bool|string
75 | */
76 | public function pollUrl()
77 | {
78 | return arr_has($this->data, 'pollurl') ? $this->data['pollurl'] : '';
79 | }
80 |
81 | /**
82 | * Gets a boolean indicating whether a request succeeded or failed
83 | * @return mixed
84 | */
85 | public function success()
86 | {
87 | return $this->success;
88 | }
89 |
90 | /**
91 | * Returns the url the user should be taken to so they can make a payment
92 | *
93 | * @return bool|string
94 | */
95 | public function redirectUrl()
96 | {
97 | if(arr_has($this->data,'browserurl')) {
98 | return $this->data['browserurl'];
99 | }
100 |
101 | return false;
102 | }
103 |
104 | /**
105 | * Get the original data sent from Paynow
106 | *
107 | * @return array
108 | */
109 | public function data()
110 | {
111 | return $this->data;
112 | }
113 | }
--------------------------------------------------------------------------------
/src/Core/Logger.php:
--------------------------------------------------------------------------------
1 | data = $response;
23 | }
24 |
25 | /**
26 | * Get the original amount of the transaction
27 | *
28 | * @return float|mixed Returns the amount of the transaction, -1 if not available
29 | */
30 | public function amount()
31 | {
32 | return arr_has($this->data, 'amount') ? $this->data['amount'] : -1;
33 | }
34 |
35 | public function reference()
36 | {
37 | return arr_has($this->data, 'reference') ? $this->data['reference'] : '';
38 | }
39 |
40 | public function paynowReference()
41 | {
42 | return arr_has($this->data, 'paynowreference') ? $this->data['paynowreference'] : '';
43 | }
44 |
45 | /**
46 | * Get the status of the transaction
47 | *
48 | * @return mixed|string
49 | */
50 | public function paid()
51 | {
52 | return $this->status() === 'paid' ? true : false;
53 | }
54 |
55 | /**
56 | * Get the status of the transaction
57 | *
58 | * @return mixed|string
59 | */
60 | public function status()
61 | {
62 | return arr_has($this->data, 'status') ? strtolower($this->data['status']) : 'Unavailable';
63 | }
64 |
65 | /**
66 | * Get the original data sent from Paynow
67 | *
68 | * @return array
69 | */
70 | public function data()
71 | {
72 | return $this->data;
73 | }
74 | }
--------------------------------------------------------------------------------
/src/Http/Client.php:
--------------------------------------------------------------------------------
1 | logger = null;
24 | }
25 |
26 | /**
27 | * Executes an HTTP request
28 | *
29 | * @param RequestInfo $info
30 | * @return mixed
31 | * @throws ConnectionException
32 | *
33 | * @todo Do not parse response from execute function (SOLID). Clean up
34 | */
35 | public function execute($info)
36 | {
37 | //Initialize Curl Options
38 | $ch = curl_init($info->getUrl());
39 |
40 | curl_setopt($ch, CURLOPT_URL, $info->getUrl());
41 | //Determine Curl Options based on Method
42 | switch ($info->getMethod()) {
43 | case 'POST':
44 | curl_setopt($ch, CURLOPT_POST, true);
45 | curl_setopt($ch, CURLOPT_POSTFIELDS, $info->getData());
46 | break;
47 | break;
48 | case 'GET':
49 | curl_setopt($ch, CURLOPT_URL, sprintf('%s?%s', $info->getUrl(), $info->getData()));
50 | break;
51 | }
52 |
53 |
54 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
55 |
56 | //Execute Curl Request
57 | $result = curl_exec($ch);
58 |
59 |
60 | //Retrieve Response Status
61 | $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
62 |
63 | //Retry if Certificate Exception
64 | if (curl_errno($ch) == 60) {
65 | /** @noinspection SpellCheckingInspection */
66 | curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/cacert.pem');
67 | $result = curl_exec($ch);
68 | //Retrieve Response Status
69 | $httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
70 | }
71 | /** @noinspection SpellCheckingInspection */
72 |
73 | //Throw Exception if Retries and Certificates doenst work
74 | if (curl_errno($ch)) {
75 | $ex = new ConnectionException(
76 | $info->getUrl() . '\n' .
77 | curl_error($ch) . '\n' .
78 | curl_errno($ch)
79 | );
80 | curl_close($ch);
81 | throw $ex;
82 | }
83 | curl_close($ch);
84 | //More Exceptions based on HttpStatus Code
85 | if ($httpStatus < 200 || $httpStatus >= 300) {
86 | $ex = new ConnectionException(
87 | $info->getUrl() . '\n' .
88 | "Got Http response code $httpStatus when accessing {$info->getUrl()}." . '\n' .
89 | $httpStatus
90 | );
91 | throw $ex;
92 | }
93 | //Return result object
94 | $data = [];
95 | parse_str($result, $data);
96 |
97 | return $data;
98 | }
99 | }
--------------------------------------------------------------------------------
/src/Http/ConnectionException.php:
--------------------------------------------------------------------------------
1 | url = $url;
40 | $this->method = $method;
41 |
42 | $this->data = http_build_query($data);
43 | }
44 |
45 | /**
46 | * Gets the data for the http request as an http query string
47 | *
48 | * @return string
49 | */
50 | public function getData()
51 | {
52 | return $this->data;
53 | }
54 |
55 | /**
56 | * Get the url for the http request
57 | *
58 | * @return string
59 | */
60 | public function getUrl()
61 | {
62 | return $this->url;
63 | }
64 |
65 |
66 | /**
67 | * Get the method for the http request
68 | *
69 | * @return string
70 | */
71 | public function getMethod()
72 | {
73 | return $this->method;
74 | }
75 |
76 | /**
77 | * Create a new RequestInfo object
78 | *
79 | * @param string $url URL of the http request
80 | * @param string $method Data to be sent with the http request
81 | * @param array $data ata to be sent with the http request
82 | *
83 | * @return RequestInfo
84 | */
85 | public static function create($url, $method, $data = [])
86 | {
87 | if(!is_string($url) || empty($url) || !filter_var($url, FILTER_VALIDATE_URL))
88 | throw new \InvalidArgumentException('Invalid URL');
89 | if(!is_string($method) || empty($method))
90 | throw new \InvalidArgumentException('Invalid Method');
91 |
92 |
93 | return new static($url, $method, $data);
94 | }
95 | }
--------------------------------------------------------------------------------
/src/Payments/EmptyCartException.php:
--------------------------------------------------------------------------------
1 | _ref = $ref;
89 | }
90 |
91 | if(!is_null($authEmail) && !empty($authEmail)) {
92 | $this->_auth_email = $authEmail;
93 | }
94 | }
95 |
96 | /**
97 | * Add a new item to the list
98 | *
99 | * @param string|array $item
100 | * @param float|int $amount
101 | * @return void
102 | */
103 | public function add($item, $amount = null)
104 | {
105 | if (is_array($item) && count($item) > 1) {
106 | $this->parseItems($item);
107 |
108 | return $this;
109 | }
110 |
111 | if (!empty($item) && !empty($item)) {
112 | $this->pushItem($item, $amount);
113 | }
114 |
115 | return $this;
116 | }
117 |
118 | /**
119 | * Parse an array of items
120 | *
121 | * @param array $items
122 | */
123 | protected function parseItems(array $items)
124 | {
125 | foreach ($items as $item) {
126 | if (!is_array($item) || count($item) <> 2) {
127 | return;
128 | }
129 |
130 | if (!arr_has($item, 'title') || !arr_has($item, 'amount')) {
131 | return;
132 | }
133 |
134 | $this->pushItem($item);
135 | }
136 | }
137 |
138 | /**
139 | * Push an item to the list
140 | *
141 | * @param string $item The name of the item
142 | * @param float|int $amount The cost of the item
143 | */
144 | private function pushItem($item, $amount = null)
145 | {
146 | $this->_recalc = true;
147 | $this->_recache = true;
148 |
149 | if (is_array($item)) {
150 | $this->_items[] = $item;
151 |
152 | return;
153 | }
154 |
155 | $this->_items[] = [
156 | 'title' => $item,
157 | 'amount' => floatval($amount)
158 | ];
159 | }
160 |
161 | public function __get($name)
162 | {
163 | switch ($name)
164 | {
165 | case 'total':
166 | return ($this->_recalc) ? $this->computeTotal() : $this->_total;
167 |
168 | case 'count':
169 | return count($this->_items);
170 | case 'description':
171 | return ($this->_recache) ? $this->itemsDescription() : $this->_description;
172 | case 'ref':
173 | return $this->_ref;
174 | case 'auth_email':
175 | return $this->_auth_email;
176 |
177 | default:
178 | return null;
179 | }
180 | }
181 |
182 | public function computeTotal()
183 | {
184 | $total = 0;
185 |
186 | foreach ($this->_items as $item) {
187 | $total += $item['amount'];
188 | }
189 |
190 | $this->_total = $total;
191 | $this->_recalc = false;
192 |
193 | return $total;
194 | }
195 |
196 | public function setDescription($description)
197 | {
198 | $this->_ov_description = $description;
199 | $this->_override_description = true;
200 | }
201 |
202 | /**
203 | * Get the description
204 | *
205 | * @return void
206 | */
207 | public function itemsDescription()
208 | {
209 | if($this->_override_description) {
210 | return $this->_ov_description;
211 | }
212 |
213 | if(!$this->_recache) {
214 | return $this->_description;
215 | }
216 |
217 | $this->_description = '';
218 | for($i = 0; $i < count($this->_items); $i++) {
219 | $this->_description .= "{$this->_items[$i]['title']}, ";
220 | }
221 |
222 | return $this->_description;
223 | }
224 |
225 |
226 |
227 | /**
228 | * Convert the builder to an array
229 | *
230 | * @return void
231 | */
232 | public function toArray()
233 | {
234 | return [
235 | 'resulturl' => '',
236 | 'returnurl' => '',
237 | 'reference' => $this->_ref,
238 | 'amount' => $this->total,
239 | 'id' => '',
240 | 'additionalinfo' => $this->itemsDescription(),
241 | 'authemail' => '',
242 | 'status' => 'Message'
243 | ];
244 | }
245 | }
--------------------------------------------------------------------------------
/src/Payments/HashMismatchException.php:
--------------------------------------------------------------------------------
1 | _is_mobile = $mobile;
110 | $this->_ref = $ref;
111 | $this->_auth_email = $authEmail;
112 | $this->_phone = $phone;
113 | $this->_method = $method;
114 | }
115 |
116 | /**
117 | * Create an instance of the payment class (for mobile payments)
118 | *
119 | * @param string $ref
120 | * @param string $authEmail
121 | * @param string $phone The mobile phone making the payment
122 | * @param string $method The mobile money method
123 | *
124 | * @return void
125 | */
126 | public static function createMobile($ref, $authEmail, $phone, $method)
127 | {
128 | if (!isset($method)) {
129 | throw new InvalidArgumentException("The mobile money method should be specified");
130 | }
131 |
132 | return new static(true, $ref, $authEmail, $phone, $method);
133 | }
134 |
135 | /**
136 | * Create an instance of the payment class (for normal payments)
137 | *
138 | * @param string $ref
139 | * @param string $authEmail
140 | *
141 | * @return void
142 | */
143 | public static function create($ref, $authEmail = '')
144 | {
145 | return new static(false, $ref, $authEmail);
146 | }
147 |
148 | /**
149 | * Add a new item to the list
150 | *
151 | * @param string|array $item
152 | * @param float|int $amount
153 | * @return void
154 | */
155 | public function add($item, $amount = null)
156 | {
157 | if (is_array($item) && count($item) > 1) {
158 | $this->parseItems($item);
159 |
160 | return $this;
161 | }
162 |
163 | if (!empty($item) && !empty($item)) {
164 | $this->pushItem($item, $amount);
165 | }
166 |
167 | return $this;
168 | }
169 |
170 | /**
171 | * Parse an array of items
172 | *
173 | * @param array $items
174 | */
175 | protected function parseItems(array $items)
176 | {
177 | foreach ($items as $item) {
178 | if (!is_array($item) || count($item) <> 2) {
179 | return;
180 | }
181 |
182 | if (!arr_has($item, 'title') || !arr_has($item, 'amount')) {
183 | return;
184 | }
185 |
186 | $this->pushItem($item);
187 | }
188 | }
189 |
190 | /**
191 | * Push an item to the list
192 | *
193 | * @param string $item The name of the item
194 | * @param float|int $amount The cost of the item
195 | */
196 | private function pushItem($item, $amount = null)
197 | {
198 | $this->_recalc = true;
199 | $this->_recache = true;
200 |
201 | if (is_array($item)) {
202 | $this->_items[] = $item;
203 |
204 | return;
205 | }
206 |
207 | $this->_items[] = [
208 | 'title' => $item,
209 | 'amount' => floatval($amount)
210 | ];
211 | }
212 |
213 | public function __get($name)
214 | {
215 | switch ($name)
216 | {
217 | case 'total':
218 | return ($this->_recalc) ? $this->computeTotal() : $this->_total;
219 |
220 | case 'count':
221 | return count($this->_items);
222 |
223 | case 'description':
224 | return ($this->_recache) ? $this->itemsDescription() : $this->_description;
225 |
226 | case 'ref':
227 | return $this->_ref;
228 |
229 | case 'auth_email':
230 | return $this->_auth_email;
231 |
232 | case 'method':
233 | return $this->_method;
234 |
235 | case 'phone':
236 | return $this->_phone;
237 |
238 | case 'is_mobile':
239 | return $this->_is_mobile;
240 |
241 |
242 | case 'authEmail':
243 | return $this->_auth_email;
244 |
245 |
246 |
247 | default:
248 | return null;
249 | }
250 | }
251 |
252 | /**
253 | * Calculate the total of the items in the 'cart'
254 | *
255 | * @return void
256 | */
257 | public function computeTotal()
258 | {
259 | $total = 0;
260 |
261 | foreach ($this->_items as $item) {
262 | $total += $item['amount'];
263 | }
264 |
265 | $this->_total = $total;
266 | $this->_recalc = false;
267 |
268 | return $total;
269 | }
270 |
271 | /**
272 | * Sets the description for the transaction
273 | *
274 | * @param string $description
275 | * @return void
276 | */
277 | public function setDescription($description)
278 | {
279 | $this->_ov_description = $description;
280 | $this->_override_description = true;
281 | }
282 |
283 | /**
284 | * Get the description for the items in the cart
285 | *
286 | * @return void
287 | */
288 | public function itemsDescription()
289 | {
290 | if($this->_override_description) {
291 | return $this->_ov_description;
292 | }
293 |
294 | if(!$this->_recache) {
295 | return $this->_description;
296 | }
297 |
298 | $this->_description = '';
299 | for($i = 0; $i < count($this->_items); $i++) {
300 | $this->_description .= "{$this->_items[$i]['title']}, ";
301 | }
302 |
303 | return $this->_description;
304 | }
305 |
306 |
307 |
308 | /**
309 | * Convert the builder to an array
310 | *
311 | * @return void
312 | */
313 | public function toArray()
314 | {
315 | return [
316 | 'resulturl' => '',
317 | 'returnurl' => '',
318 | 'reference' => $this->_ref,
319 | 'amount' => $this->total,
320 | 'id' => '',
321 | 'additionalinfo' => $this->itemsDescription(),
322 | 'authemail' => '',
323 | 'status' => 'Message'
324 | ];
325 | }
326 | }
--------------------------------------------------------------------------------
/src/Payments/Paynow.php:
--------------------------------------------------------------------------------
1 | client = new Client();
52 |
53 | $this->integrationId = $id;
54 | $this->integrationKey = strtolower($key);
55 | $this->returnUrl = $returnUrl;
56 | $this->resultUrl = $resultUrl;
57 | }
58 |
59 | /**
60 | * @param string|null $ref Transaction reference
61 | * @param string|null $authEmail The email of the person making payment
62 | *
63 | * @return FluentBuilder
64 | */
65 | public function createPayment($ref, $authEmail)
66 | {
67 | return new FluentBuilder($ref, $authEmail);
68 | }
69 |
70 |
71 | /**
72 | * Send a transaction to Paynow
73 | *
74 | * @param FluentBuilder|array $builder
75 | *
76 | * @throws HashMismatchException
77 | * @throws \Paynow\Http\ConnectionException
78 | * @throws \Paynow\Payments\EmptyCartException
79 | * @throws \Paynow\Payments\EmptyTransactionReferenceException
80 | * @throws InvalidIntegrationException
81 | * @throws InvalidUrlException
82 | *
83 | * @return InitResponse
84 | */
85 | public function send($builder)
86 | {
87 | if(is_null($this->returnUrl) || is_null($this->returnUrl)) {
88 | throw new InvalidUrlException();
89 | }
90 |
91 | if(is_array($builder)) {
92 | $builder = $this->createBuilder($builder);
93 | }
94 |
95 | if (is_null($builder->ref)) {
96 | throw new EmptyTransactionReferenceException($builder);
97 | }
98 |
99 | if ($builder->count == 0) {
100 | throw new EmptyCartException($builder);
101 | }
102 |
103 | return $this->init($builder);
104 | }
105 |
106 | /**
107 | * Create an instance of the fluent builder from the provided array of items
108 | *
109 | * @param array $items
110 | * @return void
111 | */
112 | private function createBuilder($items)
113 | {
114 | if(!isset($items['reference'], $items['amount'])) {
115 | throw new InvalidArgumentException("Payment array should have the following keys: reference, total");
116 | }
117 |
118 | $description = isset($items['description']) ? $items['description'] : "Payment";
119 |
120 | $builder = new FluentBuilder($description, $items['reference'], $items['amount']);
121 | $builder->setDescription($description);
122 |
123 | return $builder;
124 | }
125 |
126 | /**
127 | * Send a mobile transaction
128 | *
129 | * @param $phone
130 | * @param FluentBuilder $builder
131 | *
132 | * @return InitResponse
133 | *
134 | * @throws HashMismatchException
135 | * @throws NotImplementedException
136 | * @throws InvalidIntegrationException
137 | * @throws \Paynow\Http\ConnectionException
138 | */
139 | public function sendMobile(FluentBuilder $builder, $phone, $method)
140 | {
141 | if (is_null($builder->ref)) {
142 | throw new EmptyTransactionReferenceException($builder);
143 | }
144 |
145 | if ($builder->count == 0) {
146 | throw new EmptyCartException($builder);
147 | }
148 |
149 | return $this->initMobile($builder, $phone, $method);
150 | }
151 |
152 | /**
153 | * Initiate a new Paynow transaction
154 | *
155 | * @param FluentBuilder $builder The transaction to be sent to Paynow
156 | *
157 | * @throws HashMismatchException
158 | * @throws InvalidIntegrationException
159 | * @throws InvalidIntegrationException
160 | * @throws \Paynow\Http\ConnectionException
161 | *
162 | * @return InitResponse The response from Paynow
163 | */
164 | protected function init(FluentBuilder $builder)
165 | {
166 | $request = $this->formatInit($builder);
167 |
168 | $response = $this->client->execute($request);
169 |
170 | if (arr_has($response, 'hash')) {
171 | if (!Hash::verify($response, $this->integrationKey)) {
172 | throw new HashMismatchException();
173 | }
174 | }
175 |
176 | return new InitResponse($response);
177 | }
178 |
179 | /**
180 | * Initiate a new Paynow transaction
181 | *
182 | * @param FluentBuilder $builder The transaction to be sent to Paynow
183 | * @param string $phone The user's phone number
184 | * @param string $method The mobile transaction method i.e ecocash, telecash
185 | *
186 | * @throws HashMismatchException
187 | * @throws NotImplementedException
188 | * @throws InvalidIntegrationException
189 | * @throws \Paynow\Http\ConnectionException
190 | *
191 | * @note Only ecocash is currently supported
192 | *
193 | * @return InitResponse The response from Paynow
194 | */
195 | protected function initMobile(FluentBuilder $builder, $phone, $method)
196 | {
197 | if (!isset($method)) {
198 | throw new InvalidArgumentException("The mobile money method should be specified");
199 | }
200 |
201 | $request = $this->formatInitMobile($builder, $phone, $method);
202 |
203 | if(!$builder->auth_email || !filter_var($builder->auth_email, FILTER_VALIDATE_EMAIL)) {
204 | throw new InvalidArgumentException('Auth email is required for mobile transactions. When creating a mobile payment, please make sure you pass the auth email as the second parameter to the createPayment method');
205 | }
206 |
207 | $response = $this->client->execute($request);
208 |
209 | if (arr_has($response, 'hash')) {
210 | if (!Hash::verify($response, $this->integrationKey)) {
211 | throw new HashMismatchException();
212 | }
213 | }
214 |
215 | return new InitResponse($response);
216 | }
217 |
218 |
219 | /**
220 | * Format a request before it's sent to Paynow
221 | *
222 | * @param FluentBuilder $builder The transaction to send to Paynow
223 | *
224 | * @return RequestInfo The formatted transaction
225 | */
226 | private function formatInit(FluentBuilder $builder)
227 | {
228 | $items = $builder->toArray();
229 | $items['resulturl'] = $this->resultUrl;
230 | $items['returnurl'] = $this->returnUrl;
231 | $items['id'] = $this->integrationId;
232 | $items['authemail'] = $builder->auth_email;
233 |
234 | foreach ($items as $key => $item) {
235 | $items[$key] = trim(mb_convert_encoding($item, 'UTF-8', 'ISO-8859-1'));
236 | }
237 |
238 | $items['hash'] = Hash::make($items, $this->integrationKey);
239 |
240 | return RequestInfo::create(Constants::URL_INITIATE_TRANSACTION, 'POST', $items);
241 | }
242 |
243 | /**
244 | * Format a request before it's sent to Paynow
245 | *
246 | * @param FluentBuilder $builder The transaction to send to Paynow
247 | *
248 | * @param string $phone The mobile phone making the payment
249 | * @param string $method The mobile money method
250 | *
251 | * @return RequestInfo The formatted transaction
252 | */
253 | private function formatInitMobile(FluentBuilder $builder, $phone, $method)
254 | {
255 | $items = $builder->toArray();
256 |
257 | $items['resulturl'] = $this->resultUrl;
258 | $items['returnurl'] = $this->returnUrl;
259 | $items['id'] = $this->integrationId;
260 | $items['phone'] = $phone;
261 | $items['method'] = $method;
262 | $items['authemail'] = $builder->auth_email;
263 |
264 | foreach ($items as $key => $item) {
265 | $items[$key] = trim(mb_convert_encoding($item, 'UTF-8', 'ISO-8859-1'));
266 | }
267 |
268 | $items['hash'] = Hash::make($items, strtolower($this->integrationKey));
269 |
270 |
271 | return RequestInfo::create(Constants::URL_INITIATE_MOBILE_TRANSACTION, 'POST', $items);
272 | }
273 |
274 | /**
275 | * Get the merchant's return url
276 | * @return string
277 | */
278 | public function getReturnUrl()
279 | {
280 | return $this->returnUrl;
281 | }
282 |
283 | /**
284 | * Sets the merchant's return url
285 | *
286 | * @param string $returnUrl
287 | */
288 | public function setReturnUrl($returnUrl)
289 | {
290 | $this->returnUrl = $returnUrl;
291 | }
292 |
293 | /**
294 | * Check the status of a transaction
295 | *
296 | * @param $url
297 | *
298 | * @throws \Paynow\Http\ConnectionException
299 | * @throws HashMismatchException
300 | *
301 | * @return StatusResponse
302 | */
303 | public function pollTransaction($url)
304 | {
305 | $response = $this->client->execute(RequestInfo::create(trim($url), 'METHOD', []));
306 |
307 | if (arr_has($response, 'hash')) {
308 | if (!Hash::verify($response, $this->integrationKey)) {
309 | throw new HashMismatchException();
310 | }
311 | }
312 |
313 | return new StatusResponse($response);
314 | }
315 |
316 | /**
317 | * Process a status update from Paynow
318 | *
319 | * @return StatusResponse
320 | * @throws HashMismatchException
321 | */
322 | public function processStatusUpdate()
323 | {
324 | $data = $_POST;
325 | if (!arr_has($data, 'hash') || !Hash::verify($data, $this->integrationKey)) {
326 | throw new HashMismatchException();
327 | }
328 |
329 | return new StatusResponse($data);
330 | }
331 |
332 | /**
333 | * Get the result url for the merchant
334 | *
335 | * @return string
336 | */
337 | public function getResultUrl()
338 | {
339 | return $this->resultUrl;
340 | }
341 |
342 | /**
343 | * Sets the merchant's result url
344 | *
345 | * @param string $resultUrl
346 | */
347 | public function setResultUrl($resultUrl)
348 | {
349 | $this->resultUrl = $resultUrl;
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/src/Util/Hash.php:
--------------------------------------------------------------------------------
1 | $value) {
11 | if( strtoupper($key) != "HASH" ){
12 | $string .= $value;
13 | }
14 | }
15 | $string .= $integration_key;
16 |
17 | $hash = hash("sha512", $string);
18 |
19 | return strtoupper($hash);
20 | }
21 |
22 | public static function verify($values, $key)
23 | {
24 | return self::make($values, $key) === $values['hash'];
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/helper.php:
--------------------------------------------------------------------------------
1 | createPayment([
22 | ['title' => 'Candles', 'amount' => 1.5],
23 | ['title' => 'Sandwich', 'amount' => 2],
24 | ['title' => 'Bacon', 'amount' => 4],
25 | ]);
26 |
27 | $this->assertEquals(3, $payment->count);
28 | }
29 |
30 | public function testBuilderCanComputeTotalOfItems()
31 | {
32 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
33 |
34 |
35 | $payment = $paynow->createPayment([
36 | ['title' => 'Candles', 'amount' => 1.5],
37 | ['title' => 'Sandwich', 'amount' => 2],
38 | ['title' => 'Bacon', 'amount' => 4],
39 | ]);
40 |
41 | $this->assertEquals(7.5, $payment->total);
42 | }
43 |
44 | public function testBuilderCanAddItemsFluentsAfterInit()
45 | {
46 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
47 |
48 |
49 | $payment = $paynow->createPayment([
50 | ['title' => 'Candles', 'amount' => 1.5],
51 | ['title' => 'Sandwich', 'amount' => 2],
52 | ['title' => 'Bacon', 'amount' => 4],
53 | ]);
54 |
55 | $payment->add('Tomatoes', 3);
56 | $payment->add('Pork', 12);
57 | $payment->add('Apple Pie', 2);
58 |
59 |
60 | $this->assertEquals(6, $payment->count);
61 | }
62 |
63 | public function testBuilderCanAddItemsFluently()
64 | {
65 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
66 |
67 |
68 | $payment = $paynow->createPayment();
69 |
70 | $payment
71 | ->add('Green Beans', 3)
72 | ->add('Tomatoes', 3)
73 | ->add('Pork', 12)
74 | ->add('Apple Pie', 2);
75 |
76 |
77 | $this->assertEquals(4, $payment->count);
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/tests/HttpRequestTest.php:
--------------------------------------------------------------------------------
1 | execute(RequestInfo::create('http://localhost/client/', 'GET', []));
19 |
20 | $this->assertEquals('success', $data);
21 | }
22 |
23 | public function testCanSendHttpRequestWithOneArgument(): void
24 | {
25 | $new = new \Paynow\Http\Client(new \Paynow\Core\Logger());
26 |
27 | $data = $new->execute(RequestInfo::create('http://localhost/client/', 'GET', ['json' => 'true']));
28 |
29 | $json = json_decode($data);
30 |
31 | $this->assertTrue(!is_null($json));
32 | }
33 |
34 | public function testCanSendHttpRequestWithMultipleArguments(): void
35 | {
36 | $new = new \Paynow\Http\Client(new \Paynow\Core\Logger());
37 |
38 | $data = $new->execute(RequestInfo::create('http://localhost/client/', 'GET', ['json' => 'true', 'fruits' => 'true']));
39 |
40 | $json = json_decode($data);
41 |
42 | $this->assertTrue(is_array($json) && count($json) == 6);
43 | }
44 |
45 | public function testCanSendPostHttpRequest(): void
46 | {
47 | $new = new \Paynow\Http\Client(new \Paynow\Core\Logger());
48 |
49 | $data = $new->execute(RequestInfo::create('http://localhost/client/', 'POST', []));
50 |
51 | $this->assertEquals('Yatta!', $data);
52 | }
53 |
54 | public function testCanSendPostHttpRequestWithOneArgument(): void
55 | {
56 | $new = new \Paynow\Http\Client(new \Paynow\Core\Logger());
57 |
58 | $data = $new->execute(RequestInfo::create('http://localhost/client/', 'POST', ['json' => 'true']));
59 |
60 | $this->assertEquals('JSON!!!', $data);
61 | }
62 |
63 | public function testCanSendPostHttpRequestWithMultipleArguments(): void
64 | {
65 | $new = new \Paynow\Http\Client(new \Paynow\Core\Logger());
66 |
67 | $data = $new->execute(RequestInfo::create('http://localhost/client/', 'POST', ['fruits' => 'true']));
68 |
69 | $this->assertEquals('FRUITY JSON!!!', $data);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/PayNowTest.php:
--------------------------------------------------------------------------------
1 | createPayment();
21 |
22 | $this->assertTrue($payment instanceof \Paynow\Payments\FluentBuilder);
23 | }
24 |
25 | public function testSendNoReferenceThrowsEmptyTransactionReferenceException()
26 | {
27 | $this->expectException(\Paynow\Payments\EmptyTransactionReferenceException::class);
28 |
29 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
30 |
31 | $payment = $paynow->createPayment([
32 | ['title' => 'Candles', 'amount' => 1.5],
33 | ['title' => 'Sandwich', 'amount' => 2],
34 | ['title' => 'Bacon', 'amount' => 4],
35 | ]);
36 |
37 |
38 | $response = $paynow->send($payment);
39 | }
40 |
41 | public function testSendNoItemsThrowsEmptyCartException()
42 | {
43 | $this->expectException(\Paynow\Payments\EmptyCartException::class);
44 |
45 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
46 |
47 | $payment = $paynow->createPayment(null, 10092);
48 |
49 |
50 | $response = $paynow->send($payment);
51 |
52 | $this->assertInstanceOf(\Paynow\Core\InitResponse::class, $response);
53 | }
54 |
55 | public function testSendReturnsInitResponse()
56 | {
57 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
58 |
59 | $payment = $paynow->createPayment([
60 | ['title' => 'Candles', 'amount' => 1.5],
61 | ['title' => 'Sandwich', 'amount' => 2],
62 | ['title' => 'Bacon', 'amount' => 4],
63 | ], '0B921');
64 |
65 | $response = $paynow->send($payment);
66 |
67 | $this->assertInstanceOf(\Paynow\Core\InitResponse::class, $response);
68 | }
69 |
70 | public function testSendThrowsInvalidIntegrationExceptionIfNoOrWrongIdOrIntegrationKey()
71 | {
72 | $this->expectException(\Paynow\Payments\InvalidIntegrationException::class);
73 |
74 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
75 |
76 | $payment = $paynow->createPayment([
77 | ['title' => 'Candles', 'amount' => 1.5],
78 | ['title' => 'Sandwich', 'amount' => 2],
79 | ['title' => 'Bacon', 'amount' => 4],
80 | ], '0B921');
81 |
82 | $response = $paynow->send($payment);
83 | }
84 |
85 |
86 | public function testSend()
87 | {
88 | $this->expectException(\Paynow\Payments\InvalidIntegrationException::class);
89 |
90 | $paynow = new Paynow(new \Paynow\Http\Client(), '', '');
91 |
92 | $payment = $paynow->createPayment([
93 | ['title' => 'Candles', 'amount' => 1.5],
94 | ['title' => 'Sandwich', 'amount' => 2],
95 | ['title' => 'Bacon', 'amount' => 4],
96 | ], '0B921');
97 |
98 | $response = $paynow->send($payment);
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/autoload.php:
--------------------------------------------------------------------------------
1 | addPsr4("Paynow\\", dirname(__DIR__) . '/src/', true);
6 | $classLoader->add('', dirname(__DIR__) . '/src/helper.php');
7 | $classLoader->register();
--------------------------------------------------------------------------------