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' },