croatia 0.1.0 → 0.3.0
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/lib/croatia/config.rb +30 -0
- data/lib/croatia/fiscalizer/xml_builder.rb +58 -0
- data/lib/croatia/fiscalizer.rb +92 -0
- data/lib/croatia/invoice/e_invoicable.rb +4 -0
- data/lib/croatia/invoice/fiscalizable.rb +30 -0
- data/lib/croatia/invoice/line_item.rb +103 -0
- data/lib/croatia/invoice/party.rb +20 -0
- data/lib/croatia/invoice/payable.rb +37 -0
- data/lib/croatia/invoice/tax.rb +27 -0
- data/lib/croatia/invoice.rb +117 -0
- data/lib/croatia/payment_barcode.rb +95 -0
- data/lib/croatia/pdf417.rb +48 -0
- data/lib/croatia/pin.rb +2 -0
- data/lib/croatia/qr_code.rb +34 -0
- data/lib/croatia/umcn.rb +191 -0
- data/lib/croatia/utils/enum.rb +57 -0
- data/lib/croatia/version.rb +1 -1
- data/lib/croatia.rb +35 -0
- metadata +63 -8
- data/.rubocop.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 950fcecdaee0439b153f3bccc88c0dcc1bd65707200d6f88b7c08729752d0384
|
4
|
+
data.tar.gz: 56d5d0073b5059d2a70767ff82be379c92183f16a37bfcbb318c576cccc3b797
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6388cb5613f183c1bbea376ab1eb2975b9e6f90c00c98c1e272a94a69ca19819b509aadc86d27fd63f1bf194b9597b5d391598015366294dbf6c1cbebdea306
|
7
|
+
data.tar.gz: 8f27f7baabdbb619e7b3048fc34f204b7279137437e0983de40492b5e5af2c1b23ba80c03e01b3c471a035756bfc7d6cb5106a147534e4a372220b46328a3ca0
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Croatia::Config
|
4
|
+
DEFAULT_TAX_RATES = {
|
5
|
+
value_added_tax: {
|
6
|
+
standard: 0.25,
|
7
|
+
lower_rate: 0.13,
|
8
|
+
exempt: 0.0,
|
9
|
+
zero_rated: 0.0,
|
10
|
+
outside_scope: 0.0,
|
11
|
+
reverse_charge: 0.0
|
12
|
+
},
|
13
|
+
consumption_tax: Hash.new(0.0),
|
14
|
+
other: Hash.new(0.0)
|
15
|
+
}
|
16
|
+
DEFAULT_FISCALIZATION = {}
|
17
|
+
|
18
|
+
attr_accessor :tax_rates, :fiscalization
|
19
|
+
|
20
|
+
def initialize(**options)
|
21
|
+
self.tax_rates = options.delete(:tax_rates) { deep_dup(DEFAULT_TAX_RATES) }
|
22
|
+
self.fiscalization = options.delete(:fiscalization) { deep_dup(DEFAULT_FISCALIZATION) }
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def deep_dup(object)
|
28
|
+
Marshal.load(Marshal.dump(object))
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rexml/document"
|
4
|
+
|
5
|
+
module Croatia::Fiscalizer::XMLBuilder
|
6
|
+
TNS = "http://www.apis-it.hr/fin/2012/types/f73"
|
7
|
+
XSI = "http://www.w3.org/2001/XMLSchema-instance"
|
8
|
+
SEQUENCE_MARK = {
|
9
|
+
register: "N", # N - sequential by register
|
10
|
+
business_location: "P" # P - sequential by business location
|
11
|
+
}.freeze
|
12
|
+
PAYMENT_METHODS = {
|
13
|
+
cash: "G", # G - gotovina
|
14
|
+
card: "K", # K - kartica
|
15
|
+
check: "C", # C - ček
|
16
|
+
transfer: "T", # T - prijenos
|
17
|
+
other: "O" # O - ostalo
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def invoice_request(invoice:, message_id:, specific_purpose: nil, subsequent_delivery: false, timezone: Croatia::Fiscalizer::TZ)
|
22
|
+
REXML::Document.new.tap do |doc|
|
23
|
+
envelope = doc.add_element("tns:RacunZahtjev", {
|
24
|
+
"xmlns:tns" => TNS,
|
25
|
+
"xmlns:xsi" => XSI
|
26
|
+
})
|
27
|
+
|
28
|
+
envelope.add_element("tns:Zaglavlje").tap do |header|
|
29
|
+
header.add_element("tns:IdPoruke").text = message_id
|
30
|
+
header.add_element("tns:DatumVrijemeSlanja").text = timezone.now.strftime("%d.%m.%YT%H:%M:%S")
|
31
|
+
end
|
32
|
+
|
33
|
+
envelope.add_element("tns:Racun").tap do |payload|
|
34
|
+
payload.add_element("tns:Oib").text = invoice.seller.pin
|
35
|
+
payload.add_element("tns:USustPdv").text = invoice.seller.pays_vat ? "true" : "false"
|
36
|
+
payload.add_element("tns:DatVrijeme").text = timezone.to_local(invoice.issue_date).strftime("%d.%m.%YT%H:%M:%S")
|
37
|
+
payload.add_element("tns:OznSlijed").text = SEQUENCE_MARK[invoice.sequential_by]
|
38
|
+
|
39
|
+
payload.add_element("tns:BrRac").tap do |invoice_number|
|
40
|
+
invoice_number.add_element("tns:BrOznRac").text = invoice.sequential_number.to_s
|
41
|
+
invoice_number.add_element("tns:OznPosPr").text = invoice.business_location_identifier.to_s
|
42
|
+
invoice_number.add_element("tns:OznNapUr").text = invoice.register_identifier.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
# TODO: Add taxes
|
46
|
+
|
47
|
+
payload.add_element("tns:IznosUkupno").text = invoice.total.to_f.to_s
|
48
|
+
payload.add_element("tns:NacinPlac").text = PAYMENT_METHODS[invoice.payment_method]
|
49
|
+
payload.add_element("tns:OibOper").text = invoice.issuer.pin
|
50
|
+
payload.add_element("tns:ZastKod").text = invoice.issuer_protection_code
|
51
|
+
payload.add_element("tns:NakDost").text = subsequent_delivery ? "true" : "false"
|
52
|
+
payload.add_element("tns:ParagonBrRac").text = invoice.number
|
53
|
+
payload.add_element("tns:SpecNamj").text = specific_purpose if specific_purpose
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/md5"
|
4
|
+
require "openssl"
|
5
|
+
require "securerandom"
|
6
|
+
require "tzinfo"
|
7
|
+
require "uri"
|
8
|
+
|
9
|
+
class Croatia::Fiscalizer
|
10
|
+
autoload :XMLBuilder, "croatia/fiscalizer/xml_builder"
|
11
|
+
|
12
|
+
TZ = TZInfo::Timezone.get("Europe/Zagreb")
|
13
|
+
QR_CODE_BASE_URL = "https://porezna.gov.hr/rn"
|
14
|
+
|
15
|
+
attr_reader :certificate
|
16
|
+
|
17
|
+
def initialize(certificate: nil, password: nil)
|
18
|
+
certificate ||= Croatia.config.fiscalization[:certificate]
|
19
|
+
password ||= Croatia.config.fiscalization[:password]
|
20
|
+
|
21
|
+
@certificate = load_certificate(certificate, password)
|
22
|
+
end
|
23
|
+
|
24
|
+
def fiscalize(invoice:, message_id: SecureRandom.uuid)
|
25
|
+
_document = XMLBuilder.invoice_request(invoice: invoice, message_id: message_id, timezone: TZ)
|
26
|
+
raise NotImplementedError, "Fiscalization XML generation is not implemented yet"
|
27
|
+
end
|
28
|
+
|
29
|
+
def generate_issuer_protection_code(invoice)
|
30
|
+
buffer = []
|
31
|
+
buffer << invoice.issuer.pin
|
32
|
+
buffer << TZ.to_local(invoice.issue_date).strftime("%d.%m.%Y %H:%M:%S")
|
33
|
+
buffer << invoice.sequential_number
|
34
|
+
buffer << invoice.business_location_identifier
|
35
|
+
buffer << invoice.register_identifier
|
36
|
+
buffer << invoice.total.to_f
|
37
|
+
|
38
|
+
digest = OpenSSL::Digest::SHA1.new
|
39
|
+
signature = certificate.sign(digest, buffer.join)
|
40
|
+
|
41
|
+
Digest::MD5.hexdigest(signature).downcase
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_verification_qr_code(invoice)
|
45
|
+
Croatia::QRCode.ensure_supported!
|
46
|
+
|
47
|
+
params = {
|
48
|
+
datv: TZ.to_local(invoice.issue_date).strftime("%Y%m%d_%H%M"),
|
49
|
+
izn: invoice.total_cents.to_s
|
50
|
+
}
|
51
|
+
|
52
|
+
if params[:izn].length > 10
|
53
|
+
raise ArgumentError, "Total amount exceeds 10 digits: #{params[:izn]}"
|
54
|
+
end
|
55
|
+
|
56
|
+
if invoice.unique_invoice_identifier
|
57
|
+
params[:jir] = invoice.unique_invoice_identifier
|
58
|
+
else
|
59
|
+
params[:zki] = generate_issuer_protection_code(invoice)
|
60
|
+
end
|
61
|
+
|
62
|
+
if params[:jir].nil? && params[:zki].nil?
|
63
|
+
raise ArgumentError, "Either unique_invoice_identifier or issuer_protection_code must be provided"
|
64
|
+
end
|
65
|
+
|
66
|
+
query_string = URI.encode_www_form(params)
|
67
|
+
url = "#{QR_CODE_BASE_URL}?#{query_string}"
|
68
|
+
|
69
|
+
Croatia::QRCode.new(url)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def load_certificate(cert, password)
|
75
|
+
if cert.is_a?(OpenSSL::PKCS12)
|
76
|
+
cert.key
|
77
|
+
elsif cert.is_a?(OpenSSL::PKey::PKey)
|
78
|
+
cert
|
79
|
+
else
|
80
|
+
begin
|
81
|
+
cert = File.read(cert) if cert.respond_to?(:to_s) && File.exist?(cert.to_s)
|
82
|
+
rescue ArgumentError
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
OpenSSL::PKey.read(cert)
|
87
|
+
rescue OpenSSL::PKey::PKeyError
|
88
|
+
OpenSSL::PKCS12.new(cert, password).key
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croatia::Invoice::Fiscalizable
|
4
|
+
def self.included(base)
|
5
|
+
base.include InstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def fiscalize!(**options)
|
10
|
+
Croatia::Fiscalizer.new(**options).fiscalize(invoice: self)
|
11
|
+
end
|
12
|
+
|
13
|
+
def reverse!(**options)
|
14
|
+
line_items.each(&:reverse)
|
15
|
+
Croatia::Fiscalizer.new(**options).fiscalize(invoice: self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def issuer_protection_code(**options)
|
19
|
+
Croatia::Fiscalizer
|
20
|
+
.new(**options)
|
21
|
+
.generate_issuer_protection_code(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
def fiscalization_qr_code(**options)
|
25
|
+
Croatia::Fiscalizer
|
26
|
+
.new(**options)
|
27
|
+
.generate_verification_qr_code(self)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "bigdecimal/util"
|
5
|
+
|
6
|
+
class Croatia::Invoice::LineItem
|
7
|
+
include Croatia::Enum
|
8
|
+
|
9
|
+
attr_accessor :description, :unit, :taxes
|
10
|
+
attr_reader :quantity, :unit_price, :discount_rate
|
11
|
+
|
12
|
+
def initialize(**options)
|
13
|
+
self.description = options[:description]
|
14
|
+
self.quantity = options.fetch(:quantity, 1)
|
15
|
+
self.unit = options[:unit]
|
16
|
+
self.unit_price = options.fetch(:unit_price, 0.0)
|
17
|
+
self.taxes = options.fetch(:taxes, {})
|
18
|
+
end
|
19
|
+
|
20
|
+
def quantity=(value)
|
21
|
+
unless value.is_a?(Numeric)
|
22
|
+
raise ArgumentError, "Quantity must be a number"
|
23
|
+
end
|
24
|
+
|
25
|
+
@quantity = value.to_d
|
26
|
+
end
|
27
|
+
|
28
|
+
def unit_price=(value)
|
29
|
+
unless value.is_a?(Numeric) && value >= 0
|
30
|
+
raise ArgumentError, "Unit price must be a non-negative number"
|
31
|
+
end
|
32
|
+
|
33
|
+
@unit_price = value.to_d
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
def discount_rate=(value)
|
38
|
+
if value.nil?
|
39
|
+
@discount_rate = nil
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
unless value.is_a?(Numeric) && value >= 0 && value <= 1
|
44
|
+
raise ArgumentError, "Discount rate must be a non-negative number between 0 and 1"
|
45
|
+
end
|
46
|
+
|
47
|
+
@discount_rate = value.to_d
|
48
|
+
end
|
49
|
+
|
50
|
+
def discount=(value)
|
51
|
+
if value.nil?
|
52
|
+
@discount = nil
|
53
|
+
return
|
54
|
+
end
|
55
|
+
|
56
|
+
unless value.is_a?(Numeric) && value >= 0
|
57
|
+
raise ArgumentError, "Discount must be a non-negative number"
|
58
|
+
end
|
59
|
+
|
60
|
+
@discount = value.to_d.round(2, BigDecimal::ROUND_HALF_UP)
|
61
|
+
end
|
62
|
+
|
63
|
+
def discount
|
64
|
+
if @discount
|
65
|
+
@discount
|
66
|
+
elsif @discount_rate
|
67
|
+
(gross * @discount_rate).round(2, BigDecimal::ROUND_HALF_UP)
|
68
|
+
else
|
69
|
+
BigDecimal("0.0")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def reverse
|
74
|
+
self.quantity *= -1
|
75
|
+
end
|
76
|
+
|
77
|
+
def gross
|
78
|
+
(quantity * unit_price).round(2, BigDecimal::ROUND_HALF_UP)
|
79
|
+
end
|
80
|
+
|
81
|
+
def subtotal
|
82
|
+
gross - discount
|
83
|
+
end
|
84
|
+
|
85
|
+
def tax
|
86
|
+
taxes.values.sum { |tax_obj| (subtotal * tax_obj.rate).round(2, BigDecimal::ROUND_HALF_UP) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def total
|
90
|
+
(subtotal + tax).round(2, BigDecimal::ROUND_HALF_UP)
|
91
|
+
end
|
92
|
+
|
93
|
+
def add_tax(tax = nil, **options, &block)
|
94
|
+
if tax.nil?
|
95
|
+
tax = Croatia::Invoice::Tax.new(**options)
|
96
|
+
end
|
97
|
+
|
98
|
+
tax.tap(&block) if block_given?
|
99
|
+
|
100
|
+
taxes[tax.type] = tax
|
101
|
+
tax
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Croatia::Invoice::Party
|
4
|
+
attr_accessor \
|
5
|
+
:address,
|
6
|
+
:city,
|
7
|
+
:country_code,
|
8
|
+
:einvoice_id,
|
9
|
+
:iban,
|
10
|
+
:name,
|
11
|
+
:pays_vat,
|
12
|
+
:pin,
|
13
|
+
:postal_code
|
14
|
+
|
15
|
+
def initialize(**options)
|
16
|
+
options.each do |key, value|
|
17
|
+
public_send("#{key}=", value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croatia::Invoice::Payable
|
4
|
+
def self.included(base)
|
5
|
+
base.include InstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module InstanceMethods
|
9
|
+
def payment_barcode(description: nil, model: nil, reference_number: nil, payment_purpose_code: nil)
|
10
|
+
if buyer.nil? || seller.nil?
|
11
|
+
raise ArgumentError, "Both buyer and seller must be set before generating a payment barcode"
|
12
|
+
end
|
13
|
+
|
14
|
+
description ||= "Račun #{number}"
|
15
|
+
|
16
|
+
options = {
|
17
|
+
currency: currency,
|
18
|
+
total_cents: total_cents,
|
19
|
+
buyer_name: buyer.name,
|
20
|
+
buyer_address: buyer.address,
|
21
|
+
buyer_postal_code: buyer.postal_code,
|
22
|
+
buyer_city: buyer.city,
|
23
|
+
seller_name: seller.name,
|
24
|
+
seller_address: seller.address,
|
25
|
+
seller_postal_code: seller.postal_code,
|
26
|
+
seller_city: seller.city,
|
27
|
+
seller_iban: seller.iban,
|
28
|
+
model: model,
|
29
|
+
reference_number: reference_number,
|
30
|
+
payment_purpose_code: payment_purpose_code,
|
31
|
+
description: description
|
32
|
+
}
|
33
|
+
|
34
|
+
Croatia::PaymentBarcode.new(**options)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "bigdecimal/util"
|
5
|
+
|
6
|
+
class Croatia::Invoice::Tax
|
7
|
+
include Croatia::Enum
|
8
|
+
|
9
|
+
attr_reader :rate
|
10
|
+
|
11
|
+
enum :type, %i[ value_added_tax consumption_tax other ]
|
12
|
+
enum :category, %i[ standard lower_rate exempt zero_rated outside_scope reverse_charge ]
|
13
|
+
|
14
|
+
def initialize(rate: nil, type: :value_added_tax, category: :standard)
|
15
|
+
self.type = type
|
16
|
+
self.category = category
|
17
|
+
self.rate = rate ? rate : Croatia.config.tax_rates[type][category]
|
18
|
+
end
|
19
|
+
|
20
|
+
def rate=(value)
|
21
|
+
if !value.is_a?(Numeric) || value < 0 || value > 1
|
22
|
+
raise ArgumentError, "Tax rate must be a number between 0 and 1"
|
23
|
+
end
|
24
|
+
|
25
|
+
@rate = value.to_d
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bigdecimal"
|
4
|
+
require "bigdecimal/util"
|
5
|
+
|
6
|
+
class Croatia::Invoice
|
7
|
+
autoload :Fiscalizable, "croatia/invoice/fiscalizable"
|
8
|
+
autoload :EInvoicable, "croatia/invoice/e_invoicable"
|
9
|
+
autoload :Payable, "croatia/invoice/payable"
|
10
|
+
autoload :Party, "croatia/invoice/party"
|
11
|
+
autoload :Tax, "croatia/invoice/tax"
|
12
|
+
autoload :LineItem, "croatia/invoice/line_item"
|
13
|
+
|
14
|
+
include Croatia::Enum
|
15
|
+
include Fiscalizable
|
16
|
+
include EInvoicable
|
17
|
+
include Payable
|
18
|
+
|
19
|
+
attr_reader :issue_date, :due_date
|
20
|
+
attr_writer :issuer, :seller, :buyer
|
21
|
+
attr_accessor \
|
22
|
+
:business_location_identifier, # oznaka poslovnog prostora
|
23
|
+
:currency,
|
24
|
+
:line_items,
|
25
|
+
:register_identifier, # oznaka naplatnog uredaja
|
26
|
+
:sequential_number, # redni broj racuna
|
27
|
+
:unique_invoice_identifier # jir
|
28
|
+
|
29
|
+
enum :payment_method, %i[ cash card check transfer other ].freeze, allow_nil: true, prefix: :payment_method
|
30
|
+
enum :sequential_by, %i[ register business_location ].freeze, allow_nil: true, prefix: :sequential_by
|
31
|
+
|
32
|
+
def initialize(**options)
|
33
|
+
self.line_items = options.delete(:line_items) { [] }
|
34
|
+
self.payment_method = :card
|
35
|
+
self.sequential_by = :register
|
36
|
+
|
37
|
+
options.each do |key, value|
|
38
|
+
public_send("#{key}=", value)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def number
|
43
|
+
[ sequential_number, business_location_identifier, register_identifier ].join("/")
|
44
|
+
end
|
45
|
+
|
46
|
+
def subtotal
|
47
|
+
line_items.sum(&:subtotal).to_d
|
48
|
+
end
|
49
|
+
|
50
|
+
def tax
|
51
|
+
line_items.sum(&:tax).to_d
|
52
|
+
end
|
53
|
+
|
54
|
+
def total
|
55
|
+
line_items.sum(&:total).to_d
|
56
|
+
end
|
57
|
+
|
58
|
+
def total_cents
|
59
|
+
(total * 100).to_i
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_line_item(line_item = nil, &block)
|
63
|
+
if line_item.nil? && block.nil?
|
64
|
+
raise ArgumentError, "You must provide a line item or a block"
|
65
|
+
end
|
66
|
+
|
67
|
+
line_item ||= LineItem.new.tap(&block)
|
68
|
+
|
69
|
+
self.line_items ||= []
|
70
|
+
line_items << line_item
|
71
|
+
|
72
|
+
line_item
|
73
|
+
end
|
74
|
+
|
75
|
+
def issuer(&block)
|
76
|
+
if block_given?
|
77
|
+
self.issuer = Party.new.tap(&block)
|
78
|
+
else
|
79
|
+
@issuer
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def buyer(&block)
|
84
|
+
if block_given?
|
85
|
+
self.buyer = Party.new.tap(&block)
|
86
|
+
else
|
87
|
+
@buyer
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def seller(&block)
|
92
|
+
if block_given?
|
93
|
+
self.seller = Party.new.tap(&block)
|
94
|
+
else
|
95
|
+
@seller
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def issue_date=(value)
|
100
|
+
@issue_date = value.nil? ? nil : parse_date(value)
|
101
|
+
end
|
102
|
+
|
103
|
+
def due_date=(value)
|
104
|
+
@due_date = value.nil? ? nil : parse_date(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def parse_date(value)
|
110
|
+
case value
|
111
|
+
when Date, DateTime
|
112
|
+
value
|
113
|
+
else
|
114
|
+
DateTime.parse(value.to_s)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Croatia::PaymentBarcode
|
4
|
+
HEADER = "HRVHUB30"
|
5
|
+
EXACT_LENGTH_FIELDS = %i[ header currency total_cents model payment_purpose_code ].freeze
|
6
|
+
FIELD_MAX_LENGTHS = {
|
7
|
+
header: 8,
|
8
|
+
currency: 3,
|
9
|
+
total_cents: 15,
|
10
|
+
buyer_name: 30,
|
11
|
+
buyer_address: 27,
|
12
|
+
buyer_postal_code_and_city: 27,
|
13
|
+
seller_name: 25,
|
14
|
+
seller_address: 25,
|
15
|
+
seller_postal_code_and_city: 27,
|
16
|
+
seller_iban: 21,
|
17
|
+
model: 4,
|
18
|
+
reference_number: 22,
|
19
|
+
payment_purpose_code: 4,
|
20
|
+
description: 35
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
attr_accessor \
|
24
|
+
:buyer_address,
|
25
|
+
:buyer_city,
|
26
|
+
:buyer_name,
|
27
|
+
:buyer_postal_code,
|
28
|
+
:currency,
|
29
|
+
:description,
|
30
|
+
:model,
|
31
|
+
:payment_purpose_code,
|
32
|
+
:reference_number,
|
33
|
+
:seller_address,
|
34
|
+
:seller_city,
|
35
|
+
:seller_iban,
|
36
|
+
:seller_name,
|
37
|
+
:seller_postal_code,
|
38
|
+
:total_cents
|
39
|
+
|
40
|
+
def initialize(**options)
|
41
|
+
options.each do |key, value|
|
42
|
+
public_send("#{key}=", value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def data
|
47
|
+
data = {
|
48
|
+
header: HEADER,
|
49
|
+
currency: currency,
|
50
|
+
total_cents: total_cents.to_s.rjust(FIELD_MAX_LENGTHS[:total_cents], "0"),
|
51
|
+
buyer_name: buyer_name,
|
52
|
+
buyer_address: buyer_address,
|
53
|
+
buyer_postal_code_and_city: "#{buyer_postal_code} #{buyer_city}".strip,
|
54
|
+
seller_name: seller_name,
|
55
|
+
seller_address: seller_address,
|
56
|
+
seller_postal_code_and_city: "#{seller_postal_code} #{seller_city}".strip,
|
57
|
+
seller_iban: seller_iban,
|
58
|
+
model: model,
|
59
|
+
reference_number: reference_number,
|
60
|
+
payment_purpose_code: payment_purpose_code,
|
61
|
+
description: description
|
62
|
+
}
|
63
|
+
|
64
|
+
data.each do |key, value|
|
65
|
+
next if value.nil?
|
66
|
+
|
67
|
+
max_length = FIELD_MAX_LENGTHS[key]
|
68
|
+
|
69
|
+
if EXACT_LENGTH_FIELDS.include?(key) && value.length != max_length
|
70
|
+
raise ArgumentError, "Value '#{value}' of field '#{key}' must be exactly #{max_length} characters long"
|
71
|
+
elsif value.length > max_length
|
72
|
+
raise ArgumentError, "Value '#{value}' of field '#{key}' exceeds maximum length of #{max_length} characters"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
if data[:seller_iban] && (!data[:seller_iban].match?(/\A[a-z]{2}\d{19}\Z/i) && !data[:seller_iban].match?(/\A\d{7}-\d{10}\Z/i))
|
77
|
+
raise ArgumentError, "Invalid IBAN format '#{data[:seller_iban]}' expected IBAN or account number"
|
78
|
+
end
|
79
|
+
|
80
|
+
data.values.join("\n")
|
81
|
+
end
|
82
|
+
|
83
|
+
def barcode
|
84
|
+
Croatia::PDF417.ensure_supported!
|
85
|
+
Croatia::PDF417.new(data)
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_png(...)
|
89
|
+
barcode.to_png(...)
|
90
|
+
end
|
91
|
+
|
92
|
+
def to_svg(...)
|
93
|
+
barcode.to_svg(...)
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Croatia::PDF417
|
4
|
+
def self.ensure_supported!
|
5
|
+
return if supported?
|
6
|
+
|
7
|
+
raise LoadError, "Zint library is not loaded. Please ensure you have the ruby-zint gem installed and required."
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.supported?
|
11
|
+
defined?(Zint)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :data, :options
|
15
|
+
|
16
|
+
def initialize(data, **options)
|
17
|
+
@data = data
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_svg(**options)
|
22
|
+
vec = barcode.to_vector
|
23
|
+
|
24
|
+
foreground_color = options[:foreground_color] || "black"
|
25
|
+
background_color = options[:background_color] || "white"
|
26
|
+
|
27
|
+
svg = []
|
28
|
+
svg << %Q(<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="#{vec.width.to_i}" height="#{vec.height.to_i}" viewBox="0 0 #{vec.width.to_i} #{vec.height.to_i}">)
|
29
|
+
svg << %Q(<rect width="#{vec.width.to_i}" height="#{vec.height.to_i}" fill="#{background_color}" />)
|
30
|
+
|
31
|
+
vec.each_rectangle do |rect|
|
32
|
+
svg << %Q(<rect x="#{rect.x.to_i}" y="#{rect.y.to_i}" width="#{rect.width.to_i}" height="#{rect.height.to_i}" fill="#{foreground_color}" />)
|
33
|
+
end
|
34
|
+
|
35
|
+
svg << "</svg>"
|
36
|
+
svg.join("\n")
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_png(**options)
|
40
|
+
barcode.to_memory_file(extension: ".png")
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def barcode
|
46
|
+
@barcode ||= Zint::Barcode.new(**options, value: data, symbology: Zint::BARCODE_PDF417)
|
47
|
+
end
|
48
|
+
end
|
data/lib/croatia/pin.rb
CHANGED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Croatia::QRCode
|
4
|
+
def self.ensure_supported!
|
5
|
+
return if supported?
|
6
|
+
|
7
|
+
raise LoadError, "RQRCode library is not loaded. Please ensure you have the rqrcode gem installed and required."
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.supported?
|
11
|
+
defined?(RQRCode)
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :data, :options
|
15
|
+
|
16
|
+
def initialize(data, **options)
|
17
|
+
@data = data
|
18
|
+
@options = options
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_svg(**options)
|
22
|
+
qr_code.as_svg(**options)
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_png(**options)
|
26
|
+
qr_code.as_png(**options)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def qr_code
|
32
|
+
@qr_code ||= RQRCode::QRCode.new(data, **options)
|
33
|
+
end
|
34
|
+
end
|
data/lib/croatia/umcn.rb
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# JMBG - Jedinstveni Matični Broj Građana
|
4
|
+
# UMCN - Unique Master Citizen Number
|
5
|
+
class Croatia::UMCN
|
6
|
+
WEIGHTS = [ 7, 6, 5, 4, 3, 2, 7, 6, 5, 4, 3, 2 ]
|
7
|
+
REGION_CODES = {
|
8
|
+
# Special / Foreign
|
9
|
+
0 => "Yugoslavia",
|
10
|
+
1 => "Foreigner in BiH",
|
11
|
+
2 => "Foreigner in Montenegro",
|
12
|
+
3 => "Foreigner in Croatia",
|
13
|
+
4 => "Foreigner in North Macedonia",
|
14
|
+
5 => "Foreigner in Slovenia",
|
15
|
+
6 => "Foreigner in Serbia",
|
16
|
+
7 => "Foreigner in Vojvodina",
|
17
|
+
8 => "Foreigner in Kosovo",
|
18
|
+
9 => "Yugoslavia",
|
19
|
+
|
20
|
+
# Bosnia and Herzegovina (10–19)
|
21
|
+
10 => "Banja Luka",
|
22
|
+
11 => "Bihac",
|
23
|
+
12 => "Doboj",
|
24
|
+
13 => "Gorazde",
|
25
|
+
14 => "Livno",
|
26
|
+
15 => "Mostar",
|
27
|
+
16 => "Prijedor",
|
28
|
+
17 => "Sarajevo",
|
29
|
+
18 => "Tuzla",
|
30
|
+
19 => "Zenica",
|
31
|
+
|
32
|
+
# Montenegro (21–29)
|
33
|
+
21 => "Podgorica",
|
34
|
+
22 => "Bar",
|
35
|
+
23 => "Budva",
|
36
|
+
24 => "Herceg Novi",
|
37
|
+
25 => "Cetinje",
|
38
|
+
26 => "Niksic",
|
39
|
+
27 => "Berane",
|
40
|
+
28 => "Bijelo Polje",
|
41
|
+
29 => "Pljevlja",
|
42
|
+
|
43
|
+
# Croatia (30–39)
|
44
|
+
30 => "Slavonia",
|
45
|
+
31 => "Podravina",
|
46
|
+
32 => "Medimurje",
|
47
|
+
33 => "Zagreb",
|
48
|
+
34 => "Kordun",
|
49
|
+
35 => "Lika",
|
50
|
+
36 => "Istria",
|
51
|
+
37 => "Banovina",
|
52
|
+
38 => "Dalmatia",
|
53
|
+
39 => "Zagorje",
|
54
|
+
|
55
|
+
# North Macedonia (41–49)
|
56
|
+
41 => "Bitola",
|
57
|
+
42 => "Kumanovo",
|
58
|
+
43 => "Ohrid",
|
59
|
+
44 => "Prilep",
|
60
|
+
45 => "Skopje",
|
61
|
+
46 => "Strumica",
|
62
|
+
47 => "Tetovo",
|
63
|
+
48 => "Veles",
|
64
|
+
49 => "Stip",
|
65
|
+
|
66
|
+
# Slovenia (50)
|
67
|
+
50 => "Slovenia",
|
68
|
+
|
69
|
+
# Serbia (70–79)
|
70
|
+
70 => "Serbia Abroad",
|
71
|
+
71 => "Belgrade",
|
72
|
+
72 => "Sumadija",
|
73
|
+
73 => "Nis",
|
74
|
+
74 => "Morava",
|
75
|
+
75 => "Zajecar",
|
76
|
+
76 => "Podunavlje",
|
77
|
+
77 => "Kolubara",
|
78
|
+
78 => "Kraljevo",
|
79
|
+
79 => "Uzice",
|
80
|
+
|
81
|
+
# Vojvodina (80–89)
|
82
|
+
80 => "Novi Sad",
|
83
|
+
81 => "Sombor",
|
84
|
+
82 => "Subotica",
|
85
|
+
83 => "Zrenjanin",
|
86
|
+
84 => "Kikinda",
|
87
|
+
85 => "Pancevo",
|
88
|
+
86 => "Vrbas",
|
89
|
+
87 => "Sremska Mitrovica",
|
90
|
+
88 => "Ruma",
|
91
|
+
89 => "Backa Topola",
|
92
|
+
|
93
|
+
# Kosovo (90–99)
|
94
|
+
90 => "Pristina",
|
95
|
+
91 => "Prizren",
|
96
|
+
92 => "Pec",
|
97
|
+
93 => "Djakovica",
|
98
|
+
94 => "Mitrovica",
|
99
|
+
95 => "Gnjilane",
|
100
|
+
96 => "Ferizaj",
|
101
|
+
97 => "Decan",
|
102
|
+
98 => "Klina",
|
103
|
+
99 => "Malisevo"
|
104
|
+
}.freeze
|
105
|
+
|
106
|
+
|
107
|
+
attr_accessor :birthday, :region_code, :sequence_number, :checkusm
|
108
|
+
|
109
|
+
def self.valid?(umcn)
|
110
|
+
return false if umcn.nil?
|
111
|
+
return false unless umcn.match?(/\A\d{13}\Z/)
|
112
|
+
|
113
|
+
parse(umcn).checksum == umcn.strip[-1].to_i
|
114
|
+
rescue Date::Error, ArgumentError
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.parse(umcn)
|
119
|
+
digits = umcn.chars.map(&:to_i)
|
120
|
+
|
121
|
+
day = digits[0..1].join.to_i
|
122
|
+
month = digits[2..3].join.to_i
|
123
|
+
year = digits[4..6].join.to_i
|
124
|
+
millenium = case digits[4]
|
125
|
+
when 0 then 2000
|
126
|
+
else 1000
|
127
|
+
end
|
128
|
+
full_year = millenium + year
|
129
|
+
|
130
|
+
birthday = Date.new(full_year, month, day)
|
131
|
+
region_code = digits[7..8].join.to_i
|
132
|
+
sequence_number = digits[9..11].join.to_i
|
133
|
+
|
134
|
+
new(birthday: birthday, region_code: region_code, sequence_number: sequence_number)
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize(birthday:, region_code:, sequence_number:)
|
138
|
+
@birthday = birthday
|
139
|
+
@region_code = region_code
|
140
|
+
@sequence_number = sequence_number
|
141
|
+
end
|
142
|
+
|
143
|
+
def region_code=(value)
|
144
|
+
value = value.to_i
|
145
|
+
|
146
|
+
if REGION_CODES.key?(value)
|
147
|
+
@region_code = value
|
148
|
+
else
|
149
|
+
raise ArgumentError, "Invalid region code: #{value}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def sequence_number=(value)
|
154
|
+
value = value.to_i
|
155
|
+
|
156
|
+
if value < 0 || value > 999
|
157
|
+
raise ArgumentError, "Sequence number must be between 0 and 999"
|
158
|
+
end
|
159
|
+
|
160
|
+
@sequence_number = value
|
161
|
+
end
|
162
|
+
|
163
|
+
def sex
|
164
|
+
sequence_number <= 499 ? :male : :female
|
165
|
+
end
|
166
|
+
|
167
|
+
def region_of_birth
|
168
|
+
REGION_CODES[region_code]
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_s
|
172
|
+
parts = []
|
173
|
+
parts << birthday.strftime("%d%m")
|
174
|
+
parts << format("%03d", birthday.year % 1000)
|
175
|
+
parts << format("%02d", region_code)
|
176
|
+
parts << format("%03d", sequence_number)
|
177
|
+
|
178
|
+
digits = parts.join.chars.map(&:to_i)
|
179
|
+
sum = digits.each_with_index.sum { |digit, i| digit * WEIGHTS[i] }
|
180
|
+
mod = sum % 11
|
181
|
+
|
182
|
+
checksum = (mod == 0 || mod == 1) ? 0 : (11 - mod)
|
183
|
+
|
184
|
+
parts << checksum.to_s
|
185
|
+
parts.join
|
186
|
+
end
|
187
|
+
|
188
|
+
def checksum
|
189
|
+
to_s[-1].to_i
|
190
|
+
end
|
191
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Croatia::Enum
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def enum(name, values, **options)
|
10
|
+
name = name.to_s.to_sym unless name.is_a?(Symbol)
|
11
|
+
values = values.zip(values) if values.is_a?(Array)
|
12
|
+
|
13
|
+
unless values.respond_to?(:to_h)
|
14
|
+
raise ArgumentError, "Enum values must be defined as a Hash or as an Array"
|
15
|
+
end
|
16
|
+
|
17
|
+
values = values.to_h.freeze
|
18
|
+
values_method_name = "#{name}_values".to_sym
|
19
|
+
|
20
|
+
define_singleton_method(values_method_name) do
|
21
|
+
values
|
22
|
+
end
|
23
|
+
|
24
|
+
define_method("#{name}=") do |value|
|
25
|
+
values = self.class.public_send(values_method_name)
|
26
|
+
|
27
|
+
enum_value = values[value]
|
28
|
+
enum_value = value if enum_value.nil? && values.has_value?(value)
|
29
|
+
|
30
|
+
if enum_value.nil? && !options[:allow_nil]
|
31
|
+
raise ArgumentError, "Invalid value for enum #{name}: #{value.inspect}"
|
32
|
+
end
|
33
|
+
|
34
|
+
instance_variable_set("@#{name}", enum_value)
|
35
|
+
end
|
36
|
+
|
37
|
+
define_method(name) do
|
38
|
+
instance_variable_get("@#{name}")
|
39
|
+
end
|
40
|
+
|
41
|
+
prefix = options[:prefix] ? "#{options[:prefix]}_" : ""
|
42
|
+
suffix = options[:suffix] ? "_#{options[:suffix]}" : ""
|
43
|
+
|
44
|
+
values.each do |key, value|
|
45
|
+
value_method_name = "#{prefix}#{key}#{suffix}"
|
46
|
+
|
47
|
+
define_method("#{value_method_name}?") do
|
48
|
+
instance_variable_get("@#{name}") == value
|
49
|
+
end
|
50
|
+
|
51
|
+
define_method("#{value_method_name}!") do
|
52
|
+
public_send("#{name}=", value)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/croatia/version.rb
CHANGED
data/lib/croatia.rb
CHANGED
@@ -4,4 +4,39 @@ require_relative "croatia/version"
|
|
4
4
|
|
5
5
|
module Croatia
|
6
6
|
class Error < StandardError; end
|
7
|
+
|
8
|
+
autoload :Config, "croatia/config"
|
9
|
+
autoload :Enum, "croatia/utils/enum"
|
10
|
+
autoload :QRCode, "croatia/qr_code"
|
11
|
+
autoload :PDF417, "croatia/pdf417"
|
12
|
+
autoload :PIN, "croatia/pin"
|
13
|
+
autoload :UMCN, "croatia/umcn"
|
14
|
+
autoload :PaymentBarcode, "croatia/payment_barcode"
|
15
|
+
autoload :Fiscalizer, "croatia/fiscalizer"
|
16
|
+
autoload :Invoice, "croatia/invoice"
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def with_config(config = nil, &block)
|
20
|
+
Thread.current[:tmp_croatia_config] = config
|
21
|
+
block.call
|
22
|
+
ensure
|
23
|
+
Thread.current[:tmp_croatia_config] = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def config
|
27
|
+
return Thread.current[:tmp_croatia_config] if Thread.current[:tmp_croatia_config]
|
28
|
+
|
29
|
+
@config ||= Croatia::Config.new
|
30
|
+
end
|
31
|
+
|
32
|
+
def configure(config = nil, &block)
|
33
|
+
if config.is_a?(Croatia::Config)
|
34
|
+
@config = config
|
35
|
+
elsif block
|
36
|
+
self.config.tap(&block)
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Either a Croatia::Config instance or a block is required"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
7
42
|
end
|
metadata
CHANGED
@@ -1,29 +1,84 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: croatia
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stanko K.R.
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
11
|
-
dependencies:
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
date: 2025-06-04 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: openssl
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rexml
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: tzinfo
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
description: |
|
55
|
+
Croatia is a gem that contains various utilities for performing Croatia-specific actions like:
|
56
|
+
- PIN/OIB validation
|
16
57
|
email:
|
17
58
|
- hey@stanko.io
|
18
59
|
executables: []
|
19
60
|
extensions: []
|
20
61
|
extra_rdoc_files: []
|
21
62
|
files:
|
22
|
-
- ".rubocop.yml"
|
23
63
|
- LICENSE.txt
|
24
64
|
- Rakefile
|
25
65
|
- lib/croatia.rb
|
66
|
+
- lib/croatia/config.rb
|
67
|
+
- lib/croatia/fiscalizer.rb
|
68
|
+
- lib/croatia/fiscalizer/xml_builder.rb
|
69
|
+
- lib/croatia/invoice.rb
|
70
|
+
- lib/croatia/invoice/e_invoicable.rb
|
71
|
+
- lib/croatia/invoice/fiscalizable.rb
|
72
|
+
- lib/croatia/invoice/line_item.rb
|
73
|
+
- lib/croatia/invoice/party.rb
|
74
|
+
- lib/croatia/invoice/payable.rb
|
75
|
+
- lib/croatia/invoice/tax.rb
|
76
|
+
- lib/croatia/payment_barcode.rb
|
77
|
+
- lib/croatia/pdf417.rb
|
26
78
|
- lib/croatia/pin.rb
|
79
|
+
- lib/croatia/qr_code.rb
|
80
|
+
- lib/croatia/umcn.rb
|
81
|
+
- lib/croatia/utils/enum.rb
|
27
82
|
- lib/croatia/version.rb
|
28
83
|
- sig/croatia.rbs
|
29
84
|
homepage: https://github.com/monorkin/croatia
|