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 +4 -4
- data/Gemfile.lock +12 -1
- data/README.md +39 -42
- data/lib/orange_data/credentials.rb +57 -30
- data/lib/orange_data/receipt.rb +294 -48
- data/lib/orange_data/schema_definitions.yml +776 -0
- data/lib/orange_data/transport.rb +20 -12
- data/lib/orange_data/version.rb +1 -1
- data/lib/orange_data.rb +1 -9
- data/orangedata.gemspec +2 -0
- metadata +31 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6087498a7c12ae0d3250d75c1ee5148cfea56a7f
|
4
|
+
data.tar.gz: c7ac45d748527a88a20f04853b4fbb5dde802ca9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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
|
-
|
40
|
-
|
41
|
-
|
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
|
53
|
-
transport.get_document(
|
54
|
-
|
44
|
+
# wait some time
|
45
|
+
res = transport.get_document(receipt.inn, receipt.id)
|
46
|
+
|
47
|
+
# => (внутри такое, а вернет объект)
|
55
48
|
# {
|
56
|
-
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
66
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
70
|
-
# "
|
71
|
-
#
|
72
|
-
#
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
#
|
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.
|
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
|
-
|
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:
|
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
|
-
|
151
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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(
|
data/lib/orange_data/receipt.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
127
|
+
# base class for semi-generated classes
|
128
|
+
class PayloadContent
|
129
|
+
def initialize payload={}
|
130
|
+
@payload = payload
|
131
|
+
end
|
47
132
|
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
178
|
+
end
|
63
179
|
|
64
|
-
def
|
65
|
-
|
66
|
-
|
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
|
70
|
-
|
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
|
74
|
-
@
|
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
|