From ab93967ffea05910c577be6d640b8e7b04371b33 Mon Sep 17 00:00:00 2001 From: Stierlitz Date: Thu, 4 May 2023 22:05:47 +0300 Subject: [PATCH] MVP --- README.md | 103 ++++++++++++++++++++++++++ composer.json | 23 ++++++ src/Client.php | 88 ++++++++++++++++++++++ src/Payment.php | 164 +++++++++++++++++++++++++++++++++++++++++ src/RequestBuilder.php | 30 ++++++++ src/Webhook.php | 53 +++++++++++++ 6 files changed, 461 insertions(+) create mode 100644 README.md create mode 100644 composer.json create mode 100644 src/Client.php create mode 100644 src/Payment.php create mode 100644 src/RequestBuilder.php create mode 100644 src/Webhook.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..74f77d0 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Проста бібліотека для [MonobankPay](https://api.monobank.ua/) +Документація по REST API [тут](https://api.monobank.ua/docs/acquiring.html) + +Для ведення запитів вам знадобиться токен з особистого кабінету [https://fop.monobank.ua/](https://fop.monobank.ua/) або тестовий токен з [https://api.monobank.ua/](https://api.monobank.ua/) + +Встановити бібліотеку: +```bash +composer require stierlitz/monobank-pay +``` + +### Мінімальні вимоги: +* php >=7.4 +* guzzlehttp/guzzle >= 7.0 +* starkbank/ecdsa >= 0.0.5 + +### Приклади використання: +```php +require_once('vendor/autoload.php'); + +//створили клієнта - через нього запити будуть слатись +$monoClient = new \MonoPay\Client('YOUR_TOKEN_HERE'); + +//із клієнта можна отримати id та назву мерчанта +echo $monoClient->getMerchantId(); +echo $monoClient->getMerchantName(); + +//для створення платежів створюємо цей об'єкт +$monoPayment = new \MonoPay\Payment($monoClient); + +//створення платежу +$invoice = $monoPayment->create(1000,[ + 'merchantPaymInfo' => [ //деталі оплати + 'reference' => 'my_shop_order_28142', //Номер чека, замовлення, тощо; визначається мерчантом (вами) + 'destination' => 'Оплата за замовлення #28142', //Призначення платежу + 'basketOrder' => [ //Склад замовлення, використовується для відображення кошика замовлення + [ + 'name' => 'Товар1', //Назва товару + 'qty' => 2, //Кількість + 'sum' => 500, //Сума у мінімальних одиницях валюти за одиницю товару + 'icon' => 'https://example.com/images/product1.jpg', //Посилання на зображення товару + 'unit' => 'уп.', //Назва одиниці вимiру товару + ] + ] + ], + 'redirectUrl' => 'https://example.com/order-result', //Адреса для повернення (GET) - на цю адресу буде переадресовано користувача після завершення оплати (у разі успіху або помилки) + 'webHookUrl' => 'https://example.com/mono-webhook', //Адреса для CallBack (POST) – на цю адресу буде надіслано дані про стан платежу при кожній зміні статусу. Зміст тіла запиту ідентичний відповіді запиту “перевірки статусу рахунку” + 'validity' => 3600*24*7, //Строк дії в секундах, за замовчуванням рахунок перестає бути дійсним через 24 години + 'paymentType' => 'debit', //debit | hold. Тип операції. Для значення hold термін складає 9 днів. Якщо через 9 днів холд не буде фіналізовано — він скасовується + ]); +print_r($invoice); + +//інформація про платіж +$invoice = $monoPayment->info('2305046jUBEj8WfyaBdB'); +print_r($invoice); + +//відшкодування +$result = $monoPayment->refund('2305046jUBEj8WfyaBdB'); +print_r($result); + +//скасування посилання на оплату +$result = $monoPayment->cancel('2305046jUBEj8WfyaBdB'); +print_r($result); + +//деталі успішної оплати +$invoiceDetails = $monoPayment->successDetails('2305046jUBEj8WfyaBdB'); +print_r($invoiceDetails); + +//списати заблоковану сумму +//зверніть увагу: списати можна тільки таку самму або меншу сумму яку ви заблокували +$result = $monoPayment->captureHold('2305046jUBEj8WfyaBdB',500); +print_r($result); + +//список успішних оплат за останні сутки +$list = $monoPayment->items(time()-60*60*24); +print_r($list); +``` + +### Отримання вебхуку: +```php +require_once('vendor/autoload.php'); + +//створили клієнта - через нього запити будуть слатись +$monoClient = new \MonoPay\Client('YOUR_TOKEN_HERE'); + +//отримання публічного ключа (бажано закешувати) +$publicKey = $monoClient->getPublicKey(); + +//класс для роботи з вебхуком +$monoWebhook = new \MonoPay\Webhook($monoClient,$publicKey,$_SERVER['HTTP_X_SIGN']); +//отримуємо вхідні дані +$body = file_get_contents('php://input'); +//валідуємо дані +if($monoWebhook->verify($body)){ + echo "Ці дані прислав монобанк, можна обробляти"; +}else{ + echo "Дані прислав шахрай, ігноруємо"; +} +``` + +#### TODO List: +* Доробити методи стосовно токенізації карт +* Переробити вхідні параметри і вихідні дані на класи з описаними методами +* Попросити в Гороховського баночку пива \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4acb210 --- /dev/null +++ b/composer.json @@ -0,0 +1,23 @@ +{ + "name": "plakidan/monobank-pay", + "description": "This is simple library for Monobank Acquiring called \"MonoPay\" based on REST API (v2304) docs located at https://api.monobank.ua/docs/acquiring.html", + "type": "library", + "license": "Apache-2.0", + "authors": [ + { + "name": "Stierlitz", + "email": "y.plakida@aiw.systems" + } + ], + "minimum-stability": "dev", + "require": { + "php": ">=7.4", + "guzzlehttp/guzzle": "^7.0", + "starkbank/ecdsa": "0.0.5" + }, + "autoload": { + "psr-4": { + "MonoPay\\": "src/" + } + } +} diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..7605625 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,88 @@ +httpClient = new \GuzzleHttp\Client([ + 'base_uri' => $this->apiEndpoint, + \GuzzleHttp\RequestOptions::TIMEOUT => 5, + \GuzzleHttp\RequestOptions::HEADERS => [ + 'X-Token' => $token, + ], + \GuzzleHttp\RequestOptions::HTTP_ERRORS => false + ]); + $response = $this->httpClient->request('GET', '/api/merchant/details'); + $json = $response->getBody()->getContents(); + $data = json_decode($json, true); + if ($response->getStatusCode() == '200') { + if ($data && isset($data['merchantId']) && isset($data['merchantName'])) { + $this->merchantId = $data['merchantId']; + $this->merchantName = $data['merchantName']; + } else { + throw new \Exception('Cannot decode json response from Mono', 500); + } + } else { + throw new \Exception($data['errorDescription'] ?? 'Unknown error response: ' . $json, $response->getStatusCode()); + } + } + + public function getMerchantId(): string + { + return $this->merchantId; + } + + public function getMerchantName(): string + { + return $this->merchantName; + } + + public function getClient(): \GuzzleHttp\Client + { + return $this->httpClient; + } + + /** + * Відкритий ключ для верифікації підписів + * Отримання відкритого ключа для перевірки підпису, який включено у вебхуки. Ключ можна кешувати і робити запит на отримання нового, коли верифікація підпису з поточним ключем перестане працювати. Кожного разу робити запит на отримання ключа не треба + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1pubkey/get + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + */ + public function getPublicKey(): string + { + $response = $this->getClient()->request('GET','/api/merchant/pubkey'); + $data = $this->getDataFromGuzzleResponse($response); + if(!isset($data['key'])){ + throw new \Exception('Invalid response from Mono API',500); + } + return $data['key']; + } + + /** + * Дані мерчанта + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1details/get + * @return array Масив з ключами merchantId та merchantName + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + */ + public function getMerchant(): array + { + $response = $this->getClient()->request('GET','/api/merchant/details'); + return $this->getDataFromGuzzleResponse($response); + } +} \ No newline at end of file diff --git a/src/Payment.php b/src/Payment.php new file mode 100644 index 0000000..8aabce4 --- /dev/null +++ b/src/Payment.php @@ -0,0 +1,164 @@ +client = $client; + } + + /** + * Створення рахунку + * Створення рахунку для оплати + * @param int $amount Сума оплати у мінімальних одиницях (копійки для гривні) + * @param array $options Додаткові параметри (Див. посилання) + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1create/post + */ + public function create(int $amount, array $options=[]): array + { + if($amount < 1){ + throw new \Exception('Amount must be a natural number',500); + } + $options['amount']=$amount; + $response = $this->client->getClient()->request('POST','/api/merchant/invoice/create',[ + \GuzzleHttp\RequestOptions::JSON => $options + ]); + + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Статус рахунку + * Метод перевірки статусу рахунку при розсинхронізації з боку продавця або відсутності webHookUrl при створенні рахунку. + * @param string $invoiceId ID рахунку + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1status?invoiceId=%7BinvoiceId%7D/get + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + */ + public function info(string $invoiceId): array + { + $response = $this->client->getClient()->request('GET','/api/merchant/invoice/status',[ + \GuzzleHttp\RequestOptions::QUERY => [ + 'invoiceId' => $invoiceId + ] + ]); + + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Скасування оплати + * Скасування успішної оплати рахунку + * @param string $invoiceId ID рахунку + * @param array $options Додаткові параметри (Див. посилання) + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1cancel/post + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + */ + public function refund(string $invoiceId, array $options=[]): array + { + $options['invoiceId'] = $invoiceId; + + $response = $this->client->getClient()->request('POST','/api/merchant/invoice/cancel',[ + \GuzzleHttp\RequestOptions::JSON => $options + ]); + + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Інвалідація рахунку + * Інвалідація рахунку, якщо за ним ще не було здіснено оплати + * @param string $invoiceId ID рахунку + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1remove/post + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function cancel(string $invoiceId): array + { + $response = $this->client->getClient()->request('POST','/api/merchant/invoice/remove',[ + \GuzzleHttp\RequestOptions::JSON => [ + 'invoiceId' => $invoiceId + ] + ]); + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Розширена інформація про успішну оплату + * Дані про успішну оплату, якщо вона була здійснена + * @param string $invoiceId Ідентифікатор рахунку + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + *@link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1payment-info?invoiceId=%7BinvoiceId%7D/get + */ + public function successDetails(string $invoiceId): array + { + $response = $this->client->getClient()->request('GET','/api/merchant/invoice/payment-info',[ + \GuzzleHttp\RequestOptions::QUERY => [ + 'invoiceId' => $invoiceId + ] + ]); + + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Фіналізація суми холду + * Фінальна сумма списання має бути нижчою або дорівнювати суммі холду + * @param string $invoiceId Ідентифікатор рахунку + * @param int|null $amount Сума у мінімальних одиницях, якщо бажаєте змінити сумму списання + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1invoice~1finalize/post + */ + public function captureHold(string $invoiceId, int $amount = null): array + { + $body = [ + 'invoiceId' => $invoiceId + ]; + if(isset($amount)){ + $body['amount'] = $amount; + } + $response = $this->client->getClient()->request('POST','/api/merchant/invoice/finalize',[ + \GuzzleHttp\RequestOptions::JSON => $body + ]); + + return $this->getDataFromGuzzleResponse($response); + } + + /** + * Виписка за період + * Список платежів за вказаний період + * @param int $fromTimestamp UTC Unix timestamp + * @param int|null $toTimestamp UTC Unix timestamp + * @return array + * @throws \GuzzleHttp\Exception\GuzzleException + * @throws \Exception + * @link https://api.monobank.ua/docs/acquiring.html#/paths/~1api~1merchant~1statement/get + */ + public function items(int $fromTimestamp, int $toTimestamp=null): array + { + $query = [ + 'from' => $fromTimestamp + ]; + if(isset($toTimestamp)){ + $query['to'] = $toTimestamp; + } + $response = $this->client->getClient()->request('GET','/api/merchant/statement',[ + \GuzzleHttp\RequestOptions::QUERY => $query + ]); + + $data = $this->getDataFromGuzzleResponse($response); + return $data['list']??[]; + } + +} \ No newline at end of file diff --git a/src/RequestBuilder.php b/src/RequestBuilder.php new file mode 100644 index 0000000..12f5ab6 --- /dev/null +++ b/src/RequestBuilder.php @@ -0,0 +1,30 @@ +getBody()->getContents(); + if(!$json){ + throw new \Exception('Empty request from Mono API',500); + } + $data = json_decode($json,true); + if(!$data){ + throw new \Exception('Cannot decode json response from Mono', 500); + } + if ($response->getStatusCode() == '200') { + return $data; + } elseif(isset($data['errorDescription'])){ + throw new \Exception($data['errorDescription'], $response->getStatusCode()); + } elseif (isset($data['errCode']) && isset($data['errText'])){ + throw new \Exception('Error: '.$data['errText'].'. Error code: '.$data['errCode'], $response->getStatusCode()); + }else{ + throw new \Exception('Unknown error response: '.$json, $response->getStatusCode()); + } + } + +} \ No newline at end of file diff --git a/src/Webhook.php b/src/Webhook.php new file mode 100644 index 0000000..c564b75 --- /dev/null +++ b/src/Webhook.php @@ -0,0 +1,53 @@ +getPublicKey(); + } + if(empty($publicKeyBase64)){ + throw new \Exception('Cannot retrieve public key'); + } + $this->publicKeyBase64 = $publicKeyBase64; + if(!empty($xSignBase64)){ + $this->xSignBase64 = $xSignBase64; + }elseif(!empty($_SERVER['HTTP_X_SIGN'])){ + $this->xSignBase64 = $_SERVER['HTTP_X_SIGN']; + }else{ + throw new \Exception('Cannot retrieve X-Sign header value'); + } + } + + /** + * Перевіряє чи можна довіряти даним з вебхуку + * @param string|null $requestBody Тіло запиту. Зазвичай це json body вхідного запиту який можна отримати через функцію file_get_contents('php://input') + * @return bool Чи коректні вхідні дані + */ + public function verify(string $requestBody=null): bool + { + if(empty($requestBody)){ + $requestBody = file_get_contents('php://input'); + } + $publicKey = \EllipticCurve\PublicKey::fromPem(base64_decode($this->publicKeyBase64)); + $signature = \EllipticCurve\Signature::fromBase64($this->xSignBase64); + + return \EllipticCurve\Ecdsa::verify($requestBody, $signature, $publicKey); + } +} \ No newline at end of file