atol 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 739a43f61f1406eeeb5ef5233e3788c7609d9d480fbe1dfab694073b200e181e
4
- data.tar.gz: f967cee6aeb248b5c00217d9548cbd671d771bfc94d4d1d1840b747b36dcb205
3
+ metadata.gz: 17a1f879f910bd053721385602bc4e3c158dc10625534db847d78bdb54ee75e4
4
+ data.tar.gz: 8408f2c974e2e49c3dd89745e942cc06375fbcbf050d843015fdd17d8839eae4
5
5
  SHA512:
6
- metadata.gz: fc2d5a6482f6856b7700602148b29698b1b362e004255a28fa27a7cd6b903349ec2d7c33f10c38a31c3b6c98d4f3bed70537fdc2cb69d92edc4b19e78586f73a
7
- data.tar.gz: 303cf4af75af283fc6b0917ce4a5f8c7d081c4f4e776b9f5120170e63ed8a6b78a0dddda4d7ab8c516f768c459048a82f2dba632eb4feee61f58e0ec80e1d83c
6
+ metadata.gz: 05c52df6c9b2bfcd04c47a09a2e71f1e1eec304809cba41c1e858431c6a141eeca774191ebd8488c88a97225dad0a3524af12d9bc018d0c66a29a0a279b81698
7
+ data.tar.gz: 5e034691a9b942dab227b3c2341b1379aebe787d6b71b9eee895387c8caee014e17da56a1588b2479d9852c21e5fe5646ef2ef7bcb15fc26141e4be7bdc90c37
data/.rubocop.yml CHANGED
@@ -11,4 +11,6 @@ Metrics/LineLength:
11
11
 
12
12
  Metrics/ParameterLists:
13
13
  Exclude:
14
- - lib/atol/request/post_document/item/body.rb
14
+ - lib/atol/request/post_document/item/body.rb
15
+ - lib/atol/request/post_document/sell/body.rb
16
+ - lib/atol/request/post_document/correction/v5/body.rb
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- atol (0.8.1)
4
+ atol (1.1.0)
5
5
  anyway_config (~> 1.0)
6
6
 
7
7
  GEM
@@ -9,7 +9,7 @@ GEM
9
9
  specs:
10
10
  addressable (2.5.2)
11
11
  public_suffix (>= 2.0.2, < 4.0)
12
- anyway_config (1.1.3)
12
+ anyway_config (1.4.4)
13
13
  ast (2.4.1)
14
14
  coveralls (0.8.21)
15
15
  json (>= 1.8, < 3)
@@ -22,7 +22,7 @@ GEM
22
22
  diff-lcs (1.3)
23
23
  docile (1.1.5)
24
24
  hashdiff (0.3.7)
25
- json (2.1.0)
25
+ json (2.18.1)
26
26
  parallel (1.19.2)
27
27
  parser (2.7.1.4)
28
28
  ast (~> 2.4.1)
@@ -77,7 +77,7 @@ PLATFORMS
77
77
 
78
78
  DEPENDENCIES
79
79
  atol!
80
- bundler (~> 1.15)
80
+ bundler (~> 2.1.4)
81
81
  coveralls
82
82
  rake (~> 13.0)
83
83
  rspec (~> 3.0)
data/README.md CHANGED
@@ -9,8 +9,7 @@
9
9
 
10
10
  ##### Совместимость
11
11
 
12
- Для корректной работы необходим интерпретатор Руби версии 2.5. Пакет работает с версией протокола v4.
13
-
12
+ Для корректной работы необходим интерпретатор Руби версии ~> 3.0.0. Пакет работает с версиями протокола [v4](http://atol.online/api_v4) и [v5](http://atol.online/api_v5) (см. настройку ATOL_API_URL).
14
13
 
15
14
  ## Использование
16
15
 
@@ -26,58 +25,10 @@ gem 'atol'
26
25
  ```
27
26
  $ bundle install
28
27
  ```
29
- ### Конфигурация
30
-
31
- Для обращения к сервису необходимы данные учетной записи.
32
-
33
- При инициализации приложение попытается найти необходимые параметры в константе `ENV`.
34
-
35
- Для корректной работы потребуются следующие переменные окружения.
36
-
37
- **Все переменные являются обязательными**.
38
-
39
- ```bash
40
- # .env
41
- ATOL_INN=123456789010
42
- ATOL_LOGIN=example-login
43
- ATOL_PASSWORD=example-password
44
- ATOL_PAYMENT_ADDRESS="г. Москва, ул. Ленина, д.1 к.2"
45
- ATOL_GROUP_CODE=example-group-code
46
- ATOL_DEFAULT_SNO=esn
47
- ATOL_DEFAULT_TAX=vat18
48
- ATOL_CALLBACK_URL=https://www.example.com/callback_path
49
- ATOL_COMPANY_EMAIL=example@email.com
50
- ATOL_DEFAULT_PAYMENT_TYPE=1
51
- ```
52
-
53
- Значения `ATOL_INN`, `ATOL_LOGIN`, `ATOL_PASSWORD`, `ATOL_PAYMENT_ADDRESS` и `ATOL_GROUP_CODE` вы получаете при регистрации в сервисе.
54
-
55
- `ATOL_DEFAULT_SNO` - система налогообложения. Возможные значения:
56
- 1) "osn" – общая СН;
57
- 2) "usn_income" – упрощенная СН (доходы);
58
- 3) "usn_income_outcome" – упрощенная СН (доходы минус расходы);
59
- 4) "envd" – единый налог на вмененный доход;
60
- 5) "esn" – единый сельскохозяйственный налог;
61
- 6) "patent" – патентная СН.
62
-
63
- `ATOL_DEFAULT_TAX` - номер налога в ККТ. Возможные значения:
64
- 1) "none" – без НДС;
65
- 2) "vat0" – НДС по ставке 0%;
66
- 3) "vat10" – НДС чека по ставке 10%;
67
- 4) "vat18" – НДС чека по ставке 18%;
68
- 5) "vat110" – НДС чека по расчетной ставке 10/110;
69
- 6) "vat118" – НДС чека по расчетной ставке 18/118.
70
-
71
- `ATOL_CALLBACK_URL` - адрес, по которому сервис будет отправлять информацию после создания чека.
72
-
73
- `ATOL_DEFAULT_PAYMENT_TYPE` - вид оплаты. Возможные значения:
74
- 1) "1" – электронный;
75
- 2) "2" – "9" – расширенные типы оплаты. Для каждого фискального типа оплаты можно указать расширенный тип оплаты.
76
-
77
- `ATOL_COMPANY_EMAIL` - адрес электронной почты вашей компании.
78
-
79
28
  ### Конфигурация в инициализаторе
80
29
 
30
+ Для обращения к сервису необходимы данные учетной записи. `login`, `password` и `group_code`
31
+
81
32
  Для Rails-приложений так же можно создать файл инициализации и задать параметры непосредственно в коде:
82
33
 
83
34
  ```ruby
@@ -88,13 +39,15 @@ Rails.application.config.after_initialize do
88
39
  config.inn = '123456789010'
89
40
  config.login = 'example-login'
90
41
  config.password = 'example-password'
91
- config.payment_address = 'г. Москва, ул. Ленина, д.1 к.2'
42
+ config.payment_address = 'г. Москва, ул. Ленина, д.1 к.2' # тэг 1187
92
43
  config.group_code = 'example-group-code'
93
- config.default_sno = 'esn'
44
+ config.default_sno = 'esn' # тэг 1055
94
45
  config.default_tax = 'vat18'
95
46
  config.callback_url = 'https://www.example.com/callback_path'
96
47
  config.company_email = 'example@email.com'
97
- config.default_payment_type = '1'
48
+ config.default_payment_type = '1' # тэг 1031
49
+ config.api_url = 'https://online.atol.ru/possystem/v5' # по умолчанию 'https://online.atol.ru/possystem/v4' ФФД 1.05
50
+ config.internet = true # тэг 1125, по умолчанию false
98
51
  end
99
52
  end
100
53
  ```
@@ -151,8 +104,7 @@ body = Atol::Request::PostDocument::Sell::Body.new(
151
104
  email: 'example@example.com',
152
105
  items: [
153
106
  ...
154
- ],
155
- agent_info_type: 'bank_paying_agent'
107
+ ]
156
108
  ).to_json
157
109
  ```
158
110
 
@@ -160,6 +112,8 @@ body = Atol::Request::PostDocument::Sell::Body.new(
160
112
 
161
113
  Массив `items` должен включать в себя объекты, которые так же соответствуют схеме.
162
114
 
115
+ #### Items для версии V4
116
+
163
117
  Для создания `items` можно использовать класс `Atol::Request::PostDocument::Item::Body`.
164
118
 
165
119
  Его конструктор принимает обязательные аргументы `name`, `price`, `payment_method`, `payment_object` и опциональные `quantity` (по умолчанию 1), `supplier_info_inn`, `supplier_info_name`, `agent_info_type` (тег ФФД - 1222).
@@ -197,7 +151,7 @@ item = Atol::Request::PostDocument::Item::Body.new(
197
151
  Тогда создание всего тела запроса будет выглядеть так:
198
152
 
199
153
  ```ruby
200
- body = Atol::Request::PostDocument::Sell::Body.new(
154
+ Atol::Request::PostDocument::Sell::Body.new(
201
155
  external_id: 123,
202
156
  email: 'example@example.com',
203
157
  items: [
@@ -271,8 +225,76 @@ body = Atol::Request::PostDocument::Sell::Body.new(
271
225
  "timestamp":"06.02.2018 12:35:00",
272
226
  "external_id":123
273
227
  }
228
+ ```
229
+
230
+ #### Items для версии V5
231
+
232
+ Создается как в v4 только
233
+
234
+ - Добавлено обязательное поле `measure`
235
+ - Добавлено обязательное поле `vat`
236
+ - Изменен тип поля `payment_object` на int
237
+
238
+ Доступные значения для `measure`
274
239
 
275
240
  ```
241
+ 0, 10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 70, 71, 72, 73, 80, 81, 82, 83, 255
242
+ ```
243
+
244
+ `vat` имеет вид объекта, пример
245
+
246
+ ```
247
+ { type: 'none' }
248
+ ```
249
+ Доступные type для vat
250
+ `none vat0 vat5 vat7 vat10 vat22 vat105 vat107 vat110 vat20 vat120 vat122`
251
+
252
+ Доступные значения для `payment_object`
253
+
254
+ ```
255
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23
256
+ ```
257
+
258
+ #### Кастомные payments
259
+
260
+ По умолчанию `Sell::Body` формирует один платёж с типом из конфигурации (`default_payment_type`) и суммой, равной итогу позиций. Если нужны другие виды оплаты (например, зачёт аванса — тег 1215, тип `2`), можно передать массив объектов `Atol::Request::PostDocument::Payment` явно. Сумма платежей должна совпадать с итогом позиций, иначе конструктор бросит `Atol::PaymentsTotalMismatchError`.
261
+
262
+ ```ruby
263
+ payments = [
264
+ Atol::Request::PostDocument::Payment.new(
265
+ type: Atol::Request::PostDocument::Payment::PREPAID_TYPE,
266
+ sum: 200.0
267
+ )
268
+ ]
269
+ Atol::Request::PostDocument::Sell::Body.new(
270
+ external_id: 123,
271
+ email: 'example@example.com',
272
+ items: [item],
273
+ payments: payments
274
+ ).to_json
275
+ ```
276
+
277
+ Виды оплаты (`type`) — константы класса `Atol::Request::PostDocument::Payment`: `CASH_TYPE` наличные (1031), `CASHLESS_TYPE` безналичный (1081), `PREPAID_TYPE` предоплата/зачёт аванса (1215), `POSTPAID_TYPE` постоплата/кредит (1216), `COUNTER_PROVISION_TYPE` встречное предоставление (1217), `EXTENDED_5_TYPE`–`EXTENDED_9_TYPE` расширенные.
278
+
279
+ #### Чек коррекции для версии V5
280
+
281
+ Для тела чека коррекции используется класс `Atol::Request::PostDocument::Correction::Body` — он выбирает реализацию по `config.api_url`, как и `Item::Body`. Реализован только ФФД 1.2 (`Correction::V5::Body`, операции `sell_refund_correction`, `sell_correction` и т.п.); для v4-эндпоинта (ФФД 1.05) тело коррекции не реализовано и конструктор бросит `Atol::Request::PostDocument::Correction::Body::BadApiUrlError`.
282
+
283
+ Обязательные аргументы: `external_id`, `items`, `correction_type` (`self` или `instruction`) и `base_date` (дата корректируемого расчёта в формате `dd.mm.yyyy`, тег 1178), а также хотя бы один контакт клиента — `phone` или `email`. Необязательные: `base_number` — номер документа-основания (тег 1179, не более 32 байт); `additional_check_props` — дополнительный реквизит чека/БСО (тег 1192, не более 16 байт); и `payments` (как и в `Sell::Body`, при отсутствии формируется один платёж из `default_payment_type` с суммой, равной итогу позиций; переданная сумма платежей должна совпадать с итогом позиций).
284
+
285
+ ```ruby
286
+ body = Atol::Request::PostDocument::Correction::Body.new(
287
+ external_id: 123,
288
+ email: 'example@example.com',
289
+ items: [...],
290
+ correction_type: 'self',
291
+ base_date: '01.01.2026',
292
+ base_number: '12345'
293
+ ).to_json
294
+
295
+ Atol::Transaction::PostDocument.new(operation: :sell_refund_correction, token: token, body: body).call
296
+ ```
297
+
276
298
  #### Отправка документа
277
299
 
278
300
  Когда токен и тело запроса составлены, остается только сделать post-запрос.
data/atol-rb.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_dependency 'anyway_config', '~> 1.0'
27
27
 
28
- spec.add_development_dependency 'bundler', '~> 1.15'
28
+ spec.add_development_dependency 'bundler', '~> 2.1.4'
29
29
  spec.add_development_dependency 'rake', '~> 13.0'
30
30
  spec.add_development_dependency 'rspec', '~> 3.0'
31
31
  spec.add_development_dependency 'rubocop', '~> 0.90'
data/lib/atol/config.rb CHANGED
@@ -15,9 +15,10 @@ module Atol
15
15
  :default_tax,
16
16
  :callback_url,
17
17
  :company_email,
18
+ internet: false,
18
19
  req_tries_number: 3,
19
20
  default_payment_type: 1,
20
21
  http_client: Net::HTTP,
21
- api_url: "https://online.atol.ru/possystem/#{Atol::Version::API}"
22
+ api_url: Atol::Version::V4
22
23
  end
23
24
  end
data/lib/atol/errors.rb CHANGED
@@ -18,6 +18,9 @@ module Atol
18
18
  class StateMissingUuidError < StandardError; end
19
19
  class StateNotFoundError < StandardError; end
20
20
  class ZeroItemQuantityError < StandardError; end
21
+ class BadPaymentError < StandardError; end
22
+ class EmptyPaymentsError < StandardError; end
23
+ class PaymentsTotalMismatchError < StandardError; end
21
24
 
22
25
  ERRORS = Hash[
23
26
  0 => BadJSONError,
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Atol
4
+ module Request
5
+ class PostDocument
6
+ module Correction
7
+ class Body
8
+ BadApiUrlError = Class.new(StandardError)
9
+
10
+ attr_reader :instance
11
+
12
+ def initialize(**kwargs)
13
+ config = kwargs[:config] || Atol.config
14
+ @instance = case config.api_url
15
+ when Atol::Version::V5, Atol::Version::V5_TEST
16
+ Atol::Request::PostDocument::Correction::V5::Body.new(**kwargs)
17
+ when Atol::Version::V4, Atol::Version::V4_TEST
18
+ raise BadApiUrlError, 'correction body for the v4 api is not implemented'
19
+ else
20
+ raise BadApiUrlError
21
+ end
22
+ end
23
+
24
+ def to_h
25
+ instance.to_h
26
+ end
27
+
28
+ def to_json(*args)
29
+ instance.to_json(*args)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atol/errors'
4
+ require 'atol/request/post_document/payment'
5
+
6
+ module Atol
7
+ module Request
8
+ class PostDocument
9
+ module Correction
10
+ module V5
11
+ class Body
12
+ class BadCorrectionTypeError < StandardError; end
13
+ class BadCorrectionBaseDateError < StandardError; end
14
+ class BadCorrectionBaseNumberError < StandardError; end
15
+ class BadAdditionalCheckPropsError < StandardError; end
16
+
17
+ CORRECTION_TYPES = %w[self instruction].freeze
18
+ BASE_DATE_FORMAT = /\A\d{2}\.\d{2}\.\d{4}\z/
19
+ BASE_NUMBER_MAX_BYTESIZE = 32
20
+ ADDITIONAL_CHECK_PROPS_MAX_BYTESIZE = 16
21
+
22
+ def initialize(external_id:, items:, correction_type:, base_date:, phone: '', email: '',
23
+ payments: nil, base_number: nil, additional_check_props: nil, config: nil, **options)
24
+ raise(Atol::EmptyClientContactError) if phone.empty? && email.empty?
25
+ raise(Atol::EmptySellItemsError) if items.empty?
26
+ raise(BadCorrectionTypeError) unless CORRECTION_TYPES.include?(correction_type)
27
+ raise(BadCorrectionBaseDateError) unless base_date.to_s.match?(BASE_DATE_FORMAT)
28
+ raise(BadCorrectionBaseNumberError) if base_number && base_number.to_s.bytesize > BASE_NUMBER_MAX_BYTESIZE
29
+ if additional_check_props && additional_check_props.to_s.bytesize > ADDITIONAL_CHECK_PROPS_MAX_BYTESIZE
30
+ raise(BadAdditionalCheckPropsError)
31
+ end
32
+
33
+ total = items.sum { |item| item[:sum] }
34
+ validate_payments!(payments, total)
35
+
36
+ @config = config || Atol.config
37
+ @external_id = external_id
38
+ @phone = phone
39
+ @email = email
40
+ @items = items
41
+ @correction_type = correction_type
42
+ @base_date = base_date
43
+ @base_number = base_number
44
+ @additional_check_props = additional_check_props
45
+ @total = total
46
+ @payments = payments
47
+ end
48
+
49
+ def to_h
50
+ build_body.clone
51
+ end
52
+
53
+ def to_json(*_args)
54
+ build_body.to_json
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :config, :external_id, :phone, :email, :items, :payments,
60
+ :correction_type, :base_date, :base_number, :additional_check_props, :total
61
+
62
+ def validate_payments!(payments, total)
63
+ return if payments.nil?
64
+
65
+ raise(Atol::EmptyPaymentsError) if payments.empty?
66
+ raise(Atol::BadPaymentError) if payments.any? { |payment| !payment.is_a?(Payment) }
67
+ raise(Atol::PaymentsTotalMismatchError) unless payments.sum(&:sum).round(2) == total.round(2)
68
+ end
69
+
70
+ def build_body
71
+ {
72
+ external_id: external_id,
73
+ correction: correction,
74
+ service: service,
75
+ timestamp: Time.now.strftime(Atol::TIMESTAMP_FORMAT)
76
+ }
77
+ end
78
+
79
+ def correction
80
+ result = {
81
+ client: client,
82
+ company: company,
83
+ correction_info: correction_info,
84
+ items: items,
85
+ payments: build_payments,
86
+ total: total,
87
+ internet: config.internet
88
+ }
89
+ result[:additional_check_props] = additional_check_props if additional_check_props
90
+
91
+ result
92
+ end
93
+
94
+ def client
95
+ result = {}
96
+ result[:email] = email unless email.empty?
97
+ result[:phone] = phone unless phone.empty?
98
+
99
+ result
100
+ end
101
+
102
+ def company
103
+ {
104
+ inn: config.inn.to_s,
105
+ sno: config.default_sno,
106
+ payment_address: config.payment_address,
107
+ email: config.company_email
108
+ }
109
+ end
110
+
111
+ def build_payments
112
+ (payments || [Payment.new(type: config.default_payment_type, sum: total)]).map(&:to_h)
113
+ end
114
+
115
+ def service
116
+ config.callback_url ? { callback_url: config.callback_url } : {}
117
+ end
118
+
119
+ def correction_info
120
+ info = { type: correction_type, base_date: base_date }
121
+ info[:base_number] = base_number if base_number
122
+
123
+ info
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,86 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'atol/errors'
4
-
5
3
  module Atol
6
4
  module Request
7
5
  class PostDocument
8
6
  module Item
9
7
  class Body
10
- BadPaymentMethodError = Class.new(StandardError)
11
- BadPaymentObjectError = Class.new(StandardError)
12
-
13
- PAYMENT_METHODS = [
14
- 'full_prepayment', 'prepayment', 'advance', 'full_payment',
15
- 'partial_payment', 'credit', 'credit_payment'
16
- ]
17
-
18
- PAYMENT_OBJECTS = [
19
- 'commodity', 'excise', 'job', 'service', 'gambling_bet', 'gambling_prize',
20
- 'lottery', 'lottery_prize', 'intellectual_activity', 'payment', 'agent_commission',
21
- 'composite', 'another'
22
- ]
8
+ BadApiUrlError = Class.new(StandardError)
23
9
 
24
- attr_accessor :config, :name, :price, :quantity, :payment_method, :payment_object,
25
- :agent_info_type, :supplier_info_inn, :supplier_info_name
10
+ attr_reader :instance
26
11
 
27
- def initialize(config: nil, name:, price:, quantity: 1, payment_method:, payment_object:, **options)
28
- setup_attributes(config, name, price, quantity, payment_method, payment_object, options)
29
- validate_attributes
12
+ def initialize(**kwargs)
13
+ config = kwargs[:config] || Atol.config
14
+ @instance = case config.api_url
15
+ when Atol::Version::V4, Atol::Version::V4_TEST
16
+ Atol::Request::PostDocument::Item::V4::Body.new(**kwargs)
17
+ when Atol::Version::V5, Atol::Version::V5_TEST
18
+ Atol::Request::PostDocument::Item::V5::Body.new(**kwargs)
19
+ else
20
+ raise BadApiUrlError
21
+ end
30
22
  end
31
23
 
32
24
  def to_h
33
- body.clone
34
- end
35
-
36
- def to_json(*_args)
37
- body.to_json
38
- end
39
-
40
- private
41
-
42
- def setup_attributes(config, name, price, quantity, payment_method, payment_object, options)
43
- self.config = config || Atol.config
44
- self.name = name
45
- self.price = price.to_f
46
- self.quantity = quantity.to_f
47
- self.payment_method = payment_method.to_s
48
- self.payment_object = payment_object.to_s
49
- self.agent_info_type = options[:agent_info_type].to_s
50
- self.supplier_info_inn = options[:supplier_info_inn].to_s
51
- self.supplier_info_name = options[:supplier_info_name].to_s
52
- end
53
-
54
- def validate_attributes
55
- raise Atol::ZeroItemQuantityError if quantity.to_f.zero?
56
- raise BadPaymentMethodError unless PAYMENT_METHODS.include?(payment_method)
57
- raise BadPaymentObjectError unless PAYMENT_OBJECTS.include?(payment_object)
58
- end
59
-
60
- def agent_info
61
- return if agent_info_type.nil? || agent_info_type.empty?
62
-
63
- { agent_info: { type: agent_info_type } }
64
- end
65
-
66
- def supplier_info
67
- return if supplier_info_inn.nil? || supplier_info_inn.empty? || agent_info.nil?
68
-
69
- info = { inn: supplier_info_inn, name: supplier_info_name }
70
- filtered_info = info.reject { |_key, value| value&.empty? }
71
- { supplier_info: filtered_info } unless filtered_info.empty?
72
- end
73
-
74
- def body
75
- @body ||= {
76
- name: name,
77
- price: price,
78
- quantity: quantity,
79
- sum: (price * quantity).round(2),
80
- tax: config.default_tax,
81
- payment_method: payment_method,
82
- payment_object: payment_object
83
- }.merge(supplier_info.to_h, agent_info.to_h)
25
+ instance.to_h
84
26
  end
85
27
  end
86
28
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atol/errors'
4
+
5
+ module Atol
6
+ module Request
7
+ class PostDocument
8
+ module Item
9
+ module V4
10
+ class Body
11
+ BadPaymentMethodError = Class.new(StandardError)
12
+ BadPaymentObjectError = Class.new(StandardError)
13
+
14
+ PAYMENT_METHODS = %w[
15
+ full_prepayment prepayment advance full_payment
16
+ partial_payment credit credit_payment
17
+ ].freeze
18
+
19
+ PAYMENT_OBJECTS = %w[
20
+ commodity excise job service gambling_bet gambling_prize
21
+ lottery lottery_prize intellectual_activity payment agent_commission
22
+ composite another
23
+ ].freeze
24
+
25
+ attr_accessor :config,
26
+ :name,
27
+ :price,
28
+ :quantity,
29
+ :payment_method,
30
+ :payment_object,
31
+ :agent_info_type,
32
+ :supplier_info_inn,
33
+ :supplier_info_name
34
+
35
+ def initialize(config: nil, name:, price:, quantity: 1, payment_method:, payment_object:, **options)
36
+ setup_attributes(config, name, price, quantity, payment_method, payment_object, options)
37
+ validate_attributes
38
+ end
39
+
40
+ def to_h
41
+ body.clone
42
+ end
43
+
44
+ def to_json(*_args)
45
+ body.to_json
46
+ end
47
+
48
+ private
49
+
50
+ def setup_attributes(config, name, price, quantity, payment_method, payment_object, options)
51
+ self.config = config || Atol.config
52
+ self.name = name
53
+ self.price = price.to_f
54
+ self.quantity = quantity.to_f
55
+ self.payment_method = payment_method.to_s
56
+ self.payment_object = payment_object.to_s
57
+ self.agent_info_type = options[:agent_info_type].to_s
58
+ self.supplier_info_inn = options[:supplier_info_inn].to_s
59
+ self.supplier_info_name = options[:supplier_info_name].to_s
60
+ end
61
+
62
+ def validate_attributes
63
+ raise Atol::ZeroItemQuantityError if quantity.to_f.zero?
64
+ raise BadPaymentMethodError unless PAYMENT_METHODS.include?(payment_method)
65
+ raise BadPaymentObjectError unless PAYMENT_OBJECTS.include?(payment_object)
66
+ end
67
+
68
+ def agent_info
69
+ return if agent_info_type.nil? || agent_info_type.empty?
70
+
71
+ { agent_info: { type: agent_info_type } }
72
+ end
73
+
74
+ def supplier_info
75
+ return if supplier_info_inn.nil? || supplier_info_inn.empty? || agent_info.nil?
76
+
77
+ info = { inn: supplier_info_inn, name: supplier_info_name }
78
+ filtered_info = info.reject { |_key, value| value&.empty? }
79
+ { supplier_info: filtered_info } unless filtered_info.empty?
80
+ end
81
+
82
+ def body
83
+ @body ||= {
84
+ name: name,
85
+ price: price,
86
+ quantity: quantity,
87
+ sum: (price * quantity).round(2),
88
+ tax: config.default_tax,
89
+ payment_method: payment_method,
90
+ payment_object: payment_object
91
+ }.merge(supplier_info.to_h, agent_info.to_h)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atol/errors'
4
+
5
+ module Atol
6
+ module Request
7
+ class PostDocument
8
+ module Item
9
+ module V5
10
+ class Body
11
+ BadPaymentMethodError = Class.new(StandardError)
12
+ BadPaymentObjectError = Class.new(StandardError)
13
+ BadMeasureError = Class.new(StandardError)
14
+ BadVatTypeError = Class.new(StandardError)
15
+
16
+ # https://atol-kassa.ru/wp-content/nfiles/files/ATOL/kassa/АТОЛ%20Онлайн%20-%20Описание%20протокола%20v5%20(ФФД%201.2).pdf
17
+
18
+ PAYMENT_METHODS = %w[full_prepayment prepayment advance full_payment partial_payment credit credit_payment].freeze
19
+ PAYMENT_OBJECTS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23].freeze
20
+ MEASURE = [0, 10, 11, 12, 20, 21, 22, 30, 31, 32, 40, 41, 42, 50, 51, 70, 71, 72, 73, 80, 81, 82, 83, 255].freeze
21
+ VAT_TYPES = %w[none vat0 vat5 vat7 vat10 vat22 vat105 vat107 vat110 vat20 vat120 vat122].freeze
22
+
23
+ attr_accessor :name,
24
+ :price,
25
+ :quantity,
26
+ :payment_method,
27
+ :payment_object,
28
+ :agent_info_type,
29
+ :supplier_info_inn,
30
+ :supplier_info_name,
31
+ :measure,
32
+ :vat
33
+
34
+ def initialize(name:, price:, quantity: 1, payment_method:, payment_object:, measure:, vat:, **options)
35
+ setup_attributes(name, price, quantity, payment_method, payment_object, measure, vat, options)
36
+ validate_attributes
37
+ end
38
+
39
+ def to_h
40
+ body.clone
41
+ end
42
+
43
+ def to_json(*_args)
44
+ body.to_json
45
+ end
46
+
47
+ private
48
+
49
+ def setup_attributes(name, price, quantity, payment_method, payment_object, measure, vat, options)
50
+ self.name = name
51
+ self.price = price.to_f
52
+ self.quantity = quantity.to_f
53
+ self.payment_method = payment_method.to_s
54
+ self.payment_object = payment_object
55
+ self.agent_info_type = options[:agent_info_type].to_s
56
+ self.supplier_info_inn = options[:supplier_info_inn].to_s
57
+ self.supplier_info_name = options[:supplier_info_name].to_s
58
+ self.measure = measure
59
+ self.vat = vat
60
+ end
61
+
62
+ def validate_attributes
63
+ raise Atol::ZeroItemQuantityError if quantity.to_f.zero?
64
+ raise BadPaymentMethodError unless PAYMENT_METHODS.include?(payment_method)
65
+ raise BadPaymentObjectError unless PAYMENT_OBJECTS.include?(payment_object)
66
+ raise BadMeasureError unless MEASURE.include?(measure)
67
+ raise BadVatTypeError unless VAT_TYPES.include?(vat&.[](:type))
68
+ end
69
+
70
+ def agent_info
71
+ return if agent_info_type.nil? || agent_info_type.empty?
72
+
73
+ { agent_info: { type: agent_info_type } }
74
+ end
75
+
76
+ def supplier_info
77
+ return if supplier_info_inn.nil? || supplier_info_inn.empty? || agent_info.nil?
78
+
79
+ info = { inn: supplier_info_inn, name: supplier_info_name }
80
+ filtered_info = info.reject { |_key, value| value&.empty? }
81
+ { supplier_info: filtered_info } unless filtered_info.empty?
82
+ end
83
+
84
+ def body
85
+ @body ||= {
86
+ name: name,
87
+ price: price,
88
+ quantity: quantity,
89
+ sum: (price * quantity).round(2),
90
+ payment_method: payment_method,
91
+ payment_object: payment_object,
92
+ measure: measure,
93
+ vat: vat
94
+ }.merge(supplier_info.to_h, agent_info.to_h)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'atol/errors'
4
+
5
+ module Atol
6
+ module Request
7
+ class PostDocument
8
+ class Payment
9
+ CASH_TYPE = 0 # наличные
10
+ CASHLESS_TYPE = 1 # безналичный
11
+ PREPAID_TYPE = 2 # предоплата/зачёт аванса (тег 1215)
12
+ POSTPAID_TYPE = 3 # постоплата/кредит
13
+ COUNTER_PROVISION_TYPE = 4 # встречное предоставление
14
+ EXTENDED_5_TYPE = 5 # расширенный тип оплаты 5
15
+ EXTENDED_6_TYPE = 6 # расширенный тип оплаты 6
16
+ EXTENDED_7_TYPE = 7 # расширенный тип оплаты 7
17
+ EXTENDED_8_TYPE = 8 # расширенный тип оплаты 8
18
+ EXTENDED_9_TYPE = 9 # расширенный тип оплаты 9
19
+
20
+ TYPES = [
21
+ CASH_TYPE, CASHLESS_TYPE, PREPAID_TYPE, POSTPAID_TYPE, COUNTER_PROVISION_TYPE,
22
+ EXTENDED_5_TYPE, EXTENDED_6_TYPE, EXTENDED_7_TYPE, EXTENDED_8_TYPE, EXTENDED_9_TYPE
23
+ ].freeze
24
+
25
+ attr_reader :type, :sum
26
+
27
+ def initialize(type:, sum:)
28
+ raise(Atol::BadPaymentError, 'not allowed type') unless TYPES.include?(type)
29
+ raise(Atol::BadPaymentError, 'sum must be zero or a positive number') unless sum.is_a?(Numeric) && sum >= 0
30
+
31
+ @type = type
32
+ @sum = sum
33
+ end
34
+
35
+ def to_h
36
+ { sum: sum, type: type }
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,89 +1,88 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'atol/errors'
4
+ require 'atol/request/post_document/payment'
4
5
 
5
6
  module Atol
6
7
  module Request
7
8
  class PostDocument
8
9
  module Sell
9
10
  class Body
10
- def initialize(external_id:, phone: '', email: '', items:, config: nil, **options)
11
+ def initialize(external_id:, phone: '', email: '', items:, payments: nil, config: nil, **options)
11
12
  raise(Atol::EmptyClientContactError) if phone.empty? && email.empty?
12
13
  raise(Atol::EmptySellItemsError) if items.empty?
13
14
 
15
+ total = items.sum { |item| item[:sum] }
16
+ validate_payments!(payments, total)
17
+
14
18
  @config = config || Atol.config
15
19
  @external_id = external_id
16
20
  @phone = phone
17
21
  @email = email
18
22
  @items = items
19
- @agent_info_type = options[:agent_info_type]
23
+ @total = total
24
+ @payments = payments
20
25
  end
21
26
 
22
27
  def to_h
23
- body.clone
28
+ build_body.clone
24
29
  end
25
30
 
26
31
  def to_json(*_args)
27
- body.to_json
32
+ build_body.to_json
28
33
  end
29
34
 
30
35
  private
31
36
 
32
- def body
33
- body_template.tap do |result|
34
- receipt = result[:receipt]
35
- client = receipt[:client]
37
+ attr_reader :config, :external_id, :phone, :email, :items, :payments, :total
36
38
 
37
- result[:external_id] = @external_id
38
- result[:service][:callback_url] = @config.callback_url if @config.callback_url
39
+ def validate_payments!(payments, total)
40
+ return if payments.nil?
39
41
 
40
- add_client_data(client)
41
- add_receipt_data(receipt)
42
- end
42
+ raise(Atol::EmptyPaymentsError) if payments.empty?
43
+ raise(Atol::BadPaymentError) if payments.any? { |payment| !payment.is_a?(Payment) }
44
+ raise(Atol::PaymentsTotalMismatchError) unless payments.sum(&:sum).round(2) == total.round(2)
43
45
  end
44
46
 
45
- def body_template
47
+ def build_body
46
48
  {
49
+ external_id: external_id,
47
50
  receipt: {
48
- client: {},
49
- company: {
50
- inn: @config.inn.to_s,
51
- sno: @config.default_sno,
52
- payment_address: @config.payment_address,
53
- email: @config.company_email
54
- },
55
- items: [],
56
- payments: [
57
- {
58
- sum: 0,
59
- type: @config.default_payment_type
60
- }
61
- ]
51
+ client: client,
52
+ company: company,
53
+ items: items,
54
+ payments: build_payments,
55
+ total: total,
56
+ internet: config.internet
62
57
  },
63
- service: {},
58
+ service: service,
64
59
  timestamp: Time.now.strftime(Atol::TIMESTAMP_FORMAT)
65
60
  }
66
61
  end
67
62
 
68
- def add_client_data(client)
69
- client[:email] = @email unless @email.empty?
70
- client[:phone] = @phone unless @phone.empty?
71
- end
63
+ def client
64
+ result = {}
65
+ result[:email] = email unless email.empty?
66
+ result[:phone] = phone unless phone.empty?
72
67
 
73
- def add_receipt_data(receipt)
74
- receipt[:total] = receipt[:payments][0][:sum] = total
75
- receipt[:items] = @items
76
- add_agent_and_supplier_info(receipt)
68
+ result
77
69
  end
78
70
 
79
- def add_agent_and_supplier_info(receipt)
80
- return if @agent_info_type.nil? || @agent_info_type.empty?
71
+ def company
72
+ {
73
+ inn: config.inn.to_s,
74
+ sno: config.default_sno,
75
+ payment_address: config.payment_address,
76
+ email: config.company_email
77
+ }
78
+ end
81
79
 
82
- receipt[:agent_info] = { type: @agent_info_type }
80
+ def build_payments
81
+ (payments || [Payment.new(type: config.default_payment_type, sum: total)]).map(&:to_h)
83
82
  end
84
83
 
85
- def total
86
- @total ||= @items.inject(0) { |sum, item| sum += item[:sum] }
84
+ def service
85
+ config.callback_url ? { callback_url: config.callback_url } : {}
87
86
  end
88
87
  end
89
88
  end
@@ -3,12 +3,16 @@
3
3
  require 'atol'
4
4
  require 'atol/errors'
5
5
  require 'atol/request/post_document/item/body'
6
+ require 'atol/request/post_document/item/v4/body'
7
+ require 'atol/request/post_document/item/v5/body'
6
8
  require 'atol/request/post_document/sell/body'
9
+ require 'atol/request/post_document/correction/body'
10
+ require 'atol/request/post_document/correction/v5/body'
7
11
 
8
12
  module Atol
9
13
  module Request
10
14
  class PostDocument
11
- OPERATIONS = %i[sell sell_refund sell_correction buy buy_refund buy_correction].freeze
15
+ OPERATIONS = %i[sell sell_refund sell_correction sell_refund_correction buy buy_refund buy_correction buy_refund_correction].freeze
12
16
  HEADERS = { 'Content-Type' => 'application/json; charset=utf-8' }.freeze
13
17
 
14
18
  def initialize(operation:, token:, body:, config: nil, req_logger: nil, res_logger: nil)
data/lib/atol/version.rb CHANGED
@@ -2,7 +2,12 @@
2
2
 
3
3
  module Atol
4
4
  module Version
5
- API = 'v4'
6
- LIB = '1.0.0'
5
+ V4 = 'https://online.atol.ru/possystem/v4'
6
+ V5 = 'https://online.atol.ru/possystem/v5'
7
+
8
+ V4_TEST = 'https://testonline.atol.ru/possystem/v4'
9
+ V5_TEST = 'https://testonline.atol.ru/possystem/v5'
10
+
11
+ LIB = '1.2.0'
7
12
  end
8
13
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atol
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GeorgeGorbanev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-14 00:00:00.000000000 Z
11
+ date: 2026-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.15'
33
+ version: 2.1.4
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.15'
40
+ version: 2.1.4
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rake
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -119,7 +119,12 @@ files:
119
119
  - lib/atol/request/get_document_state.rb
120
120
  - lib/atol/request/get_token.rb
121
121
  - lib/atol/request/post_document.rb
122
+ - lib/atol/request/post_document/correction/body.rb
123
+ - lib/atol/request/post_document/correction/v5/body.rb
122
124
  - lib/atol/request/post_document/item/body.rb
125
+ - lib/atol/request/post_document/item/v4/body.rb
126
+ - lib/atol/request/post_document/item/v5/body.rb
127
+ - lib/atol/request/post_document/payment.rb
123
128
  - lib/atol/request/post_document/sell/body.rb
124
129
  - lib/atol/transaction.rb
125
130
  - lib/atol/transaction/get_document_state.rb