atol 1.1.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: 4959c1454e46f59413f73210058077f89a59eb076d698d1b9b18d3fe9c5df262
4
- data.tar.gz: 82ecf864b3a298e28e96c845177809e9a11a72be7e27feedd05c95a6b86a3452
3
+ metadata.gz: 17a1f879f910bd053721385602bc4e3c158dc10625534db847d78bdb54ee75e4
4
+ data.tar.gz: 8408f2c974e2e49c3dd89745e942cc06375fbcbf050d843015fdd17d8839eae4
5
5
  SHA512:
6
- metadata.gz: ee8fbffc698f9f766c57d9d2651ff66763841c683e8d1a082664cf9b82b222ccc11cda53bfca79561f33009a9caac566cb1e13e64613ed18c4abb2dbb3e98af1
7
- data.tar.gz: 4bc13aae1a3a6a0b5942c1de81dd724ca496289c584254fc622b103fabb1834471c9b7c01bc065f5f880cdbfcda30c2752172dde4f7e661c51ec5674d6b0102d
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/README.md CHANGED
@@ -255,6 +255,46 @@ Atol::Request::PostDocument::Sell::Body.new(
255
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
256
  ```
257
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
+
258
298
  #### Отправка документа
259
299
 
260
300
  Когда токен и тело запроса составлены, остается только сделать post-запрос.
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
@@ -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,82 +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
23
+ @total = total
24
+ @payments = payments
19
25
  end
20
26
 
21
27
  def to_h
22
- body.clone
28
+ build_body.clone
23
29
  end
24
30
 
25
31
  def to_json(*_args)
26
- body.to_json
32
+ build_body.to_json
27
33
  end
28
34
 
29
35
  private
30
36
 
31
- def body
32
- body_template.tap do |result|
33
- receipt = result[:receipt]
34
- client = receipt[:client]
37
+ attr_reader :config, :external_id, :phone, :email, :items, :payments, :total
35
38
 
36
- result[:external_id] = @external_id
37
- result[:service][:callback_url] = @config.callback_url if @config.callback_url
39
+ def validate_payments!(payments, total)
40
+ return if payments.nil?
38
41
 
39
- add_client_data(client)
40
- add_receipt_data(receipt)
41
- 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)
42
45
  end
43
46
 
44
- def body_template
47
+ def build_body
45
48
  {
49
+ external_id: external_id,
46
50
  receipt: {
47
- client: {},
48
- company: {
49
- inn: @config.inn.to_s,
50
- sno: @config.default_sno,
51
- payment_address: @config.payment_address,
52
- email: @config.company_email
53
- },
54
- items: [],
55
- payments: [
56
- {
57
- sum: 0,
58
- type: @config.default_payment_type
59
- }
60
- ]
51
+ client: client,
52
+ company: company,
53
+ items: items,
54
+ payments: build_payments,
55
+ total: total,
56
+ internet: config.internet
61
57
  },
62
- service: {},
58
+ service: service,
63
59
  timestamp: Time.now.strftime(Atol::TIMESTAMP_FORMAT)
64
60
  }
65
61
  end
66
62
 
67
- def add_client_data(client)
68
- client[:email] = @email unless @email.empty?
69
- client[:phone] = @phone unless @phone.empty?
63
+ def client
64
+ result = {}
65
+ result[:email] = email unless email.empty?
66
+ result[:phone] = phone unless phone.empty?
67
+
68
+ result
69
+ end
70
+
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
+ }
70
78
  end
71
79
 
72
- def add_receipt_data(receipt)
73
- receipt[:total] = receipt[:payments][0][:sum] = total
74
- receipt[:items] = @items
75
- receipt[:internet] = @config.internet
80
+ def build_payments
81
+ (payments || [Payment.new(type: config.default_payment_type, sum: total)]).map(&:to_h)
76
82
  end
77
83
 
78
- def total
79
- @total ||= @items.inject(0) { |sum, item| sum += item[:sum] }
84
+ def service
85
+ config.callback_url ? { callback_url: config.callback_url } : {}
80
86
  end
81
87
  end
82
88
  end
@@ -6,6 +6,8 @@ require 'atol/request/post_document/item/body'
6
6
  require 'atol/request/post_document/item/v4/body'
7
7
  require 'atol/request/post_document/item/v5/body'
8
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'
9
11
 
10
12
  module Atol
11
13
  module Request
data/lib/atol/version.rb CHANGED
@@ -8,6 +8,6 @@ module Atol
8
8
  V4_TEST = 'https://testonline.atol.ru/possystem/v4'
9
9
  V5_TEST = 'https://testonline.atol.ru/possystem/v5'
10
10
 
11
- LIB = '1.1.0'
11
+ LIB = '1.2.0'
12
12
  end
13
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.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GeorgeGorbanev
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-24 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
@@ -94,7 +94,7 @@ dependencies:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: '3.0'
97
- description:
97
+ description:
98
98
  email:
99
99
  - GeorgeGorbanev@gmail.com
100
100
  executables: []
@@ -119,9 +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
123
125
  - lib/atol/request/post_document/item/v4/body.rb
124
126
  - lib/atol/request/post_document/item/v5/body.rb
127
+ - lib/atol/request/post_document/payment.rb
125
128
  - lib/atol/request/post_document/sell/body.rb
126
129
  - lib/atol/transaction.rb
127
130
  - lib/atol/transaction/get_document_state.rb
@@ -132,7 +135,7 @@ homepage: https://github.com/sputnik8/atol-rb
132
135
  licenses:
133
136
  - MIT
134
137
  metadata: {}
135
- post_install_message:
138
+ post_install_message:
136
139
  rdoc_options: []
137
140
  require_paths:
138
141
  - lib
@@ -147,8 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
150
  - !ruby/object:Gem::Version
148
151
  version: '0'
149
152
  requirements: []
150
- rubygems_version: 3.2.33
151
- signing_key:
153
+ rubygems_version: 3.0.3.1
154
+ signing_key:
152
155
  specification_version: 4
153
156
  summary: ATOL KaaS client for Ruby
154
157
  test_files: []