ticketbai 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +32 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +107 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/ticketbai/api/client.rb +63 -0
- data/lib/ticketbai/api/registry.rb +34 -0
- data/lib/ticketbai/api/request.rb +125 -0
- data/lib/ticketbai/api/response_parser.rb +36 -0
- data/lib/ticketbai/checksum_calculator.rb +15 -0
- data/lib/ticketbai/document.rb +21 -0
- data/lib/ticketbai/document_validator.rb +42 -0
- data/lib/ticketbai/documents/annulment.rb +45 -0
- data/lib/ticketbai/documents/api_payload.rb +47 -0
- data/lib/ticketbai/documents/issuance.rb +55 -0
- data/lib/ticketbai/documents/issuance_unsigned.rb +58 -0
- data/lib/ticketbai/errors.rb +27 -0
- data/lib/ticketbai/nodes/breakdown_type.rb +72 -0
- data/lib/ticketbai/nodes/invoice_chaining.rb +22 -0
- data/lib/ticketbai/nodes/invoice_data.rb +24 -0
- data/lib/ticketbai/nodes/invoice_header.rb +26 -0
- data/lib/ticketbai/nodes/issuer.rb +18 -0
- data/lib/ticketbai/nodes/lroe_header.rb +29 -0
- data/lib/ticketbai/nodes/lroe_issued_invoices.rb +28 -0
- data/lib/ticketbai/nodes/receiver.rb +36 -0
- data/lib/ticketbai/nodes/software.rb +17 -0
- data/lib/ticketbai/operation.rb +59 -0
- data/lib/ticketbai/operations/annulment.rb +41 -0
- data/lib/ticketbai/operations/issuance.rb +93 -0
- data/lib/ticketbai/operations/issuance_unsigned.rb +89 -0
- data/lib/ticketbai/signer.rb +273 -0
- data/lib/ticketbai/tbai_identifier.rb +33 -0
- data/lib/ticketbai/tbai_qr.rb +50 -0
- data/lib/ticketbai/version.rb +5 -0
- data/lib/ticketbai/xmldsig-core-schema.xsd +318 -0
- data/lib/ticketbai/xsd_validators/annulment.xsd +392 -0
- data/lib/ticketbai/xsd_validators/issuance.xsd +865 -0
- data/lib/ticketbai.rb +89 -0
- data/ticketbai.gemspec +42 -0
- metadata +186 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
module Ticketbai
|
2
|
+
module Operations
|
3
|
+
# In this operation, the document is not signed and it is not encoded in Base64 when making the request to the API as issuance and annulment
|
4
|
+
# operations, instead it is directly appended to the tag FacturaEmitida of the api payload document.
|
5
|
+
class IssuanceUnsigned
|
6
|
+
# Sujetos > Destinatario
|
7
|
+
attr_reader :receiver_nif, :receiver_name
|
8
|
+
# Factura > CabeceraFactura
|
9
|
+
attr_reader :invoice_serial, :invoice_number, :invoice_date, :invoice_time
|
10
|
+
# Factura > DatosFactura
|
11
|
+
attr_reader :invoice_description, :invoice_total, :invoice_vat_key
|
12
|
+
# Factura > TipoDesglose
|
13
|
+
attr_reader :invoice_amount, :invoice_vat, :invoice_vat_total
|
14
|
+
|
15
|
+
OPERATION_NAME = :issuance_unsigned
|
16
|
+
|
17
|
+
###
|
18
|
+
# @param [String] receiver_nif Spanish NIF or official identification document in case of being another country
|
19
|
+
# @param [String] receiver_name Name of the receiver
|
20
|
+
# @param [String] receiver_country Country code of the receiver (Ex: ES|DE)
|
21
|
+
# @param [Boolean] receiver_in_eu The receiver residence is in Europe?
|
22
|
+
# @param [String] invoice_serial Invoices serial number
|
23
|
+
# @param [String] invoice_number Invoices number
|
24
|
+
# @param [String] invoice_date Invoices emission date (Format: d-m-Y)
|
25
|
+
# @param [Boolean] simplified_invoice is a simplified invoice?
|
26
|
+
# @param [String] invoice_description Invoices description text
|
27
|
+
# @param [String] invoice_total Total invoice amount
|
28
|
+
# @param [String] invoice_vat_key Key of the VAT regime
|
29
|
+
# @param [String] invoice_amount Taxable base of the invoice
|
30
|
+
# @param [Float] invoice_vat Invoice VAT (Ex: 21.0)
|
31
|
+
# @param [String] invoice_vat_total Invoices number
|
32
|
+
###
|
33
|
+
def initialize(**args)
|
34
|
+
@receiver_nif = args[:receiver_nif].strip
|
35
|
+
@receiver_name = args[:receiver_name]
|
36
|
+
@receiver_country = args[:receiver_country]&.upcase || 'ES'
|
37
|
+
@receiver_in_eu = args[:receiver_in_eu]
|
38
|
+
@invoice_serial = args[:invoice_serial]
|
39
|
+
@invoice_number = args[:invoice_number]
|
40
|
+
@invoice_date = args[:invoice_date]
|
41
|
+
@simplified_invoice = args[:simplified_invoice]
|
42
|
+
@invoice_description = args[:invoice_description]
|
43
|
+
@invoice_total = args[:invoice_total]
|
44
|
+
@invoice_vat_key = args[:invoice_vat_key]
|
45
|
+
@invoice_amount = args[:invoice_amount]
|
46
|
+
@invoice_vat = args[:invoice_vat]
|
47
|
+
@invoice_vat_total = args[:invoice_vat_total]
|
48
|
+
end
|
49
|
+
|
50
|
+
def create
|
51
|
+
build_document
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_document
|
55
|
+
if @receiver_nif && @receiver_name
|
56
|
+
@receiver = Ticketbai::Nodes::Receiver.new(receiver_country: @receiver_country, receiver_nif: @receiver_nif, receiver_name: @receiver_name)
|
57
|
+
end
|
58
|
+
|
59
|
+
@invoice_header = Ticketbai::Nodes::InvoiceHeader.new(
|
60
|
+
invoice_serial: @invoice_serial,
|
61
|
+
invoice_number: @invoice_number,
|
62
|
+
invoice_date: @invoice_date,
|
63
|
+
invoice_time: @invoice_time,
|
64
|
+
simplified_invoice: @simplified_invoice
|
65
|
+
)
|
66
|
+
@invoice_data = Ticketbai::Nodes::InvoiceData.new(
|
67
|
+
invoice_description: @invoice_description,
|
68
|
+
invoice_total: @invoice_total,
|
69
|
+
invoice_vat_key: @invoice_vat_key
|
70
|
+
)
|
71
|
+
@breakdown_type = Ticketbai::Nodes::BreakdownType.new(
|
72
|
+
receiver_country: @receiver_country,
|
73
|
+
invoice_amount: @invoice_amount,
|
74
|
+
invoice_vat: @invoice_vat,
|
75
|
+
invoice_vat_total: @invoice_vat_total,
|
76
|
+
receiver_in_eu: @receiver_in_eu,
|
77
|
+
simplified_invoice: @simplified_invoice
|
78
|
+
)
|
79
|
+
|
80
|
+
Ticketbai::Documents::IssuanceUnsigned.new(
|
81
|
+
receiver: @receiver,
|
82
|
+
invoice_header: @invoice_header,
|
83
|
+
invoice_data: @invoice_data,
|
84
|
+
breakdown_type: @breakdown_type
|
85
|
+
).create
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
require 'rexml/document'
|
4
|
+
require 'rexml/xpath'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'date'
|
7
|
+
require 'nokogiri'
|
8
|
+
|
9
|
+
module Ticketbai
|
10
|
+
class Signer
|
11
|
+
C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'.freeze # "http://www.w3.org/2001/10/xml-exc-c14n#"
|
12
|
+
DSIG = 'http://www.w3.org/2000/09/xmldsig#'.freeze
|
13
|
+
NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET | Nokogiri::XML::ParseOptions::NOENT
|
14
|
+
RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'.freeze
|
15
|
+
SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'.freeze
|
16
|
+
SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'.freeze
|
17
|
+
SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'.freeze
|
18
|
+
SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'.freeze
|
19
|
+
ENVELOPED_SIG = 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'.freeze
|
20
|
+
NAMESPACES = '#default ds xs xsi xades xsd'.freeze
|
21
|
+
XADES = 'http://uri.etsi.org/01903/v1.3.2#'.freeze
|
22
|
+
|
23
|
+
POLITICA_NAME = 'Politica de firma TicketBAI 1.0'.freeze
|
24
|
+
POLITICA_URL = 'https://www.batuz.eus/fitxategiak/batuz/ticketbai/sinadura_elektronikoaren_zehaztapenak_especificaciones_de_la_firma_electronica_v1_0.pdf'.freeze
|
25
|
+
POLITICA_DIGEST = 'Quzn98x3PMbSHwbUzaj5f5KOpiH0u8bvmwbbbNkO9Es='.freeze
|
26
|
+
|
27
|
+
def initialize(args = {})
|
28
|
+
xml = args[:xml]
|
29
|
+
certificate = args[:certificate]
|
30
|
+
key = args[:key]
|
31
|
+
@output_path = args[:output_path]
|
32
|
+
|
33
|
+
raise ArgumentError, 'Invalid arguments' if xml.nil? || certificate.nil? || key.nil?
|
34
|
+
|
35
|
+
@doc = Nokogiri::XML(xml) do |config|
|
36
|
+
config.options = Nokogiri::XML::ParseOptions::NOBLANKS | Nokogiri::XML::ParseOptions::NOENT | Nokogiri::XML::ParseOptions::NOENT
|
37
|
+
end
|
38
|
+
|
39
|
+
@p12 = OpenSSL::PKCS12.new(certificate, key)
|
40
|
+
|
41
|
+
@x509 = @p12.certificate
|
42
|
+
@document_tag = @doc.elements.first.name
|
43
|
+
end
|
44
|
+
|
45
|
+
def sign
|
46
|
+
# Build parts for Digest Calculation
|
47
|
+
key_info = build_key_info_element
|
48
|
+
signed_properties = build_signed_properties_element
|
49
|
+
signed_info_element = build_signed_info_element(key_info, signed_properties)
|
50
|
+
|
51
|
+
# Compute Signature
|
52
|
+
signed_info_canon = canonicalize_document(signed_info_element)
|
53
|
+
signature_value = compute_signature(@p12.key, algorithm(RSA_SHA256).new, signed_info_canon)
|
54
|
+
|
55
|
+
ds = Nokogiri::XML::Node.new('ds:Signature', @doc)
|
56
|
+
|
57
|
+
ds['xmlns:ds'] = DSIG
|
58
|
+
ds['Id'] = "xmldsig-#{uuid}"
|
59
|
+
ds.add_child(signed_info_element.root)
|
60
|
+
|
61
|
+
sv = Nokogiri::XML::Node.new('ds:SignatureValue', @doc)
|
62
|
+
sv['Id'] = "xmldsig-#{uuid}-sigvalue"
|
63
|
+
sv.content = signature_value
|
64
|
+
ds.add_child(sv)
|
65
|
+
|
66
|
+
ds.add_child(key_info.root)
|
67
|
+
|
68
|
+
dsobj = Nokogiri::XML::Node.new('ds:Object', @doc)
|
69
|
+
dsobj['Id'] = "XadesObjectId-xmldsig-#{uuid}" # XADES_OBJECT_ID
|
70
|
+
qp = Nokogiri::XML::Node.new('xades:QualifyingProperties', @doc)
|
71
|
+
qp['Id'] = "QualifyingProperties-xmldsig-#{uuid}"
|
72
|
+
qp['xmlns:ds'] = DSIG
|
73
|
+
qp['Target'] = "#xmldsig-#{uuid}"
|
74
|
+
qp['xmlns:xades'] = XADES
|
75
|
+
|
76
|
+
qp.add_child(signed_properties.root)
|
77
|
+
|
78
|
+
dsobj.add_child(qp)
|
79
|
+
ds.add_child(dsobj)
|
80
|
+
|
81
|
+
name = @doc.root.namespace
|
82
|
+
@doc.root.namespace = nil
|
83
|
+
@doc.root.add_child(ds)
|
84
|
+
@doc.root.namespace = name
|
85
|
+
|
86
|
+
@doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML).gsub(/\r|\n/, '')
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def build_key_info_element
|
92
|
+
builder = Nokogiri::XML::Builder.new
|
93
|
+
attributes = {
|
94
|
+
'xmlns:T' => 'urn:ticketbai:emision',
|
95
|
+
'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
|
96
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
|
97
|
+
'Id' => "KeyInfoId-xmldsig-#{uuid}"
|
98
|
+
}
|
99
|
+
|
100
|
+
builder.send('ds:KeyInfo', attributes) do |ki|
|
101
|
+
ki.send('ds:X509Data') do |kd|
|
102
|
+
kd.send('ds:X509Certificate', @x509.to_pem.to_s.gsub('-----BEGIN CERTIFICATE-----', '').gsub('-----END CERTIFICATE-----', '').gsub(/\n|\r/, ''))
|
103
|
+
end
|
104
|
+
ki.send('ds:KeyValue') do |kv|
|
105
|
+
kv.send('ds:RSAKeyValue') do |rv|
|
106
|
+
rv.send('ds:Modulus', Base64.encode64(@x509.public_key.params['n'].to_s(2)).gsub("\n", ''))
|
107
|
+
rv.send('ds:Exponent', Base64.encode64(@x509.public_key.params['e'].to_s(2)).gsub("\n", ''))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
builder.doc
|
113
|
+
end
|
114
|
+
|
115
|
+
def build_signed_properties_element
|
116
|
+
cert_digest = compute_digest(@x509.to_der, algorithm(SHA256))
|
117
|
+
signing_time = DateTime.now.rfc3339
|
118
|
+
|
119
|
+
builder = Nokogiri::XML::Builder.new
|
120
|
+
attributes = {
|
121
|
+
'xmlns:T' => 'urn:ticketbai:emision',
|
122
|
+
'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
|
123
|
+
'xmlns:xades' => 'http://uri.etsi.org/01903/v1.3.2#',
|
124
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
|
125
|
+
'Id' => "xmldsig-#{uuid}-signedprops"
|
126
|
+
}
|
127
|
+
|
128
|
+
builder.send('xades:SignedProperties', attributes) do |sp|
|
129
|
+
sp.send('xades:SignedSignatureProperties') do |ssp|
|
130
|
+
ssp.send('xades:SigningTime', signing_time)
|
131
|
+
ssp.send('xades:SigningCertificate') do |sc|
|
132
|
+
sc.send('xades:Cert') do |c|
|
133
|
+
c.send('xades:CertDigest') do |xcd|
|
134
|
+
xcd.send('ds:DigestMethod', { 'Algorithm' => SHA256 })
|
135
|
+
xcd.send('ds:DigestValue', cert_digest)
|
136
|
+
end
|
137
|
+
c.send('xades:IssuerSerial') do |is|
|
138
|
+
is.send('ds:X509IssuerName', @x509.issuer.to_a.reverse.map { |x| x[0..1].join('=') }.join(', '))
|
139
|
+
is.send('ds:X509SerialNumber', @x509.serial.to_s)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
ssp.send('xades:SignaturePolicyIdentifier') do |spi|
|
145
|
+
spi.send('xades:SignaturePolicyId') do |spi2|
|
146
|
+
spi2.send('xades:SigPolicyId') do |spi3|
|
147
|
+
spi3.send('xades:Identifier', POLITICA_URL)
|
148
|
+
spi3.send('xades:Description', POLITICA_NAME)
|
149
|
+
end
|
150
|
+
|
151
|
+
spi2.send('xades:SigPolicyHash') do |sph|
|
152
|
+
sph.send('ds:DigestMethod', { 'Algorithm' => 'http://www.w3.org/2001/04/xmlenc#sha256' })
|
153
|
+
sph.send('ds:DigestValue', POLITICA_DIGEST)
|
154
|
+
end
|
155
|
+
|
156
|
+
spi2.send('xades:SigPolicyQualifiers') do |spqs|
|
157
|
+
spqs.send('xades:SigPolicyQualifier') do |spq|
|
158
|
+
spq.send('xades:SPURI', POLITICA_URL)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
sp.send('xades:SignedDataObjectProperties') do |sdop|
|
165
|
+
sdop.send('xades:DataObjectFormat', { 'ObjectReference' => "#xmldsig-#{uuid}-ref0" }) do |dof|
|
166
|
+
dof.send('xades:ObjectIdentifier') do |oi|
|
167
|
+
oi.send('xades:Identifier', { 'Qualifier' => 'OIDAsURN' }, 'urn:oid:1.2.840.10003.5.109.10')
|
168
|
+
end
|
169
|
+
dof.send('xades:MimeType', 'text/xml')
|
170
|
+
dof.send('xades:Encoding', 'UTF-8')
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
builder.doc
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_signed_info_element(key_info_element, signed_props_element)
|
179
|
+
builder = Nokogiri::XML::Builder.new
|
180
|
+
|
181
|
+
builder.send('ds:SignedInfo',
|
182
|
+
{ 'xmlns:T' => 'urn:ticketbai:emision', 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#',
|
183
|
+
'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' }) do |si|
|
184
|
+
si.send('ds:CanonicalizationMethod', { 'Algorithm' => C14N })
|
185
|
+
si.send('ds:SignatureMethod', { 'Algorithm' => RSA_SHA256 })
|
186
|
+
|
187
|
+
si.send('ds:Reference', { 'Type' => 'http://uri.etsi.org/01903#SignedProperties', 'URI' => "#xmldsig-#{uuid}-signedprops" }) do |r|
|
188
|
+
r.send('ds:Transforms') do |t|
|
189
|
+
t.send('ds:Transform', { 'Algorithm' => C14N })
|
190
|
+
end
|
191
|
+
r.send('ds:DigestMethod', { 'Algorithm' => SHA256 })
|
192
|
+
r.send('ds:DigestValue', digest_document(signed_props_element, SHA256))
|
193
|
+
end
|
194
|
+
|
195
|
+
si.send('ds:Reference', { 'Id' => 'ReferenceKeyInfo', 'URI' => "#KeyInfoId-xmldsig-#{uuid}" }) do |r|
|
196
|
+
r.send('ds:Transforms') do |t|
|
197
|
+
t.send('ds:Transform', { 'Algorithm' => C14N })
|
198
|
+
end
|
199
|
+
r.send('ds:DigestMethod', { 'Algorithm' => SHA256 })
|
200
|
+
r.send('ds:DigestValue', digest_document(key_info_element, SHA256))
|
201
|
+
end
|
202
|
+
|
203
|
+
si.send('ds:Reference', { 'Id' => "xmldsig-#{uuid}-ref0", 'Type' => 'http://www.w3.org/2000/09/xmldsig#Object', 'URI' => '' }) do |r|
|
204
|
+
r.send('ds:Transforms') do |t|
|
205
|
+
t.send('ds:Transform', { 'Algorithm' => ENVELOPED_SIG })
|
206
|
+
t.send('ds:Transform', { 'Algorithm' => C14N })
|
207
|
+
end
|
208
|
+
r.send('ds:DigestMethod', { 'Algorithm' => SHA256 })
|
209
|
+
r.send('ds:DigestValue', digest_document(@doc, SHA256))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
builder.doc
|
214
|
+
end
|
215
|
+
|
216
|
+
def digest_document(doc, digest_algorithm = SHA256)
|
217
|
+
compute_digest(canonicalize_document(doc), algorithm(digest_algorithm))
|
218
|
+
end
|
219
|
+
|
220
|
+
def canonicalize_document(doc)
|
221
|
+
doc.canonicalize(canon_algorithm(C14N), NAMESPACES.split)
|
222
|
+
end
|
223
|
+
|
224
|
+
def uuid
|
225
|
+
@uuid ||= SecureRandom.uuid
|
226
|
+
end
|
227
|
+
|
228
|
+
def canon_algorithm(element)
|
229
|
+
algorithm = element
|
230
|
+
|
231
|
+
case algorithm
|
232
|
+
when 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
|
233
|
+
'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'
|
234
|
+
Nokogiri::XML::XML_C14N_1_0
|
235
|
+
when 'http://www.w3.org/2006/12/xml-c14n11',
|
236
|
+
'http://www.w3.org/2006/12/xml-c14n11#WithComments'
|
237
|
+
Nokogiri::XML::XML_C14N_1_1
|
238
|
+
else
|
239
|
+
Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def algorithm(element)
|
244
|
+
algorithm = element
|
245
|
+
|
246
|
+
case algorithm
|
247
|
+
when REXML::Element
|
248
|
+
algorithm = element.attribute('Algorithm').value
|
249
|
+
when Nokogiri::XML::Element
|
250
|
+
algorithm = element.xpath('//@Algorithm', 'xmlns:ds' => 'http://www.w3.org/2000/09/xmldsig#').first.value
|
251
|
+
end
|
252
|
+
|
253
|
+
algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && Regexp.last_match(2).to_i
|
254
|
+
|
255
|
+
case algorithm
|
256
|
+
when 256 then OpenSSL::Digest::SHA256
|
257
|
+
when 384 then OpenSSL::Digest::SHA384
|
258
|
+
when 512 then OpenSSL::Digest::SHA512
|
259
|
+
else
|
260
|
+
OpenSSL::Digest::SHA1
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def compute_signature(private_key, signature_algorithm, document)
|
265
|
+
Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\r|\n/, '')
|
266
|
+
end
|
267
|
+
|
268
|
+
def compute_digest(document, digest_algorithm)
|
269
|
+
digest = digest_algorithm.digest(document)
|
270
|
+
Base64.encode64(digest).strip!
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Ticketbai
|
2
|
+
class TbaiIdentifier
|
3
|
+
ID = 'TBAI'.freeze
|
4
|
+
|
5
|
+
attr_accessor :nif, :invoice_date, :signature_value
|
6
|
+
|
7
|
+
###
|
8
|
+
# @param [String] nif The issuer NIF
|
9
|
+
# @param [String] invoice_date Format DDMMYY
|
10
|
+
# @param [String] signature_value First 13 characters of the signatureValue present in the signed xml
|
11
|
+
###
|
12
|
+
def initialize(**args)
|
13
|
+
self.nif = args[:nif]
|
14
|
+
self.invoice_date = args[:invoice_date]
|
15
|
+
self.signature_value = args[:signature_value][0..12]
|
16
|
+
end
|
17
|
+
|
18
|
+
###
|
19
|
+
# Builds the TBAI Identifier with the following format: TBAI-NIF-FechaExpedicionFactura(DDMMAA)-SignatureValue(13)-CRC(3)
|
20
|
+
# @return [String] The TBAI identifier.
|
21
|
+
###
|
22
|
+
def create
|
23
|
+
identifier = [ID, @nif, @invoice_date, @signature_value].join('-')
|
24
|
+
identifier << '-'
|
25
|
+
|
26
|
+
crc = ChecksumCalculator.new(identifier).calculate
|
27
|
+
|
28
|
+
identifier << crc
|
29
|
+
|
30
|
+
identifier
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Ticketbai
|
2
|
+
class TbaiQr
|
3
|
+
BASE_URL = 'https://batuz.eus/QRTBAI/?'.freeze
|
4
|
+
|
5
|
+
# param names
|
6
|
+
ID_TBAI_NAME = 'id'.freeze
|
7
|
+
SERIAL_NAME = 's'.freeze
|
8
|
+
NUMBER_NAME = 'nf'.freeze
|
9
|
+
TOTAL_NAME = 'i'.freeze
|
10
|
+
CRC_NAME = 'cr'.freeze
|
11
|
+
|
12
|
+
attr_accessor :id_tbai, :number, :serial, :total
|
13
|
+
|
14
|
+
###
|
15
|
+
# @param [String] id_tbai The Tbai Identifier
|
16
|
+
# @param [String] number Invoice number
|
17
|
+
# @param [String] total Invoice total amount (Max 2 decimals) Ex: 14.20
|
18
|
+
# @param [String] serial The invoice serial number
|
19
|
+
###
|
20
|
+
def initialize(**args)
|
21
|
+
self.id_tbai = args[:id_tbai]
|
22
|
+
self.serial = args[:serial]
|
23
|
+
self.number = args[:number]
|
24
|
+
self.total = args[:total]
|
25
|
+
end
|
26
|
+
|
27
|
+
###
|
28
|
+
# Builds the Ticketbai QR URL. Example: https://batuz.eus/QRTBAI/?id=TBAI-00000006Y-251019-btFpwP8dcLGAF-237&s=T&nf=27174&i=4.70&cr=007
|
29
|
+
# @return [String] Ticketbai QR URL
|
30
|
+
###
|
31
|
+
def create
|
32
|
+
encoded_params = [
|
33
|
+
format_param(ID_TBAI_NAME, @id_tbai),
|
34
|
+
format_param(SERIAL_NAME, @serial),
|
35
|
+
format_param(NUMBER_NAME, @number),
|
36
|
+
format_param(TOTAL_NAME, @total)
|
37
|
+
].join('&')
|
38
|
+
|
39
|
+
crc = ChecksumCalculator.new(BASE_URL + encoded_params).calculate
|
40
|
+
|
41
|
+
BASE_URL + [encoded_params, format_param(CRC_NAME, crc)].join('&')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def format_param(name, value)
|
47
|
+
"#{name}=#{ERB::Util.url_encode(value)}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|