├── .gitignore
├── Dockerfile
├── Paycom
├── Application.php
├── Database.php
├── Format.php
├── Merchant.php
├── Order.php
├── PaycomException.php
├── Request.php
├── Response.php
└── Transaction.php
├── README.md
├── composer.json
├── docker-compose.yml
├── functions.php
├── index.php
├── password.paycom
├── paycom.config.sample.php
└── vendor
├── autoload.php
└── composer
├── ClassLoader.php
├── LICENSE
├── autoload_classmap.php
├── autoload_namespaces.php
├── autoload_psr4.php
├── autoload_real.php
└── autoload_static.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | paycom.config.php
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7-apache
2 |
3 | SHELL ["/bin/bash", "-c"]
4 |
5 | # Enable PDO, PDO_MySQL extensions
6 | RUN docker-php-ext-install pdo pdo_mysql
7 |
8 | # Install the Composer
9 | RUN set -eux \
10 | && apt-get update \
11 | && apt-get install --no-install-recommends -y wget mc \
12 | && rm -r /var/lib/apt/lists/* \
13 | && EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) \
14 | && php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
15 | && ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") \
16 | && SIGNATURE_VERIFIED=$(php -r "echo ('${ACTUAL_SIGNATURE}' === '${EXPECTED_SIGNATURE}');") \
17 | && if [[ "$SIGNATURE_VERIFIED" == "1" ]]; then \
18 | php composer-setup.php --quiet; \
19 | fi; \
20 | rm composer-setup.php
21 |
--------------------------------------------------------------------------------
/Paycom/Application.php:
--------------------------------------------------------------------------------
1 | merchant_id, login, keyFile keys.
15 | */
16 | public function __construct($config)
17 | {
18 | $this->config = $config;
19 | $this->request = new Request();
20 | $this->response = new Response($this->request);
21 | $this->merchant = new Merchant($this->config);
22 | }
23 |
24 | /**
25 | * Authorizes session and handles requests.
26 | */
27 | public function run()
28 | {
29 | try {
30 | // authorize session
31 | $this->merchant->Authorize($this->request->id);
32 |
33 | // handle request
34 | switch ($this->request->method) {
35 | case 'CheckPerformTransaction':
36 | $this->CheckPerformTransaction();
37 | break;
38 | case 'CheckTransaction':
39 | $this->CheckTransaction();
40 | break;
41 | case 'CreateTransaction':
42 | $this->CreateTransaction();
43 | break;
44 | case 'PerformTransaction':
45 | $this->PerformTransaction();
46 | break;
47 | case 'CancelTransaction':
48 | $this->CancelTransaction();
49 | break;
50 | case 'ChangePassword':
51 | $this->ChangePassword();
52 | break;
53 | case 'GetStatement':
54 | $this->GetStatement();
55 | break;
56 | default:
57 | $this->response->error(
58 | PaycomException::ERROR_METHOD_NOT_FOUND,
59 | 'Method not found.',
60 | $this->request->method
61 | );
62 | break;
63 | }
64 | } catch (PaycomException $exc) {
65 | $exc->send();
66 | }
67 | }
68 |
69 | private function CheckPerformTransaction()
70 | {
71 | $order = new Order($this->request->id);
72 | $order->find($this->request->params['account']);
73 |
74 | // validate parameters
75 | $order->validate($this->request->params);
76 |
77 | // todo: Check is there another active or completed transaction for this order
78 | $transaction = new Transaction();
79 | $found = $transaction->find($this->request->params);
80 | if ($found && ($found->state == Transaction::STATE_CREATED || $found->state == Transaction::STATE_COMPLETED)) {
81 | $this->response->error(
82 | PaycomException::ERROR_COULD_NOT_PERFORM,
83 | 'There is other active/completed transaction for this order.'
84 | );
85 | }
86 |
87 | // if control is here, then we pass all validations and checks
88 | // send response, that order is ready to be paid.
89 | $this->response->send(['allow' => true]);
90 | }
91 |
92 | private function CheckTransaction()
93 | {
94 | // todo: Find transaction by id
95 | $transaction = new Transaction();
96 | $found = $transaction->find($this->request->params);
97 | if (!$found) {
98 | $this->response->error(
99 | PaycomException::ERROR_TRANSACTION_NOT_FOUND,
100 | 'Transaction not found.'
101 | );
102 | }
103 |
104 | // todo: Prepare and send found transaction
105 | $this->response->send([
106 | 'create_time' => Format::datetime2timestamp($found->create_time),
107 | 'perform_time' => Format::datetime2timestamp($found->perform_time),
108 | 'cancel_time' => Format::datetime2timestamp($found->cancel_time),
109 | 'transaction' => $found->id,
110 | 'state' => $found->state,
111 | 'reason' => isset($found->reason) ? 1 * $found->reason : null,
112 | ]);
113 | }
114 |
115 | private function CreateTransaction()
116 | {
117 | $order = new Order($this->request->id);
118 | $order->find($this->request->params['account']);
119 |
120 | // validate parameters
121 | $order->validate($this->request->params);
122 |
123 | // todo: Check, is there any other transaction for this order/service
124 | $transaction = new Transaction();
125 | $found = $transaction->find(['account' => $this->request->params['account']]);
126 | if ($found) {
127 | if (($found->state == Transaction::STATE_CREATED || $found->state == Transaction::STATE_COMPLETED)
128 | && $found->paycom_transaction_id !== $this->request->params['id']) {
129 | $this->response->error(
130 | PaycomException::ERROR_INVALID_ACCOUNT,
131 | 'There is other active/completed transaction for this order.'
132 | );
133 | }
134 | }
135 |
136 | // todo: Find transaction by id
137 | $transaction = new Transaction();
138 | $found = $transaction->find($this->request->params);
139 |
140 | if ($found) {
141 | if ($found->state != Transaction::STATE_CREATED) { // validate transaction state
142 | $this->response->error(
143 | PaycomException::ERROR_COULD_NOT_PERFORM,
144 | 'Transaction found, but is not active.'
145 | );
146 | } elseif ($found->isExpired()) { // if transaction timed out, cancel it and send error
147 | $found->cancel(Transaction::REASON_CANCELLED_BY_TIMEOUT);
148 | $this->response->error(
149 | PaycomException::ERROR_COULD_NOT_PERFORM,
150 | 'Transaction is expired.'
151 | );
152 | } else { // if transaction found and active, send it as response
153 | $this->response->send([
154 | 'create_time' => Format::datetime2timestamp($found->create_time),
155 | 'transaction' => $found->id,
156 | 'state' => $found->state,
157 | 'receivers' => $found->receivers,
158 | ]);
159 | }
160 | } else { // transaction not found, create new one
161 |
162 | // validate new transaction time
163 | if (Format::timestamp2milliseconds(1 * $this->request->params['time']) - Format::timestamp(true) >= Transaction::TIMEOUT) {
164 | $this->response->error(
165 | PaycomException::ERROR_INVALID_ACCOUNT,
166 | PaycomException::message(
167 | 'С даты создания транзакции прошло ' . Transaction::TIMEOUT . 'мс',
168 | 'Tranzaksiya yaratilgan sanadan ' . Transaction::TIMEOUT . 'ms o`tgan',
169 | 'Since create time of the transaction passed ' . Transaction::TIMEOUT . 'ms'
170 | ),
171 | 'time'
172 | );
173 | }
174 |
175 | // create new transaction
176 | // keep create_time as timestamp, it is necessary in response
177 | $create_time = Format::timestamp(true);
178 | $transaction->paycom_transaction_id = $this->request->params['id'];
179 | $transaction->paycom_time = $this->request->params['time'];
180 | $transaction->paycom_time_datetime = Format::timestamp2datetime($this->request->params['time']);
181 | $transaction->create_time = Format::timestamp2datetime($create_time);
182 | $transaction->state = Transaction::STATE_CREATED;
183 | $transaction->amount = $this->request->amount;
184 | $transaction->order_id = $this->request->account('order_id');
185 | $transaction->save(); // after save $transaction->id will be populated with the newly created transaction's id.
186 |
187 | // send response
188 | $this->response->send([
189 | 'create_time' => $create_time,
190 | 'transaction' => $transaction->id,
191 | 'state' => $transaction->state,
192 | 'receivers' => null,
193 | ]);
194 | }
195 | }
196 |
197 | private function PerformTransaction()
198 | {
199 | $transaction = new Transaction();
200 | // search transaction by id
201 | $found = $transaction->find($this->request->params);
202 |
203 | // if transaction not found, send error
204 | if (!$found) {
205 | $this->response->error(PaycomException::ERROR_TRANSACTION_NOT_FOUND, 'Transaction not found.');
206 | }
207 |
208 | switch ($found->state) {
209 | case Transaction::STATE_CREATED: // handle active transaction
210 | if ($found->isExpired()) { // if transaction is expired, then cancel it and send error
211 | $found->cancel(Transaction::REASON_CANCELLED_BY_TIMEOUT);
212 | $this->response->error(
213 | PaycomException::ERROR_COULD_NOT_PERFORM,
214 | 'Transaction is expired.'
215 | );
216 | } else { // perform active transaction
217 | // todo: Mark order/service as completed
218 | $params = ['order_id' => $found->order_id];
219 | $order = new Order($this->request->id);
220 | $order->find($params);
221 | $order->changeState(Order::STATE_PAY_ACCEPTED);
222 |
223 | // todo: Mark transaction as completed
224 | $perform_time = Format::timestamp(true);
225 | $found->state = Transaction::STATE_COMPLETED;
226 | $found->perform_time = Format::timestamp2datetime($perform_time);
227 | $found->save();
228 |
229 | $this->response->send([
230 | 'transaction' => $found->id,
231 | 'perform_time' => $perform_time,
232 | 'state' => $found->state,
233 | ]);
234 | }
235 | break;
236 |
237 | case Transaction::STATE_COMPLETED: // handle complete transaction
238 | // todo: If transaction completed, just return it
239 | $this->response->send([
240 | 'transaction' => $found->id,
241 | 'perform_time' => Format::datetime2timestamp($found->perform_time),
242 | 'state' => $found->state,
243 | ]);
244 | break;
245 |
246 | default:
247 | // unknown situation
248 | $this->response->error(
249 | PaycomException::ERROR_COULD_NOT_PERFORM,
250 | 'Could not perform this operation.'
251 | );
252 | break;
253 | }
254 | }
255 |
256 | private function CancelTransaction()
257 | {
258 | $transaction = new Transaction();
259 |
260 | // search transaction by id
261 | $found = $transaction->find($this->request->params);
262 |
263 | // if transaction not found, send error
264 | if (!$found) {
265 | $this->response->error(PaycomException::ERROR_TRANSACTION_NOT_FOUND, 'Transaction not found.');
266 | }
267 |
268 | switch ($found->state) {
269 | // if already cancelled, just send it
270 | case Transaction::STATE_CANCELLED:
271 | case Transaction::STATE_CANCELLED_AFTER_COMPLETE:
272 | $this->response->send([
273 | 'transaction' => $found->id,
274 | 'cancel_time' => Format::datetime2timestamp($found->cancel_time),
275 | 'state' => $found->state,
276 | ]);
277 | break;
278 |
279 | // cancel active transaction
280 | case Transaction::STATE_CREATED:
281 | // cancel transaction with given reason
282 | $found->cancel(1 * $this->request->params['reason']);
283 | // after $found->cancel(), cancel_time and state properties populated with data
284 |
285 | // change order state to cancelled
286 | $order = new Order($this->request->id);
287 | $order->find($this->request->params);
288 | $order->changeState(Order::STATE_CANCELLED);
289 |
290 | // send response
291 | $this->response->send([
292 | 'transaction' => $found->id,
293 | 'cancel_time' => Format::datetime2timestamp($found->cancel_time),
294 | 'state' => $found->state,
295 | ]);
296 | break;
297 |
298 | case Transaction::STATE_COMPLETED:
299 | // find order and check, whether cancelling is possible this order
300 | $order = new Order($this->request->id);
301 | $order->find($this->request->params);
302 | if ($order->allowCancel()) {
303 | // cancel and change state to cancelled
304 | $found->cancel(1 * $this->request->params['reason']);
305 | // after $found->cancel(), cancel_time and state properties populated with data
306 |
307 | $order->changeState(Order::STATE_CANCELLED);
308 |
309 | // send response
310 | $this->response->send([
311 | 'transaction' => $found->id,
312 | 'cancel_time' => Format::datetime2timestamp($found->cancel_time),
313 | 'state' => $found->state,
314 | ]);
315 | } else {
316 | // todo: If cancelling after performing transaction is not possible, then return error -31007
317 | $this->response->error(
318 | PaycomException::ERROR_COULD_NOT_CANCEL,
319 | 'Could not cancel transaction. Order is delivered/Service is completed.'
320 | );
321 | }
322 | break;
323 | }
324 | }
325 |
326 | private function ChangePassword()
327 | {
328 | // validate, password is specified, otherwise send error
329 | if (!isset($this->request->params['password']) || !trim($this->request->params['password'])) {
330 | $this->response->error(PaycomException::ERROR_INVALID_ACCOUNT, 'New password not specified.', 'password');
331 | }
332 |
333 | // if current password specified as new, then send error
334 | if ($this->merchant->config['key'] == $this->request->params['password']) {
335 | $this->response->error(PaycomException::ERROR_INSUFFICIENT_PRIVILEGE, 'Insufficient privilege. Incorrect new password.');
336 | }
337 |
338 | // todo: Implement saving password into data store or file
339 | // example implementation, that saves new password into file specified in the configuration
340 | if (!file_put_contents($this->config['keyFile'], $this->request->params['password'])) {
341 | $this->response->error(PaycomException::ERROR_INTERNAL_SYSTEM, 'Internal System Error.');
342 | }
343 |
344 | // if control is here, then password is saved into data store
345 | // send success response
346 | $this->response->send(['success' => true]);
347 | }
348 |
349 | private function GetStatement()
350 | {
351 | // validate 'from'
352 | if (!isset($this->request->params['from'])) {
353 | $this->response->error(PaycomException::ERROR_INVALID_ACCOUNT, 'Incorrect period.', 'from');
354 | }
355 |
356 | // validate 'to'
357 | if (!isset($this->request->params['to'])) {
358 | $this->response->error(PaycomException::ERROR_INVALID_ACCOUNT, 'Incorrect period.', 'to');
359 | }
360 |
361 | // validate period
362 | if (1 * $this->request->params['from'] >= 1 * $this->request->params['to']) {
363 | $this->response->error(PaycomException::ERROR_INVALID_ACCOUNT, 'Incorrect period. (from >= to)', 'from');
364 | }
365 |
366 | // get list of transactions for specified period
367 | $transaction = new Transaction();
368 | $transactions = $transaction->report($this->request->params['from'], $this->request->params['to']);
369 |
370 | // send results back
371 | $this->response->send(['transactions' => $transactions]);
372 | }
373 | }
374 |
--------------------------------------------------------------------------------
/Paycom/Database.php:
--------------------------------------------------------------------------------
1 | config = $config;
14 | }
15 |
16 | public function new_connection()
17 | {
18 | $db = null;
19 |
20 | // connect to the database
21 | $db_options = [
22 | \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, // fetch rows as associative array
23 | \PDO::ATTR_PERSISTENT => true // use existing connection if exists
24 | ];
25 |
26 | $db = new \PDO(
27 | 'mysql:dbname=' . $this->config['db']['database'] . ';host=' . $this->config['db']['host'] . ';charset=utf8',
28 | $this->config['db']['username'],
29 | $this->config['db']['password'],
30 | $db_options
31 | );
32 |
33 | return $db;
34 | }
35 |
36 | /**
37 | * Connects to the database
38 | * @return null|\PDO connection
39 | */
40 | public static function db()
41 | {
42 | if (!self::$db) {
43 | $config = require_once CONFIG_FILE;
44 | $instance = new self($config);
45 | self::$db = $instance->new_connection();
46 | }
47 |
48 | return self::$db;
49 | }
50 | }
--------------------------------------------------------------------------------
/Paycom/Format.php:
--------------------------------------------------------------------------------
1 | config = $config;
12 |
13 | // read key from key file
14 | if ($this->config['keyFile']) {
15 | $this->config['key'] = trim(file_get_contents($this->config['keyFile']));
16 | }
17 | }
18 |
19 | public function Authorize($request_id)
20 | {
21 | $headers = getallheaders();
22 |
23 | if (!$headers || !isset($headers['Authorization']) ||
24 | !preg_match('/^\s*Basic\s+(\S+)\s*$/i', $headers['Authorization'], $matches) ||
25 | base64_decode($matches[1]) != $this->config['login'] . ":" . $this->config['key']
26 | ) {
27 | throw new PaycomException(
28 | $request_id,
29 | 'Insufficient privilege to perform this method.',
30 | PaycomException::ERROR_INSUFFICIENT_PRIVILEGE
31 | );
32 | }
33 |
34 | return true;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Paycom/Order.php:
--------------------------------------------------------------------------------
1 | request_id = $request_id;
73 | }
74 |
75 | /**
76 | * Validates amount and account values.
77 | * @param array $params amount and account parameters to validate.
78 | * @return bool true - if validation passes
79 | * @throws PaycomException - if validation fails
80 | */
81 | public function validate(array $params)
82 | {
83 | // todo: Validate amount, if failed throw error
84 | // for example, check amount is numeric
85 | if (!is_numeric($params['amount'])) {
86 | throw new PaycomException(
87 | $this->request_id,
88 | 'Incorrect amount.',
89 | PaycomException::ERROR_INVALID_AMOUNT
90 | );
91 | }
92 |
93 | // todo: Validate account, if failed throw error
94 | // assume, we should have order_id
95 | if (!isset($params['account']['order_id']) || !$params['account']['order_id']) {
96 | throw new PaycomException(
97 | $this->request_id,
98 | PaycomException::message(
99 | 'Неверный код заказа.',
100 | 'Harid kodida xatolik.',
101 | 'Incorrect order code.'
102 | ),
103 | PaycomException::ERROR_INVALID_ACCOUNT,
104 | 'order_id'
105 | );
106 | }
107 |
108 | // todo: Check is order available
109 |
110 | // assume, after find() $this will be populated with Order data
111 | $order = $this->find($params['account']);
112 |
113 | // Check, is order found by specified order_id
114 | if (!$order || !$order->id) {
115 | throw new PaycomException(
116 | $this->request_id,
117 | PaycomException::message(
118 | 'Неверный код заказа.',
119 | 'Harid kodida xatolik.',
120 | 'Incorrect order code.'
121 | ),
122 | PaycomException::ERROR_INVALID_ACCOUNT,
123 | 'order_id'
124 | );
125 | }
126 |
127 | // validate amount
128 | // convert $this->amount to coins
129 | // $params['amount'] already in coins
130 | if ((100 * $this->amount) != (1 * $params['amount'])) {
131 | throw new PaycomException(
132 | $this->request_id,
133 | 'Incorrect amount.',
134 | PaycomException::ERROR_INVALID_AMOUNT
135 | );
136 | }
137 |
138 | // for example, order state before payment should be 'waiting pay'
139 | if ($this->state != self::STATE_WAITING_PAY) {
140 | throw new PaycomException(
141 | $this->request_id,
142 | 'Order state is invalid.',
143 | PaycomException::ERROR_COULD_NOT_PERFORM
144 | );
145 | }
146 |
147 | // keep params for further use
148 | $this->params = $params;
149 |
150 | return true;
151 | }
152 |
153 | /**
154 | * Find order by given parameters.
155 | * @param mixed $params parameters.
156 | * @return Order|Order[] found order or array of orders.
157 | */
158 | public function find($params)
159 | {
160 | // todo: Implement searching order(s) by given parameters, populate current instance with data
161 |
162 | // Example implementation to load order by id
163 | if (isset($params['order_id'])) {
164 |
165 | $sql = "select * from orders where id=:orderId";
166 | $sth = self::db()->prepare($sql);
167 | $is_success = $sth->execute([':orderId' => $params['order_id']]);
168 |
169 | if ($is_success) {
170 |
171 | $row = $sth->fetch();
172 |
173 | if ($row) {
174 |
175 | $this->id = 1 * $row['id'];
176 | $this->amount = 1 * $row['amount'];
177 | $this->product_ids = json_decode($row['product_ids'], true);
178 | $this->state = 1 * $row['state'];
179 | $this->user_id = 1 * $row['user_id'];
180 | $this->phone = $row['phone'];
181 |
182 | return $this;
183 |
184 | }
185 |
186 | }
187 |
188 | }
189 |
190 | return null;
191 | }
192 |
193 | /**
194 | * Change order's state to specified one.
195 | * @param int $state new state of the order
196 | * @return void
197 | */
198 | public function changeState($state)
199 | {
200 | // todo: Implement changing order state (reserve order after create transaction or free order after cancel)
201 |
202 | // Example implementation
203 | $this->state = 1 * $state;
204 | $this->save();
205 | }
206 |
207 | /**
208 | * Check, whether order can be cancelled or not.
209 | * @return bool true - order is cancellable, otherwise false.
210 | */
211 | public function allowCancel()
212 | {
213 | // todo: Implement order cancelling allowance check
214 |
215 | // Example implementation
216 | return false; // do not allow cancellation
217 | }
218 |
219 | /**
220 | * Saves this order.
221 | * @throws PaycomException
222 | */
223 | public function save()
224 | {
225 | $db = self::db();
226 |
227 | if (!$this->id) {
228 |
229 | // If new order, set its state to waiting
230 | $this->state = self::STATE_WAITING_PAY;
231 |
232 | // todo: Set customer ID
233 | // $this->user_id = 1 * SomeSessionManager::get('user_id');
234 |
235 | $sql = "insert into orders set product_ids = :pProdIds, amount = :pAmount, state = :pState, user_id = :pUserId, phone = :pPhone";
236 | $sth = $db->prepare($sql);
237 | $is_success = $sth->execute([
238 | ':pProdIds' => json_encode($this->product_ids),
239 | ':pAmount' => $this->amount,
240 | ':pState' => $this->state,
241 | ':pUserId' => $this->user_id,
242 | ':pPhone' => $this->phone,
243 | ]);
244 |
245 | if ($is_success) {
246 | $this->id = $db->lastInsertId();
247 | }
248 | } else {
249 |
250 | $sql = "update orders set state = :pState where id = :pId";
251 | $sth = $db->prepare($sql);
252 | $is_success = $sth->execute([':pState' => $this->state, ':pId' => $this->id]);
253 |
254 | }
255 |
256 | if (!$is_success) {
257 | throw new PaycomException($this->request_id, 'Could not save order.', PaycomException::ERROR_INTERNAL_SYSTEM);
258 | }
259 | }
260 | }
--------------------------------------------------------------------------------
/Paycom/PaycomException.php:
--------------------------------------------------------------------------------
1 | request_id = $request_id;
31 | $this->message = $message;
32 | $this->code = $code;
33 | $this->data = $data;
34 |
35 | // prepare error data
36 | $this->error = ['code' => $this->code];
37 |
38 | if ($this->message) {
39 | $this->error['message'] = $this->message;
40 | }
41 |
42 | if ($this->data) {
43 | $this->error['data'] = $this->data;
44 | }
45 | }
46 |
47 | public function send()
48 | {
49 | header('Content-Type: application/json; charset=UTF-8');
50 |
51 | // create response
52 | $response['id'] = $this->request_id;
53 | $response['result'] = null;
54 | $response['error'] = $this->error;
55 |
56 | echo json_encode($response);
57 | }
58 |
59 | public static function message($ru, $uz = '', $en = '')
60 | {
61 | return ['ru' => $ru, 'uz' => $uz, 'en' => $en];
62 | }
63 | }
--------------------------------------------------------------------------------
/Paycom/Request.php:
--------------------------------------------------------------------------------
1 | CreateTransaction */
14 | public $method;
15 |
16 | /** @var array request parameters, such as amount, account */
17 | public $params;
18 |
19 | /** @var int amount value in coins */
20 | public $amount;
21 |
22 | /**
23 | * Request constructor.
24 | * Parses request payload and populates properties with values.
25 | */
26 | public function __construct()
27 | {
28 | $request_body = file_get_contents('php://input');
29 | $this->payload = json_decode($request_body, true);
30 |
31 | if (!$this->payload) {
32 | throw new PaycomException(
33 | null,
34 | 'Invalid JSON-RPC object.',
35 | PaycomException::ERROR_INVALID_JSON_RPC_OBJECT
36 | );
37 | }
38 |
39 | // populate request object with data
40 | $this->id = isset($this->payload['id']) ? 1 * $this->payload['id'] : null;
41 | $this->method = isset($this->payload['method']) ? trim($this->payload['method']) : null;
42 | $this->params = isset($this->payload['params']) ? $this->payload['params'] : [];
43 | $this->amount = isset($this->payload['params']['amount']) ? 1 * $this->payload['params']['amount'] : null;
44 |
45 | // add request id into params too
46 | $this->params['request_id'] = $this->id;
47 | }
48 |
49 | /**
50 | * Gets account parameter if such exists, otherwise returns null.
51 | * @param string $param name of the parameter.
52 | * @return mixed|null account parameter value or null if such parameter doesn't exists.
53 | */
54 | public function account($param)
55 | {
56 | return isset($this->params['account'], $this->params['account'][$param]) ? $this->params['account'][$param] : null;
57 | }
58 | }
--------------------------------------------------------------------------------
/Paycom/Response.php:
--------------------------------------------------------------------------------
1 | request = $request;
14 | }
15 |
16 | /**
17 | * Sends response with the given result and error.
18 | * @param mixed $result result of the request.
19 | * @param mixed|null $error error.
20 | */
21 | public function send($result, $error = null)
22 | {
23 | header('Content-Type: application/json; charset=UTF-8');
24 |
25 | $response['jsonrpc'] = '2.0';
26 | $response['id'] = $this->request->id;
27 | $response['result'] = $result;
28 | $response['error'] = $error;
29 |
30 | echo json_encode($response);
31 | }
32 |
33 | /**
34 | * Generates PaycomException exception with given parameters.
35 | * @param int $code error code.
36 | * @param string|array $message error message.
37 | * @param string $data parameter name, that resulted to this error.
38 | * @throws PaycomException
39 | */
40 | public function error($code, $message = null, $data = null)
41 | {
42 | throw new PaycomException($this->request->id, $message, $code, $data);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Paycom/Transaction.php:
--------------------------------------------------------------------------------
1 | id) {
102 |
103 | // Create a new transaction
104 |
105 | $this->create_time = Format::timestamp2datetime(Format::timestamp());
106 | $sql = "INSERT INTO transactions SET
107 | paycom_transaction_id = :pPaycomTxId,
108 | paycom_time = :pPaycomTime,
109 | paycom_time_datetime = :pPaycomTimeStr,
110 | create_time = :pCreateTime,
111 | amount = :pAmount,
112 | state = :pState,
113 | receivers = :pReceivers,
114 | order_id = :pOrderId";
115 |
116 | $sth = $db->prepare($sql);
117 |
118 | $is_success = $sth->execute([
119 | ':pPaycomTxId' => $this->paycom_transaction_id,
120 | ':pPaycomTime' => $this->paycom_time,
121 | ':pPaycomTimeStr' => $this->paycom_time_datetime,
122 | ':pCreateTime' => $this->create_time,
123 | ':pAmount' => 1 * $this->amount,
124 | ':pState' => $this->state,
125 | ':pReceivers' => $this->receivers ? json_encode($this->receivers, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null,
126 | ':pOrderId' => 1 * $this->order_id,
127 | ]);
128 |
129 | if ($is_success) {
130 | // set the newly inserted transaction id
131 | $this->id = $db->lastInsertId();
132 | }
133 | } else {
134 |
135 | // Update an existing transaction by id
136 |
137 | $sql = "UPDATE transactions SET
138 | perform_time = :pPerformTime,
139 | cancel_time = :pCancelTime,
140 | state = :pState,
141 | reason = :pReason";
142 |
143 | $params = [];
144 |
145 | if ($this->amount) {
146 | $sql .= ", amount = :pAmount";
147 | $params[':pAmount'] = 1 * $this->amount;
148 | }
149 |
150 | $sql .= " where paycom_transaction_id = :pPaycomTxId and id=:pId";
151 |
152 | $sth = $db->prepare($sql);
153 |
154 | $perform_time = $this->perform_time ? $this->perform_time : null;
155 | $cancel_time = $this->cancel_time ? $this->cancel_time : null;
156 | $reason = $this->reason ? 1 * $this->reason : null;
157 |
158 | $params[':pPerformTime'] = $perform_time;
159 | $params[':pCancelTime'] = $cancel_time;
160 | $params[':pState'] = 1 * $this->state;
161 | $params[':pReason'] = $reason;
162 | $params[':pPaycomTxId'] = $this->paycom_transaction_id;
163 | $params[':pId'] = $this->id;
164 |
165 | $is_success = $sth->execute($params);
166 | }
167 |
168 | return $is_success;
169 | }
170 |
171 | /**
172 | * Cancels transaction with the specified reason.
173 | * @param int $reason cancelling reason.
174 | * @return void
175 | */
176 | public function cancel($reason)
177 | {
178 | // todo: Implement transaction cancelling on data store
179 |
180 | // todo: Populate $cancel_time with value
181 | $this->cancel_time = Format::timestamp2datetime(Format::timestamp());
182 |
183 | // todo: Change $state to cancelled (-1 or -2) according to the current state
184 |
185 | if ($this->state == self::STATE_COMPLETED) {
186 | // Scenario: CreateTransaction -> PerformTransaction -> CancelTransaction
187 | $this->state = self::STATE_CANCELLED_AFTER_COMPLETE;
188 | } else {
189 | // Scenario: CreateTransaction -> CancelTransaction
190 | $this->state = self::STATE_CANCELLED;
191 | }
192 |
193 | // set reason
194 | $this->reason = $reason;
195 |
196 | // todo: Update transaction on data store
197 | $this->save();
198 | }
199 |
200 | /**
201 | * Determines whether current transaction is expired or not.
202 | * @return bool true - if current instance of the transaction is expired, false - otherwise.
203 | */
204 | public function isExpired()
205 | {
206 | // todo: Implement transaction expiration check
207 | // for example, if transaction is active and passed TIMEOUT milliseconds after its creation, then it is expired
208 | return $this->state == self::STATE_CREATED && abs(Format::datetime2timestamp($this->create_time) - Format::timestamp(true)) > self::TIMEOUT;
209 | }
210 |
211 | /**
212 | * Find transaction by given parameters.
213 | * @param mixed $params parameters
214 | * @return Transaction|Transaction[]
215 | * @throws PaycomException invalid parameters specified
216 | */
217 | public function find($params)
218 | {
219 | $db = self::db();
220 |
221 | // todo: Implement searching transaction by id, populate current instance with data and return it
222 | if (isset($params['id'])) {
223 | $sql = "SELECT * FROM transactions WHERE paycom_transaction_id = :pPaycomTxId";
224 | $sth = $db->prepare($sql);
225 | $is_success = $sth->execute([':pPaycomTxId' => $params['id']]);
226 | } elseif (isset($params['account'], $params['account']['order_id'])) {
227 | // todo: Implement searching transactions by given parameters and return the list of transactions
228 | // search by order id active or completed transaction
229 | $sql = "SELECT * FROM transactions WHERE state IN (1, 2) AND order_id = :pOrderId";
230 | $sth = $db->prepare($sql);
231 | $is_success = $sth->execute([':pOrderId' => $params['account']['order_id']]);
232 | } else {
233 | throw new PaycomException(
234 | $params['request_id'],
235 | 'Parameter to find a transaction is not specified.',
236 | PaycomException::ERROR_INTERNAL_SYSTEM
237 | );
238 | }
239 |
240 | // if SQL operation succeeded, then try to populate instance properties with values
241 | if ($is_success) {
242 |
243 | $row = $sth->fetch();
244 |
245 | if ($row) {
246 |
247 | $this->id = $row['id'];
248 | $this->paycom_transaction_id = $row['paycom_transaction_id'];
249 | $this->paycom_time = 1 * $row['paycom_time'];
250 | $this->paycom_time_datetime = $row['paycom_time_datetime'];
251 | $this->create_time = $row['create_time'];
252 | $this->perform_time = $row['perform_time'];
253 | $this->cancel_time = $row['cancel_time'];
254 | $this->state = 1 * $row['state'];
255 | $this->reason = $row['reason'] ? 1 * $row['reason'] : null;
256 | $this->amount = 1 * $row['amount'];
257 | $this->receivers = $row['receivers'] ? json_decode($row['receivers'], true) : null;
258 | $this->order_id = 1 * $row['order_id'];
259 |
260 | return $this;
261 | }
262 |
263 | }
264 |
265 | // transaction not found, return null
266 | return null;
267 |
268 | // Possible features:
269 | // Search transaction by product/order id that specified in $params
270 | // Search transactions for a given period of time that specified in $params
271 | }
272 |
273 | /**
274 | * Gets list of transactions for the given period including period boundaries.
275 | * @param int $from_date start of the period in timestamp.
276 | * @param int $to_date end of the period in timestamp.
277 | * @return array list of found transactions converted into report format for send as a response.
278 | */
279 | public function report($from_date, $to_date)
280 | {
281 | $from_date = Format::timestamp2datetime($from_date);
282 | $to_date = Format::timestamp2datetime($to_date);
283 |
284 | // container to hold rows/document from data store
285 | $rows = [];
286 |
287 | // todo: Retrieve transactions for the specified period from data store
288 |
289 | // Example implementation
290 |
291 | $db = self::db();
292 |
293 | $sql = "SELECT * FROM transactions
294 | WHERE paycom_time_datetime BETWEEN :from_date AND :to_date
295 | ORDER BY paycom_time_datetime";
296 |
297 | $sth = $db->prepare($sql);
298 | $is_success = $sth->execute([':from_date' => $from_date, ':to_date' => $to_date]);
299 | if ($is_success) {
300 | $rows = $sth->fetchAll();
301 | }
302 |
303 | // assume, here we have $rows variable that is populated with transactions from data store
304 | // normalize data for response
305 | $result = [];
306 | foreach ($rows as $row) {
307 | $result[] = [
308 | 'id' => $row['paycom_transaction_id'], // paycom transaction id
309 | 'time' => 1 * $row['paycom_time'], // paycom transaction timestamp as is
310 | 'amount' => 1 * $row['amount'],
311 | 'account' => [
312 | 'order_id' => 1 * $row['order_id'], // account parameters to identify client/order/service
313 | // ... additional parameters may be listed here, which are belongs to the account
314 | ],
315 | 'create_time' => Format::datetime2timestamp($row['create_time']),
316 | 'perform_time' => Format::datetime2timestamp($row['perform_time']),
317 | 'cancel_time' => Format::datetime2timestamp($row['cancel_time']),
318 | 'transaction' => 1 * $row['id'],
319 | 'state' => 1 * $row['state'],
320 | 'reason' => isset($row['reason']) ? 1 * $row['reason'] : null,
321 | 'receivers' => isset($row['receivers']) ? json_decode($row['receivers'], true) : null,
322 | ];
323 | }
324 |
325 | return $result;
326 |
327 | }
328 | }
329 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Paycom integration template
2 |
3 | This is not a complete implementation of the Merchant API, instead a basic template.
4 | One **MUST** implement all the `todo:` entries found in the source files according his/her own requirements.
5 |
6 | ## Table of Content
7 |
8 | - [Prerequisites](#prerequisites)
9 | - [Installation](#installation)
10 | - [Transactions table](#transactions-table)
11 | - [Orders table](#orders-table)
12 | - [Additional resources](#additional-resources)
13 | - [Endpoint](#endpoint)
14 | - [File Structure](#file-structure)
15 | - [Set Up and Run Merchant API implementation on Docker Containers](#set-up-and-run-merchant-api-implementation-on-docker-containers)
16 | - [Contributing](#contributing)
17 |
18 | ## Prerequisites
19 |
20 | - `PHP 5.4` or greater
21 | - `MySQL` or `MariaDB` latest stable version
22 | - [PDO](http://php.net/manual/en/book.pdo.php) extension
23 | - [Composer](https://getcomposer.org/download/) dependency manager
24 |
25 | ## Installation
26 |
27 | Clone this repository:
28 |
29 | ```bash
30 | $ git clone https://github.com/PaycomUZ/paycom-integration-php-template.git
31 | ```
32 |
33 | Change working directory:
34 |
35 | ```bash
36 | $ cd paycom-integration-php-template
37 | ```
38 |
39 | Generate auto-loader, this step will create **`vendor/`** folder with autoloader:
40 |
41 | ```bash
42 | $ composer dumpautoload
43 | ```
44 |
45 | Copy the sample config file as `paycom.config.php` and adjust the settings according to your needs:
46 |
47 | ```bash
48 | $ cp paycom.config.sample.php paycom.config.php
49 | ```
50 |
51 | Edit `paycom.config.php` and set your settings there:
52 |
53 | - Set `merchant_id`;
54 | - Do not change the `login`, it is always `Paycom`;
55 | - Set a path to the password file in the `keyFile`;
56 | - Adjust connection settings in the `db` key to your `mysql` database.
57 |
58 | Following is an example `paycom.config.php` configuration file content:
59 |
60 | ```php
61 | '69240ea9058e46ea7a1b806a',
64 | 'login' => 'Paycom',
65 | 'keyFile' => 'password.paycom',
66 | 'db' => [
67 | 'host' => 'localhost',
68 | 'database' => 'db_shop',
69 | 'username' => 'db_shop_admin',
70 | 'password' => 'bh6U8M8tR5sQGsfLVHdB'
71 | ],
72 | ];
73 | ```
74 |
75 | and an example `password.paycom` file content:
76 |
77 | ```
78 | fkWW6UNrzvzyV6DhrdHJ6aEhr3dRcvJYkaGx
79 | ```
80 |
81 | If you need to adjust other database settings, such as character set, you can do that in the `Paycom/Database.php` file.
82 |
83 | ### Transactions table
84 |
85 | This template requires `transactions` table at least with the following structure:
86 |
87 | ```sql
88 | CREATE TABLE `transactions` (
89 | `id` INT(11) NOT NULL AUTO_INCREMENT,
90 | `paycom_transaction_id` VARCHAR(25) NOT NULL COLLATE 'utf8_unicode_ci',
91 | `paycom_time` VARCHAR(13) NOT NULL COLLATE 'utf8_unicode_ci',
92 | `paycom_time_datetime` DATETIME NOT NULL,
93 | `create_time` DATETIME NOT NULL,
94 | `perform_time` DATETIME NULL DEFAULT NULL,
95 | `cancel_time` DATETIME NULL DEFAULT NULL,
96 | `amount` INT(11) NOT NULL,
97 | `state` TINYINT(2) NOT NULL,
98 | `reason` TINYINT(2) NULL DEFAULT NULL,
99 | `receivers` VARCHAR(500) NULL DEFAULT NULL COMMENT 'JSON array of receivers' COLLATE 'utf8_unicode_ci',
100 | `order_id` INT(11) NOT NULL,
101 |
102 | PRIMARY KEY (`id`)
103 | )
104 |
105 | COLLATE='utf8_unicode_ci'
106 | ENGINE=InnoDB
107 | AUTO_INCREMENT=1;
108 | ```
109 |
110 | Additional fields can be added into this table or above data types and sizes can be adjusted.
111 |
112 | ### Orders table
113 |
114 | You also need a table to store info about orders.
115 | Here is a sample table definition:
116 |
117 | ```sql
118 | CREATE TABLE orders
119 | (
120 | id INT AUTO_INCREMENT PRIMARY KEY,
121 | product_ids VARCHAR(255) NOT NULL,
122 | amount DECIMAL(18, 2) NOT NULL,
123 | state TINYINT(1) NOT NULL,
124 | user_id INT NOT NULL,
125 | phone VARCHAR(15) NOT NULL
126 | ) ENGINE = InnoDB;
127 | ```
128 |
129 | Additional fields can be added into this table or above data types and sizes can be adjusted.
130 |
131 | ## Additional resources
132 |
133 | - To test your [Merchant API](https://help.paycom.uz/pw/protokol-merchant-api) implementation we highly recommend using the following tools:
134 | - [Test Merchant Cabinet](http://merchant.test.paycom.uz);
135 | - [Merchant Sandbox](http://test.paycom.uz/).
136 | - For production use [Merchant Cabinet](https://merchant.paycom.uz).
137 |
138 | ## Endpoint
139 |
140 | In the merchant cabinet on the cashbox settings point the `endpoint` to your Merchant API implementation.
141 | Assuming your domain is `https://example.com`, and your `Merchant API` implementation is located under `api/` folder
142 | or a URL rewriting is configured to access API by `https://example.com/api/`, then `endpoint` should be set as `https://example.com/api/index.php`.
143 |
144 | ## File Structure
145 |
146 | Following is the brief description of the files:
147 |
148 | | File/Folder | Description |
149 | | -------------------------- | ----------------------------------------------------------------------------------------- |
150 | | `index.php` | An entry script, that loads configuration, initializes and runs an application instances. |
151 | | `paycom.config.sample.php` | Sample configuration file with fake values. |
152 | | `paycom.config.php` | Configuration file with actual values. Initially isn't present. Should be copied from `paycom.config.sample.php` |
153 | | `paycom.password` | Default file to set the `KEY` obtained from the Merchant Cabinet. Set in the config file via `keyFile`. Remove any whitespaces and `EOL` characters before save. |
154 | | `functions.php` | Contains additional functions. Right now it has only one function to retrieve headers from `$_SERVER` superglobal variable on Apache and Nginx. |
155 | | `Paycom/` | A folder, that contains all required and helper classes. |
156 | | `Application.php` | A main class to instantiate the new application and handle all requests. |
157 | | `Database.php` | A class to setup the new connections to the underlying database. |
158 | | `Request.php` | A helper class to parse request's payload. |
159 | | `Response.php` | A helper class to send responses to the requester. |
160 | | `Format.php` | A utility class to format data. |
161 | | `Transaction.php` | A class to handle transaction related tasks. Contains `todo: ` items that must be implemented. |
162 | | `Merchant.php` | A helper class to authorize requesters of the API. |
163 | | `PaycomException.php` | A custom exception class to send error responses. |
164 | | `Order.php` | A class to handle order/service related tasks. |
165 | | `vendor/` | Auto generated with Composer folder that contains autoloader. |
166 | | `Dockerfile` | Dockerfile to build an image with `PHP v7`, `Apache v2.4`, `Composer`. |
167 | | `docker-compose.yml` | Compose file to easily setup & run Merchant API implementation on the docker containers. |
168 | | `.gitignore` | Git ignore file. |
169 | | `composer.json` | Config file to handle dependencies and autoloader. Read more [here](https://getcomposer.org/doc/04-schema.md) |
170 | | `README.md` | Description and documentation of this template. |
171 |
172 | ## Set Up and Run Merchant API implementation on Docker Containers
173 |
174 | In cases that there is no `PHP`/`Apache`/`Nginx`/`MySQL` on your production platforms you can easily and quickly setup and run the `Merchant API` implementation using docker containers.
175 |
176 | Here we will build docker images for `Paycom Merchant API` and optionally for `MySQL`.
177 |
178 | `Dockerfile` contains statements to build an image for `Merchant API`.
179 | This image is based on `PHP v7` and `Apache 2.4`, but also includes `PDO` and `PDO_MYSQL` extensions.
180 | There are also statements to install the latest version of `Composer`.
181 |
182 | By editing `docker-compose.yml` file you can adjust exposed ports, volumes.
183 |
184 | If you need more info about base images and docker commands look at the following links:
185 |
186 | - [Official php:7-apache docker image](https://hub.docker.com/_/php/);
187 | - [Official mysql docker image](https://hub.docker.com/_/mysql/);
188 | - [Dockerfile reference](https://docs.docker.com/engine/reference/builder/);
189 | - [Docker Compose file reference](https://docs.docker.com/compose/compose-file/);
190 | - [Docker Compose CLI reference](https://docs.docker.com/compose/reference/overview/);
191 | - [Docker CLI reference](https://docs.docker.com/engine/reference/commandline/cli/).
192 |
193 | Build the images:
194 |
195 | ```bash
196 | docker-compose build
197 | ```
198 |
199 | Run the containers:
200 |
201 | ```bash
202 | docker-compose up -d
203 | ```
204 |
205 | Show the logs:
206 |
207 | ```bash
208 | docker-compose logs -f
209 | ```
210 |
211 | Stop the containers:
212 |
213 | ```bash
214 | docker-compose stop
215 | ```
216 |
217 | Stop and remove the containers:
218 |
219 | ```bash
220 | docker-compose down
221 | ```
222 |
223 | Test the endpoint via cURL command:
224 |
225 | ```bash
226 | curl -X POST \
227 | http://localhost:8888/ \
228 | -H 'Authorization: Basic UGF5Y29tOktleUZyb21NZXJjaGFudENhYmluZXQ=' \
229 | -H 'Cache-Control: no-cache' \
230 | -H 'Content-Type: application/json' \
231 | -d '{
232 | "id": 1,
233 | "method" : "CheckPerformTransaction",
234 | "params" : {
235 | "amount" : 50000,
236 | "account" : {
237 | "phone" : "901304050"
238 | }
239 | }
240 | }'
241 | ```
242 |
243 | `Authorization` header contains `Base64` decoded `Paycom:KEY_FROM_CABINET` login and password.
244 |
245 | For testing purposes you can quickly Base64 decode the login & password with [this online tool](https://www.base64encode.org/).
246 |
247 | You should get something like the following response (below the response is formatted, but you will get raw responses):
248 |
249 | ```bash
250 | {
251 | "id": 1,
252 | "result": null,
253 | "error": {
254 | "code": -31050,
255 | "message": {
256 | "ru": "Неверный код заказа.",
257 | "uz": "Harid kodida xatolik.",
258 | "en": "Incorrect order code."
259 | },
260 | "data": "order_id"
261 | }
262 | }
263 | ```
264 |
265 | ## Contributing
266 |
267 | PRs are welcome. GL&HF!
268 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "paycom/integration-template",
3 | "description": "Set of classes to easy integration with Paycom (Payme) payment system",
4 | "type": "library",
5 | "keywords": [
6 | "paycom",
7 | "payme"
8 | ],
9 | "authors": [
10 | {
11 | "name": "Umidjons",
12 | "email": "almatov.us@gmail.com"
13 | }
14 | ],
15 | "autoload": {
16 | "psr-4": {
17 | "Paycom\\": "Paycom/"
18 | }
19 | },
20 | "require": {
21 | "php": ">=5.4.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | api:
5 | build:
6 | context: .
7 | image: paycom-api:latest
8 | container_name: paycom-api
9 | ports:
10 | - '8888:80'
11 | volumes:
12 | - ./:/var/www/html
13 | networks:
14 | - paycom
15 | restart: always
16 |
17 | db:
18 | image: mysql:latest
19 | container_name: paycom-db
20 | environment:
21 | - MYSQL_ROOT_PASSWORD=7YEBk6bqjNQMHZXT
22 | - MYSQL_DATABASE=shop
23 | - MYSQL_USER=shopadmin
24 | - MYSQL_PASSWORD=rr9pdrmd6LsSv4S5
25 | ports:
26 | - '3333:3306'
27 | volumes:
28 | - paycomgwdb:/var/lib/mysql
29 | networks:
30 | - paycom
31 | restart: always
32 |
33 | networks:
34 | paycom:
35 |
36 | volumes:
37 | paycomgwdb:
38 |
--------------------------------------------------------------------------------
/functions.php:
--------------------------------------------------------------------------------
1 | $value) {
9 | if (substr($name, 0, 5) == 'HTTP_') {
10 | $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
11 | }
12 | }
13 | return $headers;
14 | }
15 | }
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | run();
18 |
--------------------------------------------------------------------------------
/password.paycom:
--------------------------------------------------------------------------------
1 | Replace this line with your KEY, that is obtained from the merchant's cabinet
--------------------------------------------------------------------------------
/paycom.config.sample.php:
--------------------------------------------------------------------------------
1 | '69240ea9058e46ea7a1b806a',
6 |
7 | // Login is always "Paycom"
8 | 'login' => 'Paycom',
9 |
10 | // File with cashbox key (key can be found in cashbox settings)
11 | 'keyFile' => 'password.paycom',
12 |
13 | // Your database settings
14 | 'db' => [
15 | 'host' => '',
16 | 'database' => '',
17 | 'username' => '',
18 | 'password' => '',
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/vendor/autoload.php:
--------------------------------------------------------------------------------
1 |
7 | * Jordi Boggiano
8 | *
9 | * For the full copyright and license information, please view the LICENSE
10 | * file that was distributed with this source code.
11 | */
12 |
13 | namespace Composer\Autoload;
14 |
15 | /**
16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
17 | *
18 | * $loader = new \Composer\Autoload\ClassLoader();
19 | *
20 | * // register classes with namespaces
21 | * $loader->add('Symfony\Component', __DIR__.'/component');
22 | * $loader->add('Symfony', __DIR__.'/framework');
23 | *
24 | * // activate the autoloader
25 | * $loader->register();
26 | *
27 | * // to enable searching the include path (eg. for PEAR packages)
28 | * $loader->setUseIncludePath(true);
29 | *
30 | * In this example, if you try to use a class in the Symfony\Component
31 | * namespace or one of its children (Symfony\Component\Console for instance),
32 | * the autoloader will first look for the class under the component/
33 | * directory, and it will then fallback to the framework/ directory if not
34 | * found before giving up.
35 | *
36 | * This class is loosely based on the Symfony UniversalClassLoader.
37 | *
38 | * @author Fabien Potencier
39 | * @author Jordi Boggiano
40 | * @see http://www.php-fig.org/psr/psr-0/
41 | * @see http://www.php-fig.org/psr/psr-4/
42 | */
43 | class ClassLoader
44 | {
45 | // PSR-4
46 | private $prefixLengthsPsr4 = array();
47 | private $prefixDirsPsr4 = array();
48 | private $fallbackDirsPsr4 = array();
49 |
50 | // PSR-0
51 | private $prefixesPsr0 = array();
52 | private $fallbackDirsPsr0 = array();
53 |
54 | private $useIncludePath = false;
55 | private $classMap = array();
56 | private $classMapAuthoritative = false;
57 | private $missingClasses = array();
58 | private $apcuPrefix;
59 |
60 | public function getPrefixes()
61 | {
62 | if (!empty($this->prefixesPsr0)) {
63 | return call_user_func_array('array_merge', $this->prefixesPsr0);
64 | }
65 |
66 | return array();
67 | }
68 |
69 | public function getPrefixesPsr4()
70 | {
71 | return $this->prefixDirsPsr4;
72 | }
73 |
74 | public function getFallbackDirs()
75 | {
76 | return $this->fallbackDirsPsr0;
77 | }
78 |
79 | public function getFallbackDirsPsr4()
80 | {
81 | return $this->fallbackDirsPsr4;
82 | }
83 |
84 | public function getClassMap()
85 | {
86 | return $this->classMap;
87 | }
88 |
89 | /**
90 | * @param array $classMap Class to filename map
91 | */
92 | public function addClassMap(array $classMap)
93 | {
94 | if ($this->classMap) {
95 | $this->classMap = array_merge($this->classMap, $classMap);
96 | } else {
97 | $this->classMap = $classMap;
98 | }
99 | }
100 |
101 | /**
102 | * Registers a set of PSR-0 directories for a given prefix, either
103 | * appending or prepending to the ones previously set for this prefix.
104 | *
105 | * @param string $prefix The prefix
106 | * @param array|string $paths The PSR-0 root directories
107 | * @param bool $prepend Whether to prepend the directories
108 | */
109 | public function add($prefix, $paths, $prepend = false)
110 | {
111 | if (!$prefix) {
112 | if ($prepend) {
113 | $this->fallbackDirsPsr0 = array_merge(
114 | (array) $paths,
115 | $this->fallbackDirsPsr0
116 | );
117 | } else {
118 | $this->fallbackDirsPsr0 = array_merge(
119 | $this->fallbackDirsPsr0,
120 | (array) $paths
121 | );
122 | }
123 |
124 | return;
125 | }
126 |
127 | $first = $prefix[0];
128 | if (!isset($this->prefixesPsr0[$first][$prefix])) {
129 | $this->prefixesPsr0[$first][$prefix] = (array) $paths;
130 |
131 | return;
132 | }
133 | if ($prepend) {
134 | $this->prefixesPsr0[$first][$prefix] = array_merge(
135 | (array) $paths,
136 | $this->prefixesPsr0[$first][$prefix]
137 | );
138 | } else {
139 | $this->prefixesPsr0[$first][$prefix] = array_merge(
140 | $this->prefixesPsr0[$first][$prefix],
141 | (array) $paths
142 | );
143 | }
144 | }
145 |
146 | /**
147 | * Registers a set of PSR-4 directories for a given namespace, either
148 | * appending or prepending to the ones previously set for this namespace.
149 | *
150 | * @param string $prefix The prefix/namespace, with trailing '\\'
151 | * @param array|string $paths The PSR-4 base directories
152 | * @param bool $prepend Whether to prepend the directories
153 | *
154 | * @throws \InvalidArgumentException
155 | */
156 | public function addPsr4($prefix, $paths, $prepend = false)
157 | {
158 | if (!$prefix) {
159 | // Register directories for the root namespace.
160 | if ($prepend) {
161 | $this->fallbackDirsPsr4 = array_merge(
162 | (array) $paths,
163 | $this->fallbackDirsPsr4
164 | );
165 | } else {
166 | $this->fallbackDirsPsr4 = array_merge(
167 | $this->fallbackDirsPsr4,
168 | (array) $paths
169 | );
170 | }
171 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
172 | // Register directories for a new namespace.
173 | $length = strlen($prefix);
174 | if ('\\' !== $prefix[$length - 1]) {
175 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
176 | }
177 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
178 | $this->prefixDirsPsr4[$prefix] = (array) $paths;
179 | } elseif ($prepend) {
180 | // Prepend directories for an already registered namespace.
181 | $this->prefixDirsPsr4[$prefix] = array_merge(
182 | (array) $paths,
183 | $this->prefixDirsPsr4[$prefix]
184 | );
185 | } else {
186 | // Append directories for an already registered namespace.
187 | $this->prefixDirsPsr4[$prefix] = array_merge(
188 | $this->prefixDirsPsr4[$prefix],
189 | (array) $paths
190 | );
191 | }
192 | }
193 |
194 | /**
195 | * Registers a set of PSR-0 directories for a given prefix,
196 | * replacing any others previously set for this prefix.
197 | *
198 | * @param string $prefix The prefix
199 | * @param array|string $paths The PSR-0 base directories
200 | */
201 | public function set($prefix, $paths)
202 | {
203 | if (!$prefix) {
204 | $this->fallbackDirsPsr0 = (array) $paths;
205 | } else {
206 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
207 | }
208 | }
209 |
210 | /**
211 | * Registers a set of PSR-4 directories for a given namespace,
212 | * replacing any others previously set for this namespace.
213 | *
214 | * @param string $prefix The prefix/namespace, with trailing '\\'
215 | * @param array|string $paths The PSR-4 base directories
216 | *
217 | * @throws \InvalidArgumentException
218 | */
219 | public function setPsr4($prefix, $paths)
220 | {
221 | if (!$prefix) {
222 | $this->fallbackDirsPsr4 = (array) $paths;
223 | } else {
224 | $length = strlen($prefix);
225 | if ('\\' !== $prefix[$length - 1]) {
226 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
227 | }
228 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
229 | $this->prefixDirsPsr4[$prefix] = (array) $paths;
230 | }
231 | }
232 |
233 | /**
234 | * Turns on searching the include path for class files.
235 | *
236 | * @param bool $useIncludePath
237 | */
238 | public function setUseIncludePath($useIncludePath)
239 | {
240 | $this->useIncludePath = $useIncludePath;
241 | }
242 |
243 | /**
244 | * Can be used to check if the autoloader uses the include path to check
245 | * for classes.
246 | *
247 | * @return bool
248 | */
249 | public function getUseIncludePath()
250 | {
251 | return $this->useIncludePath;
252 | }
253 |
254 | /**
255 | * Turns off searching the prefix and fallback directories for classes
256 | * that have not been registered with the class map.
257 | *
258 | * @param bool $classMapAuthoritative
259 | */
260 | public function setClassMapAuthoritative($classMapAuthoritative)
261 | {
262 | $this->classMapAuthoritative = $classMapAuthoritative;
263 | }
264 |
265 | /**
266 | * Should class lookup fail if not found in the current class map?
267 | *
268 | * @return bool
269 | */
270 | public function isClassMapAuthoritative()
271 | {
272 | return $this->classMapAuthoritative;
273 | }
274 |
275 | /**
276 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
277 | *
278 | * @param string|null $apcuPrefix
279 | */
280 | public function setApcuPrefix($apcuPrefix)
281 | {
282 | $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null;
283 | }
284 |
285 | /**
286 | * The APCu prefix in use, or null if APCu caching is not enabled.
287 | *
288 | * @return string|null
289 | */
290 | public function getApcuPrefix()
291 | {
292 | return $this->apcuPrefix;
293 | }
294 |
295 | /**
296 | * Registers this instance as an autoloader.
297 | *
298 | * @param bool $prepend Whether to prepend the autoloader or not
299 | */
300 | public function register($prepend = false)
301 | {
302 | spl_autoload_register(array($this, 'loadClass'), true, $prepend);
303 | }
304 |
305 | /**
306 | * Unregisters this instance as an autoloader.
307 | */
308 | public function unregister()
309 | {
310 | spl_autoload_unregister(array($this, 'loadClass'));
311 | }
312 |
313 | /**
314 | * Loads the given class or interface.
315 | *
316 | * @param string $class The name of the class
317 | * @return bool|null True if loaded, null otherwise
318 | */
319 | public function loadClass($class)
320 | {
321 | if ($file = $this->findFile($class)) {
322 | includeFile($file);
323 |
324 | return true;
325 | }
326 | }
327 |
328 | /**
329 | * Finds the path to the file where the class is defined.
330 | *
331 | * @param string $class The name of the class
332 | *
333 | * @return string|false The path if found, false otherwise
334 | */
335 | public function findFile($class)
336 | {
337 | // class map lookup
338 | if (isset($this->classMap[$class])) {
339 | return $this->classMap[$class];
340 | }
341 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
342 | return false;
343 | }
344 | if (null !== $this->apcuPrefix) {
345 | $file = apcu_fetch($this->apcuPrefix.$class, $hit);
346 | if ($hit) {
347 | return $file;
348 | }
349 | }
350 |
351 | $file = $this->findFileWithExtension($class, '.php');
352 |
353 | // Search for Hack files if we are running on HHVM
354 | if (false === $file && defined('HHVM_VERSION')) {
355 | $file = $this->findFileWithExtension($class, '.hh');
356 | }
357 |
358 | if (null !== $this->apcuPrefix) {
359 | apcu_add($this->apcuPrefix.$class, $file);
360 | }
361 |
362 | if (false === $file) {
363 | // Remember that this class does not exist.
364 | $this->missingClasses[$class] = true;
365 | }
366 |
367 | return $file;
368 | }
369 |
370 | private function findFileWithExtension($class, $ext)
371 | {
372 | // PSR-4 lookup
373 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
374 |
375 | $first = $class[0];
376 | if (isset($this->prefixLengthsPsr4[$first])) {
377 | $subPath = $class;
378 | while (false !== $lastPos = strrpos($subPath, '\\')) {
379 | $subPath = substr($subPath, 0, $lastPos);
380 | $search = $subPath.'\\';
381 | if (isset($this->prefixDirsPsr4[$search])) {
382 | foreach ($this->prefixDirsPsr4[$search] as $dir) {
383 | $length = $this->prefixLengthsPsr4[$first][$search];
384 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
385 | return $file;
386 | }
387 | }
388 | }
389 | }
390 | }
391 |
392 | // PSR-4 fallback dirs
393 | foreach ($this->fallbackDirsPsr4 as $dir) {
394 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
395 | return $file;
396 | }
397 | }
398 |
399 | // PSR-0 lookup
400 | if (false !== $pos = strrpos($class, '\\')) {
401 | // namespaced class name
402 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
403 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
404 | } else {
405 | // PEAR-like class name
406 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
407 | }
408 |
409 | if (isset($this->prefixesPsr0[$first])) {
410 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
411 | if (0 === strpos($class, $prefix)) {
412 | foreach ($dirs as $dir) {
413 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
414 | return $file;
415 | }
416 | }
417 | }
418 | }
419 | }
420 |
421 | // PSR-0 fallback dirs
422 | foreach ($this->fallbackDirsPsr0 as $dir) {
423 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
424 | return $file;
425 | }
426 | }
427 |
428 | // PSR-0 include paths.
429 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
430 | return $file;
431 | }
432 |
433 | return false;
434 | }
435 | }
436 |
437 | /**
438 | * Scope isolated include.
439 | *
440 | * Prevents access to $this/self from included files.
441 | */
442 | function includeFile($file)
443 | {
444 | include $file;
445 | }
446 |
--------------------------------------------------------------------------------
/vendor/composer/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) Nils Adermann, Jordi Boggiano
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is furnished
9 | to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_classmap.php:
--------------------------------------------------------------------------------
1 | array($baseDir . '/Paycom'),
10 | );
11 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_real.php:
--------------------------------------------------------------------------------
1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
27 | if ($useStaticLoader) {
28 | require_once __DIR__ . '/autoload_static.php';
29 |
30 | call_user_func(\Composer\Autoload\ComposerStaticInitd41575b0352a30cc84dc2fe650f3a671::getInitializer($loader));
31 | } else {
32 | $map = require __DIR__ . '/autoload_namespaces.php';
33 | foreach ($map as $namespace => $path) {
34 | $loader->set($namespace, $path);
35 | }
36 |
37 | $map = require __DIR__ . '/autoload_psr4.php';
38 | foreach ($map as $namespace => $path) {
39 | $loader->setPsr4($namespace, $path);
40 | }
41 |
42 | $classMap = require __DIR__ . '/autoload_classmap.php';
43 | if ($classMap) {
44 | $loader->addClassMap($classMap);
45 | }
46 | }
47 |
48 | $loader->register(true);
49 |
50 | return $loader;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/vendor/composer/autoload_static.php:
--------------------------------------------------------------------------------
1 |
11 | array (
12 | 'Paycom\\' => 7,
13 | ),
14 | );
15 |
16 | public static $prefixDirsPsr4 = array (
17 | 'Paycom\\' =>
18 | array (
19 | 0 => __DIR__ . '/../..' . '/Paycom',
20 | ),
21 | );
22 |
23 | public static function getInitializer(ClassLoader $loader)
24 | {
25 | return \Closure::bind(function () use ($loader) {
26 | $loader->prefixLengthsPsr4 = ComposerStaticInitd41575b0352a30cc84dc2fe650f3a671::$prefixLengthsPsr4;
27 | $loader->prefixDirsPsr4 = ComposerStaticInitd41575b0352a30cc84dc2fe650f3a671::$prefixDirsPsr4;
28 |
29 | }, null, ClassLoader::class);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------