package payments::Provider::Sber; use strict; use warnings 'all'; use base 'payments::Provider::Base'; use Contenido::Globals; use payments::Keeper; use URI; use URI::QueryParam; use JSON::XS; use Data::Dumper; use constant ( REG => 0, # Заказ зарегистрирован, но не оплачен; HOLD => 1, # Предавторизованная сумма захолдирована (для двухстадийных платежей); PAY => 2, # Проведена полная авторизация суммы заказа; CANCEL => 3, # Авторизация отменена; REFUND => 4, # По транзакции была проведена операция возврата; ACS => 5, # Инициирована авторизация через ACS банка-эмитента; REJECT => 6, # Авторизация отклонена. ); our %STATUS = ( 0 => { name => 'Init', message => 'Ожидание оплаты' }, 1 => { name => 'Authorized', message => 'Предавторизованная сумма захолдирована', }, 2 => { name => 'Charged', message => 'Успешная оплата', }, 3 => { name => 'Rejected', message => 'Авторизация отменена', }, 4 => { name => 'Refunded', message => 'По транзакции была проведена операция возврата', }, 5 => { name => '3DS', message => 'Инициирована авторизация через ACS банка-эмитента', }, 6 => { name => 'Error', message => 'Авторизация отклонена', }, ); sub new { my ($proto, %params) = @_; my $class = ref($proto) || $proto; my $self = {}; my $prefix = $class =~ /\:\:(\w+)$/ ? lc($1) : undef; return unless $prefix; $self->{payment_system} = $prefix; $self->{app_id} = $state->{payments}{$prefix."_app_id"}; $self->{secret} = $state->{payments}{$prefix."_app_secret"}; $self->{token} = $state->{payments}{$prefix."_app_token"}; $self->{session_timeout} = exists $params{session_timeout} ? $params{session_timeout} : $state->{payments}->{$prefix."_session_timeout"}; $self->{test_mode} = exists $params{test_mode} ? $params{test_mode} : $state->{payments}->{$prefix."_test_mode"}; $self->{return_url} = $params{return_url} || $state->{payments}{$prefix."_return_url"}; $self->{fail_url} = $params{fail_url} || $state->{payments}{$prefix."_fail_url"}; $self->{currency} = $state->{payments}{$prefix."_currency_code"}; $self->{payment_statuses} = \%STATUS; my $host = 'https://'. ($self->{test_mode} ? '3dsec.sberbank.ru' : 'securepayments.sberbank.ru'); $self->{api} = { init => "$host/payment/rest/register.do", # Регистрация заказа pay => "$host/payment/rest/deposit.do", # Запрос завершения оплаты заказа cancel => "$host/payment/rest/reverse.do", # Запрос отмены оплаты заказа refund => "$host/payment/rest/refund.do", # Запрос возврата средств оплаты заказа status => "$host/payment/rest/getOrderStatusExtended.do", # Получение статуса заказа is3ds => "$host/payment/rest/verifyEnrollment.do", # Запрос проверки вовлеченности карты в 3DS }; # $self->{return_url} = ''; $self->{result} = {}; bless $self, $class; return $self; } ############################################################################################################ # Одностадийные операции ############################################################################################################ =for rem INIT # Регистрация заказа $payment->init({ # обязательные: uid => User ID orderNumber => ID заказа amount => Сумма платежа в копейках или в формате 0.00 # необязательные: returnUrl => Адрес, на который требуется перенаправить пользователя в случае успешной оплаты. Если не прописан в config.mk, параметр ОБЯЗАТЕЛЬНЫЙ failUrl => Адрес, на который требуется перенаправить пользователя в случае неуспешной оплаты. description => Описание заказа в свободной форме. В процессинг банка для включения в финансовую отчётность продавца передаются только первые 24 символа этого поля language => Язык в кодировке ISO 639-1 pageView => DESKTOP || MOBILE (см. доку) jsonParams => { хеш дополнительныех параметров } sessionTimeoutSecs => Продолжительность жизни заказа в секундах. expirationDate => Дата и время окончания жизни заказа. Формат: yyyy-MM-ddTHH:mm:ss. }); =cut ########################################################## sub init { my $self = shift; my $opts = shift // {}; unless ( %$opts && (exists $opts->{order} || exists $opts->{orderNumber} && exists $opts->{amount}) ) { $self->{result}{error} = 'Не указаны обязательные параметры: order, orderNumber или amount'; return $self; } my $method = 'init'; if ( !exists $opts->{returnUrl} ) { if ( $self->{return_url} ) { $opts->{returnUrl} = $self->{return_url}; } else { $self->{result}{error} = 'Не указан параметр returnUrl и не заполнено значение по умолчанию в конфиге SBER_RETURN_URL'; return $self; } } if ( !exists $opts->{failUrl} && $self->{fail_url} ) { $opts->{failUrl} = $self->{fail_url}; } if ( !exists $opts->{sessionTimeoutSecs} && $self->{session_timeout} ) { $opts->{sessionTimeoutSecs} = $self->{session_timeout}; } my $order; if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) { $order = delete $opts->{order}; $opts->{orderNumber} = $order->id; } my $uid = delete $opts->{uid}; unless ( $uid ) { if ( ref $order ) { $uid = $order->uid; } else { $self->{result}{error} = 'Не указан user id'; return $self; } } ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки if ( ref $order ) { $opts->{amount} = int($order->sum_total * 100); } else { my $sum = $opts->{amount}; if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) { $self->{result}{error} = 'Не указана или неправильно указана сумма транзакции'; return $self; } if ( $sum =~ /[,.]/ ) { $sum =~ s/\,/\./; $opts->{amount} = int($sum * 100); } } $opts->{jsonParams} = {} unless exists $opts->{jsonParams}; $opts->{jsonParams}{uid} = $uid; warn "Sberbank init args: ".Dumper($opts) if $DEBUG; my $operation = $self->payment_operation_register({ order_id => $opts->{orderNumber}, name => 'create', uid => $uid, sum => $opts->{amount}, }); return $self unless ref $operation; my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} ); if ( ref $transaction && $transaction->name ne 'Expired' ) { ### Transaction already exists $self->{result}{success} = 1; $self->{result}{session_id} = $transaction->session_id; $self->{result}{transaction} = $transaction; } else { if ( ref $transaction && $transaction->name eq 'Expired' ) { $transaction->delete; $transaction = undef; } if ( !ref $transaction ) { my $req = $self->_createRequestGet( $method, $opts ); my $ua = LWP::UserAgent->new; $ua->agent('Mozilla/5.0'); my $result = $ua->get( $req ); if ( $result->code == 200 ) { warn "Sberbank Init result: [".$result->decoded_content."]\n" if $DEBUG; my $content = JSON::XS->new->decode( $result->decoded_content ); warn Dumper $content if $DEBUG; if ( ref $content && exists $content->{orderId} ) { my $now = Contenido::DateTime->new; $transaction = payments::Transaction->new( $keeper ); $transaction->dtime( $now->ymd('-').' '.$now->hms ); $transaction->provider( $self->{payment_system} ); $transaction->session_id( $content->{orderId} ); $transaction->status( $self->{test_mode} ); $transaction->order_id( $opts->{orderNumber} ); $transaction->operation_id( $operation->id ); $transaction->currency_code( 'RUR' ); $transaction->sum( $opts->{amount} ); $transaction->form_url( $content->{formUrl} ); $transaction->name( 'Init' ); $transaction->success( 0 ); $transaction->store; $self->{result}{success} = 1; $self->{result}{session_id} = $content->{orderId}; $self->{result}{transaction} = $transaction; } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) { $self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage}); warn "[".$result->decoded_content."]\n"; } else { $self->{result}{error} = 'Sberbank Init failed'; $self->{result}{responce} = $result->decoded_content; warn $self->{result}{error}."\n"; warn "[".$result->decoded_content."]\n"; } } else { $self->{result}{error} = 'Sberbank Init failed'; $self->{result}{responce} = $result->status_line; warn $self->{result}{error}.": ".$result->status_line."\n"; warn Dumper $result; } } } return $self; } =for rem STATUS # Расширенный запрос состояния заказа $payment->status({ # обязательные: orderNumber => ID заказа в магазине. Если в объекте присутствует транзакция, будет браться из транзакции # необязательные: language => Язык в кодировке ISO 639-1 }); Результат: orderStatus: По значению этого параметра определяется состояние заказа в платёжной системе. Список возможных значений приведён в списке ниже. Отсутствует, если заказ не был найден. 0 - Заказ зарегистрирован, но не оплачен; 1 - Предавторизованная сумма захолдирована (для двухстадийных платежей); 2 - Проведена полная авторизация суммы заказа; 3 - Авторизация отменена; 4 - По транзакции была проведена операция возврата; 5 - Инициирована авторизация через ACS банка-эмитента; 6 - Авторизация отклонена. errorCode: Код ошибки. Возможны следующие варианты. 0 - Обработка запроса прошла без системных ошибок; 1 - Ожидается [orderId] или [orderNumber]; 5 - Доступ запрещён; 5 - Пользователь должен сменить свой пароль; 6 - Заказ не найден; 7 - Системная ошибка. =cut ########################################################## sub status { my $self = shift; my $opts = shift // {}; unless ( exists $opts->{orderNumber} || exists $self->{result} && exists $self->{result}{transaction} && ref $self->{result}{transaction} || exists $opts->{transaction} && ref $opts->{transaction} ) { $self->{result}{error} = 'Не указан обязательный параметр orderNumber или не получена транзакция'; return $self; } my $method = 'status'; my $transaction; if ( exists $opts->{transaction} ) { $transaction = delete $opts->{transaction}; } elsif ( exists $self->{result}{transaction} ) { $transaction = $self->{result}{transaction}; } else { $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} ); } unless ( ref $transaction ) { $self->{result}{error} = "Не найдена транзакция для order_id=".$opts->{orderNumber}; return $self; } $opts->{orderNumber} = $transaction->order_id; $opts->{orderId} = $transaction->session_id; warn "Sberbank status opts: ".Dumper($opts) if $DEBUG; my $req = $self->_createRequestGet( $method, $opts ); my $ua = LWP::UserAgent->new; $ua->agent('Mozilla/5.0'); my $result = $ua->get( $req ); my $return_data = {}; if ( $result->code == 200 ) { warn "Sberbank Status: [".$result->decoded_content."]\n" if $DEBUG; my $content = JSON::XS->new->decode( $result->decoded_content ); warn Dumper $content if $DEBUG; if ( ref $content && exists $content->{orderStatus} && exists $content->{orderNumber} ) { $self->{result} = { success => 1, status => $content->{orderStatus}, action => $content->{actionCode}, action_description => Encode::encode('utf-8', $content->{actionCodeDescription}), amount => $content->{amount}, time_ms => $content->{date}, ip => $content->{ip}, transaction => $transaction, }; } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) { $self->{result}{error} = $content->{errorMessage}; warn "[".$result->decoded_content."]\n"; } else { $self->{result}{error} = 'Sberbank Status failed'; $self->{result}{responce} = $result->decoded_content; warn $self->{result}{error}."\n"; warn "[".$result->decoded_content."]\n"; } } else { $self->{result}{error} = 'Sberbank Status failed'; $self->{result}{responce} = $result->status_line; warn $self->{result}{error}.": ".$result->status_line."\n"; warn Dumper $result; } return $self; } =for rem REFUND # Возврат средств $payment->refund({ # обязательные: uid => User ID orderNumber => ID заказа # orderId => Номер заказа в платежной системе. Уникален в пределах системы (session_id). # Если в объекте присутствует транзакция, будет браться из транзакции amount => Сумма платежа в копейках или в формате 0.00 }); =cut ########################################################## sub refund { my $self = shift; my $opts = shift // {}; unless ( %$opts && exists $opts->{orderNumber} && exists $opts->{amount} ) { $self->{result}{error} = 'Не указаны обязательные параметры: orderNumber или amount'; return $self; } my $method = 'refund'; my $uid = delete $opts->{uid}; unless ( $uid ) { $self->{result}{error} = 'Не указан user id'; return $self; } ### Сумма должна быть в копейках. Если дробное (рубли.копейки) - преобразуем в копейки my $sum = $opts->{amount}; if ( !$sum || $sum !~ /^[\d\,\.]+$/ ) { $self->{result}{error} = 'Не указана или неправильно указана сумма транзакции'; return $self; } if ( $sum =~ /[,.]/ ) { $sum =~ s/\,/\./; $opts->{amount} = int($sum * 100); } warn "Sberbank refund args: ".Dumper($opts) if $DEBUG; my $operation = $self->payment_operation_register({ order_id => $opts->{orderNumber}, name => 'refund', uid => $uid, sum => $opts->{amount}, }); return $self unless ref $operation; my $transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} ); if ( ref $transaction && $transaction->name eq 'Charged' ) { $opts->{orderId} = $transaction->session_id; my $order_id = delete $opts->{orderNumber}; my $req = $self->_createRequestGet( $method, $opts ); my $ua = LWP::UserAgent->new; $ua->agent('Mozilla/5.0'); my $result = $ua->get( $req ); if ( $result->code == 200 ) { warn "Sberbank Refund result: [".$result->decoded_content."]\n" if $DEBUG; my $content = JSON::XS->new->decode( $result->decoded_content ); warn Dumper $content if $DEBUG; if ( ref $content && exists $content->{orderId} ) { my $now = Contenido::DateTime->new; my $transaction = payments::Transaction->new( $keeper ); $transaction->dtime( $now->ymd('-').' '.$now->hms ); $transaction->provider( $self->{payment_system} ); $transaction->session_id( $opts->{orderId} ); $transaction->status( $self->{test_mode} ); $transaction->order_id( $order_id ); $transaction->operation_id( $operation->id ); $transaction->currency_code( 'RUR' ); $transaction->sum( $opts->{amount} ); $transaction->name( 'Refunded' ); $transaction->success( 0 ); $transaction->store; $self->{result}{success} = 1; $self->{result}{session_id} = $content->{orderId}; $self->{result}{transaction} = $transaction; } elsif ( ref $content && exists $content->{errorCode} && $content->{errorCode} ) { $self->{result}{error} = Encode::encode('utf-8', $content->{errorMessage}); warn "[".$result->decoded_content."]\n"; } else { $self->{result}{error} = 'Sberbank Refund failed'; $self->{result}{responce} = $result->decoded_content; warn $self->{result}{error}."\n"; warn "[".$result->decoded_content."]\n"; } } else { $self->{result}{error} = 'Sberbank Refund failed'; $self->{result}{responce} = $result->status_line; warn $self->{result}{error}.": ".$result->status_line."\n"; warn Dumper $result; } } return $self; } sub GetNameByResultStatus { my ($self, $status) = @_; if ( exists $STATUS{$status} ) { return $STATUS{$status}{name}; } else { return 'Error'; } } sub _createRequestGet { my ($self, $method, $opts) = @_; return unless $method && exists $self->{api}{$method}; $opts //= {}; my $req = URI->new( $self->{api}{$method} ); if ( $self->{token} ) { $req->query_param( token => $self->{token} ); } else { $req->query_param( userName => $self->{app_id} ); $req->query_param( password => $self->{secret} ); } foreach my $key ( keys %$opts ) { if ( $key eq 'jsonParams' && ref $opts->{$key} ) { $opts->{$key} = encode_json $opts->{$key}; } $req->query_param( $key => $opts->{$key} ); } return $req; } 1;