2namespace Sale\Handlers\PaySystem;
5use Bitrix\Crm\EntityPreset;
6use Bitrix\Crm\EntityRequisite;
9use Bitrix\Main\Localization\Loc;
10use Bitrix\Main\PhoneNumber;
11use Bitrix\Main\Request;
13use Bitrix\Sale\BasketItem;
15use Bitrix\Sale\PayableBasketItem;
16use Bitrix\Sale\Payment;
17use Bitrix\Sale\PaymentCollection;
18use Bitrix\Sale\PaySystem;
19use Bitrix\Sale\PriceMaths;
20use Bitrix\Sale\Services\Base\RestrictionInfoCollection;
21use Bitrix\Sale\Services\PaySystem\Restrictions\RestrictableServiceHandler;
22use Bitrix\Sale\Services\PaySystem\Restrictions\RestrictionCurrencyTrait;
23use Bitrix\Sale\Services\PaySystem\Restrictions\RestrictionPersonTypeTrait;
29 protected const SANDBOX_URL =
'https://business.tbank.ru/openapi/sandbox/';
30 protected const PRODUCTION_URL =
'https://business.tbank.ru/openapi/';
32 public const ERROR_CODE_SYSTEM_FAILURE = -1;
33 public const ERROR_CODE_RESPONSE_FAILURE = -2;
34 public const ERROR_CODE_BAD_INVOICE_SUM = -3;
35 public const ERROR_CODE_BAD_INVOICE_PARAMS = -4;
37 protected const ERROR_STATUS_BAD_JSON = -1;
39 protected const PAYMENT_STATUS_DRAFT =
'DRAFT';
40 protected const PAYMENT_STATUS_SUBMITTED =
'SUBMITTED';
41 protected const PAYMENT_STATUS_EXECUTED =
'EXECUTED';
43 protected const PAYMENT_CURRENCY_RUB =
'RUB';
45 protected const RESPONSE_STATUS_200 = 200;
46 protected const RESPONSE_STATUS_400 = 400;
47 protected const RESPONSE_STATUS_401 = 401;
48 protected const RESPONSE_STATUS_403 = 403;
49 protected const RESPONSE_STATUS_422 = 422;
50 protected const RESPONSE_STATUS_429 = 429;
51 protected const RESPONSE_STATUS_500 = 500;
53 protected const RESPONSE_ERROR_INVOICE_NOT_FOUND =
'INVOICE_NOT_FOUND';
55 protected const TEMPLATE_INVOICE_INFO =
'template';
56 protected const TEMPLATE_INVOICE_STATUS =
'template_status';
57 protected const TEMPLATE_INVOICE_EXPIRED =
'template_expired';
59 public function initiatePay(Payment
$payment, ?Request
$request =
null): PaySystem\ServiceResult
61 $result =
new PaySystem\ServiceResult();
63 if (!Loader::includeModule(
'crm'))
65 $result->addError(
new Main\
Error(Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_MODULE_CRM_ABSENT')));
71 if (
$payment->getField(
'PS_INVOICE_ID'))
73 $checkInvoiceResult = $this->checkInvoiceState(
$payment);
74 if ($this->isInvalidCheckInvoiceResult($checkInvoiceResult))
76 return $checkInvoiceResult;
78 $invoiceState = $checkInvoiceResult->getData();
79 unset($checkInvoiceResult);
82 if ($this->needCreateInvoice($invoiceState))
84 $createInvoiceResult = $this->createInvoice(
$payment);
85 if (!$createInvoiceResult->isSuccess())
87 return $createInvoiceResult;
90 $invoiceState = $createInvoiceResult->getData();
92 if (isset($invoiceState[
'response'][
'invoiceId']))
94 $result->setPsData([
'PS_INVOICE_ID' => $invoiceState[
'response'][
'invoiceId']]);
115 self::PAYMENT_CURRENCY_RUB,
127 $collection =
new RestrictionInfoCollection();
139 protected function isTestMode(?Payment
$payment =
null): bool
147 public function getClientType($psMode): string
149 return PaySystem\ClientType::B2B;
159 self::TEST_URL => self::SANDBOX_URL .
'api/v1/invoice/send',
160 self::ACTIVE_URL => self::PRODUCTION_URL.
'api/v1/invoice/send',
163 self::TEST_URL => self::SANDBOX_URL .
'api/v1/openapi/invoice/{invoiceId}/info',
164 self::ACTIVE_URL => self::PRODUCTION_URL .
'api/v1/openapi/invoice/{invoiceId}/info',
174 protected function getTBankAuthToken(): ?string
176 if (!Main\Loader::includeModule(
'seo'))
181 \Bitrix\Seo\Service::clearClientsCache();
182 $authAdapter = \Bitrix\Seo\Checkout\Service::getAuthAdapter(\Bitrix\Seo\Checkout\Service::TYPE_TBANK_BUSINESS);
184 return $authAdapter->getToken();
192 protected function getTBankTestToken(): string
194 return 'TinkoffOpenApiSandboxSecretToken';
207 ? $this->getTBankTestToken()
208 : $this->getTBankAuthToken()
212 'Content-Type' =>
'application/json',
213 'Accept' =>
'application/json',
214 'Authorization' =>
'Bearer ' . $token,
228 $this->isExistDatePayBefore(
$payment)
229 && !$this->isValidDatePayBefore(
$payment)
232 $template = self::TEMPLATE_INVOICE_EXPIRED;
237 if ($this->isValidPaymentStatus($currentStatus))
239 $template = self::TEMPLATE_INVOICE_STATUS;
243 return $template ?? self::TEMPLATE_INVOICE_INFO;
251 case self::TEMPLATE_INVOICE_INFO:
254 if (isset($invoiceState[
'response'][
'pdfUrl']))
256 $result[
'INVOICE_PDF_URL'] = $invoiceState[
'response'][
'pdfUrl'];
258 if (isset($invoiceState[
'response'][
'incomingInvoiceUrl']))
260 $result[
'INVOICE_PERSONAL_ACCOUNT_URL'] = $invoiceState[
'response'][
'incomingInvoiceUrl'];
263 if (isset($invoiceState[
'params'][
'dueDate']))
266 $invoiceState[
'params'][
'dueDate'],
272 case self::TEMPLATE_INVOICE_STATUS:
275 $result[
'INVOICE_STATUS'] = $this->getInvoiceStatusTitle(
276 $invoiceState[
'response'][
'status'] ??
''
290 protected function getInvoiceSumTemplateParams(Payment
$payment):
array
292 Loader::includeModule(
'currency');
300 $result[
'INVOICE_SUM_FORMATTED'] = \CCurrencyLang::CurrencyFormat(
302 self::PAYMENT_CURRENCY_RUB
313 private static function encode(
array $data): bool|string
317 return Web\Json::encode(
$data, JSON_UNESCAPED_UNICODE);
319 catch (Main\ArgumentException)
329 private static function decode(
string $data): mixed
333 return Web\Json::decode(
$data);
335 catch (Main\ArgumentException)
341 protected function checkInvoiceState(Payment
$payment): PaySystem\ServiceResult
345 $payment->getField(
'PS_INVOICE_ID'),
346 $this->getUrl(
$payment,
'invoiceCheck')
350 Web\Http\Method::GET,
356 protected function isInvalidCheckInvoiceResult(PaySystem\ServiceResult
$result): bool
363 if ($this->isInvoiceNotFound(
$result->getData()))
371 protected function isInvoiceNotFound(
array $invoiceState): bool
373 $status = (int)($invoiceState[
'status'] ?? 0);
374 $errorCode = $invoiceState[
'response'][
'errorCode'] ??
'';
377 $status === self::RESPONSE_STATUS_422
378 && $errorCode === self::RESPONSE_ERROR_INVOICE_NOT_FOUND
382 protected function needCreateInvoice(
array $invoiceState = []): bool
384 if (empty($invoiceState))
388 $currentStatus = ($invoiceState[
'response'][
'status'] ??
'');
389 if ($this->isValidPaymentStatus($currentStatus))
394 if ($this->isInvoiceNotFound($invoiceState))
406 protected function createInvoice(Payment
$payment): PaySystem\ServiceResult
408 if (!$this->isInvoiceSumCorrect(
$payment))
410 $result =
new PaySystem\ServiceResult();
412 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_BAD_INVOICE_SUM'),
413 self::ERROR_CODE_BAD_INVOICE_SUM
419 $invoiceQueryParams = $this->getInvoiceQueryParams(
$payment);
420 $result = $this->checkInvoiceQueryParams($invoiceQueryParams);
424 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_INVALID_INVOICE_PARAMS'),
432 Web\Http\Method::POST,
439 protected function getInvoiceQueryParams(Payment
$payment):
array
442 'invoiceNumber' => (string)
$payment->getId(),
443 'items' => $this->getProductsForInvoice(
$payment),
444 'contactPhone' => $this->getContactPhone(
$payment),
469 $payer = $this->getPayerData(
$payment);
478 protected function checkInvoiceQueryParams(
array $invoiceQueryParams): PaySystem\ServiceResult
480 $result =
new PaySystem\ServiceResult();
483 if (empty($invoiceQueryParams[
'payer']))
486 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_INVALID_PAYER_DATA'),
487 self::ERROR_CODE_BAD_INVOICE_PARAMS
491 if (empty($invoiceQueryParams[
'items']))
494 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_INVALID_INVOICE_ITEMS'),
495 self::ERROR_CODE_BAD_INVOICE_PARAMS
502 protected function getDueDate(Payment
$payment): string
504 if (!$this->isValidDatePayBefore(
$payment))
509 $datePayBefore =
$payment->getField(
'DATE_PAY_BEFORE');
511 return $datePayBefore->format(
'Y-m-d');
516 $payerData = $this->getCompanyPayerData(
$payment);
518 if (empty($payerData))
520 $payerData = $this->getContactPayerData(
$payment);
523 return !empty($payerData) ? $payerData :
null;
526 protected function getContactPayerData(Payment
$payment): ?
array
530 $contactFullName = $this->getContactFullName(
$payment);
532 if (!empty($contactFullName))
534 $result[
'name'] = $contactFullName;
540 protected function getCompanyPayerData(Payment
$payment): ?
array
543 $collection =
$payment->getCollection();
544 $order = $collection->getOrder();
546 $clientCollection = $this->getClientCollection(
$order);
547 if ($clientCollection ===
null)
552 $company = $clientCollection->getPrimaryCompany();
553 if ($company ===
null)
560 new Crm\Requisite\DefaultRequisite(
561 new Crm\ItemIdentifier(
562 \CCrmOwnerType::Company,
563 (
int)$company->getField(
'ENTITY_ID')
567 ->setCheckPermissions(
false)
569 $requisiteValues = $requisite->get();
571 $title = trim((
string)$company->getCustomerName());
572 $inn = trim((
string)($requisiteValues[
'RQ_INN'] ??
''));
573 $kpp = trim((
string)($requisiteValues[
'RQ_KPP'] ??
''));
575 if (
$title ===
'' || $inn ===
'')
582 if (!isset($requisiteValues[
'PRESET_ID']))
587 $companyPreset = EntityPreset::getSingleInstance()->getById($requisiteValues[
'PRESET_ID']);
590 is_array($companyPreset)
591 && isset($companyPreset[
'XML_ID'])
592 && (
string)$companyPreset[
'XML_ID'] === EntityRequisite::XML_ID_DEFAULT_PRESET_RU_COMPANY
608 protected function getProductsForInvoice(Payment
$payment):
array
610 $catalogIncluded = Loader::includeModule(
'catalog');
615 $basket =
$order->getBasket();
617 $defaultMeasure =
'';
618 if ($catalogIncluded)
620 $measureDescription = \CCatalogMeasure::getDefaultMeasure(
true,
false);
621 $defaultMeasure = $measureDescription[
'SYMBOL_RUS'];
622 unset($measureDescription);
652 foreach (
$payment->getPayableItemCollection()->getBasketItems() as $payableBasketItem)
655 $basketItem = $basket->getItemById($payableBasketItem->getField(
'ENTITY_ID'));
659 'name' => $basketItem->getField(
'NAME'),
660 'amount' => $basketItem->getQuantity(),
661 'price' => $basketItem->getPriceWithVat(),
664 $vat = $basketItem->getVatRate();
671 $vat = (string)((
int)((
float)
$vat * 100));
674 $row[
'unit'] = (string)$basketItem->getField(
'MEASURE_NAME') ?: $defaultMeasure;
689 protected function getContactPhone(Payment
$payment): string
692 $collection =
$payment->getCollection();
693 $phoneNumber = $this->getClientPhoneNumber($collection->getOrder());
697 ? $this->normalizePhoneNumber($phoneNumber)
702 protected function getContactFullName(Payment
$payment): ?string
705 $collection =
$payment->getCollection();
706 $order = $collection->getOrder();
708 $clientCollection = $this->getClientCollection(
$order);
709 if (!$clientCollection)
714 $clientFullName =
null;
716 $clientId = $this->getPrimaryContactId($clientCollection);
718 $factory = Crm\Service\Container::getInstance()->getFactory(\CCrmOwnerType::Contact);
721 $contactItem = $factory->getItem(
$clientId, [
'FULL_NAME']);
722 $clientFullName = $contactItem->getFullName();
725 return $clientFullName;
734 protected function getClientPhoneNumber(
Order $order): ?string
736 $clientCollection = $this->getClientCollection(
$order);
737 if (!$clientCollection)
744 $clientId = $this->getPrimaryContactId($clientCollection);
749 $clientId = $this->getPrimaryCompanyId($clientCollection);
755 $crmFieldMultiResult = \CCrmFieldMulti::GetList(
762 'TYPE_ID' =>
'PHONE',
765 while ($crmFieldMultiData = $crmFieldMultiResult->Fetch())
767 $phoneNumber = $crmFieldMultiData[
'VALUE'];
775 $crmFieldMultiResult,
788 protected function normalizePhoneNumber(
string $phoneNumber): string
790 $phoneNumber = trim($phoneNumber);
791 if ($phoneNumber ===
'')
796 $parser = PhoneNumber\Parser::getInstance();
797 $number = $parser->parse($phoneNumber);
799 return (
string)$number->format(PhoneNumber\
Format::E164);
802 protected function getEmail(Payment
$payment): ?string
805 $collection =
$payment->getCollection();
806 $order = $collection->getOrder();
807 $userEmail =
$order->getPropertyCollection()->getUserEmail();
809 return $userEmail?->getValue();
812 protected function getInvoiceComment(Payment
$payment): string
815 $collection =
$payment->getCollection();
816 $order = $collection->getOrder();
827 $payment->getField(
'ACCOUNT_NUMBER'),
828 $order->getField(
'ACCOUNT_NUMBER'),
833 $this->getBusinessValue(
$payment,
'TBB_COMMENT_TEMPLATE') ??
''
839 protected function getInvoiceSum(Payment
$payment): ?float
845 $basket =
$order->getBasket();
858 foreach (
$payment->getPayableItemCollection()->getBasketItems() as $payableBasketItem)
861 $basketItem = $basket->getItemById($payableBasketItem->getField(
'ENTITY_ID'));
865 $price += $basketItem->getFinalPrice();
882 protected function isInvoiceSumCorrect(Payment
$payment): bool
884 $paymentSum = PriceMaths::roundPrecision(
$payment->getSum());
891 'paymentSum' => $paymentSum,
900 $result =
new PaySystem\ServiceResult();
902 $httpClient =
new Web\HttpClient();
903 $httpClient->setHeaders($headers);
907 case Web\Http\Method::GET:
910 case Web\Http\Method::POST:
916 $this->addDebugInfo(
'request data',
$postData);
927 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_UNSUPPORTED_PROTOCOL'),
928 self::ERROR_CODE_SYSTEM_FAILURE
934 $this->addDebugInfo(
'response data',
$response);
936 $status = $httpClient->getStatus();
940 $status = self::ERROR_STATUS_BAD_JSON;
945 case self::ERROR_STATUS_BAD_JSON:
951 Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_UNKNOWN_ANSWER'),
952 self::ERROR_CODE_SYSTEM_FAILURE
955 case self::RESPONSE_STATUS_200:
962 case self::RESPONSE_STATUS_400:
963 case self::RESPONSE_STATUS_401:
964 case self::RESPONSE_STATUS_403:
965 case self::RESPONSE_STATUS_422:
966 case self::RESPONSE_STATUS_429:
967 case self::RESPONSE_STATUS_500:
975 $this->addDebugInfo(
'invalid request',
$response);
982 protected function addDebugInfo(
string $message, mixed
$data): void
994 if (method_exists(
$data,
'__toString'))
1003 PaySystem\Logger::addDebugInfo(__CLASS__ .
': ' .
$message .
': ' .
$data);
1010 self::RESPONSE_STATUS_400 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_400_INVALID_REQUEST'),
1011 self::RESPONSE_STATUS_401 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_401_BAD_AUTHENTICATION'),
1012 self::RESPONSE_STATUS_403 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_403_BAD_AUTHORIZATION'),
1013 self::RESPONSE_STATUS_422 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_422_BAD_DATA'),
1014 self::RESPONSE_STATUS_429 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_429_TOO_MANY_REQUESTS'),
1015 self::RESPONSE_STATUS_500 => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_ERROR_500_SERVER'),
1016 default => Loc::getMessage(
1017 'SALE_HPS_TBANK_BUSINESS_ERROR_UNKNOWN_STATUS',
1034 return new Main\Error(
1036 $response[
'errorCode'] ?? self::ERROR_CODE_RESPONSE_FAILURE
1040 protected function getInvoiceStatusTitle(
string $status): string
1042 return (
string)match (
$status)
1044 self::PAYMENT_STATUS_DRAFT => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_INVOICE_STATUS_DRAFT'),
1045 self::PAYMENT_STATUS_SUBMITTED => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_INVOICE_STATUS_SUBMITTED'),
1046 self::PAYMENT_STATUS_EXECUTED => Loc::getMessage(
'SALE_HPS_TBANK_BUSINESS_INVOICE_STATUS_EXECUTED'),
1051 protected function isValidPaymentStatus(
string $status): bool
1054 $status === self::PAYMENT_STATUS_DRAFT
1055 ||
$status === self::PAYMENT_STATUS_SUBMITTED
1056 ||
$status === self::PAYMENT_STATUS_EXECUTED
1060 protected function isValidDatePayBefore(Payment
$payment): bool
1062 $datePayBefore =
$payment->getField(
'DATE_PAY_BEFORE');
1063 if ($datePayBefore instanceof Main\Type\Date)
1065 $currentDate =
new Main\Type\Date();
1066 if ($datePayBefore->getTimestamp() >= $currentDate->getTimestamp())
1075 protected function isExistDatePayBefore(Payment
$payment): bool
1077 return $payment->getField(
'DATE_PAY_BEFORE') instanceof Main\Type\Date;
1080 protected function getClientCollection(
Order $order): ?Crm\
Order\ContactCompanyCollection
1084 return $order->getContactCompanyCollection();
1090 protected function getPrimaryCompanyId(Crm\
Order\ContactCompanyCollection $collection): ?int
1092 $company = $collection->getPrimaryCompany();
1093 if ($company !==
null)
1095 $companyId = $company->getField(
'ENTITY_ID');
1097 return $companyId ===
null ? null : (int)$companyId;
1103 protected function getPrimaryContactId(Crm\
Order\ContactCompanyCollection $collection): ?int
1105 $contact = $collection->getPrimaryContact();
1106 if ($contact !==
null)
1108 $contactId = $contact->getField(
'ENTITY_ID');
1110 return $contactId ===
null ? null : (int)$contactId;
if(!Loader::includeModule('catalog')) if(!AccessController::getCurrent() ->check(ActionDictionary::ACTION_PRICE_EDIT)) if(!check_bitrix_sessid()) $request
getUrl(Payment $payment=null, $action)
showTemplate(Payment $payment=null, $template='')
setExtraParams(array $values)
getBusinessValue(Payment $payment=null, $code)
</td ></tr ></table ></td ></tr >< tr >< td class="bx-popup-label bx-width30"><?=GetMessage("PAGE_NEW_TAGS")?> array( $site)
if(Loader::includeModule( 'bitrix24')) elseif(Loader::includeModule('intranet') &&CIntranetUtils::getPortalZone() !=='ru') $description
mydump($thing, $maxdepth=-1, $depth=0)
trait RestrictionCurrencyTrait
getRestrictionCurrency(RestrictionInfoCollection $collection)
if( $daysToExpire >=0 &&$daysToExpire< 60 elseif)( $daysToExpire< 0)
if($inWords) echo htmlspecialcharsbx(Number2Word_Rus(roundEx($totalVatSum $params['CURRENCY']