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->{provider} = $prefix;
$self->{app_id} = $state->{money}{$prefix."_app_id"};
$self->{secret} = $state->{money}{$prefix."_app_secret"};
$self->{token} = $state->{money}{$prefix."_token"};
$self->{tax_mode} = $state->{money}{$prefix."_tax_mode"};
unless ( exists $TAX_MODES{$self->{tax_mode}} ) {
warn "Неверная мнемоника типа налоговой системы [".$self->{tax_mode}."]\n";
return undef;
}
$self->{tax_nds} = $state->{money}{$prefix."_tax_nds"};
unless ( exists $TAX_NDS{$self->{tax_nds}} ) {
warn "Неверная мнемоника типа НДС [".$self->{tax_nds}."]\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
# или
payments => ARRAY_REF по стандарту примера
});
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;
}
$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 $MR;
if ( exists $opts->{order} ) {
$MR = $self->_GetLastMoneyReceipt( $opts->{order}->id );
}
if ( ref $MR && $MR->session_id && $MR->name eq $opts->{type} ) {
$self->{result}{receipt} = $MR;
return $self;
}
unless ( $MR ) {
$MR = money::Receipt->new( $keeper );
$MR->name( $opts->{type} );
$MR->provider( $self->{prefix} );
$MR->status( $self->{test_mode} );
$MR->success( 0 );
if ( ref $opts->{order} ) {
$MR->order_id( $opts->{order}->id );
}
$MR->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 = [];
my $discount = 0;
if ( ref $opts->{order} && $opts->{order}->sum_discount > 0 ) {
$discount = $opts->{order}->sum_discount / $opts->{order}->sum;
}
foreach my $bi ( @{$opts->{basket}} ) {
my $item = $bi->{item};
next unless ref $item;
my $price = int(($bi->{item}->price - $discount * $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;
}
if ( ref $opts->{order} && $opts->{order}->sum_delivery > 0 ) {
my $price = int($opts->{order}->sum_delivery * 100);
my $pos = {
name => 'Доставка',
type => 'COUNTABLE',
quantity => 1,
price => $price,
priceSum => $price,
tax => $self->{tax_nds},
taxSum => $price * $TAX_NDS{$self->{tax_nds}},
};
push @$positions, $pos;
}
$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;
} elsif ( exists $opts->{order} && exists $keeper->{users} ) {
my $profile = $keeper->{users}->get_profile( id => $opts->{order}->uid );
if ( ref $profile ) {
my $email = $profile->email;
my $attributes = { email => "$email" };
$data->{attributes} = $attributes;
}
}
# Заполняем параметры оплаты: total и payments
if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) {
$data->{total}{priceSum} = int($opts->{order}->sum_total * 100);
} else {
if ( $opts->{total} ) {
$data->{total}{priceSum} = $opts->{total};
}
}
unless ( $data->{total}{priceSum} ) {
$self->{result}{error} = 'Не указана итоговая сумма. Необходимо передать параметр total или order';
return $self;
}
$MR->sum( sprintf("%.2f", $data->{total}{priceSum} / 100) );
if ( exists $opts->{payments} && ref $opts->{payments} eq 'ARRAY' ) {
$data->{payments} = $opts->{payments};
} else {
my $payment = {sum => $data->{total}{priceSum}};
if ( exists $opts->{payment_type} ) {
$payment->{type} = $opts->{payment_type};
} else {
$payment->{type} = 'CASHLESS';
}
$data->{payments} = [$payment];
}
my $api_url = 'receipts';
warn "DREAMKAS receipt data: ".Data::Dumper::Dumper( $data ) if $DEBUG;
$self->_MakeRequest( $api_url, 'post', $data );
warn Data::Dumper::Dumper( $self->{result} ) if $DEBUG;
if ( $self->{result}{code} == 202 || $self->{result}{code} == 200 ) {
$MR->success( $OP_STATUS{$self->{result}{content}{status}} );
$MR->session_id( $self->{result}{content}{id} );
$MR->store;
$self->{result}{receipt} = $MR;
} else {
$self->{result}{error} = $self->{result}{status};
}
return $self;
}
=for rem CHECK
# Информация о статусе операции
$mm->check({ operation_id => $operation_id });
$mm->check({ receipt => $receipt });
Передается 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 $MR;
if ( exists $self->{result}{receipt} ) {
$MR = $self->{result}{receipt};
} elsif ( exists $opts->{receipt} ) {
$MR = $opts->{receipt};
} elsif ( $opts->{operation_id} ) {
($MR) = $self->_GetReceiptByOperationId( $opts->{operation_id} );
}
unless ( ref $MR ) {
$self->{result}{error} = 'Не найден объект "движение денежных средств". Проверьте входные параметры';
return $self;
}
my $api_url = 'operations/'.$MR->session_id;
$self->_MakeRequest( $api_url, 'get' );
warn Data::Dumper::Dumper( $self->{result} ) if $DEBUG;
if ( $self->{result}{code} == 200 ) {
if ( $self->{result}{content}{status} eq 'ERROR' ) {
$self->{result}{error} = Encode::encode('utf-8', $self->{result}{content}{data}{error}{message});
$self->{result}{code} = $self->{result}{content}{data}{error}{code};
$MR->comment( $self->{result}{error} );
}
$MR->success( $OP_STATUS{$self->{result}{content}{status}} );
$MR->store;
$self->{result}{receipt} = $MR;
} 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} ) {
warn "Auth by token: ".$self->{token}."\n" if $DEBUG;
$ua->default_header( 'Authorization' => "Bearer ".$self->{token} );
} elsif ( $self->{app_id} && $self->{secret} ) {
warn "Auth by app id: ".$self->{app_id}."|".$self->{secret}."\n" if $DEBUG;
my $auth = encode_base64($self->{app_id}.':'.$self->{secret});
warn "base64: $auth\n" if $DEBUG;
$ua->default_header( 'Authorization' => "Application: {$auth}" );
}
$ua->default_header( 'Content-Type' => 'application/json' );
if ( ref $body ) {
$body = encode_json( $body );
}
my $uri = URI->new( $self->{base_url}.($url =~ /^\// ? '' : '/').$url );
my $res;
if ( $type eq 'post' ) {
my $req = HTTP::Request->new(POST => $uri);
$req->content_type('application/json');
$req->content($body);
warn "DREAMKAS post JSON: $body\n" if $DEBUG;
$res = $ua->request($req);
} elsif ( $type eq 'delete' ) {
$res = $ua->delete( $uri );
} else {
$res = $ua->get( $uri );
}
$self->{result} = {
code => $res->code,
status => $res->status_line,
content => Data::Recursive::Encode->encode_utf8( JSON::XS->new->utf8->decode( $res->decoded_content ) ),
};
return $self;
}
sub _GetLastMoneyReceipt {
my $self = shift;
my $order_id = shift;
my ($mm) = $keeper->get_documents(
class => 'money::Receipt',
limit => 1,
provider => $self->{prefix},
order_id => $order_id,
order_by => 'id desc',
);
return $mm;
}
sub _GetReceiptByOperationId {
my $self = shift;
my $op_id = shift;
my ($mm) = $keeper->get_documents(
class => 'money::Receipt',
limit => 1,
provider => $self->{prefix},
session_id => $op_id,
);
return $mm;
}
1;