orangedata 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2dde9cd360d680c7c81b2daa172585325a888a57
4
- data.tar.gz: fc2569d5d679490fb5315873fd94c3e26a8fc8af
3
+ metadata.gz: 6087498a7c12ae0d3250d75c1ee5148cfea56a7f
4
+ data.tar.gz: c7ac45d748527a88a20f04853b4fbb5dde802ca9
5
5
  SHA512:
6
- metadata.gz: b62936f69fd98b21c29ba5eb028206aa1ea8a8d073262acba0dc3aaef7ee86a7a49f5237d8deaed690391e4b0a541679f1b6899583e23102e7da100522531560
7
- data.tar.gz: f1b7e6ef44e611925d8a3147b27df7b7413be86bb9c686b329d171dd25fe4ece1028b55e1aea2de59d5712541f8e034596adaf509709f758d13dca3621baace7
6
+ metadata.gz: e12efa52f2a06f1393b2c951f84e9e3fa102bc2a5a5d4d592d7b69b62a930a3c3cfc3c23b13a516187296a0e5d72a98cfbce6ae5539bcea1555c87ea8fda8232
7
+ data.tar.gz: 76d6d37502cd2dde226b18b6b0cc342770b0304272bae7d998ed01cbd4811f11f5496fa7877afd98a957115df2c193bb8a6a63709bfb33895e1686ebf77dc404
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- orangedata (0.0.2)
4
+ orangedata (0.0.3)
5
5
  faraday (>= 0.15)
6
6
  faraday_middleware
7
7
 
@@ -14,12 +14,16 @@ GEM
14
14
  crack (0.4.3)
15
15
  safe_yaml (~> 1.0.0)
16
16
  diff-lcs (1.3)
17
+ docile (1.3.1)
17
18
  faraday (0.15.2)
18
19
  multipart-post (>= 1.2, < 3)
19
20
  faraday_middleware (0.12.2)
20
21
  faraday (>= 0.7.4, < 1.0)
21
22
  hashdiff (0.3.7)
22
23
  jaro_winkler (1.5.1)
24
+ json (2.1.0)
25
+ json-schema (2.8.1)
26
+ addressable (>= 2.4)
23
27
  multipart-post (2.0.0)
24
28
  parallel (1.12.1)
25
29
  parser (2.5.1.0)
@@ -51,6 +55,11 @@ GEM
51
55
  unicode-display_width (~> 1.0, >= 1.0.1)
52
56
  ruby-progressbar (1.9.0)
53
57
  safe_yaml (1.0.4)
58
+ simplecov (0.16.1)
59
+ docile (~> 1.1)
60
+ json (>= 1.8, < 3)
61
+ simplecov-html (~> 0.10.0)
62
+ simplecov-html (0.10.2)
54
63
  unicode-display_width (1.4.0)
55
64
  webmock (3.4.2)
56
65
  addressable (>= 2.3.6)
@@ -62,10 +71,12 @@ PLATFORMS
62
71
 
63
72
  DEPENDENCIES
64
73
  bundler (~> 1.16)
74
+ json-schema (~> 2.8)
65
75
  orangedata!
66
76
  rake (~> 10.0)
67
77
  rspec
68
78
  rubocop
79
+ simplecov
69
80
  webmock
70
81
 
71
82
  BUNDLED WITH
data/README.md CHANGED
@@ -35,50 +35,46 @@ gem 'orangedata'
35
35
 
36
36
  ```ruby
37
37
  transport = OrangeData::Transport.new("https://apip.orangedata.ru:2443/api/v2/", OrangeData::Credentials.default_test)
38
-
39
- receipt = {
40
- id: SecureRandom.uuid,
41
- inn: '1234567890', key:'1234567890',
42
- content: { # тут собрать данные можно по официальному мануалу
43
- type: 1,
44
- positions:[{ quantity: 1, price: 0.01, tax: 4, text: "Товар на копейку"}],
45
- checkClose:{
46
- payments: [{ type:2, amount:'0.01' }],
47
- taxationSystem: 1
48
- }
49
- }
38
+ receipt = OrangeData::Receipt.income(inn:"1234567890"){|r|
39
+ r.customer = "Иван Иваныч"
40
+ r.add_position("Спички", price: 12.34){|pos| pos.tax = :vat_not_charged }
41
+ r.add_payment(50, :cash)
50
42
  }
51
43
  transport.post_document(receipt)
52
- # wait some time, then
53
- transport.get_document("1234567890", receipt[:id])
54
- # =>
44
+ # wait some time
45
+ res = transport.get_document(receipt.inn, receipt.id)
46
+
47
+ # => (внутри такое, а вернет объект)
55
48
  # {
56
- # "id"=>"a88b6b30-20ab-47ea-95ca-f12f22ef03d3",
57
- # "deviceSN"=>"1400000000001033",
58
- # "deviceRN"=>"0000000400054952",
59
- # "fsNumber"=>"9999078900001341",
60
- # "ofdName"=>"ООО \"Ярус\" (\"ОФД-Я\")",
61
- # "ofdWebsite"=>"www.ofd-ya.ru",
62
- # "ofdinn"=>"7728699517",
63
- # "fnsWebsite"=>"www.nalog.ru",
64
- # "companyINN"=>"1234567890",
65
- # "companyName"=>"Тест",
66
- # "documentNumber"=>5548,
67
- # "shiftNumber"=>6072,
68
- # "documentIndex"=>3045,
69
- # "processedAt"=>"2018-10-22T19:36:00",
70
- # "content"=>
71
- # {
72
- # "type"=>1,
73
- # "positions"=>[{"quantity"=>1.0, "price"=>0.01, "tax"=>4, "text"=>"Товар на копейку"}],
74
- # "checkClose"=>{
75
- # "payments"=>[{"type"=>2, "amount"=>0.01}],
76
- # "taxationSystem"=>1
77
- # }
78
- # },
79
- # "change"=>0.0,
80
- # "fp"=>"787980846"
49
+ # "id"=>"50152258-a9aa-4d19-9216-5a3eecec7241",
50
+ # "deviceSN"=>"1400000000001033",
51
+ # "deviceRN"=>"0000000400054952",
52
+ # "fsNumber"=>"9999078900001341",
53
+ # "ofdName"=>"ООО \"Ярус\" (\"ОФД-Я\")",
54
+ # "ofdWebsite"=>"www.ofd-ya.ru",
55
+ # "ofdinn"=>"7728699517",
56
+ # "fnsWebsite"=>"www.nalog.ru",
57
+ # "companyINN"=>"1234567890",
58
+ # "companyName"=>"Тест",
59
+ # "documentNumber"=>3243,
60
+ # "shiftNumber"=>234,
61
+ # "documentIndex"=>7062, "processedAt"=>"2018-10-26T20:21:00",
62
+ # "content"=>{
63
+ # "type"=>1,
64
+ # "positions"=>[{"price"=>12.34, "tax"=>6, "text"=>"Спички"}],
65
+ # "checkClose"=>{"payments"=>[{"type"=>1, "amount"=>50.0}], "taxationSystem"=>0},
66
+ # "customer"=>"Иван Иваныч"
67
+ # },
68
+ # "change"=>37.66,
69
+ # "fp"=>"301645583"
81
70
  # }
71
+
72
+ res.device_sn
73
+ # => "1400000000001033"
74
+
75
+ # и даже так:
76
+ res.qr_code_content
77
+ # => "t=20181026T2021&s=50.0&fn=9999078900001341&i=3243&fp=301645583&n=1"
82
78
  ```
83
79
 
84
80
  ### Получаем сертификаты
@@ -90,8 +86,9 @@ gem 'orangedata'
90
86
 
91
87
  ```ruby
92
88
  c = OrangeData::Credentials.read_certs_from_pack('~/Downloads/1234567890', title:'My production', cert_key_pass:'1234') # cert_key_pass берем из readme_v2.txt, но есть подозрение что он у всех 1234
93
- # Generated public signature key: <RSAKeyValue>...</Exponent></RSAKeyValue>
94
89
  File.open("my_production.yml", "wt"){|f| f.write c.to_yaml }
90
+ c.signature_public_xml
91
+ # "<RSAKeyValue>...</Exponent></RSAKeyValue>"
95
92
 
96
93
  # опционально на маке копируем публичный ключ в буфер обмена:
97
94
  system("echo '#{c.signature_public_xml}' | pbcopy")
@@ -102,7 +99,7 @@ gem 'orangedata'
102
99
  Дальше публичный ключ с предыдущего шага отправляется в ЛК, там его сохряняем, "подключаем интеграцию", и пользуемся:
103
100
 
104
101
  ```ruby
105
- transport = OrangeData::Transport.new(OrangeData::Transport::DEFAULT_PRODUCTION_API_URL, OrangeData::Credentials.from_hash(YAML.load('my_production.yml')))
102
+ transport = OrangeData::Transport.new(OrangeData::Transport::DEFAULT_PRODUCTION_API_URL, OrangeData::Credentials.from_hash(YAML.load_file('my_production.yml')))
106
103
  transport.post_document # и далее по тексту, осторожно - не пробейте лишние чеки во время проверок
107
104
  ```
108
105
 
@@ -51,6 +51,25 @@ module OrangeData
51
51
  end
52
52
  end
53
53
  end
54
+
55
+ def load_from(val, key_pass=nil)
56
+ return val unless val
57
+ case val
58
+ when self
59
+ val
60
+ when Hash
61
+ from_hash(val)
62
+ when String
63
+ if val.start_with?('<')
64
+ from_xml(val)
65
+ else
66
+ new(val, key_pass)
67
+ end
68
+ else
69
+ raise ArgumentError, "cannot load from #{val.class}"
70
+ end
71
+ end
72
+
54
73
  end
55
74
 
56
75
  end
@@ -72,29 +91,28 @@ module OrangeData
72
91
  def valid?
73
92
  signature_key_name &&
74
93
  signature_key && signature_key.private? &&
75
- certificate && certificate_key && certificate_key.private?
94
+ (signature_key.n.num_bits >= 489) && # minimum working key length for sha256 signature
95
+ certificate && certificate_key &&
96
+ certificate_key.private? && certificate.check_private_key(certificate_key)
97
+ end
98
+
99
+ def ==(other)
100
+ return false unless %i[signature_key_name title].all?{|m| self.send(m) == other.send(m) }
101
+ # certificates/keys cannot be compared directly, so dump
102
+ %i[signature_key certificate certificate_key].all?{|m|
103
+ c1 = self.send(m)
104
+ c2 = other.send(m)
105
+ c1 == c2 || (c1 && c2 && c1.to_der == c2.to_der)
106
+ }
76
107
  end
77
108
 
78
109
  def self.from_hash(creds)
79
- key = nil
80
- if creds[:signature_key]
81
- key = if creds[:signature_key].is_a?(OpenSSL::PKey::RSA)
82
- creds[:signature_key]
83
- elsif creds[:signature_key].is_a?(Hash)
84
- OpenSSL::PKey::RSA.from_hash(creds[:signature_key])
85
- elsif creds[:signature_key].is_a?(String) && creds[:signature_key].start_with?('<')
86
- OpenSSL::PKey::RSA.from_xml(creds[:signature_key])
87
- else
88
- OpenSSL::PKey::RSA.new(creds[:signature_key], creds[:signature_key_pass])
89
- end
90
- end
91
110
  new(
111
+ title: creds[:title],
92
112
  signature_key_name: creds[:signature_key_name],
93
- signature_key: key,
113
+ signature_key: OpenSSL::PKey::RSA.load_from(creds[:signature_key], creds[:signature_key_pass]),
94
114
  certificate: creds[:certificate] && OpenSSL::X509::Certificate.new(creds[:certificate]),
95
- certificate_key: creds[:certificate_key] &&
96
- OpenSSL::PKey::RSA.new(creds[:certificate_key], creds[:certificate_key_pass]),
97
- title: creds[:title]
115
+ certificate_key: OpenSSL::PKey::RSA.load_from(creds[:certificate_key], creds[:certificate_key_pass]),
98
116
  )
99
117
  end
100
118
 
@@ -109,9 +127,9 @@ module OrangeData
109
127
  {
110
128
  title: title,
111
129
  signature_key_name: signature_key_name,
112
- signature_key: signature_key && signature_key.to_pem(OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
130
+ signature_key: signature_key && signature_key.to_pem(key_pass && OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
113
131
  certificate: certificate && certificate.to_pem,
114
- certificate_key: certificate_key && certificate_key.to_pem(OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
132
+ certificate_key: certificate_key && certificate_key.to_pem(key_pass && OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
115
133
  }.tap do |h|
116
134
  h.delete(:title) if !title || title == ''
117
135
  if save_pass
@@ -147,24 +165,33 @@ module OrangeData
147
165
  "#<#{self.class.name}:#{object_id} #{info_fields.map{|(k, v)| "#{k}=#{v}" }.join(' ')}>"
148
166
  end
149
167
 
150
- def generate_signature_key!(key_length=2048)
151
- self.signature_key = OpenSSL::PKey::RSA.new(key_length)
168
+ DEFAULT_KEY_LENGTH = 2048
169
+
170
+ #deprecated
171
+ def generate_signature_key!(key_length=DEFAULT_KEY_LENGTH)
172
+ self.signature_key = self.class.generate_signature_key(key_length)
173
+ end
174
+
175
+ def self.generate_signature_key(key_length=DEFAULT_KEY_LENGTH)
176
+ raise ArgumentError, "key length should be >= 489, recomended #{DEFAULT_KEY_LENGTH}" unless key_length >= 489
177
+ OpenSSL::PKey::RSA.new(key_length)
152
178
  end
153
179
 
154
- def self.read_certs_from_pack(path, signature_key_name:nil, cert_key_pass:nil, title:nil)
180
+ def self.read_certs_from_pack(path, signature_key_name:nil, cert_key_pass:nil, title:nil, signature_key:nil)
155
181
  path = File.expand_path(path)
156
182
  client_cert = Dir.glob(path + '/*.{crt}').select{|f| File.file?(f.sub(/.crt\z/, '.key'))}
157
183
  raise 'Expect to find exactly one <num>.crt with corresponding <num>.key file' unless client_cert.size == 1
158
184
  client_cert = client_cert.first
159
185
 
160
- # private_key_test.xml || rsa_\d+_private_key.xml
161
- xmls = Dir.glob(path + '/*.{xml}').select{|f| f =~ /private/}
162
- signature_key = if xmls.size == 1
163
- File.read(xmls.first)
164
- else
165
- OpenSSL::PKey::RSA.new(2048).tap{|k|
166
- puts "Generated public signature key: #{k.public_key.to_xml}"
167
- }
186
+ unless signature_key
187
+ # private_key_test.xml || rsa_\d+_private_key.xml
188
+ xmls = Dir.glob(path + '/*.{xml}').select{|f| f =~ /private/}
189
+ signature_key = if xmls.size == 1
190
+ File.read(xmls.first)
191
+ else
192
+ generate_signature_key(DEFAULT_KEY_LENGTH)
193
+ # .tap{|k| logger.info("Generated public signature key: #{k.public_key.to_xml}") }
194
+ end
168
195
  end
169
196
 
170
197
  from_hash(
@@ -1,79 +1,325 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+ require 'json'
5
+
3
6
  module OrangeData
4
7
 
5
- # main class for receipt
6
- class Receipt
8
+ PAYLOAD_SCHEMA = YAML.load_file(File.expand_path('schema_definitions.yml', __dir__)).freeze
9
+
10
+ # taken from ActiveSupport
11
+ module StringExt
12
+ refine String do
13
+ def underscore
14
+ self.gsub(/::/, '/').
15
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
16
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
17
+ tr("-", "_").
18
+ downcase
19
+ end
20
+ end
21
+ end
22
+ using StringExt unless "".respond_to?(:underscore)
23
+
24
+ # main class for receipt/correction
25
+ class Document
7
26
 
8
- attr_accessor :id, :inn, :group, :key_name
27
+ attr_accessor :id, :inn, :group, :key_name, :content
9
28
 
10
- def initialize(id:SecureRandom.uuid, inn:, group:nil, key_name:nil)
29
+ def initialize(id:SecureRandom.uuid, inn:, group:nil, key_name:nil, content:nil)
11
30
  @id = id
12
31
  @inn = inn
13
32
  @group = group
14
- @key_name = key_name
15
- yield self if block_given?
33
+ @key_name = key_name || inn
34
+ @content = content if content
35
+ yield @content if block_given?
16
36
  end
17
37
 
38
+ def to_json(*args)
39
+ {
40
+ id: id,
41
+ inn: inn,
42
+ group: group || 'Main',
43
+ content: content,
44
+ key: key_name
45
+ }.to_json(*args)
46
+ end
18
47
  end
19
48
 
20
- # nodoc
21
- class ReceiptContent
22
-
23
- # for agent type bit mask
24
- module AgentTypeSerializer
25
- AGENT_TYPE_BITS = { # 1057 (в чеках/БСО должно соответствовать отчету о (пере)регистрации ККТ)
26
- bank_payment_agent: (1 << 0), # банковский платежный агент
27
- bank_payment_subagent: (1 << 1), # банковский платежный субагент
28
- payment_agent: (1 << 2), # платежный агент
29
- payment_subagent: (1 << 3), # платежный субагент
30
- attorney: (1 << 4), # поверенный
31
- commission_agent: (1 << 5), # комиссионер
32
- other_agent: (1 << 6), # иной агент
33
- }.freeze
34
-
35
- def self.load(data)
36
- data = data.to_i
37
- AGENT_TYPE_BITS.reject{|(_, v)| (data & v).zero? }.map(&:first)
49
+ class Receipt < Document
50
+ def initialize(id:SecureRandom.uuid, inn:, group:nil, key_name:nil, content:nil)
51
+ @content = ReceiptContent.new(content || {})
52
+ super
53
+ end
54
+ PAYLOAD_SCHEMA["definitions"]["CheckContent"]["properties"]["type"]["x-enum"].each_pair do |slug, info|
55
+ define_singleton_method(slug) do |**args, &block|
56
+ new(**args, &block).tap{|doc|
57
+ doc.content.type = slug
58
+ }
38
59
  end
60
+ end
61
+ end
39
62
 
40
- def self.dump(val)
41
- val = [val] unless val.is_a?(Array)
42
- val.map{|v| AGENT_TYPE_BITS[v] || raise("unknown agent_type #{v}") }.reduce(:|)
63
+ module GeneratedAttributes
64
+ def self.from_schema klass, schema
65
+ klass.class_eval{
66
+ extend GeneratedAttributes
67
+ generate_accessors_from_schema(schema)
68
+ }
69
+ end
70
+
71
+ protected
72
+ def generate_accessors_from_schema schema
73
+ plain_types = %w[integer string number]
74
+ schema["properties"].each_pair do |property, info|
75
+ if plain_types.include?(info["type"])
76
+ if info["x-enum"]
77
+ inverse_map = info["x-enum"].map{|k,v| [v['val'], k.to_sym]}.to_h
78
+ define_method(property.underscore){
79
+ return nil if @payload[property].nil?
80
+ inverse_map[@payload[property]] || "unknown value #{@payload[property].inspect} for field #{property}"
81
+ }
82
+ define_method(:"#{property.underscore}="){|val|
83
+ unless val.nil?
84
+ val = (info["x-enum"][val.to_s] || raise(ArgumentError, "unknown value #{val.inspect} for property #{property}"))["val"]
85
+ end
86
+ @payload[property] = val
87
+ }
88
+
89
+ elsif info["x-bitfield"]
90
+ bitmap = info["x-bitfield"].map{|k,v| [k.to_sym, 1 << v['bit']]}.to_h
91
+ # TODO: return wrapper so that :<< etc will work
92
+ define_method(property.underscore){
93
+ return nil if @payload[property].nil?
94
+ data = @payload[property].to_i
95
+ # FIXME: unknown bits will be silently lost
96
+ bitmap.reject{|_,v| (data & v).zero? }.map(&:first)
97
+ }
98
+ define_method(:"#{property.underscore}="){|val|
99
+ unless val.nil?
100
+ val = [val] unless val.is_a?(Array)
101
+ val = val.map{|v| bitmap[v] || raise(ArgumentError, "unknown value #{v.inspect} for property #{property}") }.reduce(:|)
102
+ end
103
+ @payload[property] = val
104
+ }
105
+ else
106
+ # primitive
107
+ define_method(property.underscore){ @payload[property] }
108
+ define_method(:"#{property.underscore}="){|val| @payload[property] = val }
109
+ end
110
+ elsif info["type"] == 'array'
111
+ if info["items"] && plain_types.include?(info["items"]["type"])
112
+ define_method(property.underscore){ @payload[property] }
113
+ define_method(:"#{property.underscore}="){|val|
114
+ val = [val] unless val.is_a?(Array)
115
+ @payload[property] = val
116
+ }
117
+ else
118
+ # ref?
119
+ end
120
+ else
121
+
122
+ end
43
123
  end
44
124
  end
125
+ end
45
126
 
46
- RECEIPT_TYPES = { # 1054:
127
+ # base class for semi-generated classes
128
+ class PayloadContent
129
+ def initialize payload={}
130
+ @payload = payload
131
+ end
47
132
 
48
- }.freeze
133
+ def assign_attributes options
134
+ options.each_pair{|k,v|
135
+ setter = :"#{k}="
136
+ send(setter, v) if respond_to?(setter)
137
+ }
138
+ # for chaining:
139
+ self
140
+ end
49
141
 
50
- FIELDS = {
51
- type: {
52
- name: 'Признак расчета',
53
- tag_num: 1054,
54
- mapper: :enum,
55
- enum_values: {
56
- income: 1, # Приход
57
- return_income: 2, # Возврат прихода
58
- expense: 3, # Расход
59
- return_expense: 4 # Возврат расхода
60
- }
142
+ def ==(other)
143
+ self.class == other.class && @payload == other.instance_variable_get(:@payload)
144
+ end
145
+
146
+ def to_hash
147
+ @payload
148
+ end
149
+
150
+ def to_json(*args)
151
+ to_hash.to_json(*args)
152
+ end
153
+ end
154
+
155
+ class ReceiptContent < PayloadContent
156
+ def initialize payload={}
157
+ @payload = payload || {}
158
+ # TODO: import...
159
+ # TODO: taxationSystem default in checkclose
160
+ @check_close = CheckClose.new(@payload['checkClose'])
161
+ @positions = (@payload['positions'] || []).map{|pos| Position.new(pos) }
162
+ if @payload["additionalUserAttribute"]
163
+ @additional_user_attribute = AdditionalUserAttribute.new(@payload["additionalUserAttribute"])
164
+ end
165
+ end
166
+
167
+ # сырой тип используется в qr_code
168
+ def raw_type
169
+ @payload["type"]
170
+ end
171
+
172
+ def to_hash
173
+ @payload.dup.tap{|h|
174
+ h["positions"] = @positions.map(&:to_hash)
175
+ h["checkClose"] = check_close.to_hash if check_close
176
+ h["additionalUserAttribute"] = additional_user_attribute.to_hash if additional_user_attribute
61
177
  }
62
- }.freeze
178
+ end
63
179
 
64
- def initialize(_type)
65
- @positions = []
66
- @payments = []
180
+ def add_position(text=nil, **options)
181
+ pos = Position.new
182
+ pos.text = text if text
183
+ pos.assign_attributes(options)
184
+ yield(pos) if block_given?
185
+ positions << pos
186
+ self
67
187
  end
68
188
 
69
- def agent_type
70
- AgentTypeSerializer.load(@agent_type)
189
+ def add_payment(amount=nil, type=nil, **options)
190
+ payment = Payment.new
191
+ payment.type = type if type
192
+ payment.amount = amount if amount
193
+ payment.assign_attributes(options)
194
+ yield(payment) if block_given?
195
+ check_close.payments << payment
196
+ self
71
197
  end
72
198
 
73
- def agent_type=(val)
74
- @agent_type = AgentTypeSerializer.dump(val)
199
+ def set_additional_user_attribute **options
200
+ @additional_user_attribute = AdditionalUserAttribute.new.assign_attributes(options)
75
201
  end
76
202
 
203
+
204
+
205
+ class Position < PayloadContent
206
+ def initialize payload={}
207
+ @payload = payload
208
+ @supplier_info = SupplierInfo.new(@payload['supplierInfo']) if @payload['supplierInfo']
209
+ @agent_info = AgentInfo.new(@payload['agentInfo']) if @payload['agentInfo']
210
+ end
211
+
212
+ def to_hash
213
+ @payload.dup.tap{|h|
214
+ h["supplierInfo"] = supplier_info.to_hash if supplier_info
215
+ h["agentInfo"] = agent_info.to_hash if agent_info
216
+ }
217
+ end
218
+
219
+ def set_supplier_info **options
220
+ @supplier_info = SupplierInfo.new.assign_attributes(options)
221
+ self
222
+ end
223
+
224
+ def set_agent_info **options
225
+ @agent_info = AgentInfo.new.assign_attributes(options)
226
+ self
227
+ end
228
+
229
+ attr_reader :agent_info, :supplier_info
230
+
231
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["CheckPosition"])
232
+ end
233
+
234
+ class AgentInfo < PayloadContent
235
+ def initialize payload={}
236
+ @payload = payload
237
+ end
238
+ def to_hash
239
+ @payload
240
+ end
241
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["AgentInfo"])
242
+ end
243
+
244
+ class SupplierInfo < PayloadContent
245
+ def initialize payload={}
246
+ @payload = payload
247
+ end
248
+ def to_hash
249
+ @payload
250
+ end
251
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["SupplierInfo"])
252
+ end
253
+
254
+ class CheckClose < PayloadContent
255
+ def initialize payload={}
256
+ payload ||= {}
257
+ @payload = payload
258
+ @payments = (payload['payments'] || []).map{|p| Payment.new(p)}
259
+ end
260
+
261
+ def to_hash
262
+ @payload.dup.tap{|h|
263
+ h["payments"] = @payments.map(&:to_hash) if @payments
264
+ }
265
+ end
266
+
267
+ attr_reader :payments
268
+
269
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["CheckClose"])
270
+ end
271
+
272
+ class Payment < PayloadContent
273
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["CheckPayment"])
274
+ end
275
+
276
+ class AdditionalUserAttribute < PayloadContent
277
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["AdditionalUserAttribute"])
278
+ end
279
+
280
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["CheckContent"])
281
+
282
+ attr_reader :positions, :check_close, :additional_user_attribute
283
+
284
+ end
285
+
286
+
287
+ class Correction < Document
288
+ # TODO: same as Receipt, but based on correctionType
289
+ end
290
+
291
+ class ReceiptResult < PayloadContent
292
+ def initialize payload
293
+ @payload = payload
294
+ @content = ReceiptContent.new(@payload["content"])
295
+ end
296
+
297
+ def self.from_hash(hash)
298
+ raise ArgumentError, 'Expect hash here' unless hash.is_a?(Hash)
299
+ new(hash)
300
+ end
301
+
302
+ attr_reader :content
303
+ GeneratedAttributes.from_schema(self, PAYLOAD_SCHEMA["definitions"]["CheckStatusViewModel[CheckContent]"])
304
+
305
+ def qr_code_content
306
+ # С живого чека: t=20180518T220500&s=975.88&fn=8710000101125654&i=99456&fp=1250448795&n=1
307
+ # Пример: t=20150720T1638&s=9999999.00&fn=000110000105&i=12345678&fp=123456&n=2
308
+ {
309
+ # - t=<date/time - дата и время осуществления расчета в формате ГГГГММДДТЧЧММ>
310
+ t: self.processed_at.gsub(/:\d{2}\z/, '').gsub(/[^0-9T]/, ''),
311
+ # - s=<сумма расчета в рублях и копейках, разделенных точкой>
312
+ s: content.check_close.payments.inject(0.0){|d, p| d + p.amount},
313
+ # - fn=<заводской номер фискального накопителя>
314
+ fn: fs_number,
315
+ # - i=<порядковый номер фискального документа, нулями не дополняется>
316
+ i: document_number, # documentIndex??
317
+ # - fp=<фискальный признак документа, нулями не дополняется>
318
+ fp: fp,
319
+ # - n=<признак расчета>.
320
+ n: content.raw_type, #??
321
+ }.map{|k, v| "#{k}=#{v}" }.join('&')
322
+ end
77
323
  end
78
324
 
79
325
  end