From d8b64bb3274e39463f0496a34f345a6b82811f95 Mon Sep 17 00:00:00 2001 From: Sergey Yarmolich Date: Mon, 17 Feb 2025 19:11:08 +0300 Subject: [PATCH] add cryptomus payment --- app/Payments/Cryptomus.php | 158 ++++++++++++++++++ library/Cryptomus/Payment.php | 119 +++++++++++++ library/Cryptomus/RequestBuilder.php | 90 ++++++++++ library/Cryptomus/RequestBuilderException.php | 45 +++++ 4 files changed, 412 insertions(+) create mode 100644 app/Payments/Cryptomus.php create mode 100644 library/Cryptomus/Payment.php create mode 100644 library/Cryptomus/RequestBuilder.php create mode 100644 library/Cryptomus/RequestBuilderException.php diff --git a/app/Payments/Cryptomus.php b/app/Payments/Cryptomus.php new file mode 100644 index 00000000..c16b7856 --- /dev/null +++ b/app/Payments/Cryptomus.php @@ -0,0 +1,158 @@ +config = $config; + } + + /** + * @return array[] + */ + public function form() + { + return [ + 'cryptomus_api_key' => [ + 'label' => 'Api key', + 'description' => 'You can find the API key in the settings of your personal account.', + 'type' => 'input', + ], + 'cryptomus_uuid' => [ + 'label' => 'UUID', + 'description' => 'You can find the Merchant UUID in the settings of your personal account.', + 'type' => 'input', + ], + 'cryptomus_subtract' => [ + 'label' => 'Substract', + 'description' => 'How much commission does the client pay (0-100%)', + 'type' => 'input', + ], + 'cryptomus_lifetime' => [ + 'label' => 'Lifetime', + 'description' => 'The lifespan of the issued invoice.(In seconds)', + 'type' => 'input', + ], + 'cryptomus_currency' => [ + 'label' => 'Fiat Currency', + 'description' => 'Default CNY', + 'type' => 'input' + ] + ]; + } + + /** + * @param $order + * @return array + * @throws \Exception + */ + public function pay($order) { + $config = $this->config; + + $paymentData = [ + 'amount' => sprintf('%.2f', $order['total_amount'] / 100), + 'currency' => $config['cryptomus_currency'] ?? 'CNY', + 'order_id' => 'v2board_' . $order['trade_no'], + 'url_return' => $order['return_url'], + 'url_callback' => $order['notify_url'], + 'lifetime' => $config['cryptomus_lifetime'] ?? '3600', + 'subtract' => $config['cryptomus_subtract'] ?? '0', + 'plugin_name' => 'v2board:1.7.4', + ]; + + $paymentInstance = $this->getPayment(); + + try { + $payment = $paymentInstance->create($paymentData); + } catch (\Exception $exception) { + info($exception->getMessage()); + abort(500, __($exception->getMessage())); + } + + return [ + 'type' => 1, // Redirect to url + 'data' => $payment['url'] + ]; + } + + + /** + * @return CryptomusPayment + * @throws \Exception + */ + private function getPayment() + { + $merchantUuid = trim($this->config['cryptomus_uuid']); + $paymentKey = trim($this->config['cryptomus_api_key']); + + if (!$merchantUuid && !$paymentKey) { + info(__("Please fill UUID and API key")); + abort(500, __("Please fill UUID and API key")); + } + + return new CryptomusPayment($paymentKey, $merchantUuid); + } + + /** + * @param $params + * @return array|false + */ + public function notify($params) { + + $payload = trim(file_get_contents('php://input')); + $data = json_decode($payload, true); + + if (!$this->hashEqual($data)) { + abort(400, 'Signature does not match'); + } + + $success = !empty($data['is_final']) && ($data['status'] === 'paid' || $data['status'] === 'paid_over' || $data['status'] === 'wrong_amount'); + if ($success) { + $orderId = preg_replace('/^v2board(?:_upd)?_/', '', $data['order_id'] ?? ''); + return [ + 'trade_no' => $orderId, + 'callback_no' => $data['uuid'] + ]; + } + + return false; + } + + + /** + * @param $data + * @return bool + */ + private function hashEqual($data) + { + $paymentKey = trim($this->config['cryptomus_api_key']); + + if (!$paymentKey) { + return false; + } + + $signature = $data['sign']; + if (!$signature) { + return false; + } + + unset($data['sign']); + + $hash = md5(base64_encode(json_encode($data, JSON_UNESCAPED_UNICODE)) . $paymentKey); + if (!hash_equals($hash, $signature)) { + return false; + } + + return true; + } + +} + diff --git a/library/Cryptomus/Payment.php b/library/Cryptomus/Payment.php new file mode 100644 index 00000000..17fe46cb --- /dev/null +++ b/library/Cryptomus/Payment.php @@ -0,0 +1,119 @@ +requestBuilder = new RequestBuilder($paymentKey, $merchantUuid); + } + + + /** + * @param array $parameters Additional parameters + * @return bool|mixed + * @throws RequestBuilderException + */ + public function services(array $parameters = []) + { + return $this->requestBuilder->sendRequest($this->version . '/payment/services', $parameters); + } + + /** + * @param array $data + * - @var string amount: Amount to pay + * - @var string currency: Payment currency + * - @var string network: Payment network + * - @var string order_id: Order ID in your system + * - @var string url_return: Redirect link + * - @var string url_callback: Callback link + * - @var boolean is_payment_multiple: Allow surcharges on payment * + * - @var string lifetime: Payment lifetime in seconds + * - @var string to_currency: Currency to convert amount to + * @return bool|mixed + * @throws RequestBuilderException + */ + public function create(array $data) + { + return $this->requestBuilder->sendRequest($this->version . '/payment', $data); + } + + /** + * uuid or order_id + * @param array $data + * - @var string uuid + * - @var string order_id + * @return bool|mixed + * @throws RequestBuilderException + */ + public function info($data = []) + { + return $this->requestBuilder->sendRequest($this->version . '/payment/info', $data); + } + + /** + * @param string|int $page Pagination cursor + * @param array $parameters Additional parameters + * @return bool|mixed + * @throws RequestBuilderException + */ + public function history($page = 1, array $parameters = []) + { + $data = array_merge($parameters, ['cursor' => strval($page)]); + return $this->requestBuilder->sendRequest($this->version . '/payment/list', $data); + } + + /** + * @param array $parameters Additional parameters + * @return bool|mixed + * @throws RequestBuilderException + */ + public function balance(array $parameters = []) + { + return $this->requestBuilder->sendRequest($this->version . '/balance', $parameters); + } + + /** + * uuid or order_id + * @param array $data + * - @var string uuid: Payment's UUID + * - @var string order_id: Order ID in your system + * @return bool|mixed + * @throws RequestBuilderException + */ + public function reSendNotifications(array $data) + { + return $this->requestBuilder->sendRequest($this->version . '/payment/resend', $data); + } + + /** + * @param array $data + * - @var string network: Network + * - @var string currency: Payment currency + * - @var string order_id: Order ID in your system + * - @var string url_callback: Callback url + * @return bool|mixed + * @throws RequestBuilderException + */ + public function createWallet(array $data) + { + return $this->requestBuilder->sendRequest($this->version . '/wallet', $data); + } +} diff --git a/library/Cryptomus/RequestBuilder.php b/library/Cryptomus/RequestBuilder.php new file mode 100644 index 00000000..1aa1341e --- /dev/null +++ b/library/Cryptomus/RequestBuilder.php @@ -0,0 +1,90 @@ +secretKey = $secretKey; + $this->merchantUuid = $merchantUuid; + } + + /** + * @param $uri + * @param array $data + * @return bool|mixed + * @throws RequestBuilderException + */ + public function sendRequest($uri, array $data = []) + { + $curl = curl_init(); + $url = self::API_URL . $uri; + $body = json_encode($data, JSON_UNESCAPED_UNICODE); + + $headers = [ + 'Accept: application/json', + 'Content-Type: application/json;charset=UTF-8', + 'Content-Length: ' . strlen($body), + 'merchant: ' . $this->merchantUuid, + 'sign: ' . md5(base64_encode($body) . $this->secretKey) + ]; + + curl_setopt_array( + $curl, + [ + CURLOPT_URL => $url, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_POST => 1, + CURLOPT_POSTFIELDS => $body, + CURLOPT_RETURNTRANSFER => 1, + ] + ); + + $response = curl_exec($curl); + $responseCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + if ($response === false) { + throw new RequestBuilderException(curl_error($curl), $responseCode, $uri); + } + + if (false === empty($response)) { + $json = json_decode($response, true); + if (is_null($json)) { + throw new RequestBuilderException(json_last_error_msg(), $responseCode, $uri); + } + + if ($responseCode !== 200 || (!is_null($json['state']) && $json['state'] != 0)) { + if (!empty($json['message'])) { + throw new RequestBuilderException($json['message'], $responseCode, $uri); + } + + if (!empty($json['errors'])) { + throw new RequestBuilderException('Validation error', $responseCode, $uri, $json['errors']); + } + } + + if (!empty($json['result']) && !is_null($json['state']) && $json['state'] == 0) { + return $json['result']; + } + } + + return true; + } +} diff --git a/library/Cryptomus/RequestBuilderException.php b/library/Cryptomus/RequestBuilderException.php new file mode 100644 index 00000000..b0d46ed9 --- /dev/null +++ b/library/Cryptomus/RequestBuilderException.php @@ -0,0 +1,45 @@ +method = $uri; + $this->errors = $errors; + + parent::__construct($message, $responseCode, $previous); + } + + /** + * @return string + */ + public function getMethod() + { + return $this->method; + } + + /** + * @return array + */ + public function getErrors() + { + return $this->errors; + } +}