22
22
* @property-read string $payment_method_type
23
23
*
24
24
*/
25
- class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture
25
+ class tinkoffPayment extends waPayment implements waIPayment, waIPaymentRefund, waIPaymentRecurrent, waIPaymentCancel, waIPaymentCapture, waIPaymentImage
26
26
{
27
27
private $ order_id ;
28
28
private $ receipt ;
@@ -163,17 +163,25 @@ private function genToken($args)
163
163
* @throws waPaymentException
164
164
*/
165
165
private function checkToken ($ args )
166
+ {
167
+ $ token = ifset ($ args , 'Token ' , false );
168
+ unset($ args ['Token ' ]);
169
+
170
+ $ expected_token = $ this ->calculateToken ($ args );
171
+
172
+ if (empty ($ token ) || ($ token !== $ expected_token )) {
173
+ throw new waPaymentException ('Invalid token ' );
174
+ }
175
+ }
176
+
177
+ private function calculateToken ($ args )
166
178
{
167
179
$ args ['Password ' ] = trim ($ this ->getSettings ('terminal_password ' ));
168
180
169
181
if (!strlen ($ args ['Password ' ])) {
170
182
throw new waPaymentException ('Password misconfiguration ' );
171
183
}
172
184
173
- $ token = ifset ($ args , 'Token ' , false );
174
- unset($ args ['Token ' ]);
175
-
176
-
177
185
ksort ($ args );
178
186
foreach ($ args as $ k => &$ arg ) {
179
187
if (is_bool ($ arg )) {
@@ -184,17 +192,12 @@ private function checkToken($args)
184
192
}
185
193
unset($ arg );
186
194
187
- $ expected_token = hash ('sha256 ' , implode ('' , $ args ));
188
-
189
- if (empty ($ token ) || ($ token !== $ expected_token )) {
190
- throw new waPaymentException ('Invalid token ' );
191
- }
195
+ return hash ('sha256 ' , implode ('' , $ args ));
192
196
}
193
197
194
198
protected function callbackInit ($ request )
195
199
{
196
200
$ request = $ this ->sanitizeRequest ($ request );
197
-
198
201
$ pattern = '/^([a-z]+)_(\d+)_(.+)$/ ' ;
199
202
if (!empty ($ request ['OrderId ' ]) && preg_match ($ pattern , $ request ['OrderId ' ], $ match )) {
200
203
$ this ->app_id = $ match [1 ];
@@ -314,13 +317,20 @@ protected function callbackHandler($data)
314
317
// Verify token
315
318
$ this ->checkToken ($ data );
316
319
320
+ if (isset ($ data ['SBPQR ' ])) {
321
+ $ this ->sbpQrImage ($ data );
322
+ exit ;
323
+ }
324
+
317
325
$ transaction_data = $ this ->formalizeData ($ data );
318
326
319
327
$ app_payment_method = null ;
328
+ $ declare_fiscalization = false ;
320
329
321
330
switch ($ transaction_data ['type ' ]) {
322
331
case self ::OPERATION_AUTH_ONLY :
323
332
if ($ transaction_data ['result ' ]) {
333
+ $ declare_fiscalization = true ;
324
334
$ app_payment_method = self ::CALLBACK_AUTH ;
325
335
} else {
326
336
$ app_payment_method = self ::CALLBACK_DECLINE ;
@@ -329,6 +339,7 @@ protected function callbackHandler($data)
329
339
330
340
case self ::OPERATION_AUTH_CAPTURE :
331
341
if ($ transaction_data ['result ' ]) {
342
+ $ declare_fiscalization = true ;
332
343
$ app_payment_method = self ::CALLBACK_PAYMENT ;
333
344
} else {
334
345
$ app_payment_method = self ::CALLBACK_DECLINE ;
@@ -340,6 +351,7 @@ protected function callbackHandler($data)
340
351
break ;
341
352
342
353
case self ::OPERATION_CAPTURE :
354
+ $ declare_fiscalization = true ;
343
355
$ app_payment_method = self ::CALLBACK_CAPTURE ;
344
356
break ;
345
357
@@ -370,6 +382,10 @@ protected function callbackHandler($data)
370
382
//Save transaction and run app callback only if it not repeated callback;
371
383
$ transaction_data = $ this ->saveTransaction ($ transaction_data , $ data );
372
384
$ this ->execAppCallback ($ app_payment_method , $ transaction_data );
385
+
386
+ if ($ declare_fiscalization && $ this ->getSettings ('check_data_tax ' )) {
387
+ $ this ->getAdapter ()->declareFiscalization ($ transaction_data ['order_id ' ], $ this , ['id ' => $ transaction_data ['native_id ' ]]);
388
+ }
373
389
} else {
374
390
$ log = array (
375
391
'message ' => 'silent skip callback as repeated ' ,
@@ -417,7 +433,10 @@ public function refund($transaction_raw_data)
417
433
}
418
434
419
435
$ res = $ this ->apiQuery ('Cancel ' , $ args );
420
-
436
+ if (in_array (ifset ($ res ['Status ' ]), ['ASYNC_REFUNDING ' , 'REFUNDING ' ])) {
437
+ sleep (1 );
438
+ $ res = $ this ->apiQuery ('GetState ' , ['PaymentId ' => $ args ['PaymentId ' ]]);
439
+ }
421
440
422
441
$ response = array (
423
442
'result ' => 0 ,
@@ -472,7 +491,7 @@ public function refund($transaction_raw_data)
472
491
return $ response ;
473
492
} catch (Exception $ ex ) {
474
493
$ message = sprintf ("Error occurred during %s: %s " , __METHOD__ , $ ex ->getMessage ());
475
- self ::log ($ this ->id , $ message );
494
+ self ::log ($ this ->id , [ $ message, $ ex -> getTraceAsString ()] );
476
495
return array (
477
496
'result ' => -1 ,
478
497
'data ' => null ,
@@ -544,6 +563,156 @@ public function recurrent($order_data)
544
563
545
564
}
546
565
566
+ public function sbp ($ order_data )
567
+ {
568
+ $ order_data = waOrder::factory ($ order_data );
569
+
570
+ // https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP
571
+ $ args = array (
572
+ 'Amount ' => round ($ order_data ['amount ' ] * 100 ),
573
+ 'Currency ' => ifset (self ::$ currencies [$ this ->currency_id ]),
574
+ 'OrderId ' => $ this ->app_id .'_ ' .$ this ->merchant_id .'_ ' .$ order_data ['order_id ' ],
575
+ 'Description ' => ifempty ($ order_data , 'description ' , '' ),
576
+ 'PayType ' => $ this ->two_steps ? 'T ' : 'O ' ,
577
+ 'DATA ' => [],
578
+ );
579
+
580
+ if ($ this ->getSettings ('check_data_tax ' )) {
581
+ $ full_order_data = $ order_data ;
582
+ if (!$ full_order_data ->items ) {
583
+ $ full_order_data = $ this ->getAdapter ()->getOrderData ($ order_data ['order_id ' ]);
584
+ }
585
+ $ args ['Receipt ' ] = $ this ->getReceiptData ($ full_order_data , $ this );
586
+ if (!$ args ['Receipt ' ]) {
587
+ return 'Данный вариант платежа недоступен. Воспользуйтесь другим способом оплаты. ' ;
588
+ }
589
+ }
590
+
591
+ if (!empty ($ order_data ['customer_contact_id ' ])) {
592
+ $ args ['CustomerKey ' ] = $ order_data ['customer_contact_id ' ];
593
+ try {
594
+ $ c = new waContact ($ order_data ['customer_contact_id ' ]);
595
+ $ email = $ c ->get ('email ' , 'default ' );
596
+ $ phone = $ c ->get ('phone ' , 'default ' );
597
+ } catch (waException $ e ) {
598
+ // contact is deleted
599
+ }
600
+ if (empty ($ email )) {
601
+ //$email = $this->getDefaultEmail();
602
+ }
603
+ if (!empty ($ email )) {
604
+ $ args ['DATA ' ]['Email ' ] = $ email ;
605
+ }
606
+ if (!empty ($ phone )) {
607
+ $ args ['DATA ' ]['Phone ' ] = $ phone ;
608
+ }
609
+ }
610
+ if (empty ($ args ['DATA ' ])) {
611
+ unset($ args ['DATA ' ]);
612
+ }
613
+
614
+ try {
615
+ $ payment_id = null ;
616
+ $ cache_key = 'tinkoff/sbp/ ' . md5 ('SBP ' .$ args ['OrderId ' ].$ args ['Amount ' ]);
617
+ $ cache = new waSerializeCache ($ cache_key , -1 , $ this ->app_id );
618
+ if ($ cache ->isCached ()) {
619
+ $ payment_id = $ cache ->get ();
620
+ $ check_payment_data = $ this ->apiQuery ('GetState ' , ['PaymentId ' => $ payment_id ]);
621
+ if (ifset ($ check_payment_data , 'ErrorCode ' , 0 ) != 0 || !in_array (ifset ($ check_payment_data , 'State ' , '' ), ['NEW ' , 'FORM_SHOWED ' ])) {
622
+ unset($ payment_id );
623
+ }
624
+ }
625
+
626
+ if (empty ($ payment_id )) {
627
+ $ payment_data = $ this ->apiQuery ('Init ' , $ args );
628
+ $ payment_id = ifset ($ payment_data , 'PaymentId ' , '' );
629
+ }
630
+
631
+ if (empty ($ payment_id )) {
632
+ $ cache ->delete ();
633
+ return null ;
634
+ } else {
635
+ $ cache ->set ($ payment_id );
636
+ }
637
+
638
+ if ($ this ->isTestMode ()) {
639
+ try {
640
+ // Запрашивает успешную оплату по СБП для текущего счёта
641
+ // https://www.tbank.ru/kassa/dev/payments/#tag/Oplata-cherez-SBP/operation/SbpPayTest
642
+ $ test_sbp_result = $ this ->apiQuery ('SbpPayTest ' , [
643
+ 'PaymentId ' => $ payment_id ,
644
+ ]);
645
+ } catch (Exception $ ex ) {
646
+ self ::log ($ this ->id , ['Unable create test QR code, using hardcoded stub ' , $ ex ->getMessage (), $ ex ->getTraceAsString ()]);
647
+ return [
648
+ 'svg ' => file_get_contents ($ this ->path .'/img/qr-test.svg ' ),
649
+ 'url ' => wa ()->getRootUrl ().'wa-plugins/payment/tinkoff/img/qr-test.svg ' ,
650
+ ];
651
+ }
652
+ }
653
+
654
+ $ qr_data = $ this ->apiQuery ('GetQr ' , [
655
+ 'PaymentId ' => $ payment_id ,
656
+ 'DataType ' => 'IMAGE '
657
+ ]);
658
+ if (ifset ($ qr_data , 'Success ' , false )) {
659
+ $ qr_link = $ this ->apiQuery ('GetQr ' , [
660
+ 'PaymentId ' => $ payment_id ,
661
+ 'DataType ' => 'PAYLOAD '
662
+ ]);
663
+
664
+ if (ifset ($ qr_link , 'Success ' , false )) {
665
+ return [
666
+ 'svg ' => $ qr_data ['Data ' ],
667
+ 'url ' => $ qr_link ['Data ' ],
668
+ ];
669
+ }
670
+ }
671
+ $ cache ->delete ();
672
+ return null ;
673
+ } catch (Exception $ ex ) {
674
+ self ::log ($ this ->id , [$ ex ->getMessage (), $ ex ->getTraceAsString ()]);
675
+ $ cache ->delete ();
676
+ return false ;
677
+ }
678
+ }
679
+
680
+ private function sbpQrImage ($ params )
681
+ {
682
+ $ order_data = [
683
+ 'order_id ' => $ this ->order_id ,
684
+ 'amount ' => $ params ['amount ' ],
685
+ 'customer_contact_id ' => $ params ['customer_contact_id ' ],
686
+ ];
687
+
688
+ $ sbp = $ this ->sbp ($ order_data );
689
+ if (empty ($ sbp ['svg ' ])) {
690
+ throw new waException ('Не удалось получить QR-код ' );
691
+ }
692
+
693
+ $ response = wa ()->getResponse ();
694
+ $ response ->addHeader ('Content-Type ' , 'image/svg+xml ' , true );
695
+ echo $ sbp ['svg ' ];
696
+ exit ;
697
+ }
698
+
699
+ public function image ($ order_data )
700
+ {
701
+ $ args = array (
702
+ 'OrderId ' => $ this ->app_id .'_ ' .$ this ->merchant_id .'_ ' .$ order_data ['order_id ' ],
703
+ 'amount ' => $ order_data ['amount ' ],
704
+ 'description ' => ifempty ($ order_data , 'description ' , '' ),
705
+ 'customer_contact_id ' => $ order_data ['customer_contact_id ' ],
706
+ 'SBPQR ' => 1 ,
707
+ );
708
+ $ args ['Token ' ] = $ this ->calculateToken ($ args );
709
+ return [
710
+ // At least one of keys `image_url` and `image_data_url` is required. Both are ok, too.
711
+ 'image_url ' => wa ()->getRootUrl (true ) . 'payments.php/tinkoff/? ' . http_build_query ($ args ),
712
+ //'image_data_url' => 'data:image/png;base64,........',
713
+ ];
714
+ }
715
+
547
716
public function cancel ($ transaction_raw_data )
548
717
{
549
718
try {
@@ -553,6 +722,10 @@ public function cancel($transaction_raw_data)
553
722
);
554
723
555
724
$ data = $ this ->apiQuery ('Cancel ' , $ args );
725
+ if (in_array (ifset ($ data ['Status ' ]), ['ASYNC_REFUNDING ' , 'REFUNDING ' ])) {
726
+ sleep (1 );
727
+ $ data = $ this ->apiQuery ('GetState ' , ['PaymentId ' => $ args ['PaymentId ' ]]);
728
+ }
556
729
$ transaction_data = $ this ->formalizeData ($ data );
557
730
558
731
$ this ->saveTransaction ($ transaction_data , $ data );
@@ -565,7 +738,7 @@ public function cancel($transaction_raw_data)
565
738
566
739
} catch (Exception $ ex ) {
567
740
$ message = sprintf ("Error occurred during %s: %s " , __METHOD__ , $ ex ->getMessage ());
568
- self ::log ($ this ->id , $ message );
741
+ self ::log ($ this ->id , [ $ message, $ ex -> getTraceAsString ()] );
569
742
return array (
570
743
'result ' => -1 ,
571
744
'description ' => $ ex ->getMessage (),
@@ -940,6 +1113,10 @@ private function getReceiptData(waOrder $order)
940
1113
'Items ' => array (),
941
1114
'Taxation ' => $ this ->getSettings ('taxation ' ),
942
1115
'Email ' => $ email ,
1116
+ 'AddUserProp ' => [
1117
+ 'Name ' => 'Номер заказа ' ,
1118
+ 'Value ' => $ order ->id_str
1119
+ ]
943
1120
);
944
1121
if ($ phone = $ order ->getContactField ('phone ' )) {
945
1122
$ this ->receipt ['Phone ' ] = sprintf ('+%s ' , preg_replace ('/^8/ ' , '7 ' , $ phone ));
0 commit comments