Bitrix-D7 23.9
 
Загрузка...
Поиск...
Не найдено
cashboxyookassa.php
1<?php
2namespace Bitrix\Sale\Cashbox;
3
10use Bitrix\Sale;
11use Bitrix\Seo;
12
13Loc::loadMessages(__FILE__);
14
20{
21 private const MAX_NAME_LENGTH = 128;
22
23 private const URL = 'https://api.yookassa.ru/v3/receipts/';
24
25 private const CODE_NO_VAT = 1;
26 private const CODE_VAT_0 = 2;
27 private const CODE_VAT_10 = 3;
28 private const CODE_VAT_20 = 4;
29
30 private const SETTLEMENT_TYPE_PREPAYMENT = 'prepayment';
31 private const CHECK_TYPE_PAYMENT = 'payment';
32
33 private const MARK_CODE_TYPE_GS1M = 'gs_1m';
34
35 public static function getName(): string
36 {
37 return Loc::getMessage('SALE_CASHBOX_YOOKASSA_TITLE');
38 }
39
40 public function buildCheckQuery(Check $check): array
41 {
42 $checkParamsResult = $this->checkParams($check);
43 if (!$checkParamsResult->isSuccess())
44 {
45 return [];
46 }
47
48 $payment = CheckManager::getPaymentByCheck($check);
49 if (!$payment)
50 {
51 return [];
52 }
53
54 $checkData = $check->getDataForCheck();
55 $fields = [
56 'customer' => [],
57 'items' => [],
58 'tax_system_code' => $this->getValueFromSettings('TAX', 'SNO'),
59 ];
60
61 if (isset($checkData['client_email']))
62 {
63 $fields['customer']['email'] = $checkData['client_email'];
64 }
65
66 if (isset($checkData['client_phone']))
67 {
68 $phoneParser = PhoneNumber\Parser::getInstance();
69 if ($phoneParser)
70 {
71 $phoneNumber = $phoneParser->parse($checkData['client_phone']);
72 if ($phoneNumber->isValid())
73 {
74 $fields['customer']['phone'] = $phoneNumber->format(PhoneNumber\Format::E164);
75 }
76 }
77 }
78
79 $paymentModeMap = $this->getCheckTypeMap();
80 $paymentMode = $paymentModeMap[$check::getType()];
81 $paymentObjectMap = $this->getPaymentObjectMap();
82
83 foreach ($checkData['items'] as $item)
84 {
85 $vat = $this->getValueFromSettings('VAT', $item['vat']);
86 $vat = $vat ?? $this->getValueFromSettings('VAT', 'NOT_VAT');
87
88 $measure = $this->getValueFromSettings('MEASURE', $item['measure_code']);
89 $measure = $measure ?? $this->getValueFromSettings('MEASURE', 'DEFAULT');
90
91 $receiptItem = [
92 'description' => mb_substr($item['name'], 0, self::MAX_NAME_LENGTH),
93 'amount' => [
94 'value' => (string)Sale\PriceMaths::roundPrecision($item['price']),
95 'currency' => (string)$item['currency'],
96 ],
97 'vat_code' => (int)$vat,
98 'quantity' => (string)$item['quantity'],
99 'measure' => (string)$measure,
100 'payment_subject' => $paymentObjectMap[$item['payment_object']],
101 'payment_mode' => $paymentMode,
102 ];
103
104 if (!empty($item['marking_code']))
105 {
106 $receiptItem['mark_code_info'] = $this->buildPositionMarkCode($item);
107 }
108
109 $fields['items'][] = $receiptItem;
110 }
111
112 if ($this->needDataForSecondCheck($payment))
113 {
114 $fields['send'] = true;
115 $fields['type'] = self::CHECK_TYPE_PAYMENT;
116 $fields['payment_id'] = $payment->getField('PS_INVOICE_ID');
117 $fields['settlements'] = [];
118
119 foreach ($checkData['payments'] as $paymentItem)
120 {
121 $fields['settlements'][] = [
122 'type' => self::SETTLEMENT_TYPE_PREPAYMENT,
123 'amount' => [
124 'value' => (string)Sale\PriceMaths::roundPrecision($paymentItem['sum']),
125 'currency' => (string)$paymentItem['currency'],
126 ],
127 ];
128 }
129 }
130
131 return $fields;
132 }
133
134 private function buildPositionMarkCode(array $item): array
135 {
136 return [
137 self::MARK_CODE_TYPE_GS1M => $item['marking_code'],
138 ];
139 }
140
141 protected function getPrintUrl(): string
142 {
143 return self::URL;
144 }
145
146 protected function getCheckUrl(): string
147 {
148 return self::URL;
149 }
150
151 protected function getDataForCheck(Sale\Payment $payment): array
152 {
153 return [
154 'payment_id' => $payment->getField('PS_INVOICE_ID'),
155 ];
156 }
157
158 protected function send(string $url, Sale\Payment $payment, array $fields, string $method = self::SEND_METHOD_HTTP_POST): Sale\Result
159 {
160 $result = new Sale\Result();
161
162 $httpClient = new Main\Web\HttpClient();
163 $headers = $this->getHeaders($payment);
164 foreach ($headers as $name => $value)
165 {
166 $httpClient->setHeader($name, $value);
167 }
168
169 if ($method === self::SEND_METHOD_HTTP_POST)
170 {
171 $data = self::encode($fields);
172 Logger::addDebugInfo(__CLASS__ . ': request data: ' . $data);
173 $response = $httpClient->post($url, $data);
174 }
175 else
176 {
177 $uri = new Uri($url);
178 $uri->addParams($fields);
179 $response = $httpClient->get($uri->getUri());
180 }
181
182 if ($response === false || $response === '')
183 {
184 $result->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_EMPTY_RESPONSE')));
185
186 $errors = $httpClient->getError();
187 foreach ($errors as $code => $message)
188 {
189 $result->addError(new Error($message, $code));
190 }
191
192 return $result;
193 }
194
195 Logger::addDebugInfo(__CLASS__ . ': response data: ' . $response);
196
197 $response = static::decode($response);
198 if (!$response)
199 {
200 $result->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_DECODE_RESPONSE')));
201 return $result;
202 }
203
204 $result->setData($response);
205
206 return $result;
207 }
208
209 protected function processPrintResult(Sale\Result $result): Sale\Result
210 {
211 return new Sale\Result();
212 }
213
214 protected function processCheckResult(Sale\Result $result): Sale\Result
215 {
216 $processCheckResult = new Sale\Result();
217 $data = $result->getData();
218
222 if (isset($data['type']) && $data['type'] === 'error')
223 {
224 $errorCode = $data['code'] ?? '';
225 switch ($errorCode)
226 {
227 case 'internal_server_error':
228 case 'too_many_requests':
229 $processCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_WAIT')));
230 break;
231
232 default:
233 $processCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_PROCESSING')));
234 break;
235 }
236
237 return $processCheckResult;
238 }
239
240 $processCheckResult->setData($data);
241
242 return $processCheckResult;
243 }
244
245 protected function onAfterProcessCheck(Sale\Result $result, Sale\Payment $payment): Sale\Result
246 {
247 $onAfterProcessCheckResult = new Sale\Result();
248 $checkList = CheckManager::getList([
249 'select' => ['ID'],
250 'filter' => [
251 'ORDER_ID' => $payment->getOrderId(),
252 ],
253 'order' => ['ID' => 'DESC'],
254 ])->fetchAll();
255
256 $data = $result->getData();
257 $checkData = [];
258 if (isset($data['type']) && $data['type'] === 'list')
259 {
260 $checkData = $data['items'] ?? [];
261 }
262
263 if ($checkList)
264 {
265 if (!$checkData)
266 {
267 $externalCheck = [
268 'checkId' => $checkList[0]['ID'],
269 'error' => [
270 'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_NOT_FOUND'),
271 'TYPE' => Errors\Error::TYPE,
272 ],
273 ];
274 $applyCheckResult = static::applyCheckResult($externalCheck);
275 $onAfterProcessCheckResult->addErrors($applyCheckResult->getErrors());
276 }
277
278 foreach ($checkData as $key => $externalCheck)
279 {
280 $checkStatus = $externalCheck['status'] ?? '';
281 switch ($checkStatus)
282 {
283 case 'pending':
284 $externalCheck['error'] = [
285 'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_STATUS_CHECK_PENDING'),
286 'TYPE' => Errors\Warning::TYPE,
287 ];
288 break;
289
290 case 'canceled':
291 $externalCheck['error'] = [
292 'MESSAGE' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_STATUS_CHECK_CANCELLED'),
293 'TYPE' => Errors\Error::TYPE,
294 ];
295 break;
296 }
297
298 $externalCheck['checkId'] = $checkList[$key]['ID'];
299 $applyCheckResult = static::applyCheckResult($externalCheck);
300 if (!$applyCheckResult->isSuccess())
301 {
302 $onAfterProcessCheckResult->addErrors($applyCheckResult->getErrors());
303 }
304 }
305 }
306 else
307 {
308 $onAfterProcessCheckResult->addError(new Error(Loc::getMessage('SALE_CASHBOX_YOOKASSA_ERROR_CHECK_NOT_FOUND')));
309 }
310
311 return $onAfterProcessCheckResult;
312 }
313
314 protected static function extractCheckData(array $data): array
315 {
316 $result = [];
317
318 $id = $data['checkId'] ?? null;
319 if (!$id)
320 {
321 return $result;
322 }
323 $result['ID'] = $id;
324
325 if ($data['error'])
326 {
327 $result['ERROR'] = $data['error'];
328 }
329
330 if ($data['id'])
331 {
332 $result['EXTERNAL_UUID'] = $data['id'];
333 }
334
335 $check = CheckManager::getObjectById($id);
336 if ($check)
337 {
338 $result['LINK_PARAMS'] = [
339 AbstractCheck::PARAM_FISCAL_DOC_ATTR => $data['fiscal_attribute'],
340 AbstractCheck::PARAM_FISCAL_DOC_NUMBER => $data['fiscal_document_number'],
341 AbstractCheck::PARAM_FN_NUMBER => $data['fiscal_storage_number'],
342 AbstractCheck::PARAM_FISCAL_RECEIPT_NUMBER => $data['fiscal_provider_id'],
343 AbstractCheck::PARAM_DOC_SUM => (float)$check->getField('SUM'),
344 AbstractCheck::PARAM_CALCULATION_ATTR => $check::getCalculatedSign()
345 ];
346
347 if (!empty($data['registered_at']))
348 {
349 try
350 {
351 // ISO 8601 datetime
352 $dateTime = new Main\Type\DateTime($data['registered_at'], 'Y-m-d\TH:i:s.v\Z');
353 $result['LINK_PARAMS'][AbstractCheck::PARAM_DOC_TIME] = $dateTime->getTimestamp();
354 }
355 catch (Main\ObjectException $ex)
356 {}
357 }
358 }
359
360 return $result;
361 }
362
363 public static function getPaySystemCodeForKkm(): string
364 {
365 return 'YANDEX_CHECKOUT_SHOP_ID';
366 }
367
368 public static function getKkmValue(Sale\PaySystem\Service $service): array
369 {
370 if (self::isOAuth())
371 {
372 return [md5(static::getYandexToken())];
373 }
374
375 return parent::getKkmValue($service);
376 }
377
378 public static function getSettings($modelId = 0): array
379 {
380 $settings = [];
381
382 $settings['TAX'] = [
383 'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_SNO'),
384 'REQUIRED' => 'Y',
385 'ITEMS' => [
386 'SNO' => [
387 'TYPE' => 'ENUM',
388 'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_SNO_LABEL'),
389 'VALUE' => 1,
390 'OPTIONS' => [
391 1 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_OSN'),
392 2 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_UI'),
393 3 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_UIO'),
394 4 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_ENVD'),
395 5 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_ESN'),
396 6 => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SNO_PATENT'),
397 ],
398 ],
399 ],
400 ];
401
402 $settings['VAT'] = [
403 'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_VAT'),
404 'REQUIRED' => 'Y',
405 'COLLAPSED' => 'Y',
406 'ITEMS' => [
407 'NOT_VAT' => [
408 'TYPE' => 'STRING',
409 'LABEL' => Loc::getMessage('SALE_CASHBOX_YOOKASSA_SETTINGS_VAT_LABEL_NOT_VAT'),
410 'VALUE' => self::CODE_NO_VAT,
411 ],
412 ],
413 ];
414
415 if (Loader::includeModule('catalog'))
416 {
417 $dbRes = \Bitrix\Catalog\VatTable::getList(['filter' => [
418 'ACTIVE' => 'Y',
419 'EXCLUDE_VAT' => 'N',
420 ]]);
421 $vatList = $dbRes->fetchAll();
422 if ($vatList)
423 {
424 $defaultVatList = [
425 0 => self::CODE_VAT_0,
426 10 => self::CODE_VAT_10,
427 20 => self::CODE_VAT_20,
428 ];
429
430 foreach ($vatList as $vat)
431 {
432 $value = $defaultVatList[(int)$vat['RATE']] ?? '';
433
434 $settings['VAT']['ITEMS'][(int)$vat['ID']] = [
435 'TYPE' => 'STRING',
436 'LABEL' => $vat['NAME'] . ' (' . (int)$vat['RATE'] . '%)',
437 'VALUE' => $value,
438 ];
439 }
440 }
441 }
442
443 $measureItems = [
444 'DEFAULT' => [
445 'TYPE' => 'STRING',
446 'LABEL' => Loc::getMessage('SALE_CASHBOX_MEASURE_SUPPORT_SETTINGS_DEFAULT_VALUE'),
447 'VALUE' => 'piece',
448 ],
449 ];
450 if (Loader::includeModule('catalog'))
451 {
452 $measuresList = \CCatalogMeasure::getList();
453 while ($measure = $measuresList->fetch())
454 {
455 $measureItems[$measure['CODE']] = [
456 'TYPE' => 'STRING',
457 'LABEL' => $measure['MEASURE_TITLE'],
458 'VALUE' => MeasureCodeToTag2108MapperYooKassa::getTag2108Value($measure['CODE']),
459 ];
460 }
461 }
462
463 $settings['MEASURE'] = [
464 'LABEL' => Loc::getMessage('SALE_CASHBOX_MEASURE_SUPPORT_SETTINGS'),
465 'REQUIRED' => 'Y',
466 'COLLAPSED' => 'Y',
467 'ITEMS' => $measureItems,
468 ];
469
470 return $settings;
471 }
472
476 public static function getFfdVersion(): ?float
477 {
478 return 1.2;
479 }
480
484 protected function getCheckTypeMap(): array
485 {
486 return [
487 FullPrepaymentCheck::getType() => 'full_prepayment',
488 PrepaymentCheck::getType() => 'partial_prepayment',
489 AdvancePaymentCheck::getType() => 'advance',
490 SellCheck::getType() => 'full_payment',
491 CreditCheck::getType() => 'credit',
492 CreditPaymentCheck::getType() => 'credit_payment',
493 ];
494 }
495
499 private function getPaymentObjectMap(): array
500 {
501 return [
502 // FFD 1.05
503 Check::PAYMENT_OBJECT_COMMODITY => 'commodity',
509 Check::PAYMENT_OBJECT_GAMBLING_BET => 'gambling_bet',
510 Check::PAYMENT_OBJECT_GAMBLING_PRIZE => 'gambling_prize',
512 Check::PAYMENT_OBJECT_LOTTERY_PRIZE => 'lottery_prize',
513 Check::PAYMENT_OBJECT_INTELLECTUAL_ACTIVITY => 'intellectual_activity',
514 Check::PAYMENT_OBJECT_AGENT_COMMISSION => 'agent_commission',
515 Check::PAYMENT_OBJECT_PROPERTY_RIGHT => 'property_right',
516 Check::PAYMENT_OBJECT_NON_OPERATING_GAIN => 'non_operating_gain',
517 Check::PAYMENT_OBJECT_INSURANCE_PREMIUM => 'insurance_premium',
518 Check::PAYMENT_OBJECT_SALES_TAX => 'sales_tax',
519 Check::PAYMENT_OBJECT_RESORT_FEE => 'resort_fee',
520 Check::PAYMENT_OBJECT_COMPOSITE => 'composite',
522
523 // FFD 1.2
532 Check::PAYMENT_OBJECT_AGENT_WITHDRAWALS => 'agent_withdrawals',
533 Check::PAYMENT_OBJECT_PENSION_INSURANCE_IP => 'pension_insurance_without_payouts',
534 Check::PAYMENT_OBJECT_PENSION_INSURANCE => 'pension_insurance_with_payouts',
535 Check::PAYMENT_OBJECT_MEDICAL_INSURANCE_IP => 'health_insurance_without_payouts',
536 Check::PAYMENT_OBJECT_MEDICAL_INSURANCE => 'health_insurance_with_payouts',
537 Check::PAYMENT_OBJECT_SOCIAL_INSURANCE => 'health_insurance',
538 ];
539 }
540
541 protected function getCheckHttpMethod(): string
542 {
544 }
545
546 private function needDataForSecondCheck(Sale\Payment $payment): bool
547 {
548 return (bool)$payment->getField('PS_INVOICE_ID');
549 }
550
551 private function getHeaders(Sale\Payment $payment): array
552 {
553 $headers = [
554 'Content-Type' => 'application/json',
555 'Idempotence-Key' => $this->getIdempotenceKey(),
556 ];
557
558 try
559 {
560 $headers['Authorization'] = $this->getAuthorizationHeader($payment);
561 }
562 catch (\Exception $ex)
563 {
564 $headers['Authorization'] = 'Basic '.$this->getBasicAuthString($payment);
565 }
566
567 return $headers;
568 }
569
570 private function getIdempotenceKey(): string
571 {
572 return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
573 mt_rand(0, 0xffff), mt_rand(0, 0xffff),
574 mt_rand(0, 0xffff),
575 mt_rand(0, 0x0fff) | 0x4000,
576 mt_rand(0, 0x3fff) | 0x8000,
577 mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
578 );
579 }
580
581 private function getAuthorizationHeader(Sale\Payment $payment)
582 {
583 if (self::isOAuth())
584 {
585 $token = static::getYandexToken();
586 return 'Bearer '.$token;
587 }
588
589 return 'Basic '.$this->getBasicAuthString($payment);
590 }
591
592 private static function isOAuth(): bool
593 {
594 return Main\Config\Option::get('sale', 'YANDEX_CHECKOUT_OAUTH', false) == true;
595 }
596
597 private static function getYandexToken()
598 {
599 if (!Main\Loader::includeModule('seo'))
600 {
601 return null;
602 }
603
604 $authAdapter = Seo\Checkout\Service::getAuthAdapter(Seo\Checkout\Service::TYPE_YOOKASSA);
605 $token = $authAdapter->getToken();
606 if (!$token)
607 {
608 $authAdapter = Seo\Checkout\Service::getAuthAdapter(Seo\Checkout\Service::TYPE_YANDEX);
609 $token = $authAdapter->getToken();
610 }
611
612 return $token;
613 }
614
615 private function getBasicAuthString(Sale\Payment $payment)
616 {
617 return base64_encode(
618 trim((string)$this->getPaySystemSetting($payment, 'YANDEX_CHECKOUT_SHOP_ID'))
619 . ':'
620 . trim((string)$this->getPaySystemSetting($payment, 'YANDEX_CHECKOUT_SECRET_KEY'))
621 );
622 }
623
628 private static function decode(string $data)
629 {
630 try
631 {
632 return Main\Web\Json::decode($data);
633 }
634 catch (Main\ArgumentException $exception)
635 {
636 return false;
637 }
638 }
639
640 private static function encode(array $data)
641 {
642 return Main\Web\Json::encode($data, JSON_UNESCAPED_UNICODE);
643 }
644}
static loadMessages($file)
Definition loc.php:64
static getMessage($code, $replace=null, $language=null)
Definition loc.php:29
getValueFromSettings($name, $code)
Definition cashbox.php:201
getPaySystemSetting(Sale\Payment $payment, string $code)
send(string $url, Sale\Payment $payment, array $fields, string $method=self::SEND_METHOD_HTTP_POST)
onAfterProcessCheck(Sale\Result $result, Sale\Payment $payment)
static getKkmValue(Sale\PaySystem\Service $service)
const PAYMENT_OBJECT_SOCIAL_INSURANCE
Definition check.php:47
const PAYMENT_OBJECT_LOTTERY_PRIZE
Definition check.php:32
const PAYMENT_OBJECT_MEDICAL_INSURANCE_IP
Definition check.php:45
const PAYMENT_OBJECT_COMMODITY_MARKING_EXCISE
Definition check.php:50
const PAYMENT_OBJECT_NON_OPERATING_GAIN
Definition check.php:38
const PAYMENT_OBJECT_COMMODITY_MARKING_NO_MARKING_EXCISE
Definition check.php:49
const PAYMENT_OBJECT_RESORT_FEE
Definition check.php:40
const PAYMENT_OBJECT_PENSION_INSURANCE_IP
Definition check.php:43
const PAYMENT_OBJECT_PROPERTY_RIGHT
Definition check.php:37
const PAYMENT_OBJECT_GAMBLING_PRIZE
Definition check.php:30
const PAYMENT_OBJECT_COMMODITY_MARKING
Definition check.php:52
const PAYMENT_OBJECT_AGENT_COMMISSION
Definition check.php:34
const PAYMENT_OBJECT_MEDICAL_INSURANCE
Definition check.php:46
const PAYMENT_OBJECT_INTELLECTUAL_ACTIVITY
Definition check.php:33
const PAYMENT_OBJECT_INSURANCE_PREMIUM
Definition check.php:53
const PAYMENT_OBJECT_AGENT_WITHDRAWALS
Definition check.php:56
const PAYMENT_OBJECT_GAMBLING_BET
Definition check.php:29
const PAYMENT_OBJECT_CASINO_PAYMENT
Definition check.php:48
const PAYMENT_OBJECT_PENSION_INSURANCE
Definition check.php:44
const PAYMENT_OBJECT_COMMODITY_MARKING_NO_MARKING
Definition check.php:51
static getPaymentByCheck(Check $check)
static getList(array $parameters=array())
static addDebugInfo(?string $message, $cashboxId=null)
Definition logger.php:43
static roundPrecision($value)