PageRenderTime 27ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/modules/sale/handlers/paysystem/platon/handler.php

https://gitlab.com/alexprowars/bitrix
PHP | 556 lines | 388 code | 65 blank | 103 comment | 13 complexity | 348488cb3ea34814a94d192162ae0f6b MD5 | raw file
  1. <?php
  2. namespace Sale\Handlers\PaySystem;
  3. use Bitrix\Main;
  4. use Bitrix\Main\Entity\Result;
  5. use Bitrix\Main\Localization\Loc;
  6. use Bitrix\Main\Request;
  7. use Bitrix\Main\Web\HttpClient;
  8. use Bitrix\Main\Web\Json;
  9. use Bitrix\Sale\Payment;
  10. use Bitrix\Sale\PaymentCollection;
  11. use Bitrix\Sale\PaySystem;
  12. use Bitrix\Sale\PaySystem\Logger;
  13. use Bitrix\Sale\PaySystem\ServiceResult;
  14. use Bitrix\Sale\PriceMaths;
  15. class PlatonHandler extends PaySystem\ServiceHandler implements PaySystem\IRefund
  16. {
  17. private const PAYMENT_METHOD_CODE = 'CC';
  18. private const REFUND_ACTION = 'CREDITVOID';
  19. private const ANALYTICS_TAG = 'api_bitrix24ua';
  20. /*
  21. * order: payment id
  22. * ext1: the name of the handler
  23. * ext2: pay system service ID
  24. * ext3: analytics tag
  25. */
  26. private const CALLBACK_ORDER_PARAM = 'order';
  27. private const CALLBACK_EXT1_PARAM = 'ext1';
  28. private const CALLBACK_EXT2_PARAM = 'ext2';
  29. private const TRANSACTION_STATUS_SALE = 'SALE';
  30. private const TRANSACTION_STATUS_REFUND = 'REFUND';
  31. private const REFUND_STATUS_ACCEPTED = 'ACCEPTED';
  32. private const REFUND_STATUS_ERROR = 'ERROR';
  33. private const PS_MODE_BANK_CARD = 'bank_card';
  34. private const PS_MODE_GOOGLE_PAY = 'google_pay';
  35. private const PS_MODE_APPLE_PAY = 'apple_pay';
  36. private const PS_MODE_PRIVAT24 = 'privat24';
  37. /**
  38. * @inheritDoc
  39. */
  40. public function initiatePay(Payment $payment, Request $request = null)
  41. {
  42. $result = new PaySystem\ServiceResult();
  43. $params = [
  44. 'CURRENCY' => $payment->getField('CURRENCY'),
  45. 'SUM' => PriceMaths::roundPrecision($payment->getSum()),
  46. 'FORM_ACTION_URL' => $this->getUrl($payment, "formActionUrl"),
  47. 'FORM_DATA' => $this->getFormData($payment),
  48. ];
  49. $this->setExtraParams($params);
  50. $showTemplateResult = $this->showTemplate($payment, 'template');
  51. if ($showTemplateResult->isSuccess())
  52. {
  53. $result->setTemplate($showTemplateResult->getTemplate());
  54. }
  55. else
  56. {
  57. $result->addErrors($showTemplateResult->getErrors());
  58. }
  59. return $result;
  60. }
  61. /**
  62. * forms the data that will be sent with a POST request via a form
  63. * @param Payment $payment
  64. * @return array
  65. */
  66. private function getFormData(Payment $payment): array
  67. {
  68. $apiKey = $this->getBusinessValue($payment, 'PLATON_API_KEY');
  69. $paymentMethodCode = self::PAYMENT_METHOD_CODE;
  70. $paymentData = $this->getPaymentData($payment);
  71. $encodedPaymentData = $this->encodePaymentData($paymentData);
  72. $successUrl = $this->getSuccessUrl($payment);
  73. $password = $this->getBusinessValue($payment, 'PLATON_PASSWORD');
  74. $sign = $this->getPaymentFormSignature(
  75. $apiKey,
  76. $paymentMethodCode,
  77. $encodedPaymentData,
  78. $successUrl,
  79. $password
  80. );
  81. $paymentNumber = $payment->getField('ACCOUNT_NUMBER');
  82. $paySystemId = $this->service->getField('ID');
  83. $formData = [
  84. 'KEY' => $apiKey,
  85. 'PAYMENT' => $paymentMethodCode,
  86. 'DATA' => $encodedPaymentData,
  87. 'URL' => $successUrl,
  88. 'REQ_TOKEN' => 'N',
  89. 'SIGN' => $sign,
  90. 'ORDER' => $paymentNumber,
  91. 'EXT_1' => 'PLATON',
  92. 'EXT_2' => $paySystemId,
  93. 'EXT_3' => self::ANALYTICS_TAG,
  94. ];
  95. $email = $this->getUserEmailValue($payment);
  96. if ($email)
  97. {
  98. $formData['EMAIL'] = $email;
  99. }
  100. return $formData;
  101. }
  102. /**
  103. * encodes the payment data as per the documentation
  104. * @param array $paymentData
  105. * @return string
  106. */
  107. private function encodePaymentData(array $paymentData): string
  108. {
  109. return base64_encode(Json::encode($paymentData, JSON_UNESCAPED_UNICODE));
  110. }
  111. /**
  112. * @param $data
  113. * @return false|mixed
  114. */
  115. private function decode($data)
  116. {
  117. try
  118. {
  119. return Main\Web\Json::decode($data);
  120. }
  121. catch (Main\ArgumentException $exception)
  122. {
  123. return false;
  124. }
  125. }
  126. /**
  127. * @param Payment $payment
  128. * @return array
  129. */
  130. private function getPaymentData(Payment $payment)
  131. {
  132. $formattedPaymentSum = $this->getFormattedPaymentSum($payment);
  133. $paymentDescription = $this->getPaymentDescription($payment);
  134. return [
  135. 'amount' => $formattedPaymentSum,
  136. 'currency' => $payment->getField("CURRENCY"),
  137. 'description' => $paymentDescription,
  138. 'recurring' => 'N',
  139. ];
  140. }
  141. /**
  142. * @param Payment $payment
  143. * @return string
  144. */
  145. private function getFormattedPaymentSum(Payment $payment)
  146. {
  147. $paymentSum = PriceMaths::roundPrecision($payment->getSum());
  148. return $this->formatPaymentSum($paymentSum);
  149. }
  150. /**
  151. * @param $paymentSum
  152. * @return string
  153. */
  154. private function formatPaymentSum($paymentSum): string
  155. {
  156. return number_format($paymentSum, 2, '.', '');
  157. }
  158. /**
  159. * returns either the link from a handler's settings or the order confirmation page
  160. * @param Payment $payment
  161. * @return string
  162. */
  163. private function getSuccessUrl(Payment $payment): string
  164. {
  165. return $this->getBusinessValue($payment, 'PLATON_SUCCESS_URL') ?: $this->service->getContext()->getUrl();
  166. }
  167. /**
  168. * @param Payment $payment
  169. * @return string|string[]
  170. */
  171. private function getPaymentDescription(Payment $payment)
  172. {
  173. /** @var PaymentCollection $collection */
  174. $collection = $payment->getCollection();
  175. $order = $collection->getOrder();
  176. $userEmail = $order->getPropertyCollection()->getUserEmail();
  177. $descriptionTemplate = $this->getBusinessValue($payment, 'PLATON_PAYMENT_DESCRIPTION');
  178. $description = str_replace(
  179. [
  180. '#PAYMENT_NUMBER#',
  181. '#ORDER_NUMBER#',
  182. '#PAYMENT_ID#',
  183. '#ORDER_ID#',
  184. '#USER_EMAIL#',
  185. ],
  186. [
  187. $payment->getField('ACCOUNT_NUMBER'),
  188. $order->getField('ACCOUNT_NUMBER'),
  189. $payment->getId(),
  190. $order->getId(),
  191. ($userEmail) ? $userEmail->getValue() : '',
  192. ],
  193. $descriptionTemplate
  194. );
  195. return $description;
  196. }
  197. /**
  198. * calculates the control signature for the request
  199. * @param string $apiKey
  200. * @param string $paymentMethodCode
  201. * @param string $encodedPaymentData
  202. * @param string $successUrl
  203. * @param string $password
  204. * @return string
  205. */
  206. private function getPaymentFormSignature(
  207. string $apiKey,
  208. string $paymentMethodCode,
  209. string $encodedPaymentData,
  210. string $successUrl,
  211. string $password
  212. ): string
  213. {
  214. return md5(
  215. mb_strtoupper(
  216. strrev($apiKey)
  217. . strrev($paymentMethodCode)
  218. . strrev($encodedPaymentData)
  219. . strrev($successUrl)
  220. . strrev($password)
  221. )
  222. );
  223. }
  224. /**
  225. * @inheritDoc
  226. */
  227. public function getCurrencyList()
  228. {
  229. return ['UAH'];
  230. }
  231. /**
  232. * @inheritDoc
  233. */
  234. public function processRequest(Payment $payment, Request $request)
  235. {
  236. $result = new PaySystem\ServiceResult();
  237. Logger::addDebugInfo(__CLASS__ . ': request payload: ' . Json::encode($request->getValues(), JSON_UNESCAPED_UNICODE));
  238. $signatureCheckResult = $this->checkCallbackSignature($payment, $request);
  239. if ($signatureCheckResult->isSuccess())
  240. {
  241. $transactionId = $request->get('id');
  242. $transactionStatus = $request->get('status');
  243. if ($transactionId && $transactionStatus === self::TRANSACTION_STATUS_SALE)
  244. {
  245. $description = Loc::getMessage('SALE_HPS_PLATON_TRANSACTION_DESCRIPTION', [
  246. '#ID#' => $request->get('id'),
  247. '#PAYMENT_NUMBER#' => $request->get('order'),
  248. ]);
  249. $requestSum = $request->get('amount');
  250. $paymentFields = [
  251. 'PS_INVOICE_ID' => $transactionId,
  252. 'PS_STATUS_CODE' => $transactionStatus,
  253. 'PS_STATUS_DESCRIPTION' => $description,
  254. 'PS_SUM' => $requestSum,
  255. 'PS_CURRENCY' => $request->get('currency'),
  256. 'PS_RESPONSE_DATE' => new Main\Type\DateTime(),
  257. 'PS_STATUS' => 'N',
  258. 'PS_CARD_NUMBER' => $request->get('card'),
  259. ];
  260. if ($this->checkPaymentSum($payment, $requestSum))
  261. {
  262. $paymentFields['PS_STATUS'] = 'Y';
  263. $result->setOperationType(PaySystem\ServiceResult::MONEY_COMING);
  264. $result->setPsData($paymentFields);
  265. }
  266. else
  267. {
  268. $errorMessage = Loc::getMessage('SALE_HPS_PLATON_SUM_MISMATCH');
  269. $paymentFields['PS_STATUS_DESCRIPTION'] .= $description . ' ' . $errorMessage;
  270. $result->addError(new Main\Error($errorMessage));
  271. }
  272. }
  273. elseif ($transactionStatus === self::TRANSACTION_STATUS_REFUND)
  274. {
  275. $oldDescription = $payment->getField('PS_STATUS_DESCRIPTION');
  276. $newDescription = str_replace(
  277. ' ' . Loc::getMessage('SALE_HPS_PLATON_REFUND_IN_PROCESS'),
  278. '',
  279. $oldDescription
  280. );
  281. $payment->setField('PS_STATUS_DESCRIPTION', $newDescription);
  282. $result->setOperationType(PaySystem\ServiceResult::MONEY_LEAVING);
  283. }
  284. else
  285. {
  286. $errorMessage = $request->get('error_message');
  287. if (!isset($errorMessage))
  288. {
  289. $errorMessage = Loc::getMessage('SALE_HPS_PLATON_REQUEST_ERROR');
  290. }
  291. $result->addError(new Main\Error($errorMessage));
  292. }
  293. }
  294. else
  295. {
  296. $result->addError(new Main\Error(Loc::getMessage('SALE_HPS_PLATON_SIGNATURE_MISMATCH')));
  297. }
  298. return $result;
  299. }
  300. /**
  301. * checks the callback signature to see if the callback is genuine
  302. * @param Request $request
  303. * @return ServiceResult
  304. */
  305. private function checkCallbackSignature(Payment $payment, Request $request): PaySystem\ServiceResult
  306. {
  307. $result = new ServiceResult();
  308. $callbackSignature = $request->get('sign');
  309. $email = $this->getUserEmailValue($payment);
  310. $password = $this->getBusinessValue($payment, 'PLATON_PASSWORD');
  311. $order = $payment->getField('ACCOUNT_NUMBER');
  312. $card = $request->get('card');
  313. $localSignature = $this->getCallbackSignature($email, $password, $order, $card);
  314. Logger::addDebugInfo(__CLASS__ . ": local signature: $localSignature, callback signature: $callbackSignature");
  315. if ($callbackSignature !== $localSignature)
  316. {
  317. $result->addError(new Main\Error('signature mismatch'));
  318. }
  319. return $result;
  320. }
  321. /**
  322. * checks if the actual sum of the payment is equal to the sum paid by the customer
  323. * @param Payment $payment
  324. * @param $requestSum
  325. * @return bool
  326. */
  327. private function checkPaymentSum(Payment $payment, $requestSum): bool
  328. {
  329. $roundedRequestSum = PriceMaths::roundPrecision($requestSum);
  330. $roundedPaymentSum = PriceMaths::roundPrecision($payment->getSum());
  331. Logger::addDebugInfo(__CLASS__ . ": request sum: $roundedRequestSum, payment sum: $roundedPaymentSum");
  332. return $roundedRequestSum === $roundedPaymentSum;
  333. }
  334. /**
  335. * @inheritDoc
  336. */
  337. public static function getIndicativeFields()
  338. {
  339. return [
  340. self::CALLBACK_ORDER_PARAM,
  341. self::CALLBACK_EXT1_PARAM,
  342. self::CALLBACK_EXT2_PARAM,
  343. ];
  344. }
  345. /**
  346. * @inheritDoc
  347. */
  348. protected static function isMyResponseExtended(Request $request, $paySystemId)
  349. {
  350. return (int)$request->get(self::CALLBACK_EXT2_PARAM) === (int)$paySystemId;
  351. }
  352. /**
  353. * @inheritDoc
  354. */
  355. public function getPaymentIdFromRequest(Request $request)
  356. {
  357. return $request->get('order');
  358. }
  359. /**
  360. * @inheritDoc
  361. */
  362. public function refund(Payment $payment, $refundableSum)
  363. {
  364. $result = new PaySystem\ServiceResult();
  365. $transactionId = $payment->getField('PS_INVOICE_ID');
  366. $cardNumber = $payment->getField('PS_CARD_NUMBER');
  367. if ($cardNumber)
  368. {
  369. $formattedPaymentSum = $this->getFormattedPaymentSum($payment);
  370. $apiKey = $this->getBusinessValue($payment, 'PLATON_API_KEY');
  371. $email = $this->getUserEmailValue($payment);
  372. $password = $this->getBusinessValue($payment, 'PLATON_PASSWORD');
  373. $signature = $this->getCallbackSignature($email, $password, $transactionId, $cardNumber);
  374. $fields = [
  375. 'action' => self::REFUND_ACTION,
  376. 'client_key' => $apiKey,
  377. 'trans_id' => $transactionId,
  378. 'amount' => $formattedPaymentSum,
  379. 'hash' => $signature,
  380. ];
  381. $responseResult = $this->send($this->getUrl($payment, "requestUrl"), $fields);
  382. if (!$responseResult->isSuccess())
  383. {
  384. $result->addErrors($responseResult->getErrors());
  385. return $result;
  386. }
  387. $responseData = $responseResult->getData();
  388. Logger::addDebugInfo(__CLASS__ . ': refund payload: ' . Json::encode($responseData, JSON_UNESCAPED_UNICODE));
  389. switch ($responseData['result'])
  390. {
  391. case self::REFUND_STATUS_ACCEPTED:
  392. $newDescription = $payment->getField('PS_STATUS_DESCRIPTION') . ' ' . Loc::getMessage('SALE_HPS_PLATON_REFUND_IN_PROCESS');
  393. $payment->setField('PS_STATUS_DESCRIPTION', $newDescription);
  394. $result->setData($responseData);
  395. break;
  396. case self::REFUND_STATUS_ERROR:
  397. $result->addError(new Main\Error(Loc::getMessage('SALE_HPS_PLATON_RESPONSE_ERROR', [
  398. '#PS_RESPONSE#' => $responseData['error_message'],
  399. ])));
  400. break;
  401. default:
  402. $result->addError(new Main\Error(Loc::getMessage('SALE_HPS_PLATON_REFUND_ERROR')));
  403. }
  404. }
  405. else
  406. {
  407. $result->addError(new Main\Error(Loc::getMessage('SALE_HPS_PLATON_ERROR_CARD_NOT_FOUND')));
  408. }
  409. return $result;
  410. }
  411. /**
  412. * sends a request to the specified url
  413. * @param string $url
  414. * @param array $params
  415. * @return Result
  416. */
  417. private function send(string $url, array $params): Result
  418. {
  419. $result = new Result();
  420. $httpClient = new HttpClient();
  421. $response = $httpClient->post($url, $params);
  422. if ($response === false)
  423. {
  424. $errors = $httpClient->getError();
  425. foreach ($errors as $code =>$message)
  426. {
  427. $result->addError(new Main\Error($message, $code));
  428. }
  429. }
  430. else
  431. {
  432. $responseData = $this->decode($response);
  433. $result->setData($responseData);
  434. }
  435. return $result;
  436. }
  437. /**
  438. * @param string $email
  439. * @param string $password
  440. * @param string $transactionId
  441. * @param string $cardNumber
  442. * @return string
  443. */
  444. private function getCallbackSignature(string $email, string $password, string $transactionId, string $cardNumber): string
  445. {
  446. return md5(
  447. mb_strtoupper(
  448. strrev($email)
  449. . $password
  450. . $transactionId
  451. . strrev(
  452. mb_substr($cardNumber, 0, 6)
  453. . mb_substr($cardNumber, -4)
  454. )
  455. )
  456. );
  457. }
  458. /**
  459. * @param Payment $payment
  460. * @return string
  461. */
  462. private function getUserEmailValue(Payment $payment): string
  463. {
  464. $email = '';
  465. $emailProperty = $payment->getOrder()->getPropertyCollection()->getUserEmail();
  466. if ($emailProperty)
  467. {
  468. $email = $emailProperty->getValue();
  469. }
  470. return $email ?? '';
  471. }
  472. protected function getUrlList()
  473. {
  474. return [
  475. 'formActionUrl' => 'https://secure.platononline.com/payment/auth',
  476. 'requestUrl' => 'https://secure.platononline.com/post-unq/',
  477. ];
  478. }
  479. /**
  480. * @inheritDoc
  481. */
  482. public static function getHandlerModeList(): array
  483. {
  484. return [
  485. self::PS_MODE_BANK_CARD => Loc::getMessage('SALE_HPS_PLATON_MODE_CARD'),
  486. self::PS_MODE_GOOGLE_PAY => Loc::getMessage('SALE_HPS_PLATON_MODE_GOOGLE_PAY'),
  487. self::PS_MODE_APPLE_PAY => Loc::getMessage('SALE_HPS_PLATON_MODE_APPLE_PAY'),
  488. self::PS_MODE_PRIVAT24 => Loc::getMessage('SALE_HPS_PLATON_MODE_PRIVAT24'),
  489. ];
  490. }
  491. }