Revision 690
Date:
2018/07/30 10:05:12
Author:
ahitrov
Revision Log:
Sberbank gate. One-stage JSON API
Files:
Legend:
Added
Removed
Modified
utf8/plugins/payments/lib/payments/Init.pm
25
25
payments::CardSection
26
26
27
27
payments::Provider::Base
28
payments::Provider::Moneta
29
payments::Provider::Xsolla
30
28
payments::Provider::PayTure
29
payments::Provider::Sber
31
30
));
32
31
33
32
sub init {
utf8/plugins/payments/lib/payments/Provider/Base.pm
60
60
return $self->{payment_system};
61
61
}
62
62
63
#################################
64
# Пытается зарегистрировать операцию по order_id.
65
# В случае успеха возвращает объект payments::Operation
66
# В случае неуспеха выставляет ошибку и возвращает undef.
67
# Сумма заказа в копейках
68
##########################################################
69
sub payment_operation_register {
70
my $self = shift;
71
my $opts = shift // {};
72
unless ( $opts->{order_id} && $opts->{uid} && $opts->{sum} && $opts->{name} ) {
73
$self->{result}{error} = 'Переданы не все обязательные параметры';
74
return undef;
75
}
76
77
my $operation = $keeper->get_documents(
78
class => 'payments::Operation',
79
status => $self->{test_mode},
80
order_id => $opts->{order_id},
81
order_by => 'ctime',
82
return_mode => 'array_ref',
83
);
84
my $new = 0;
85
if ( ref $operation eq 'ARRAY' && @$operation ) {
86
my $last = $operation->[-1];
87
if ( $opts->{name} eq 'create' && ($last->name eq 'suspend' || $last->name eq 'cancel' || $last->name eq 'close') ) {
88
$self->{result}{error} = 'Заказ закрыт, отменен или заморожен. Оплата по нему невозможна';
89
return undef;
90
} elsif ( $opts->{name} eq 'refund' && ($last->name eq 'suspend' || $last->name eq 'close') ) {
91
$self->{result}{error} = 'Заказ закрыт или заморожен. Возврат средств по нему невозможен';
92
return undef;
93
} elsif ( $last->name eq $opts->{name} ) {
94
$operation = $last;
95
} else {
96
if ( $opts->{name} eq 'refund' && (grep { $_->name eq 'create' } @$operation) ) {
97
$new = 1;
98
}
99
}
100
} elsif ( $opts->{name} eq 'create' ) {
101
$new = 1;
102
}
103
if ( $new ) {
104
$operation = payments::Operation->new( $keeper );
105
$operation->status( $self->{test_mode} );
106
$operation->name( $opts->{name} );
107
$operation->order_id( $opts->{order_id} );
108
$operation->uid( $opts->{uid} );
109
$operation->sum( $opts->{sum} );
110
$operation->store;
111
}
112
return $operation;
113
}
114
115
sub get_transaction_by_order_id {
116
my $self = shift;
117
my $order_id = shift;
118
119
my ($transaction) = $keeper->get_documents(
120
class => 'payments::Transaction',
121
status => $self->{test_mode},
122
limit => 1,
123
order_id => $order_id,
124
order_by => 'ctime desc',
125
provider => $self->{payment_system},
126
);
127
128
return $transaction;
129
}
130
63
131
1;
utf8/plugins/payments/lib/payments/Provider/Sber.pm
1
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;
utf8/plugins/payments/lib/payments/SQL/TransactionsTable.pm
87
87
{ # ID операции в таблице операций
88
88
'attr' => 'operation_id',
89
89
'type' => 'integer',
90
'rusname' => 'ID транзакции',
90
'rusname' => 'ID операции',
91
91
'db_field' => 'operation_id',
92
92
'db_type' => 'numeric',
93
93
'db_opts' => "not null",
utf8/plugins/payments/lib/payments/State.pm.proto
79
79
$self->{payture_test_mode} = int('@PTR_TEST_MODE@' || 0);
80
80
$self->{payture_sig_code} = '@PTR_SIG_CODE@';
81
81
82
$self->{sber_app_id} = '@SBER_LOGIN@';
83
$self->{sber_app_secret} = '@SBER_PASSWORD@';
84
$self->{sber_app_token} = '@SBER_TOKEN@';
85
$self->{sber_return_url} = '@SBER_RETURN_URL@';
86
$self->{sber_fail_url} = '@SBER_FAIL_URL@';
87
$self->{sber_test_mode} = int('@SBER_TEST_MODE@' || 0);
88
$self->{sber_currency_code} = int('@SBER_CURRENCY_CODE@' || 643);
89
82
90
$self->_init_();
83
91
$self;
84
92
}
utf8/plugins/payments/lib/payments/Transaction.pm
10
10
[1, 'Тестовая оплата'],
11
11
],
12
12
},
13
{ 'attr' => 'form_url', 'type' => 'string', 'rusname' => 'URL формы оплаты' },
13
14
{ 'attr' => 'custom1', 'type' => 'string', 'rusname' => 'Параметр 1' },
14
15
{ 'attr' => 'custom2', 'type' => 'string', 'rusname' => 'Параметр 2' },
15
16
{ 'attr' => 'custom3', 'type' => 'string', 'rusname' => 'Параметр 3' },
Небольшая справка по веткам
cnddist – контейнер, в котором хранятся все дистрибутивы всех библиотек и программных пакетов, которые использовались при построении различных версий Contenido. Если какой-то библиотеки в данном хранилище нет, инсталлятор сделает попытку "подтянуть" ее с веба (например, с CPAN). Если библиотека слишком старая, есть очень большая вероятность, что ее там уже нет. Поэтому мы храним весь хлам от всех сборок. Если какой-то дистрибутив вдруг отсутствует в cnddist - напишите нам, мы положим его туда.
koi8 – отмирающая ветка, чей код, выдача и все внутренние библиотеки заточены на кодировку KOI8-R. Вносятся только те дополнения, которые касаются внешнего вида и функционала админки, баги ядра, обязательные обновления портов и мелочи, которые легко скопипастить. В дальнейшем планируется полная остановка поддержки по данной ветке.
utf8 – актуальная ветка, заточенная под UTF-8.
Внутри каждой ветки: core – исходники ядра; install – скрипт установки инсталляции; plugins – плагины; samples – "готовые к употреблению" проекты, которые можно поставить, запустить и посмотреть, как они работают.