├── .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 | --------------------------------------------------------------------------------