fiscalizer 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,13 @@
1
+ fiscal = Fiscalizer.new certificate_p12_path: "/path/to/FISKAL 1.p12",
2
+ password: "12345678"
3
+ echo_response = fiscal.echo text: "It's mearly a flesh wound!"
4
+
5
+ if echo_response.errors?
6
+ puts "There were some nasty errors!"
7
+ echo_response.errors.each do |error_code, error_message|
8
+ puts " " + error_code + " : " + error_message
9
+ end
10
+ end
11
+
12
+ puts "The server returned: " + echo_response.response
13
+ puts "The echo request was successful! Great success!" if echo_response.echo?
@@ -0,0 +1,16 @@
1
+ fiscal = Fiscalizer.new url: "https://cistest.apis-it.hr:8449/FiskalizacijaServiceTest",
2
+ key_public_path: "/path/to/fiskal1.cert",
3
+ key_private_path: "/path/to/privateKey.key",
4
+ certificate_path: "/path/to/democacert.pem",
5
+ certificate_issued_by: "OU=DEMO,O=FINA,C=HR"
6
+ echo_response = fiscal.echo text: "It's mearly a flesh wound!"
7
+
8
+ if echo_response.errors?
9
+ puts "There were some nasty errors!"
10
+ echo_response.errors.each do |error_code, error_message|
11
+ puts " " + error_code + " : " + error_message
12
+ end
13
+ end
14
+
15
+ puts "The server returned: " + echo_response.response
16
+ puts "The echo request was successful! Great success!" if echo_response.echo?
File without changes
@@ -0,0 +1,44 @@
1
+ fiscal = Fiscalizer.new certificate_p12_path: "/path/to/FISKAL 1.p12",
2
+ password: "12345678"
3
+ # Generate taxes
4
+ taxes_vat = []
5
+ taxes_spending = []
6
+ taxes_other = []
7
+ (0..5).each do |i|
8
+ tax = Fiscalizer::Tax.new
9
+ tax.base = rand(10000 * 100).to_f / 100.0
10
+ tax.rate = rand(100 * 100).to_f / 100.0
11
+ taxes_vat << tax
12
+ end
13
+ (0..5).each do |i|
14
+ tax = Fiscalizer::Tax.new
15
+ tax.base = rand(10000 * 100).to_f / 100.0
16
+ tax.rate = rand(100 * 100).to_f / 100.0
17
+ taxes_spending << tax
18
+ end
19
+ (0..5).each do |i|
20
+ tax = Fiscalizer::Tax.new
21
+ tax.base = rand(10000 * 100).to_f / 100.0
22
+ tax.rate = rand(100 * 100).to_f / 100.0
23
+ tax.name = "My Test Tax #{i}"
24
+ taxes_other << tax
25
+ end
26
+ # Generate invoice
27
+ invoice_response = fiscal.invoice uuid: UUID,
28
+ time_sent: Time.now,
29
+ pin: PIN,
30
+ in_vat_system: true,
31
+ time_issued: Time.now - 3600,
32
+ consistance_mark: "P",
33
+ issued_number: "1",
34
+ issued_office: "Pm2",
35
+ issued_machine: "3",
36
+ tax_vat: taxes_vat,
37
+ tax_spending: taxes_spending,
38
+ tax_other: taxes_other,
39
+ payment_method: "g",
40
+ operator_pin: PIN_OPERATOR,
41
+ subsequent_delivery: false,
42
+ value_non_taxable: 200.0
43
+
44
+ puts "The server returned the following JIR: " + invoice_response.unique_identifier if !invoice_response.errors?
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fiscalizer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fiscalizer"
8
+ spec.version = Fiscalizer::VERSION
9
+ spec.authors = ["Stanko Krtalić Rusendić"]
10
+ spec.email = ["stanko.krtalic-rusendic@infinum.hr"]
11
+ spec.description = %q{Automatic fiscalization}
12
+ spec.summary = %q{A gem that automatically handles fiscalization}
13
+ spec.homepage = "https://github.com/infinum/fiscalizer"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "nokogiri"
22
+ spec.add_dependency "xmldsig-fiscalizer"
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
data/lib/README.md ADDED
File without changes
data/lib/fiscalizer.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+ require "fiscalizer/version"
3
+ require "fiscalizer/fiscalizer"
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'openssl'
7
+ require 'nokogiri'
8
+ require 'xmldsig'
@@ -0,0 +1,9 @@
1
+ # Fiscalizer
2
+
3
+ Fiscalizer is the main class of this gem. It interacts with all other classes, handles certificate decription and connection establishment.
4
+
5
+ Methods:
6
+
7
+ * `echo` : Creates an echo request to the fiscalization servers
8
+ * `fiscalize_office` : Creates or recives an office object and fiscalizes it (alias methods `office` and `fiscalize_office_space`)
9
+ * `fiscalize_invoice` : Creates or recives an invoice object and fiscalizes it (alias methods `invoice`)
@@ -0,0 +1,296 @@
1
+ class Fiscalizer
2
+ class Communication
3
+ attr_accessor :url, :tns, :schemaLocation,
4
+ :key_public, :key_private, :certificate,
5
+ :certificate_issued_by
6
+
7
+ def initialize( tns: "http://www.apis-it.hr/fin/2012/types/f73",
8
+ url: "https://cis.porezna-uprava.hr:8449/FiskalizacijaService",
9
+ schemaLocation: "http://www.apis-it.hr/fin/2012/types/f73 FiskalizacijaSchema.xsd",
10
+ key_public: nil, key_private: nil, certificate: nil,
11
+ certificate_issued_by: "OU=RDC,O=FINA,C=HR" )
12
+ @tns = tns
13
+ @url = url
14
+ @schemaLocation = schemaLocation
15
+ @key_public = key_public
16
+ @key_private = key_private
17
+ @certificate = certificate
18
+ @certificate_issued_by = certificate_issued_by
19
+ end # initialize
20
+
21
+ def send object
22
+ # Check input
23
+ raise "Missing keys" if @key_public == nil || @key_private == nil
24
+ raise "Missing certificate" if @certificate == nil
25
+ raise "Can't send nil object" if object == nil
26
+
27
+ # Prepare data
28
+ uri = URI.parse(@url)
29
+ http = Net::HTTP.new(uri.host, uri.port)
30
+ http.use_ssl = true
31
+ http.cert_store = OpenSSL::X509::Store.new
32
+ http.cert_store.set_default_paths
33
+ http.cert_store.add_cert(@certificate)
34
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
35
+
36
+ # Encode object
37
+ encoded_object = nil
38
+ if object.class == Echo
39
+ encoded_object = encode_echo object
40
+ elsif object.class == Office
41
+ encoded_object = encode_office object
42
+ elsif object.class == Invoice
43
+ encoded_object = encode_invoice object
44
+ else
45
+ # Something broke so we return false,
46
+ # this enables us to check sucess in an if statement
47
+ return false
48
+ end
49
+
50
+ # Send it
51
+ request = Net::HTTP::Post.new(uri.request_uri)
52
+ request.content_type = 'application/xml'
53
+ request.body = encoded_object
54
+ response = http.request(request)
55
+
56
+ return response
57
+ end # send
58
+
59
+ private
60
+ def encode_echo object
61
+ echo_xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
62
+ xml['tns'].EchoRequest(
63
+ 'xmlns:tns' => @tns,
64
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
65
+ 'xsi:schemaLocation' => @schemaLocation
66
+ ) {
67
+ xml.text object.text
68
+ }
69
+ end
70
+ xml = soap_envelope(echo_xml).to_xml
71
+ object.generated_xml = soap_envelope(echo_xml).to_xml
72
+ return xml
73
+ end # send_echo
74
+
75
+ def encode_office object
76
+ office_request_xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
77
+ xml['tns'].PoslovniProstorZahtjev(
78
+ 'xmlns:tns' => @tns,
79
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
80
+ 'xsi:schemaLocation' => schemaLocation,
81
+ 'Id' => 'PoslovniProstorZahtjev'
82
+ ) {
83
+ # Header
84
+ xml['tns'].Zaglavlje {
85
+ xml['tns'].IdPoruke object.uuid
86
+ xml['tns'].DatumVrijeme object.time_sent_str
87
+ }
88
+ # Body
89
+ xml['tns'].PoslovniProstor {
90
+ # Important info
91
+ xml['tns'].Oib object.pin
92
+ xml['tns'].OznPoslProstora object.office_label
93
+ xml['tns'].AdresniPodatak {
94
+ xml['tns'].Adresa {
95
+ xml['tns'].Ulica object.adress_street_name
96
+ xml['tns'].KucniBroj object.adress_house_num
97
+ xml['tns'].KucniBrojDodatak object.adress_house_num_addendum
98
+ xml['tns'].BrojPoste object.adress_post_num
99
+ xml['tns'].Naselje object.adress_settlement
100
+ xml['tns'].Opcina object.adress_township
101
+ } if object.adress_other == nil
102
+ xml['tns'].OstaliTipoviPP object.adress_other if object.adress_other != nil
103
+ }
104
+ xml['tns'].RadnoVrijeme object.office_time
105
+ xml['tns'].DatumPocetkaPrimjene object.take_effect_date_str
106
+ # Optional info
107
+ xml['tns'].OznakaZatvaranja object.closure_mark if object.closure_mark != nil
108
+ xml['tns'].SpecNamj object.specific_purpose if object.specific_purpose != nil
109
+ }
110
+ xml.Signature('xmlns' => 'http://www.w3.org/2000/09/xmldsig#') {
111
+ xml.SignedInfo {
112
+ xml.CanonicalizationMethod('Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#')
113
+ xml.SignatureMethod('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1')
114
+ xml.Reference('URI' => '#PoslovniProstorZahtjev') {
115
+ xml.Transforms {
116
+ xml.Transform('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#enveloped-signature')
117
+ xml.Transform('Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#')
118
+ }
119
+ xml.DigestMethod('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1')
120
+ xml.DigestValue
121
+ }
122
+ }
123
+ xml.SignatureValue
124
+ xml.KeyInfo {
125
+ xml.X509Data {
126
+ xml.X509Certificate key_public.to_s.gsub('-----BEGIN CERTIFICATE-----','').gsub('-----END CERTIFICATE-----','').gsub("\n",'')
127
+ xml.X509IssuerSerial {
128
+ xml.X509IssuerName @certificate_issued_by
129
+ xml.X509SerialNumber "1053520622" # Explane
130
+ }
131
+ }
132
+ }
133
+ }
134
+ }
135
+ end
136
+
137
+ body = soap_envelope(office_request_xml)
138
+ unsigned_document = Xmldsig::SignedDocument.new(body.doc.root.to_xml)
139
+ signed_xml = unsigned_document.sign(@key_private)
140
+ signed_xml.sub! '<?xml version="1.0"?>', ''
141
+ signed_xml = signed_xml.gsub /^$\n/, ''
142
+
143
+ object.generated_xml = signed_xml
144
+
145
+ return signed_xml
146
+ end # send_office
147
+
148
+ def encode_invoice object
149
+ generate_security_code object
150
+ invoice_request_xml = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
151
+ xml['tns'].RacunZahtjev(
152
+ 'xmlns:tns' => @tns,
153
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
154
+ 'xsi:schemaLocation' => schemaLocation,
155
+ 'Id' => 'RacunZahtjev'
156
+ ) {
157
+ # Header
158
+ xml['tns'].Zaglavlje {
159
+ xml['tns'].IdPoruke object.uuid
160
+ xml['tns'].DatumVrijeme object.time_sent_str
161
+ }
162
+ # Body
163
+ xml['tns'].Racun {
164
+ # Important info
165
+ xml['tns'].Oib object.pin
166
+ xml['tns'].USustPdv object.in_vat_system
167
+ xml['tns'].DatVrijeme object.time_issued_str
168
+ xml['tns'].OznSlijed object.consistance_mark
169
+ # Invoice issued numbers
170
+ xml['tns'].BrRac {
171
+ xml['tns'].BrOznRac object.issued_number.to_s
172
+ xml['tns'].OznPosPr object.issued_office.to_s
173
+ xml['tns'].OznNapUr object.issued_machine.to_s
174
+ }
175
+ # Optional info
176
+ # Tax VAT
177
+ xml['tns'].Pdv {
178
+ object.tax_vat.each do |tax|
179
+ xml['tns'].Porez {
180
+ xml['tns'].Stopa tax.rate_str
181
+ xml['tns'].Osnovica tax.base_str
182
+ xml['tns'].Iznos tax.total_str
183
+ }
184
+ end
185
+ } if object.tax_vat != nil && object.tax_vat.size != 0
186
+ # Tax Spending
187
+ xml['tns'].Pnp {
188
+ object.tax_spending.each do |tax|
189
+ xml['tns'].Porez {
190
+ xml['tns'].Stopa tax.rate_str
191
+ xml['tns'].Osnovica tax.base_str
192
+ xml['tns'].Iznos tax.total_str
193
+ }
194
+ end
195
+ } if object.tax_spending != nil && object.tax_spending.size != 0
196
+ # Tax Other
197
+ xml['tns'].OstaliPor {
198
+ object.tax_other.each do |tax|
199
+ xml['tns'].Porez {
200
+ xml['tns'].Naziv tax.name
201
+ xml['tns'].Stopa tax.rate_str
202
+ xml['tns'].Osnovica tax.base_str
203
+ xml['tns'].Iznos tax.total_str
204
+ }
205
+ end
206
+ } if object.tax_other != nil && object.tax_other.size != 0
207
+ # Tax info
208
+ xml['tns'].IznosOslobPdv object.value_tax_liberation_str if object.value_tax_liberation_str != nil
209
+ xml['tns'].IznosMarza object.value_tax_margin_str if object.value_tax_margin_str != nil
210
+ xml['tns'].IznosNePodlOpor object.value_non_taxable_str if object.value_non_taxable_str != nil
211
+ # Fees
212
+ xml['tns'].Naknade {
213
+ object.tax_other.each do |tax|
214
+ xml['tns'].Naknada {
215
+ xml['tns'].NazivN tax.name
216
+ xml['tns'].IznosN tax.rate_str
217
+ }
218
+ end
219
+ } if object.fees != nil && object.fees.size != 0
220
+ # Important info
221
+ xml['tns'].IznosUkupno object.summed_total_str
222
+ xml['tns'].NacinPlac object.payment_method
223
+ xml['tns'].OibOper object.operator_pin
224
+ xml['tns'].ZastKod object.security_code
225
+ xml['tns'].NakDost object.subsequent_delivery
226
+ # Optional info
227
+ xml['tns'].ParagonBrRac object.paragon_label if object.paragon_label != nil
228
+ xml['tns'].SpecNamj object.specific_purpose if object.specific_purpose != nil
229
+ }
230
+ xml.Signature('xmlns' => 'http://www.w3.org/2000/09/xmldsig#') {
231
+ xml.SignedInfo {
232
+ xml.CanonicalizationMethod('Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#')
233
+ xml.SignatureMethod('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1')
234
+ xml.Reference('URI' => '#RacunZahtjev') {
235
+ xml.Transforms {
236
+ xml.Transform('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#enveloped-signature')
237
+ xml.Transform('Algorithm' => 'http://www.w3.org/2001/10/xml-exc-c14n#')
238
+ }
239
+ xml.DigestMethod('Algorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1')
240
+ xml.DigestValue
241
+ }
242
+ }
243
+ xml.SignatureValue
244
+ xml.KeyInfo {
245
+ xml.X509Data {
246
+ xml.X509Certificate key_public.to_s.gsub('-----BEGIN CERTIFICATE-----','').gsub('-----END CERTIFICATE-----','').gsub("\n",'')
247
+ xml.X509IssuerSerial {
248
+ xml.X509IssuerName @certificate_issued_by
249
+ xml.X509SerialNumber "1053520622" # Explane
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ end
256
+
257
+ body = soap_envelope(invoice_request_xml)
258
+ unsigned_document = Xmldsig::SignedDocument.new(body.doc.root.to_xml)
259
+ signed_xml = unsigned_document.sign(@key_private)
260
+ signed_xml.sub! '<?xml version="1.0"?>', ''
261
+ signed_xml = signed_xml.gsub /^$\n/, ''
262
+
263
+ object.generated_xml = signed_xml
264
+
265
+ return signed_xml
266
+ end # encode_invoice
267
+
268
+ def soap_envelope req_xml
269
+ return Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
270
+ xml['soapenv'].Envelope('xmlns:soapenv' => 'http://schemas.xmlsoap.org/soap/envelope/') {
271
+ xml['soapenv'].Body {
272
+ xml << req_xml.doc.root.to_xml
273
+ }
274
+ }
275
+ end
276
+ end # soap_envelope
277
+
278
+ def generate_security_code invoice
279
+ # Build data set to generate security code
280
+ unsigned_code = ""
281
+ unsigned_code += invoice.pin
282
+ unsigned_code += invoice.time_sent_str " "
283
+ unsigned_code += invoice.issued_number.to_s
284
+ unsigned_code += invoice.issued_office.to_s
285
+ unsigned_code += invoice.issued_machine.to_s
286
+ unsigned_code += invoice.summed_total_str
287
+ # Sign with my private key
288
+ signed_code = OpenSSL::PKey::RSA.new(key_private).sign(OpenSSL::Digest::SHA1.new, unsigned_code)
289
+ # Create a MD5 digest from it
290
+ md5_digest = Digest::MD5.hexdigest(signed_code)
291
+ invoice.security_code = md5_digest
292
+ return md5_digest
293
+ end # generate_security_code
294
+
295
+ end # Communication
296
+ end # Fiscalizer
@@ -0,0 +1,11 @@
1
+ class Fiscalizer
2
+ class Echo
3
+
4
+ attr_accessor :text, :generated_xml
5
+
6
+ def initialize text: "Hello World!"
7
+ @text = text
8
+ end # initialize
9
+
10
+ end # Echo
11
+ end # Fiscalizer
@@ -0,0 +1,20 @@
1
+ class Fiscalizer
2
+ class Fee
3
+
4
+ attr_accessor :name, :value
5
+
6
+ def initialize name: "", value: 0.0
7
+ @name = name
8
+ @value = value
9
+ end # initialize
10
+
11
+ def value
12
+ return @value.to_f.round(2)
13
+ end # value
14
+
15
+ def value_str
16
+ return ("%15.2f" % value).strip
17
+ end # value_str
18
+
19
+ end # Fee
20
+ end # Fiscalizer