package money::Provider::Dreamkas;
use strict;
use warnings 'all';
use base 'money::Provider::Base';
use Contenido::Globals;
use money::Keeper;
use MIME::Base64;
use URI;
use URI::QueryParam;
use JSON::XS;
use Data::Dumper;
use constant (
TAXMODE_DEFAULT => 'DEFAULT',
);
our %TAX_MODES = (
'DEFAULT' => 1, # Общая
'SIMPLE' => 1, # Упрощенная доход
'SIMPLE_WO' => 1, # Упрощенная доход минус расход
'ENVD' => 1, # Единый налог на вмененный доход
'AGRICULT' => 1, # Единый сельскохозяйственный
'PATENT' => 1, # Патентная система налогообложения
);
our %TAX_NDS = (
'NDS_NO_TAX' => 0,
'NDS_0' => 0,
'NDS_10' => 0.10,
'NDS_18' => 0.18,
'NDS_20' => 0.20,
'NDS_10_CALCULATED' => 0.10/110,
'NDS_18_CALCULATED' => 0.10/110,
);
our %OP_STATUS = (
'PENDING' => 0,
'IN_PROGRESS' => 1,
'SUCCESS' => 2,
'ERROR' => -1,
);
sub new {
my ($proto, %params) = @_;
my $class = ref($proto) || $proto;
my $self = {};
my $prefix = $class =~ /\:\:(\w+)$/ ? lc($1) : undef;
return unless $prefix;
$self->{prefix} = $prefix;
$self->{app_id} = $state->{money}{$prefix."_app_id"};
$self->{secret} = $state->{money}{$prefix."_app_secret"};
$self->{token} = $state->{money}{$prefix."_app_token"};
$self->{tax_mode} = $state->{money}{$prefix."_tax_mode"};
unless ( exists $TAX_MODES{$self->{tax_mode}} ) {
warn "Неверная мнемоника типа налоговой системы\n";
return undef;
}
$self->{tax_nds} = $state->{money}{$prefix."_tax_nds"};
unless ( exists $TAX_NDS{$self->{tax_nds}} ) {
warn "Неверная мнемоника типа НДС\n";
return undef;
}
$self->{device_id} = $state->{money}{$prefix."_device_id"};
unless ( $self->{device_id} ) {
warn "Не указан или неверно указан ID кассового аппарата\n";
return undef;
}
$self->{test_mode} = exists $params{test_mode} ? $params{test_mode} : $state->{money}->{$prefix."_test_mode"};
$self->{return_url} = $params{return_url} || $state->{money}{$prefix."_return_url"};
$self->{fail_url} = $params{fail_url} || $state->{money}{$prefix."_fail_url"};
$self->{currency} = $state->{money}{$prefix."_currency_code"};
$self->{base_url} = 'https://'. ($self->{test_mode} ? 'private-anon-f6c2f7b545-kabinet.apiary-mock.com' : 'kabinet.dreamkas.ru').'/api';
$self->{result} = {};
bless $self, $class;
return $self;
}
=for rem RECEIPT
# Фискализация чека (только для Дримкас-Ф)
$mm->receipt({
# обязательные:
order => webshop::Order
# или
order_id => ID от webshop::Order
total => общая сумма заказа, если не передан order
profile => Профиль пользователя, объект
# или
attributes => Атрибуты, пример внизу
# или
email => E-mail
phone => Phone в формате +79163332222
# необязательные:
basket => Если есть order или order_id, можно не передавать
# или
positions => ARRAY_REF, если не передан order или basket
type => SALE || REFUND || OUTFLOW || OUTFLOW_REFUND || SALE
timeout => Таймаут фискализации в секундах (по умолчанию - 300 секунд).
Если в течение этого времени не удастся произвести фискализацию,
то операция будет отменена с ошибкой.
payment_type => CASH || CASHLESS
});
JSON тела вызова:
{
"deviceId": 1385,
"type": "SALE",
"timeout": 180,
"taxMode": "DEFAULT",
"positions": [
{
"name": "Шоколад Сникерс",
"type": "COUNTABLE",
"quantity": 2,
"price": 4500,
"priceSum": 9000,
"tax": "NDS_18",
"taxSum": 1620
}
],
"payments": [
{
"sum": 9000,
"type": "CASHLESS"
}
],
"attributes": {
"email": "john.smith@example.com",
"phone": "+71239994499"
},
"total": {
"priceSum": 9000
}
}
Результат:
{
"id": "5956889136fdd7733f19cfe6",
"createdAt": "2017-06-20 12:01:47.990Z",
"status": "PENDING"
}
status:
PENDING - В обработке
IN_PROGRESS - Задача принята в обработку (например, устройство приняло чек на фискализацию)
SUCCESS - Завершено успешно
ERROR - Завершено с ошибкой
=cut
##########################################################
sub receipt {
my $self = shift;
my $opts = shift // {};
my $type = delete $opts->{type};
if ( $type && $type =~ /^(SALE|REFUND|OUTFLOW|OUTFLOW_REFUND)$/ ) {
$self->{result}{error} = 'Неверно указан тип операции';
return $self;
}
$opts->{type} ||= 'SALE';
my $data = {
type => $type,
deviceId=> $self->{device_id},
taxMode => $self->{tax_mode},
};
if ( exists $opts->{order_id} ) {
$opts->{order} = $keeper->{webshop}->get_orders( id => $opts->{order_id} );
unless ( ref $opts->{order} eq 'webshop::Order' ) {
$self->{result}{error} = 'Заказ не найден. Передан неверный order_id';
return $self;
}
}
my $MM;
if ( exists $opts->{order} ) {
$MM = $self->_GetLastMoneyCheck( $opts->{order}->id );
}
if ( ref $MM && $MM->session_id && $MM->name eq $opts->{type} ) {
$self->{result}{money_movement} = $MM;
return $self;
}
unless ( $MM ) {
$MM = money::Check->new( $keeper );
$MM->name( $opts->{type} );
$MM->provider( $self->{prefix} );
$MM->status( $self->{test_mode} );
$MM->success( 0 );
if ( ref $opts->{order} ) {
$MM->order_id( $opts->{order}->id );
}
$MM->currency_code( $self->{currency} );
}
if ( exists $opts->{order} && !exists $opts->{basket} ) {
$opts->{basket} = $keeper->{webshop}->get_basket( order_id => $opts->{order}->id, with_products => 1 );
unless ( ref $opts->{basket} eq 'ARRAY' && @{$opts->{basket}} ) {
$self->{result}{error} = 'Невозможно получить список товарных позиций в заказе';
return $self;
}
}
if ( exists $opts->{basket} && ref $opts->{basket} eq 'ARRAY' ) {
my $positions = [];
foreach my $bi ( @{$opts->{basket}} ) {
my $item = $bi->{item};
next unless ref $item;
my $price = int($bi->{item}->price * 100);
my $pos = {
name => $bi->name,
type => 'COUNTABLE',
quantity => $bi->number,
price => $price,
priceSum => $price * $bi->number,
tax => $self->{tax_nds},
taxSum => ($price * $bi->number) * $TAX_NDS{$self->{tax_nds}},
};
push @$positions, $pos;
}
unless ( @$positions ) {
$self->{result}{error} = 'Cписок товарных позиций в заказе неверный. Возможно, в состав корзины не включенны товары';
return $self;
}
$data->{positions} = $positions;
} elsif ( exists $opts->{positions} && ref $opts->{positions} eq 'ARRAY' && @{$opts->{positions}} ) {
$data->{positions} = $opts->{positions};
}
# Заполняем атрибуты плательщика
if ( exists $opts->{profile} && ref $opts->{profile} eq $state->{users}->profile_document_class ) {
my $profile = $opts->{profile};
my $email = $profile->email;
my $attributes = { email => "$email" };
$data->{attributes} = $attributes;
} elsif ( exists $opts->{attributes} ) {
$data->{attributes} = $data->{attributes};
} elsif ( exists $opts->{email} || $opts->{phone} ) {
my $attributes = {};
if ( exists $opts->{email} && $opts->{email} ) {
if ( ref $opts->{email} ) {
$attributes->{email} = $opts->{email}->name;
} else {
$attributes->{email} = $opts->{email};
}
}
if ( exists $opts->{phone} && $opts->{phone} ) {
if ( ref $opts->{phone} ) {
$attributes->{phone} = $opts->{phone}->name;
} else {
$attributes->{phone} = $opts->{phone};
}
}
$data->{attributes} = $attributes;
}
# Заполняем параметры оплаты: total и payments
if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) {
$data->{total}{priceSum} = int($opts->{order}->sum_total * 10);
} else {
if ( $opts->{total} ) {
$data->{total}{priceSum} = $opts->{total};
}
}
unless ( $data->{total}{priceSum} ) {
$self->{result}{error} = 'Не указана итоговая сумма. Необходимо передать параметр total или order';
return $self;
}
$data->{payments}{sum} = $data->{total}{priceSum};
$MM->sum( $data->{total}{priceSum} );
if ( exists $opts->{payment_type} && $opts->{payment_type} eq 'CASH' ) {
$data->{payments}{type} = 'CASH';
}
my $api_url = 'receipts';
$self->_MakeRequest( $api_url, 'post', $data );
if ( $self->{result}{code} == 202 || $self->{result}{code} == 200 ) {
$MM->success( $OP_STATUS{$self->{result}{content}{status}} );
$MM->session_id( $self->{result}{content}{id} );
$MM->store;
$self->{result}{money_movement} = $MM;
} else {
$self->{result}{error} = $self->{result}{status};
}
return $self;
}
=for rem RECEIPT
# Информация о статусе операции
$mm->check( $operation_id );
Передается ID, полученное на этапе запроса на фискализацию чека
Результат:
{
"id": "5956889136fdd7733f19cfe6",
"createdAt": "2017-06-20 12:01:47.990Z",
"status": "ERROR",
"completedAt": "2017-06-20 12:03:12.440Z",
"data": {
"error": {
"code": "NeedUpdateCash",
"message": "Требуется обновление кассы"
}
}
}
status:
PENDING - В обработке
IN_PROGRESS - Задача принята в обработку (например, устройство приняло чек на фискализацию)
SUCCESS - Завершено успешно
ERROR - Завершено с ошибкой
=cut
##########################################################
sub check {
my $self = shift;
my $opts = shift // {};
if ( exists $self->{result} && exists $self->{result}{error} ) {
return $self;
}
my $MM;
if ( exists $self->{result}{money_movement} ) {
$MM = $self->{result}{money_movement};
} elsif ( exists $opts->{money_movement} ) {
$MM = $opts->{money_movement};
} elsif ( $opts->{operation_id} ) {
($MM) = $self->_GetCheckByOperationId( $opts->{operation_id} );
}
unless ( ref $MM ) {
$self->{result}{error} = 'Не найден объект "движение денежных средств". Проверьте входные параметры';
return $self;
}
my $api_url = 'operations/'.$MM->session_id;
$self->_MakeRequest( $api_url, 'get' );
if ( $self->{result}{code} == 200 ) {
$MM->success( $OP_STATUS{$self->{result}{content}{status}} );
$MM->store;
$self->{result}{money_movement} = $MM;
if ( $self->{result}{content}{status} eq 'ERROR' ) {
$self->{result}{error} = $self->{result}{content}{data}{error}{message};
$self->{result}{code} = $self->{result}{content}{data}{error}{code};
}
} else {
$self->{result}{error} = $self->{result}{status};
}
return $self;
}
sub _MakeRequest {
my ($self, $url, $type, $body) = @_;
$type ||= 'post';
my $ua = LWP::UserAgent->new;
$ua->timeout( 10 );
$ua->agent('Mozilla/5.0');
if ( exists $self->{token} && $self->{token} ) {
$ua->default_header( 'Authorization' => "Bearer ".$self->{token} );
} elsif ( $self->{app_id} && $self->{secret} ) {
my $auth = encode_base64($self->{app_id}.':'.$self->{secret});
$ua->default_header( 'Authorization' => "Application: {$auth}" );
}
$ua->default_header( 'Content-Type' => 'application/json' );
if ( ref $body ) {
$body = encode_json( $body );
}
my $req = URI->new( $self->{base_url}.($url =~ /^\// ? '' : '/').$url );
my $res;
if ( $type eq 'post' ) {
$res = $ua->post( $req, Content => $body );
} elsif ( $type eq 'delete' ) {
$res = $ua->delete( $req );
} else {
$res = $ua->get( $req );
}
$self->{result} = {
code => $res->code,
status => $res->status_line,
content => JSON::XS->new->decode( $res->decoded_content ),
};
return $self;
}
sub _GetLastMoneyCheck {
my $self = shift;
my $order_id = shift;
my ($mm) = $keeper->get_documents(
class => 'money::Check',
limit => 1,
provider => $self->{prefix},
order_id => $order_id,
order_by => 'id desc',
);
return $mm;
}
sub _GetCheckByOperationId {
my $self = shift;
my $op_id = shift;
my ($mm) = $keeper->get_documents(
class => 'money::Check',
limit => 1,
provider => $self->{prefix},
session_id => $op_id,
);
return $mm;
}
1;