orangedata 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9f3099e2e11880ab05a035b7174a3b51d4f23568
4
+ data.tar.gz: c48823de2b7073f0e19c60903e3c6d574b08b2b9
5
+ SHA512:
6
+ metadata.gz: e8449d4a55cebf3da41ec3cae035fccdd16c62a363359ee3b5cc35b2511e84b9ebed8b4147a2d3604ca47096df09a65de39cd35f7c24363cda62402c68013949
7
+ data.tar.gz: bcf0803eaecc27e05bb5a34305cf3da9092c792309630147e093c084c89bfd01aaa78fd0860a290190c9b7f606c7a310b59d6c0d039909d9888f6d593a1d63a2
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ /.rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.3.1
7
+ before_install: gem install bundler -v 1.16.6
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # All dependencies are in orangedata.gemspec
6
+ gemspec
@@ -0,0 +1,72 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ orangedata (0.0.1)
5
+ faraday (>= 0.15)
6
+ faraday_middleware
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ addressable (2.5.2)
12
+ public_suffix (>= 2.0.2, < 4.0)
13
+ ast (2.4.0)
14
+ crack (0.4.3)
15
+ safe_yaml (~> 1.0.0)
16
+ diff-lcs (1.3)
17
+ faraday (0.15.2)
18
+ multipart-post (>= 1.2, < 3)
19
+ faraday_middleware (0.12.2)
20
+ faraday (>= 0.7.4, < 1.0)
21
+ hashdiff (0.3.7)
22
+ jaro_winkler (1.5.1)
23
+ multipart-post (2.0.0)
24
+ parallel (1.12.1)
25
+ parser (2.5.1.0)
26
+ ast (~> 2.4.0)
27
+ powerpack (0.1.2)
28
+ public_suffix (3.0.3)
29
+ rainbow (3.0.0)
30
+ rake (10.5.0)
31
+ rspec (3.7.0)
32
+ rspec-core (~> 3.7.0)
33
+ rspec-expectations (~> 3.7.0)
34
+ rspec-mocks (~> 3.7.0)
35
+ rspec-core (3.7.1)
36
+ rspec-support (~> 3.7.0)
37
+ rspec-expectations (3.7.0)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.7.0)
40
+ rspec-mocks (3.7.0)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.7.0)
43
+ rspec-support (3.7.1)
44
+ rubocop (0.57.2)
45
+ jaro_winkler (~> 1.5.1)
46
+ parallel (~> 1.10)
47
+ parser (>= 2.5)
48
+ powerpack (~> 0.1)
49
+ rainbow (>= 2.2.2, < 4.0)
50
+ ruby-progressbar (~> 1.7)
51
+ unicode-display_width (~> 1.0, >= 1.0.1)
52
+ ruby-progressbar (1.9.0)
53
+ safe_yaml (1.0.4)
54
+ unicode-display_width (1.4.0)
55
+ webmock (3.4.2)
56
+ addressable (>= 2.3.6)
57
+ crack (>= 0.3.2)
58
+ hashdiff
59
+
60
+ PLATFORMS
61
+ ruby
62
+
63
+ DEPENDENCIES
64
+ bundler (~> 1.16)
65
+ orangedata!
66
+ rake (~> 10.0)
67
+ rspec
68
+ rubocop
69
+ webmock
70
+
71
+ BUNDLED WITH
72
+ 1.16.6
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Vasily Fedoseyev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,120 @@
1
+ # Orangedata Client
2
+
3
+ A ruby client for orangedata.ru service.
4
+ Target service is pretty local to RU, so parts of readme will be in russian.
5
+
6
+ Note: This is a Work-in-progress. API might change in the future.
7
+
8
+ Умеет:
9
+ - собственно транспорт с подписью запросов
10
+ - сгенерировать ключ сразу в нужном виде
11
+ - планируется DSL для чеков
12
+
13
+ ## Установка
14
+
15
+ Все стандартно. Пишем в Gemfile:
16
+
17
+ ```ruby
18
+ gem 'orangedata'
19
+ ```
20
+
21
+ И давим:
22
+
23
+ $ bundle
24
+
25
+ Либо руками:
26
+
27
+ $ gem install orangedata
28
+
29
+ ## Использование
30
+
31
+ Для тестового окружения ключики в комплекте - [credentials_test.yml](lib/credentials_test.yml), собрано из родного `File_for_test.zip`, доступны как `OrangeData::Credentials.default_test`
32
+
33
+ ```ruby
34
+ transport = OrangeData::Transport.new("https://apip.orangedata.ru:2443/api/v2/", OrangeData::Credentials.default_test)
35
+
36
+ receipt = {
37
+ id: SecureRandom.uuid,
38
+ inn: '1234567890', key:'1234567890',
39
+ content: { # тут собрать данные можно по официальному мануалу
40
+ type: 1,
41
+ positions:[{ quantity: 1, price: 0.01, tax: 4, text: "Товар на копейку"}],
42
+ checkClose:{
43
+ payments: [{ type:2, amount:'0.01' }],
44
+ taxationSystem: 1
45
+ }
46
+ }
47
+ }
48
+ transport.post_document(receipt)
49
+ # wait some time, then
50
+ transport.get_document("1234567890", receipt[:id])
51
+ # =>
52
+ # {
53
+ # "id"=>"a88b6b30-20ab-47ea-95ca-f12f22ef03d3",
54
+ # "deviceSN"=>"1400000000001033",
55
+ # "deviceRN"=>"0000000400054952",
56
+ # "fsNumber"=>"9999078900001341",
57
+ # "ofdName"=>"ООО \"Ярус\" (\"ОФД-Я\")",
58
+ # "ofdWebsite"=>"www.ofd-ya.ru",
59
+ # "ofdinn"=>"7728699517",
60
+ # "fnsWebsite"=>"www.nalog.ru",
61
+ # "companyINN"=>"1234567890",
62
+ # "companyName"=>"Тест",
63
+ # "documentNumber"=>5548,
64
+ # "shiftNumber"=>6072,
65
+ # "documentIndex"=>3045,
66
+ # "processedAt"=>"2018-10-22T19:36:00",
67
+ # "content"=>
68
+ # {
69
+ # "type"=>1,
70
+ # "positions"=>[{"quantity"=>1.0, "price"=>0.01, "tax"=>4, "text"=>"Товар на копейку"}],
71
+ # "checkClose"=>{
72
+ # "payments"=>[{"type"=>2, "amount"=>0.01}],
73
+ # "taxationSystem"=>1
74
+ # }
75
+ # },
76
+ # "change"=>0.0,
77
+ # "fp"=>"787980846"
78
+ # }
79
+ ```
80
+
81
+ ### Получаем сертификаты
82
+
83
+ Предполагается, что всякие договоры и прочая фискализация уже успешно пройдена и у вас есть доступ
84
+ к ЛК orangedata.
85
+
86
+ Генерируем себе приватный ключ:
87
+
88
+ ```ruby
89
+ require 'yaml'
90
+ c = OrangeData::Credentials.new title:'My production'
91
+ c.generate_signature_key!(2048) # параметр - длина ключа
92
+ #=> возвращает публичный ключ в том виде, который хочет ЛК OrangeData:
93
+ # "<RSAKeyValue><Modulus>(многабукв)==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"
94
+
95
+ File.open("my_production.yml", "wt"){|f| f.write YAML.dump(c.to_hash) }
96
+ # (на выходе - yml с приватным ключом и паролем к нему, который надо сохранить и беречь)
97
+
98
+ # повторно взять публичный ключ можно так:
99
+ credentials = OrangeData::Credentials.from_hash(YAML.load_file('my_production.yml'))
100
+ credentials.signature_public_xml
101
+ ```
102
+
103
+ После чего публичный ключ (xml c `RSAKeyValue`) кормим в ЛК. Значение поля `Название ключа` с этого шага отправляется в `signature_key_name`.
104
+ Далее там выпускаем себе сертификаты и из полученного архива вставляем содержимое `<ИНН>.crt` и `<ИНН>.key`(сертификат и ключ к нему) в yml-файлик аналогично примеру.
105
+
106
+ Если все прошло гладко - теперь у вас есть файлик `my_production.yml` со всеми реквизитами доступа к продакшн-кассе. Обращаться с ним стоит как и с любой другой очень чувствительной информацией, например не стоит коммитить его (ну или как минимум, убрать из него поля `signature_key_pass` и `certificate_key_pass` и хранить отдельно)
107
+
108
+ ## Разработка
109
+
110
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
111
+
112
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
113
+
114
+ ## Contributing
115
+
116
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Vasfed/orangedata.
117
+
118
+ ## License
119
+
120
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). Copyright (c) 2018 Vasily Fedoseyev
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "orange_data"
5
+ require "irb"
6
+
7
+ using OrangeData::Credentials::KeyEncoding
8
+ IRB.start(__FILE__)
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "orange_data/version"
4
+ require "orange_data/credentials"
5
+ require "orange_data/transport"
6
+
7
+ module OrangeData
8
+
9
+
10
+ # QR: t=20180518T220500&s=975.88&fn=8710000101125654&i=99456&fp=1250448795&n=1
11
+ #
12
+
13
+ # - t=<date/time - дата и время осуществления расчета в формате ГГГГММДДТЧЧММ>
14
+ # - s=<сумма расчета в рублях и копейках, разделенных точкой>
15
+ # - fn=<заводской номер фискального накопителя>
16
+ # - i=<порядковый номер фискального документа, нулями не дополняется>
17
+ # - fp=<фискальный признак документа, нулями не дополняется>
18
+ # - n=<признак расчета>.
19
+ # Пример строки QR-кода: t=20150720T1638&s=9999999.00&fn=000110000105&i=12345678&fp=123456&n=2
20
+
21
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+ require "securerandom"
6
+
7
+ # wrapper for keys/certs used for connection auth
8
+ module OrangeData
9
+ class Credentials
10
+
11
+ # nodoc
12
+ module KeyEncoding
13
+ refine OpenSSL::PKey::RSA do
14
+ def to_xml
15
+ h_params = to_hash
16
+ h = { 'Modulus' => :n, 'Exponent' => :e }
17
+ if private?
18
+ h.merge!({
19
+ 'P' => :p, 'Q' => :q, 'DP' => :dmp1, 'DQ' => :dmq1, 'InverseQ' => :iqmp, 'D' => :d
20
+ })
21
+ end
22
+ "<RSAKeyValue>#{h.map{|(k,v)| "<#{k}>#{h_params[v.to_s]}</#{k}>"}.join('')}</RSAKeyValue>"
23
+ end
24
+
25
+ def to_hash
26
+ params.map{|k,v| v != 0 && [k, Base64.strict_encode64(v.to_s(2))] || nil}.compact.to_h
27
+ end
28
+ end
29
+
30
+ refine OpenSSL::PKey::RSA.singleton_class do
31
+ def from_xml(xml)
32
+ require "rexml/document"
33
+ kv = REXML::Document.new(xml).elements['RSAKeyValue']
34
+ raise ArgumentError, 'no RSAKeyValue in xml' unless kv&.name == 'RSAKeyValue'
35
+
36
+ mapping = {
37
+ "Modulus"=>:n, "Exponent"=>:e,
38
+ "D"=>:d, "P"=>:p, "Q"=>:q,
39
+ "DP"=>:dmp1, "DQ"=>:dmq1, "InverseQ"=>:iqmp
40
+ }
41
+ from_hash(
42
+ kv.elements.each_with_object({}){|k,h| h[mapping[k.name]] = k.text if mapping[k.name] }
43
+ )
44
+ end
45
+
46
+ def from_hash hash
47
+ OpenSSL::PKey::RSA.new.tap do |key|
48
+ key.params.keys.each{|param|
49
+ if(v = hash[param] || hash[param.to_sym])
50
+ key.send(:"#{param}=", OpenSSL::BN.new(Base64.decode64(v), 2))
51
+ end
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ using KeyEncoding
59
+
60
+ attr_accessor :signature_key_name, :signature_key, :certificate, :certificate_key, :title
61
+
62
+ def initialize signature_key_name:nil,signature_key:nil,certificate:nil,certificate_key:nil,title:nil
63
+ raise ArgumentError, "Signature key should be a private key" if signature_key && !signature_key.private?
64
+ raise ArgumentError, "Certificate key should be a private key" if certificate_key && !certificate_key.private?
65
+ @signature_key_name = signature_key_name
66
+ @signature_key = signature_key
67
+ @certificate = certificate
68
+ @certificate_key = certificate_key
69
+ @title = title
70
+ end
71
+
72
+ def valid?
73
+ signature_key_name &&
74
+ signature_key && signature_key.private? &&
75
+ certificate && certificate_key && certificate_key.private?
76
+ end
77
+
78
+ def self.from_hash creds
79
+ key = nil
80
+ if creds[:signature_key]
81
+ key = if creds[:signature_key].is_a?(Hash)
82
+ OpenSSL::PKey::RSA.from_hash(creds[:signature_key])
83
+ elsif creds[:signature_key].start_with?('<')
84
+ OpenSSL::PKey::RSA.from_xml(creds[:signature_key])
85
+ else
86
+ OpenSSL::PKey::RSA.new(creds[:signature_key], creds[:signature_key_pass])
87
+ end
88
+ end
89
+ new(
90
+ signature_key_name: creds[:signature_key_name],
91
+ signature_key: key,
92
+ certificate: creds[:certificate] && OpenSSL::X509::Certificate.new(creds[:certificate]),
93
+ certificate_key: creds[:certificate_key] && OpenSSL::PKey::RSA.new(creds[:certificate_key], creds[:certificate_key_pass]),
94
+ title: creds[:title]
95
+ )
96
+ end
97
+
98
+ def to_hash(key_pass:nil, save_pass:false)
99
+ if key_pass.nil?
100
+ key_pass = SecureRandom.hex
101
+ save_pass = true
102
+ elsif key_pass == false
103
+ key_pass = nil
104
+ end
105
+
106
+ {
107
+ title: title,
108
+ signature_key_name: signature_key_name,
109
+ signature_key: signature_key&.to_pem(OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
110
+ certificate: certificate&.to_pem,
111
+ certificate_key: certificate_key&.to_pem(OpenSSL::Cipher.new("aes-128-cbc"), key_pass),
112
+ }.tap do |h|
113
+ h.delete(:title) if !title || title == ''
114
+ if save_pass
115
+ h[:certificate_key_pass] = key_pass
116
+ h[:signature_key_pass] = key_pass
117
+ end
118
+ end
119
+ end
120
+
121
+ def self.from_json json
122
+ require 'json'
123
+ from_hash(JSON.parse(json, symbolize_names: true))
124
+ end
125
+
126
+ def to_json(key_pass:nil, save_pass:false)
127
+ to_hash(key_pass:key_pass, save_pass:save_pass).to_json
128
+ end
129
+
130
+ def inspect
131
+ info_fields = {
132
+ title: (title || 'untitled').inspect,
133
+ key_name: signature_key_name.inspect,
134
+ certificate: (certificate&.subject.to_a.select{|ent| ent.first == 'O'}.dig(0, 1)).inspect,
135
+ }.map{|(k,v)| "#{k}=#{v}"}.join(' ')
136
+
137
+ "#<#{self.class.name}:#{object_id} #{info_fields}>"
138
+ end
139
+
140
+ def generate_signature_key!(key_length=2048)
141
+ self.signature_key = OpenSSL::PKey::RSA.new(key_length)
142
+ signature_public_xml
143
+ end
144
+
145
+ # публичная часть ключа подписи в формате пригодном для отдачи в ЛК
146
+ def signature_public_xml
147
+ signature_key.public_key.to_xml
148
+ end
149
+
150
+ # ключи для тествого окружения
151
+ def self.default_test
152
+ require 'yaml'
153
+ from_hash(YAML.load_file(File.expand_path('../credentials_test.yml', __FILE__)))
154
+ end
155
+
156
+ end
157
+ end
@@ -0,0 +1,101 @@
1
+ ---
2
+ # Credentials for test environment at https://apip.orangedata.ru:2443/api/v2/
3
+ :title: 'Test credentials'
4
+ :signature_key_name: '1234567890'
5
+ :signature_key: |
6
+ <RSAKeyValue>
7
+ <Modulus>
8
+ t8nC/Eth8UabQbXu8pdro3v7NqUanV8Y+g92YgT7z1xqkBLRHXZ1guml3Pxr
9
+ qjNX9AvOmu8R+qaKOyHfJW0PcRDLzCoIUcHNAwpDO/E5j6WAaLIv7gAjTtyr
10
+ 9kJB9rfJaparViJNZu3RSUYGTvVznOmXMf7LTOTMR6HP/5H1TP5n1g4+BbLm
11
+ C9EhjUf2eNFqwZBqPtzybBb6jaHBRaJ0XdE3lh2OeE9/OF0BtLwiYPDKsVTx
12
+ IekbNf7l/DREy+YbUOxQLceeHXrvbYLiGWecP0a7CqHGj9ZNY1oJThK3AwrS
13
+ d4yHa9Wnx/GaZUNtWud1BaP9g3sVX+sRV9xtnI96dw==
14
+ </Modulus>
15
+ <Exponent>AQAB</Exponent>
16
+ <P>
17
+ 3WSb72a1erb6jcLkyZA2Y21VNIipGz+ta1RP+iacs3xnktFsxgTYgqWyt6SW
18
+ Z2rStp0u4vb/IAHyKhgJPNTUSi2u0G44MOsRxMC/FWTF8zdyrDF4BjPBM4j8
19
+ 4nAmE/FQYv5F8ldDkakc96zEPiTk5Fka3MPeN8mMk6/OA59JdF0=
20
+ </P>
21
+ <Q>
22
+ 1IRVid5SsDrOwJQAEKkdT436XEb0sVWe9AcU8JyaCEEMj0NPzownNbIrebPo
23
+ fMYdDHikopQpr2XqxZYDbb7AneoHkhEV26TfpPVbN4wBJFXih3lAP2n5hqhg
24
+ qHGp5Wq2Lu7jUS376Ruw3bhwW+MiWpXv1xhMTZ8AtDfnZFFNvOM=
25
+ </Q>
26
+ <DP>
27
+ Fo5KiNCJCtCbpFfH4XVM5UJdXPXTbNBHBdlYMJ9AddTl5IJrt50ExgLFu4oM
28
+ PMsYXryS61LI2WT5XCqIvmbcnhYbambgWLOKYuZUUYSr2kS67So5FUCunWaG
29
+ hTdx2bRLQVqwm6kiXDPDnMRAViiCHXWqk/VsrXheVymhLqNK440=
30
+ </DP>
31
+ <DQ>
32
+ mowSWMzhfV+G8+2tjnAt7KjnpSvEzyHhEr4DsGdybQZBR/4/j4nFCfukOkFn
33
+ lTXN8j/aGpF9Lx0C+uX5YFoUYcLL9qGOL8lbCu+TgnXCbtY2gybeXj+HQzI3
34
+ +MeQMlLEYqU/ks3KIOAOY2+55ljrpszbOqVk+B3luSnekMm/qtk=
35
+ </DQ>
36
+ <InverseQ>
37
+ aP5e5F1j6s82Pm7dCpH3mRZWnfZIKqoNQIq2BO8vA9/WrdFI2C27uNhxCp2Z
38
+ DMulRdBZcoeHcwJjnyDzg4I4gBZ2nSKkVdlN1REoTjLBBdlHi8XKiXzxvpIt
39
+ c2wjNC2AKHaJqj/dnh3bbTAQD1iUAxPmmLJYYkhfZ2i1IrTVxZE=
40
+ </InverseQ>
41
+ <D>
42
+ PUfM+Aq6kZSVWAetsL3EajKAxOuwQCDhVx+ovW4j+DQ8Y+WiTEyfShNV9qVD
43
+ 0PBltz3omch1GjpFhQn6OaRvraeIDH9HXttb3FOjr2zzYG4yrrYbPSRWoYj6
44
+ 3ZWiIP2O7zdl0caGQHezfNcYa2N0NTG99DGc3/q6EnhlvjWQsSbiEjmxcPx8
45
+ fmV1i4DoflMQ383nsixAFapgrROUAtCgMvhWn1kSeoojKd+e4eKZxa/SNYul
46
+ sBJWNFkmo1CZH4YTqlPM+IwYeDUOnOUGNxGurRZ3qQdWs2N2ZQhnrvlh+zpz
47
+ urD2hwAz6gQXP7mxxMR1xHtAD8XQ+w4OiJK6VWjoIQ==
48
+ </D>
49
+ </RSAKeyValue>
50
+ :certificate: |
51
+ -----BEGIN CERTIFICATE-----
52
+ MIIDYjCCAkoCAQAwDQYJKoZIhvcNAQELBQAwcTELMAkGA1UEBhMCUlUxDzANBgNV
53
+ BAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MRMwEQYDVQQKDApPcmFuZ2VkYXRh
54
+ MQ8wDQYDVQQLDAZOZWJ1bGExGjAYBgNVBAMMEXd3dy5vcmFuZ2VkYXRhLnJ1MB4X
55
+ DTE4MDMxNTE2NDYwMVoXDTI4MDMxMjE2NDYwMVowfTELMAkGA1UEBhMCUlUxDzAN
56
+ BgNVBAgMBk1vc2NvdzEPMA0GA1UEBwwGTW9zY293MR8wHQYDVQQKDBZPcmFuZ2Vk
57
+ YXRhIHRlc3QgY2xpZW50MRMwEQYDVQQLDApFLWNvbW1lcmNlMRYwFAYDVQQDDA1v
58
+ cmFuZ2VkYXRhLnJ1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo7XZ
59
+ +VUUo9p+Q0zPmlt1eThA8NmVVAgNXkVDZoz3umyEnnm2d4R5Voxf4y6fuesW3Za8
60
+ /ImKWLbQ3/S/pHZKWiz75ElSfpnYJfMRuLAaqqs0eFfxmHbHi8Mgg9zjAMdILpR6
61
+ eEaP7qeCNRom3Zb6ziYoWEmDC2ZFFu9995rjkn7CtV3noWZveOCGExjM7WTkql8L
62
+ v1PX3ee3fXaEC7Kefxl4O/4w7agEceKRHlc0l3iwVJaKittQwAQd3ieUwoqsxzPH
63
+ dRwB4IU9aI6IjfqteyD51s7xd+ayM/O4j+aJ/HBhJajDHBcGWKytxv0f6YpqPUAc
64
+ 25fRAXVa0Gsei6eY/QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCv/Vcxh2lMt8RV
65
+ Al0V9xIst0ZdjH22yTOUCOiH9PZgeagqrjTLT3ycWAdbZZUpzcFSdOmPUsgQ7Eqz
66
+ +TpcY5lmYFInLwJK/Afjqsb5LK2irGKT254p5qzD9rSRlM42wxRzQTA0BWX3mmhi
67
+ zwdrfLAvyCw1gHBbUZNf3eemBCY+8RRGPRAqD2XbyIya1bX0AHLXbx5dBe9EIOG/
68
+ F46WbTlrkR7kc06eiacTiGYwNdcywJ2KOcvmnXPup8Os6KOWe197CIathDHeiG2C
69
+ mQlsQDF/d7W4G/+l6Q66BhfRtuhp99gkT8P8j82X6ChrwbgQ5+vya3SytJ0wmIg2
70
+ 67jOKmGK
71
+ -----END CERTIFICATE-----
72
+ :certificate_key: |
73
+ -----BEGIN PRIVATE KEY-----
74
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCjtdn5VRSj2n5D
75
+ TM+aW3V5OEDw2ZVUCA1eRUNmjPe6bISeebZ3hHlWjF/jLp+56xbdlrz8iYpYttDf
76
+ 9L+kdkpaLPvkSVJ+mdgl8xG4sBqqqzR4V/GYdseLwyCD3OMAx0gulHp4Ro/up4I1
77
+ GibdlvrOJihYSYMLZkUW7333muOSfsK1XeehZm944IYTGMztZOSqXwu/U9fd57d9
78
+ doQLsp5/GXg7/jDtqARx4pEeVzSXeLBUloqK21DABB3eJ5TCiqzHM8d1HAHghT1o
79
+ joiN+q17IPnWzvF35rIz87iP5on8cGElqMMcFwZYrK3G/R/pimo9QBzbl9EBdVrQ
80
+ ax6Lp5j9AgMBAAECggEAL5qkrKT54H+bcZR3Vco8iag68g5DJvFEeeIoLDzXmGUP
81
+ 10lLLsvdwLYG9/fJyHU86+h2QfT4vr1CVa1EwN0I19n20TYk/91ahgZ9Y7gJuREZ
82
+ q9jeztfTRKfT36Quej54ldrlFe5m0h3xdeGJ5auOeL2Nw8Z0ja8KbhXsCkEG5cTx
83
+ ZvXB0XlFoAJOp8AZvU3ZNBpmpItFlcl2aBXwRCb72DUjLkpnZf2kFDNorc1wFZ2e
84
+ DO/pujT6EtQ1r5qb2kUuj4GpCaHffOB/ukz3dg3bBhompTYdhax0RlZs2vNsUusm
85
+ 6oYsUS5nWmJfnrh32Te03Fdzc2U8/XUflJzKL/0QvQKBgQDOpNQvCCxwvthZXART
86
+ q0fl9NY0fxlSqUpxd1BB4DYCg6Sg5kVvfwf7rdb5bbP4aNCC/9m4MgXTD0DGfEhM
87
+ FnYPVNKTzwLMBftBQdzDN6766j5lI49evwnh855EFAR5GyaIWh2n7tT3NUOstogp
88
+ kpwhzsPGH1WkEO1QLcBDyzPI3wKBgQDKz94V8au1EVKuRBR+c5gNJpF+zmUu2t2C
89
+ ZlPtYIuWaxMbqitmeCmNBQQZK+oLQdSUMkgMvYVpKriPk6AgnY7+1F+OOeg+ezPU
90
+ G+J4Vi8Yx/kZPhXoBuW745twux+q8WOBwEj2WeMy5p1F/V3qlu70HA3kbsrXdB+R
91
+ 0bFVAxCtowKBgFTtq4M08cbYuORpDCIzGBarvMnQnuC5US43IlYgxzHbVvMGEO2V
92
+ IPvQY7UZ4EitE11zt9CbRoeLEk1BURlsddMxQmabQwQFRVF5tzjIjvLzCPfaWJdR
93
+ Hsetr5M9QuVfQkPx/ZRCdWawjoLSdj3X0rGWYCHySOloR5CXbRiv0DWzAoGAF3XW
94
+ Ldmn0Ckx1EDB0iLS+up0OCPt5m6g4v2tRa8+VmcKbc/Qd2j8/XgQEk1XJHg3+/CZ
95
+ Dwg5T4IGmW0tP7iaGvY8G3qtV9TumOGk3+CwUACJ2xaoeA+cMZDRoUe0ERUdOpwg
96
+ lIavVmsA1GDLpWBSQeCg5sS+KBAhur9z8O6K1lsCgYEAj7TLLE0jLNXRRfkfWzy5
97
+ RsJezMCQS9fjtJrLGB3BbYxqtebP2owp1qjmKMQioW5QjRxRCOyT2KrHjb31hRsp
98
+ Hk3Wi0OKOEuKNwmAZczbjcPH4caPZPeL6LMDtFFMsFX2BW7TnC8FcoVr2KPO/FG/
99
+ xs4KtXC9j5rrvBowJ0LbJ2U=
100
+ -----END PRIVATE KEY-----
101
+ :certificate_key_pass: '1234'
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OrangeData
4
+
5
+ class Receipt
6
+ attr_accessor :id, :inn, :group, :key_name
7
+
8
+ def initialize id:SecureRandom.uuid, inn:, group:nil, key_name:nil
9
+ @id = id
10
+ @inn = inn
11
+ @group = group
12
+ @key_name = key_name
13
+ yield self if block_given?
14
+ end
15
+ end
16
+
17
+ class ReceiptContent
18
+
19
+ module AgentTypeSerializer
20
+ AGENT_TYPE_BITS = { # 1057 (в чеках/БСО должно соответствовать отчету о (пере)регистрации ККТ)
21
+ bank_payment_agent: (1 << 0), # банковский платежный агент
22
+ bank_payment_subagent: (1 << 1), # банковский платежный субагент
23
+ payment_agent: (1 << 2), # платежный агент
24
+ payment_subagent: (1 << 3), # платежный субагент
25
+ attorney: (1 << 4), # поверенный
26
+ commission_agent: (1 << 5), # комиссионер
27
+ other_agent: (1 << 6), # иной агент
28
+ }.freeze
29
+
30
+ def self.load data
31
+ data = data.to_i
32
+ AGENT_TYPE_BITS.select{|(k,v)| (data & v) > 0}.map(&:first)
33
+ end
34
+
35
+ def self.dump val
36
+ val = [val] unless val.is_a?(Array)
37
+ val.map{|v| AGENT_TYPE_BITS[v] || raise "unknown agent_type #{v}"}.reduce(:|)
38
+ end
39
+ end
40
+
41
+
42
+ RECEIPT_TYPES = { # 1054:
43
+
44
+ }.freeze
45
+
46
+ FIELDS = {
47
+ type: {
48
+ name: 'Признак расчета',
49
+ tag_num: 1054,
50
+ mapper: :enum,
51
+ enum_values: {
52
+ income: 1, # Приход
53
+ return_income: 2, # Возврат прихода
54
+ expense: 3, # Расход
55
+ return_expense: 4 # Возврат расхода
56
+ }
57
+ }
58
+ }
59
+
60
+
61
+
62
+ def initialize type
63
+ @positions = []
64
+ @payments = []
65
+ end
66
+
67
+ def agent_type
68
+ AgentTypeSerializer.load(@agent_type)
69
+ end
70
+
71
+ def agent_type= val
72
+ @agent_type = AgentTypeSerializer.dump(val)
73
+ end
74
+
75
+ end
76
+
77
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'faraday'
6
+ require 'faraday_middleware'
7
+
8
+ # handles low-level http requests to orangedata, including auth
9
+ module OrangeData
10
+ class Transport
11
+
12
+ def initialize api_url=default_api_url, credentials=Credentials.default_test
13
+ raise ArgumentError, "Need full credentials for connection" unless credentials.valid?
14
+ @credentials = credentials
15
+ @api_url = api_url
16
+ end
17
+
18
+ def default_api_url
19
+ # production: https://api.orangedata.ru:12003/api/v2/
20
+ # test: https://apip.orangedata.ru:2443/api/v2/
21
+ "https://apip.orangedata.ru:2443/api/v2/"
22
+ end
23
+
24
+ class RequestSignatureMiddleware < Faraday::Middleware
25
+ def initialize(app, signature_key)
26
+ @app = app
27
+ @signature_key = signature_key
28
+ end
29
+
30
+ def call(env)
31
+ if env.body
32
+ signature = @signature_key.sign(OpenSSL::Digest::SHA256.new, env.body)
33
+ env.request_headers['X-Signature'] = Base64.strict_encode64(signature)
34
+ end
35
+ @app.call(env)
36
+ end
37
+ end
38
+
39
+ def transport
40
+ @transport ||= Faraday.new(
41
+ url: @api_url,
42
+ ssl: {
43
+ client_cert: @credentials.certificate,
44
+ client_key: @credentials.certificate_key,
45
+ }
46
+ ) do |conn|
47
+ conn.headers['User-Agent'] = "OrangeDataRuby/#{OrangeData::VERSION}"
48
+ conn.headers['Accept'] = "application/json"
49
+ conn.request(:json)
50
+ conn.use(RequestSignatureMiddleware, @credentials.signature_key)
51
+ conn.response :json, content_type: /\bjson$/
52
+ conn.adapter(Faraday.default_adapter)
53
+ end
54
+ end
55
+
56
+ def raw_post method, data
57
+ transport.post(method, data)
58
+ end
59
+
60
+ def post_entity sub_url, data
61
+ res = raw_post(sub_url, data)
62
+
63
+ case res.status
64
+ when 201
65
+ return true
66
+ when 401
67
+ raise 'Unauthorized'
68
+ when 409
69
+ raise "Conflict"
70
+ when 400
71
+ raise "Invalid doc: #{res.body}"
72
+ else
73
+ raise "Unknown code from OD: #{res.status} #{res.reason_phrase} #{res.body}"
74
+ end
75
+ end
76
+
77
+ def get_entity sub_url
78
+ res = transport.get(sub_url)
79
+
80
+ case res.status
81
+ when 200
82
+ return res.body
83
+ when 400
84
+ raise "Cannot get doc: #{res.body}"
85
+ when 401
86
+ raise 'Unauthorized'
87
+ end
88
+ end
89
+
90
+ # Below actual methods from api
91
+
92
+ def ping
93
+ res = transport.get(''){|r|
94
+ r.headers['Accept'] = 'text/plain'
95
+ }
96
+
97
+ return res.status == 200 && res.body == "Nebula.Api v2"
98
+ rescue StandardError => e
99
+ return false
100
+ end
101
+
102
+ def post_document_validate data
103
+ res = raw_post 'validateDocument', data
104
+
105
+ case res.status
106
+ when 200
107
+ return true
108
+ when 400
109
+ return res.body
110
+ when 401
111
+ raise 'Unauthorized'
112
+ else
113
+ raise "Unexpected response: #{res.status} #{res.reason_phrase}"
114
+ end
115
+ end
116
+
117
+ def post_document data
118
+ post_entity 'documents', data
119
+ end
120
+
121
+ def get_document inn, document_id
122
+ get_entity "documents/#{inn}/status/#{document_id}"
123
+ end
124
+
125
+ def post_correction data
126
+ post_entity 'corrections', data
127
+ end
128
+
129
+ def get_correction inn, document_id
130
+ get_entity "corrections/#{inn}/status/#{document_id}"
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,3 @@
1
+ module OrangeData
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1 @@
1
+ require_relative 'orange_data'
@@ -0,0 +1,33 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "orange_data/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "orangedata"
8
+ spec.version = OrangeData::VERSION
9
+ spec.authors = ["Vasily Fedoseyev"]
10
+ spec.email = ["vasilyfedoseyev@gmail.com"]
11
+
12
+ spec.summary = %q{Ruby client for orangedata.ru service}
13
+ spec.description = %q{Ruby client for orangedata.ru service}
14
+ spec.homepage = "https://github.com/Vasfed/orangedata"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
18
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ end
20
+
21
+ # spec.bindir = "exe"
22
+ # spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_dependency "faraday", ">=0.15"
26
+ spec.add_dependency "faraday_middleware"
27
+
28
+ spec.add_development_dependency "bundler", "~> 1.16"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec"
31
+ spec.add_development_dependency "webmock"
32
+ spec.add_development_dependency "rubocop"
33
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: orangedata
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Vasily Fedoseyev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0.15'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0.15'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday_middleware
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.16'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.16'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Ruby client for orangedata.ru service
112
+ email:
113
+ - vasilyfedoseyev@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rspec"
120
+ - ".travis.yml"
121
+ - Gemfile
122
+ - Gemfile.lock
123
+ - LICENSE.txt
124
+ - README.md
125
+ - Rakefile
126
+ - bin/console
127
+ - bin/setup
128
+ - lib/orange_data.rb
129
+ - lib/orange_data/credentials.rb
130
+ - lib/orange_data/credentials_test.yml
131
+ - lib/orange_data/receipt.rb
132
+ - lib/orange_data/transport.rb
133
+ - lib/orange_data/version.rb
134
+ - lib/orangedata.rb
135
+ - orangedata.gemspec
136
+ homepage: https://github.com/Vasfed/orangedata
137
+ licenses:
138
+ - MIT
139
+ metadata: {}
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubyforge_project:
156
+ rubygems_version: 2.6.3
157
+ signing_key:
158
+ specification_version: 4
159
+ summary: Ruby client for orangedata.ru service
160
+ test_files: []