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;