Line # Revision Author
1 690 ahitrov package payments::Provider::Sber;
2
3 use strict;
4 use warnings 'all';
5
6 use base 'payments::Provider::Base';
7 use Contenido::Globals;
8 use payments::Keeper;
9 use URI;
10 use URI::QueryParam;
11 use JSON::XS;
12 use Data::Dumper;
13
14 696 ahitrov use constant (
15 REG => 0, # Заказ зарегистрирован, но не оплачен;
16 HOLD => 1, # Предавторизованная сумма захолдирована (для двухстадийных платежей);
17 PAY => 2, # Проведена полная авторизация суммы заказа;
18 CANCEL => 3, # Авторизация отменена;
19 REFUND => 4, # По транзакции была проведена операция возврата;
20 ACS => 5, # Инициирована авторизация через ACS банка-эмитента;
21 REJECT => 6, # Авторизация отклонена.
22 );
23
24 our %STATUS = (
25 708 ahitrov 0 => { name => 'Init', title => 'ожидает оплаты', message => 'Мы ожидаем подтверждения от платежной системы по вашей транзакции' },
26 1 => { name => 'Authorized', title => 'оплачен', message => 'Платеж завершен успешно. Необходимая сумма заблокирована на карте', },
27 2 => { name => 'Charged', title => 'оплачен', message => 'Платеж завершен успешно. Необходимая сумма списана с карты', },
28 3 => { name => 'Rejected', title => 'возврат средств', message => 'Успешно произведен полный или частичный возврат денежных средств на карту', },
29 4 => { name => 'Refunded', title => 'возврат средств', message => 'Успешно произведен полный или частичный возврат денежных средств на карту', },
30 5 => { name => '3DS', title => 'ожидает 3DS', message => 'Инициирована авторизация через ACS банка-эмитента', },
31 6 => { name => 'Error', title => 'платеж отклонен', message => 'Последняя операция по платежу была отклонена или завершена с ошибкой', },
32 696 ahitrov );
33
34 690 ahitrov sub new {
35 my ($proto, %params) = @_;
36 my $class = ref($proto) || $proto;
37 my $self = {};
38 my $prefix = $class =~ /\:\:(\w+)$/ ? lc($1) : undef;
39 return unless $prefix;
40
41 $self->{payment_system} = $prefix;
42 $self->{app_id} = $state->{payments}{$prefix."_app_id"};
43 $self->{secret} = $state->{payments}{$prefix."_app_secret"};
44 $self->{token} = $state->{payments}{$prefix."_app_token"};
45 691 ahitrov $self->{session_timeout} = exists $params{session_timeout} ? $params{session_timeout} : $state->{payments}->{$prefix."_session_timeout"};
46 690 ahitrov $self->{test_mode} = exists $params{test_mode} ? $params{test_mode} : $state->{payments}->{$prefix."_test_mode"};
47 $self->{return_url} = $params{return_url} || $state->{payments}{$prefix."_return_url"};
48 $self->{fail_url} = $params{fail_url} || $state->{payments}{$prefix."_fail_url"};
49
50 $self->{currency} = $state->{payments}{$prefix."_currency_code"};
51 696 ahitrov $self->{payment_statuses} = \%STATUS;
52 690 ahitrov
53 my $host = 'https://'. ($self->{test_mode} ? '3dsec.sberbank.ru' : 'securepayments.sberbank.ru');
54 $self->{api} = {
55 init => "$host/payment/rest/register.do", # Регистрация заказа
56 pay => "$host/payment/rest/deposit.do", # Запрос завершения оплаты заказа
57 cancel => "$host/payment/rest/reverse.do", # Запрос отмены оплаты заказа
58 refund => "$host/payment/rest/refund.do", # Запрос возврата средств оплаты заказа
59 status => "$host/payment/rest/getOrderStatusExtended.do", # Получение статуса заказа
60 is3ds => "$host/payment/rest/verifyEnrollment.do", # Запрос проверки вовлеченности карты в 3DS
61 };
62 691 ahitrov # $self->{return_url} = '';
63 690 ahitrov
64 $self->{result} = {};
65
66 bless $self, $class;
67
68 return $self;
69 }
70
71
72 ############################################################################################################
73 # Одностадийные операции
74 ############################################################################################################
75
76 =for rem INIT
77 # Регистрация заказа
78
79 $payment->init({
80 # обязательные:
81 uid => User ID
82 orderNumber => ID заказа
83 amount => Сумма платежа в копейках или в формате 0.00
84 # необязательные:
85 returnUrl => Адрес, на который требуется перенаправить пользователя в случае успешной оплаты.
86 Если не прописан в config.mk, параметр ОБЯЗАТЕЛЬНЫЙ
87 failUrl => Адрес, на который требуется перенаправить пользователя в случае неуспешной оплаты.
88 description => Описание заказа в свободной форме. В процессинг банка для включения в финансовую
89 отчётность продавца передаются только первые 24 символа этого поля
90 language => Язык в кодировке ISO 639-1
91 pageView => DESKTOP || MOBILE (см. доку)
92 jsonParams => { хеш дополнительныех параметров }
93 sessionTimeoutSecs => Продолжительность жизни заказа в секундах.
94 expirationDate => Дата и время окончания жизни заказа. Формат: yyyy-MM-ddTHH:mm:ss.
95 });
96 =cut
97 ##########################################################
98 sub init {
99 my $self = shift;
100 my $opts = shift // {};
101
102 696 ahitrov unless ( %$opts && (exists $opts->{order} || exists $opts->{orderNumber} && exists $opts->{amount}) ) {
103 $self->{result}{error} = 'Не указаны обязательные параметры: order, orderNumber или amount';
104 690 ahitrov return $self;
105 }
106 my $method = 'init';
107 if ( !exists $opts->{returnUrl} ) {
108 if ( $self->{return_url} ) {
109 $opts->{returnUrl} = $self->{return_url};
110 } else {
111 $self->{result}{error} = 'Не указан параметр returnUrl и не заполнено значение по умолчанию в конфиге SBER_RETURN_URL';
112 return $self;
113 }
114 }
115 if ( !exists $opts->{failUrl} && $self->{fail_url} ) {
116 $opts->{failUrl} = $self->{fail_url};
117 }
118 691 ahitrov if ( !exists $opts->{sessionTimeoutSecs} && $self->{session_timeout} ) {
119 $opts->{sessionTimeoutSecs} = $self->{session_timeout};
120 }
121 690 ahitrov
122 696 ahitrov my $order;
123 if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) {
124 $order = delete $opts->{order};
125 $opts->{orderNumber} = $order->id;
126 }
127
128 690 ahitrov my $uid = delete $opts->{uid};
129 unless ( $uid ) {
130 696 ahitrov if ( ref $order ) {
131 $uid = $order->uid;
132 } else {
133 $self->{result}{error} = 'Не указан user id';
134 return $self;
135 }
136 690 ahitrov }
137
138 ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки
139 696 ahitrov if ( ref $order ) {
140 $opts->{amount} = int($order->sum_total * 100);
141 } else {
142 my $sum = $opts->{amount};
143 if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) {
144 $self->{result}{error} = 'Не указана или неправильно указана сумма транзакции';
145 return $self;
146 }
147 if ( $sum =~ /[,.]/ ) {
148 $sum =~ s/\,/\./;
149 $opts->{amount} = int($sum * 100);
150 }
151 690 ahitrov }
152 $opts->{jsonParams} = {} unless exists $opts->{jsonParams};
153 $opts->{jsonParams}{uid} = $uid;
154
155 warn "Sberbank init args: ".Dumper($opts) if $DEBUG;
156 691 ahitrov my $operation = $self->payment_operation_register({
157 690 ahitrov order_id => $opts->{orderNumber},
158 name => 'create',
159 uid => $uid,
160 710 ahitrov sum => sprintf("%.2f", $opts->{amount} / 100),
161 691 ahitrov });
162 690 ahitrov return $self unless ref $operation;
163
164 my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
165 696 ahitrov if ( ref $transaction && $transaction->name ne 'Expired' ) {
166 690 ahitrov ### Transaction already exists
167 $self->{result}{success} = 1;
168 $self->{result}{session_id} = $transaction->session_id;
169 $self->{result}{transaction} = $transaction;
170 } else {
171 691 ahitrov if ( ref $transaction && $transaction->name eq 'Expired' ) {
172 $transaction->delete;
173 696 ahitrov $transaction = undef;
174 691 ahitrov }
175 696 ahitrov if ( !ref $transaction ) {
176 my $req = $self->_createRequestGet( $method, $opts );
177 my $ua = LWP::UserAgent->new;
178 $ua->agent('Mozilla/5.0');
179 my $result = $ua->get( $req );
180 if ( $result->code == 200 ) {
181 warn "Sberbank Init result: [".$result->decoded_content."]\n" if $DEBUG;
182 my $content = JSON::XS->new->decode( $result->decoded_content );
183 warn Dumper $content if $DEBUG;
184 690 ahitrov
185 696 ahitrov if ( ref $content && exists $content->{orderId} ) {
186 my $now = Contenido::DateTime->new;
187 $transaction = payments::Transaction->new( $keeper );
188 $transaction->dtime( $now->ymd('-').' '.$now->hms );
189 $transaction->provider( $self->{payment_system} );
190 $transaction->session_id( $content->{orderId} );
191 $transaction->status( $self->{test_mode} );
192 $transaction->order_id( $opts->{orderNumber} );
193 $transaction->operation_id( $operation->id );
194 $transaction->currency_code( 'RUR' );
195 710 ahitrov $transaction->sum( sprintf("%.2f", $opts->{amount} / 100) );
196 696 ahitrov $transaction->form_url( $content->{formUrl} );
197 $transaction->name( 'Init' );
198 $transaction->success( 0 );
199 $transaction->store;
200 690 ahitrov
201 696 ahitrov $self->{result}{success} = 1;
202 $self->{result}{session_id} = $content->{orderId};
203 $self->{result}{transaction} = $transaction;
204 } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
205 $self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage});
206 warn "[".$result->decoded_content."]\n";
207 } else {
208 $self->{result}{error} = 'Sberbank Init failed';
209 $self->{result}{responce} = $result->decoded_content;
210 warn $self->{result}{error}."\n";
211 warn "[".$result->decoded_content."]\n";
212 }
213 690 ahitrov } else {
214 $self->{result}{error} = 'Sberbank Init failed';
215 696 ahitrov $self->{result}{responce} = $result->status_line;
216 warn $self->{result}{error}.": ".$result->status_line."\n";
217 warn Dumper $result;
218 690 ahitrov }
219 }
220 }
221 return $self;
222 }
223
224
225 =for rem STATUS
226 # Расширенный запрос состояния заказа
227
228 $payment->status({
229 # обязательные:
230 orderNumber => ID заказа в магазине. Если в объекте присутствует транзакция, будет браться из транзакции
231 # необязательные:
232 language => Язык в кодировке ISO 639-1
233 });
234
235 Результат:
236
237 orderStatus:
238
239 По значению этого параметра определяется состояние заказа в платёжной системе. Список возможных значений приведён в списке
240 ниже. Отсутствует, если заказ не был найден.
241 0 - Заказ зарегистрирован, но не оплачен;
242 1 - Предавторизованная сумма захолдирована (для двухстадийных платежей);
243 2 - Проведена полная авторизация суммы заказа;
244 3 - Авторизация отменена;
245 4 - По транзакции была проведена операция возврата;
246 5 - Инициирована авторизация через ACS банка-эмитента;
247 6 - Авторизация отклонена.
248
249 errorCode:
250
251 Код ошибки. Возможны следующие варианты.
252 0 - Обработка запроса прошла без системных ошибок;
253 1 - Ожидается [orderId] или [orderNumber];
254 5 - Доступ запрещён;
255 5 - Пользователь должен сменить свой пароль;
256 6 - Заказ не найден;
257 7 - Системная ошибка.
258
259 =cut
260 ##########################################################
261 sub status {
262 my $self = shift;
263 my $opts = shift // {};
264
265 696 ahitrov unless ( exists $opts->{orderNumber}
266 || exists $self->{result} && exists $self->{result}{transaction} && ref $self->{result}{transaction}
267 || exists $opts->{transaction} && ref $opts->{transaction} ) {
268 690 ahitrov $self->{result}{error} = 'Не указан обязательный параметр orderNumber или не получена транзакция';
269 return $self;
270 }
271 my $method = 'status';
272 my $transaction;
273 696 ahitrov if ( exists $opts->{transaction} ) {
274 $transaction = delete $opts->{transaction};
275 } elsif ( exists $self->{result}{transaction} ) {
276 690 ahitrov $transaction = $self->{result}{transaction};
277 } else {
278 $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
279 }
280 unless ( ref $transaction ) {
281 $self->{result}{error} = "Не найдена транзакция для order_id=".$opts->{orderNumber};
282 return $self;
283 }
284 696 ahitrov $opts->{orderNumber} = $transaction->order_id;
285 690 ahitrov $opts->{orderId} = $transaction->session_id;
286 696 ahitrov warn "Sberbank status opts: ".Dumper($opts) if $DEBUG;
287 690 ahitrov
288 my $req = $self->_createRequestGet( $method, $opts );
289 my $ua = LWP::UserAgent->new;
290 $ua->agent('Mozilla/5.0');
291 my $result = $ua->get( $req );
292 my $return_data = {};
293 if ( $result->code == 200 ) {
294 691 ahitrov warn "Sberbank Status: [".$result->decoded_content."]\n" if $DEBUG;
295 my $content = JSON::XS->new->decode( $result->decoded_content );
296 690 ahitrov warn Dumper $content if $DEBUG;
297
298 691 ahitrov if ( ref $content && exists $content->{orderStatus} && exists $content->{orderNumber} ) {
299 690 ahitrov $self->{result} = {
300 success => 1,
301 status => $content->{orderStatus},
302 action => $content->{actionCode},
303 691 ahitrov action_description => Encode::encode('utf-8', $content->{actionCodeDescription}),
304 690 ahitrov amount => $content->{amount},
305 time_ms => $content->{date},
306 ip => $content->{ip},
307 transaction => $transaction,
308 };
309 } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
310 $self->{result}{error} = $content->{errorMessage};
311 691 ahitrov warn "[".$result->decoded_content."]\n";
312 690 ahitrov } else {
313 $self->{result}{error} = 'Sberbank Status failed';
314 691 ahitrov $self->{result}{responce} = $result->decoded_content;
315 690 ahitrov warn $self->{result}{error}."\n";
316 691 ahitrov warn "[".$result->decoded_content."]\n";
317 690 ahitrov }
318 } else {
319 $self->{result}{error} = 'Sberbank Status failed';
320 $self->{result}{responce} = $result->status_line;
321 warn $self->{result}{error}.": ".$result->status_line."\n";
322 warn Dumper $result;
323 }
324
325 return $self;
326 }
327
328
329 =for rem REFUND
330 # Возврат средств
331
332 $payment->refund({
333 # обязательные:
334 uid => User ID
335 orderNumber => ID заказа
336 # orderId => Номер заказа в платежной системе. Уникален в пределах системы (session_id).
337 # Если в объекте присутствует транзакция, будет браться из транзакции
338 amount => Сумма платежа в копейках или в формате 0.00
339 });
340 =cut
341 ##########################################################
342 sub refund {
343 my $self = shift;
344 my $opts = shift // {};
345
346 unless ( %$opts && exists $opts->{orderNumber} && exists $opts->{amount} ) {
347 $self->{result}{error} = 'Не указаны обязательные параметры: orderNumber или amount';
348 return $self;
349 }
350 my $method = 'refund';
351
352 my $uid = delete $opts->{uid};
353 unless ( $uid ) {
354 $self->{result}{error} = 'Не указан user id';
355 return $self;
356 }
357
358 ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки
359 my $sum = $opts->{amount};
360 if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) {
361 $self->{result}{error} = 'Не указана или неправильно указана сумма транзакции';
362 return $self;
363 }
364 if ( $sum =~ /[,.]/ ) {
365 $sum =~ s/\,/\./;
366 $opts->{amount} = int($sum * 100);
367 }
368
369 warn "Sberbank refund args: ".Dumper($opts) if $DEBUG;
370 691 ahitrov my $operation = $self->payment_operation_register({
371 690 ahitrov order_id => $opts->{orderNumber},
372 name => 'refund',
373 uid => $uid,
374 710 ahitrov sum => sprintf("%.2f", $opts->{amount} / 100),
375 691 ahitrov });
376 690 ahitrov return $self unless ref $operation;
377
378 my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
379 707 ahitrov if ( ref $transaction && $transaction->name eq 'Charged' ) {
380 690 ahitrov $opts->{orderId} = $transaction->session_id;
381 my $order_id = delete $opts->{orderNumber};
382 my $req = $self->_createRequestGet( $method, $opts );
383 my $ua = LWP::UserAgent->new;
384 $ua->agent('Mozilla/5.0');
385 my $result = $ua->get( $req );
386 if ( $result->code == 200 ) {
387 warn "Sberbank Refund result: [".$result->decoded_content."]\n" if $DEBUG;
388 691 ahitrov my $content = JSON::XS->new->decode( $result->decoded_content );
389 690 ahitrov warn Dumper $content if $DEBUG;
390
391 if ( ref $content && exists $content->{orderId} ) {
392 my $now = Contenido::DateTime->new;
393 my $transaction = payments::Transaction->new( $keeper );
394 $transaction->dtime( $now->ymd('-').' '.$now->hms );
395 $transaction->provider( $self->{payment_system} );
396 $transaction->session_id( $opts->{orderId} );
397 $transaction->status( $self->{test_mode} );
398 $transaction->order_id( $order_id );
399 $transaction->operation_id( $operation->id );
400 $transaction->currency_code( 'RUR' );
401 710 ahitrov $transaction->sum( sprintf("%.2f", $opts->{amount} / 100) );
402 707 ahitrov $transaction->name( 'Refunded' );
403 690 ahitrov $transaction->success( 0 );
404 $transaction->store;
405
406 $self->{result}{success} = 1;
407 $self->{result}{session_id} = $content->{orderId};
408 $self->{result}{transaction} = $transaction;
409 } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) {
410 691 ahitrov $self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage});
411 warn "[".$result->decoded_content."]\n";
412 690 ahitrov } else {
413 $self->{result}{error} = 'Sberbank Refund failed';
414 691 ahitrov $self->{result}{responce} = $result->decoded_content;
415 690 ahitrov warn $self->{result}{error}."\n";
416 691 ahitrov warn "[".$result->decoded_content."]\n";
417 690 ahitrov }
418 } else {
419 691 ahitrov $self->{result}{error} = 'Sberbank Refund failed';
420 690 ahitrov $self->{result}{responce} = $result->status_line;
421 warn $self->{result}{error}.": ".$result->status_line."\n";
422 warn Dumper $result;
423 }
424 }
425 return $self;
426 }
427
428
429 707 ahitrov sub GetNameByResultStatus {
430 my ($self, $status) = @_;
431 if ( exists $STATUS{$status} ) {
432 return $STATUS{$status}{name};
433 } else {
434 return 'Error';
435 }
436 }
437 690 ahitrov
438 sub _createRequestGet {
439 my ($self, $method, $opts) = @_;
440 return unless $method && exists $self->{api}{$method};
441 $opts //= {};
442
443 my $req = URI->new( $self->{api}{$method} );
444 if ( $self->{token} ) {
445 $req->query_param( token => $self->{token} );
446 } else {
447 $req->query_param( userName => $self->{app_id} );
448 $req->query_param( password => $self->{secret} );
449 }
450 foreach my $key ( keys %$opts ) {
451 if ( $key eq 'jsonParams' && ref $opts->{$key} ) {
452 $opts->{$key} = encode_json $opts->{$key};
453 }
454 $req->query_param( $key => $opts->{$key} );
455 }
456 return $req;
457 }
458
459 1;