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