ticketbai 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +32 -0
  4. data/CHANGELOG.md +13 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +107 -0
  8. data/Rakefile +12 -0
  9. data/bin/console +15 -0
  10. data/bin/setup +8 -0
  11. data/lib/ticketbai/api/client.rb +63 -0
  12. data/lib/ticketbai/api/registry.rb +34 -0
  13. data/lib/ticketbai/api/request.rb +125 -0
  14. data/lib/ticketbai/api/response_parser.rb +36 -0
  15. data/lib/ticketbai/checksum_calculator.rb +15 -0
  16. data/lib/ticketbai/document.rb +21 -0
  17. data/lib/ticketbai/document_validator.rb +42 -0
  18. data/lib/ticketbai/documents/annulment.rb +45 -0
  19. data/lib/ticketbai/documents/api_payload.rb +47 -0
  20. data/lib/ticketbai/documents/issuance.rb +55 -0
  21. data/lib/ticketbai/documents/issuance_unsigned.rb +58 -0
  22. data/lib/ticketbai/errors.rb +27 -0
  23. data/lib/ticketbai/nodes/breakdown_type.rb +72 -0
  24. data/lib/ticketbai/nodes/invoice_chaining.rb +22 -0
  25. data/lib/ticketbai/nodes/invoice_data.rb +24 -0
  26. data/lib/ticketbai/nodes/invoice_header.rb +26 -0
  27. data/lib/ticketbai/nodes/issuer.rb +18 -0
  28. data/lib/ticketbai/nodes/lroe_header.rb +29 -0
  29. data/lib/ticketbai/nodes/lroe_issued_invoices.rb +28 -0
  30. data/lib/ticketbai/nodes/receiver.rb +36 -0
  31. data/lib/ticketbai/nodes/software.rb +17 -0
  32. data/lib/ticketbai/operation.rb +59 -0
  33. data/lib/ticketbai/operations/annulment.rb +41 -0
  34. data/lib/ticketbai/operations/issuance.rb +93 -0
  35. data/lib/ticketbai/operations/issuance_unsigned.rb +89 -0
  36. data/lib/ticketbai/signer.rb +273 -0
  37. data/lib/ticketbai/tbai_identifier.rb +33 -0
  38. data/lib/ticketbai/tbai_qr.rb +50 -0
  39. data/lib/ticketbai/version.rb +5 -0
  40. data/lib/ticketbai/xmldsig-core-schema.xsd +318 -0
  41. data/lib/ticketbai/xsd_validators/annulment.xsd +392 -0
  42. data/lib/ticketbai/xsd_validators/issuance.xsd +865 -0
  43. data/lib/ticketbai.rb +89 -0
  44. data/ticketbai.gemspec +42 -0
  45. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ticketbai
4
+ VERSION = '0.1.1'
5
+ end