ticketbai 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|