orangedata 0.0.2 → 0.0.3

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
  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