Line # Revision Author
1 693 ahitrov package money::Provider::Dreamkas;
2
3 use strict;
4 use warnings 'all';
5
6 use base 'money::Provider::Base';
7 use Contenido::Globals;
8 use money::Keeper;
9 use MIME::Base64;
10 use URI;
11 use URI::QueryParam;
12 use JSON::XS;
13 use Data::Dumper;
14
15 use constant (
16 TAXMODE_DEFAULT => 'DEFAULT',
17 );
18
19 our %TAX_MODES = (
20 'DEFAULT' => 1, # Общая
21 'SIMPLE' => 1, # Упрощенная доход
22 'SIMPLE_WO' => 1, # Упрощенная доход минус расход
23 'ENVD' => 1, # Единый налог на вмененный доход
24 'AGRICULT' => 1, # Единый сельскохозяйственный
25 'PATENT' => 1, # Патентная система налогообложения
26 );
27
28 our %TAX_NDS = (
29 'NDS_NO_TAX' => 0,
30 'NDS_0' => 0,
31 'NDS_10' => 0.10,
32 'NDS_18' => 0.18,
33 'NDS_20' => 0.20,
34 'NDS_10_CALCULATED' => 0.10/110,
35 'NDS_18_CALCULATED' => 0.10/110,
36 );
37
38 our %OP_STATUS = (
39 'PENDING' => 0,
40 'IN_PROGRESS' => 1,
41 'SUCCESS' => 2,
42 'ERROR' => -1,
43 );
44
45 sub new {
46 my ($proto, %params) = @_;
47 my $class = ref($proto) || $proto;
48 my $self = {};
49 my $prefix = $class =~ /\:\:(\w+)$/ ? lc($1) : undef;
50 return unless $prefix;
51
52 $self->{prefix} = $prefix;
53 $self->{app_id} = $state->{money}{$prefix."_app_id"};
54 $self->{secret} = $state->{money}{$prefix."_app_secret"};
55 $self->{token} = $state->{money}{$prefix."_app_token"};
56 $self->{tax_mode} = $state->{money}{$prefix."_tax_mode"};
57 unless ( exists $TAX_MODES{$self->{tax_mode}} ) {
58 698 ahitrov warn "Неверная мнемоника типа налоговой системы [".$self->{tax_mode}."]\n";
59 693 ahitrov return undef;
60 }
61 $self->{tax_nds} = $state->{money}{$prefix."_tax_nds"};
62 unless ( exists $TAX_NDS{$self->{tax_nds}} ) {
63 698 ahitrov warn "Неверная мнемоника типа НДС [".$self->{tax_nds}."]\n";
64 693 ahitrov return undef;
65 }
66 $self->{device_id} = $state->{money}{$prefix."_device_id"};
67 unless ( $self->{device_id} ) {
68 warn "Не указан или неверно указан ID кассового аппарата\n";
69 return undef;
70 }
71 $self->{test_mode} = exists $params{test_mode} ? $params{test_mode} : $state->{money}->{$prefix."_test_mode"};
72 $self->{return_url} = $params{return_url} || $state->{money}{$prefix."_return_url"};
73 $self->{fail_url} = $params{fail_url} || $state->{money}{$prefix."_fail_url"};
74
75 $self->{currency} = $state->{money}{$prefix."_currency_code"};
76
77 694 ahitrov $self->{base_url} = 'https://'. ($self->{test_mode} ? 'private-anon-f6c2f7b545-kabinet.apiary-mock.com' : 'kabinet.dreamkas.ru').'/api';
78 693 ahitrov $self->{result} = {};
79
80 bless $self, $class;
81 return $self;
82 }
83
84
85 =for rem RECEIPT
86 # Фискализация чека (только для Дримкас-Ф)
87
88 $mm->receipt({
89 # обязательные:
90 order => webshop::Order
91 # или
92 order_id => ID от webshop::Order
93 total => общая сумма заказа, если не передан order
94
95 profile => Профиль пользователя, объект
96 # или
97 attributes => Атрибуты, пример внизу
98 # или
99 email => E-mail
100 phone => Phone в формате +79163332222
101
102 # необязательные:
103 basket => Если есть order или order_id, можно не передавать
104 # или
105 positions => ARRAY_REF, если не передан order или basket
106
107 type => SALE || REFUND || OUTFLOW || OUTFLOW_REFUND || SALE
108 timeout => Таймаут фискализации в секундах (по умолчанию - 300 секунд).
109 Если в течение этого времени не удастся произвести фискализацию,
110 то операция будет отменена с ошибкой.
111 payment_type => CASH || CASHLESS
112 });
113
114 JSON тела вызова:
115
116 {
117 "deviceId": 1385,
118 "type": "SALE",
119 "timeout": 180,
120 "taxMode": "DEFAULT",
121 "positions": [
122 {
123 "name": "Шоколад Сникерс",
124 "type": "COUNTABLE",
125 "quantity": 2,
126 "price": 4500,
127 "priceSum": 9000,
128 "tax": "NDS_18",
129 "taxSum": 1620
130 }
131 ],
132 "payments": [
133 {
134 "sum": 9000,
135 "type": "CASHLESS"
136 }
137 ],
138 "attributes": {
139 "email": "john.smith@example.com",
140 "phone": "+71239994499"
141 },
142 "total": {
143 "priceSum": 9000
144 }
145 }
146
147
148 Результат:
149
150 {
151 "id": "5956889136fdd7733f19cfe6",
152 "createdAt": "2017-06-20 12:01:47.990Z",
153 "status": "PENDING"
154 }
155
156 status:
157
158 PENDING - В обработке
159 IN_PROGRESS - Задача принята в обработку (например, устройство приняло чек на фискализацию)
160 SUCCESS - Завершено успешно
161 ERROR - Завершено с ошибкой
162
163 =cut
164 ##########################################################
165 sub receipt {
166 my $self = shift;
167 my $opts = shift // {};
168
169 my $type = delete $opts->{type};
170 if ( $type && $type =~ /^(SALE|REFUND|OUTFLOW|OUTFLOW_REFUND)$/ ) {
171 $self->{result}{error} = 'Неверно указан тип операции';
172 return $self;
173 }
174 $opts->{type} ||= 'SALE';
175
176 my $data = {
177 type => $type,
178 deviceId=> $self->{device_id},
179 taxMode => $self->{tax_mode},
180 };
181
182 if ( exists $opts->{order_id} ) {
183 $opts->{order} = $keeper->{webshop}->get_orders( id => $opts->{order_id} );
184 unless ( ref $opts->{order} eq 'webshop::Order' ) {
185 $self->{result}{error} = 'Заказ не найден. Передан неверный order_id';
186 return $self;
187 }
188 }
189
190 my $MM;
191 if ( exists $opts->{order} ) {
192 694 ahitrov $MM = $self->_GetLastMoneyCheck( $opts->{order}->id );
193 693 ahitrov }
194 if ( ref $MM && $MM->session_id && $MM->name eq $opts->{type} ) {
195 697 ahitrov $self->{result}{money_check} = $MM;
196 693 ahitrov return $self;
197 }
198 unless ( $MM ) {
199 694 ahitrov $MM = money::Check->new( $keeper );
200 693 ahitrov $MM->name( $opts->{type} );
201 $MM->provider( $self->{prefix} );
202 $MM->status( $self->{test_mode} );
203 $MM->success( 0 );
204 if ( ref $opts->{order} ) {
205 $MM->order_id( $opts->{order}->id );
206 }
207 $MM->currency_code( $self->{currency} );
208 }
209
210 if ( exists $opts->{order} && !exists $opts->{basket} ) {
211 $opts->{basket} = $keeper->{webshop}->get_basket( order_id => $opts->{order}->id, with_products => 1 );
212 unless ( ref $opts->{basket} eq 'ARRAY' && @{$opts->{basket}} ) {
213 $self->{result}{error} = 'Невозможно получить список товарных позиций в заказе';
214 return $self;
215 }
216 }
217
218 if ( exists $opts->{basket} && ref $opts->{basket} eq 'ARRAY' ) {
219 my $positions = [];
220 foreach my $bi ( @{$opts->{basket}} ) {
221 my $item = $bi->{item};
222 next unless ref $item;
223 694 ahitrov my $price = int($bi->{item}->price * 100);
224 693 ahitrov my $pos = {
225 name => $bi->name,
226 type => 'COUNTABLE',
227 quantity => $bi->number,
228 price => $price,
229 priceSum => $price * $bi->number,
230 tax => $self->{tax_nds},
231 taxSum => ($price * $bi->number) * $TAX_NDS{$self->{tax_nds}},
232 };
233 push @$positions, $pos;
234 }
235 unless ( @$positions ) {
236 $self->{result}{error} = 'Cписок товарных позиций в заказе неверный. Возможно, в состав корзины не включенны товары';
237 return $self;
238 }
239 $data->{positions} = $positions;
240 } elsif ( exists $opts->{positions} && ref $opts->{positions} eq 'ARRAY' && @{$opts->{positions}} ) {
241 $data->{positions} = $opts->{positions};
242 }
243
244 # Заполняем атрибуты плательщика
245 if ( exists $opts->{profile} && ref $opts->{profile} eq $state->{users}->profile_document_class ) {
246 my $profile = $opts->{profile};
247 my $email = $profile->email;
248 my $attributes = { email => "$email" };
249 $data->{attributes} = $attributes;
250 } elsif ( exists $opts->{attributes} ) {
251 $data->{attributes} = $data->{attributes};
252 } elsif ( exists $opts->{email} || $opts->{phone} ) {
253 my $attributes = {};
254 if ( exists $opts->{email} && $opts->{email} ) {
255 if ( ref $opts->{email} ) {
256 694 ahitrov $attributes->{email} = $opts->{email}->name;
257 693 ahitrov } else {
258 694 ahitrov $attributes->{email} = $opts->{email};
259 693 ahitrov }
260 }
261 if ( exists $opts->{phone} && $opts->{phone} ) {
262 if ( ref $opts->{phone} ) {
263 694 ahitrov $attributes->{phone} = $opts->{phone}->name;
264 693 ahitrov } else {
265 694 ahitrov $attributes->{phone} = $opts->{phone};
266 693 ahitrov }
267 }
268 $data->{attributes} = $attributes;
269 }
270
271 # Заполняем параметры оплаты: total и payments
272 if ( exists $opts->{order} && ref $opts->{order} eq 'webshop::Order' ) {
273 $data->{total}{priceSum} = int($opts->{order}->sum_total * 10);
274 } else {
275 if ( $opts->{total} ) {
276 $data->{total}{priceSum} = $opts->{total};
277 }
278 }
279 unless ( $data->{total}{priceSum} ) {
280 $self->{result}{error} = 'Не указана итоговая сумма. Необходимо передать параметр total или order';
281 return $self;
282 }
283 $data->{payments}{sum} = $data->{total}{priceSum};
284 $MM->sum( $data->{total}{priceSum} );
285 if ( exists $opts->{payment_type} && $opts->{payment_type} eq 'CASH' ) {
286 $data->{payments}{type} = 'CASH';
287 }
288
289 694 ahitrov my $api_url = 'receipts';
290 693 ahitrov
291 $self->_MakeRequest( $api_url, 'post', $data );
292 695 ahitrov if ( $self->{result}{code} == 202 || $self->{result}{code} == 200 ) {
293 693 ahitrov $MM->success( $OP_STATUS{$self->{result}{content}{status}} );
294 $MM->session_id( $self->{result}{content}{id} );
295 $MM->store;
296 697 ahitrov $self->{result}{money_check} = $MM;
297 693 ahitrov } else {
298 $self->{result}{error} = $self->{result}{status};
299 }
300
301 return $self;
302 }
303
304
305 698 ahitrov =for rem CHECK
306 693 ahitrov # Информация о статусе операции
307
308 $mm->check( $operation_id );
309
310 Передается ID, полученное на этапе запроса на фискализацию чека
311
312 Результат:
313
314 {
315 "id": "5956889136fdd7733f19cfe6",
316 "createdAt": "2017-06-20 12:01:47.990Z",
317 "status": "ERROR",
318 "completedAt": "2017-06-20 12:03:12.440Z",
319 "data": {
320 "error": {
321 "code": "NeedUpdateCash",
322 "message": "Требуется обновление кассы"
323 }
324 }
325 }
326
327 status:
328
329 PENDING - В обработке
330 IN_PROGRESS - Задача принята в обработку (например, устройство приняло чек на фискализацию)
331 SUCCESS - Завершено успешно
332 ERROR - Завершено с ошибкой
333
334 =cut
335 ##########################################################
336 sub check {
337 my $self = shift;
338 my $opts = shift // {};
339 if ( exists $self->{result} && exists $self->{result}{error} ) {
340 return $self;
341 }
342
343 my $MM;
344 697 ahitrov if ( exists $self->{result}{money_check} ) {
345 $MM = $self->{result}{money_check};
346 } elsif ( exists $opts->{money_check} ) {
347 $MM = $opts->{money_check};
348 693 ahitrov } elsif ( $opts->{operation_id} ) {
349 694 ahitrov ($MM) = $self->_GetCheckByOperationId( $opts->{operation_id} );
350 693 ahitrov }
351 unless ( ref $MM ) {
352 $self->{result}{error} = 'Не найден объект "движение денежных средств". Проверьте входные параметры';
353 return $self;
354 }
355
356 694 ahitrov my $api_url = 'operations/'.$MM->session_id;
357 693 ahitrov
358 $self->_MakeRequest( $api_url, 'get' );
359 if ( $self->{result}{code} == 200 ) {
360 $MM->success( $OP_STATUS{$self->{result}{content}{status}} );
361 $MM->store;
362 697 ahitrov $self->{result}{money_check} = $MM;
363 693 ahitrov if ( $self->{result}{content}{status} eq 'ERROR' ) {
364 $self->{result}{error} = $self->{result}{content}{data}{error}{message};
365 $self->{result}{code} = $self->{result}{content}{data}{error}{code};
366 }
367 } else {
368 $self->{result}{error} = $self->{result}{status};
369 }
370
371 return $self;
372 }
373
374
375 sub _MakeRequest {
376 my ($self, $url, $type, $body) = @_;
377 $type ||= 'post';
378
379 my $ua = LWP::UserAgent->new;
380 $ua->timeout( 10 );
381 $ua->agent('Mozilla/5.0');
382
383 695 ahitrov if ( exists $self->{token} && $self->{token} ) {
384 $ua->default_header( 'Authorization' => "Bearer ".$self->{token} );
385 } elsif ( $self->{app_id} && $self->{secret} ) {
386 my $auth = encode_base64($self->{app_id}.':'.$self->{secret});
387 $ua->default_header( 'Authorization' => "Application: {$auth}" );
388 }
389 693 ahitrov $ua->default_header( 'Content-Type' => 'application/json' );
390
391 if ( ref $body ) {
392 $body = encode_json( $body );
393 }
394
395 694 ahitrov my $req = URI->new( $self->{base_url}.($url =~ /^\// ? '' : '/').$url );
396 693 ahitrov my $res;
397 if ( $type eq 'post' ) {
398 $res = $ua->post( $req, Content => $body );
399 } elsif ( $type eq 'delete' ) {
400 $res = $ua->delete( $req );
401 } else {
402 $res = $ua->get( $req );
403 }
404 $self->{result} = {
405 code => $res->code,
406 status => $res->status_line,
407 content => JSON::XS->new->decode( $res->decoded_content ),
408 694 ahitrov };
409 693 ahitrov return $self;
410 }
411
412 694 ahitrov sub _GetLastMoneyCheck {
413 693 ahitrov my $self = shift;
414 my $order_id = shift;
415 my ($mm) = $keeper->get_documents(
416 694 ahitrov class => 'money::Check',
417 693 ahitrov limit => 1,
418 694 ahitrov provider => $self->{prefix},
419 order_id => $order_id,
420 693 ahitrov order_by => 'id desc',
421 );
422 return $mm;
423 }
424
425 694 ahitrov sub _GetCheckByOperationId {
426 693 ahitrov my $self = shift;
427 my $op_id = shift;
428 my ($mm) = $keeper->get_documents(
429 694 ahitrov class => 'money::Check',
430 693 ahitrov limit => 1,
431 694 ahitrov provider => $self->{prefix},
432 693 ahitrov session_id => $op_id,
433 );
434 return $mm;
435 }
436
437 1;