Revision 690 (by ahitrov, 2018/07/30 10:05:12) |
Sberbank gate. One-stage JSON API
|
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;
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->{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"};
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->{orderNumber} && exists $opts->{amount} ) {
$self->{result}{error} = 'Не указаны обязательные параметры: 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};
}
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);
}
$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 already exists
$self->{result}{success} = 1;
$self->{result}{session_id} = $transaction->session_id;
$self->{result}{transaction} = $transaction;
} else {
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 = decode_json $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( $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} = $content->{errorMessage};
warn "[$content]\n";
} else {
$self->{result}{error} = 'Sberbank Init failed';
$self->{result}{responce} = $content;
warn $self->{result}{error}."\n";
warn "[$content]\n";
}
} else {
$self->{result}{error} = 'PayTure 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 ( %$opts && (exists $opts->{orderNumber} || exists $self->{result} && exists $self->{result}{transaction} && ref $self->{result}{transaction}) ) {
$self->{result}{error} = 'Не указан обязательный параметр orderNumber или не получена транзакция';
return $self;
}
my $method = 'status';
my $transaction;
if ( exists $self->{result}{transaction} ) {
$transaction = $self->{result}{transaction};
$opts->{orderNumber} = $transaction->order_id;
} else {
$transaction = $self->get_transaction_by_order_id( $opts->{orderNumber} );
}
unless ( ref $transaction ) {
$self->{result}{error} = "Не найдена транзакция для order_id=".$opts->{orderNumber};
return $self;
}
$opts->{orderId} = $transaction->session_id;
warn "Sberbank Status: ".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: [".$result->content."]\n" if $DEBUG;
my $content = decode_json $result->decoded_content;
warn Dumper $content if $DEBUG;
if ( ref $content && exists $content->{orderStatus} && exists $content->{orderId} ) {
$self->{result} = {
success => 1,
status => $content->{orderStatus},
action => $content->{actionCode},
action_description => $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 "[$content]\n";
} else {
$self->{result}{error} = 'Sberbank Status failed';
$self->{result}{responce} = $content;
warn $self->{result}{error}."\n";
warn "[$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 'Charge' ) {
$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 = decode_json $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( 'Refund' );
$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} = $content->{errorMessage};
warn "[$content]\n";
} else {
$self->{result}{error} = 'Sberbank Refund failed';
$self->{result}{responce} = $content;
warn $self->{result}{error}."\n";
warn "[$content]\n";
}
} else {
$self->{result}{error} = 'PayTure Init failed';
$self->{result}{responce} = $result->status_line;
warn $self->{result}{error}.": ".$result->status_line."\n";
warn Dumper $result;
}
}
return $self;
}
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;