zatca 0.1.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -2
  3. data/bin/console +0 -0
  4. data/bin/setup +0 -0
  5. data/lib/zatca/client.rb +173 -0
  6. data/lib/zatca/hacks.rb +45 -0
  7. data/lib/zatca/hashing.rb +18 -0
  8. data/lib/zatca/qr_code_extractor.rb +31 -0
  9. data/lib/zatca/qr_code_generator.rb +9 -2
  10. data/lib/zatca/signing/certificate.rb +78 -0
  11. data/lib/zatca/signing/csr.rb +220 -0
  12. data/lib/zatca/signing/ecdsa.rb +59 -0
  13. data/lib/zatca/tag.rb +18 -8
  14. data/lib/zatca/tags.rb +5 -1
  15. data/lib/zatca/tags_schema.rb +5 -5
  16. data/lib/zatca/types.rb +7 -0
  17. data/lib/zatca/ubl/base_component.rb +142 -0
  18. data/lib/zatca/ubl/builder.rb +166 -0
  19. data/lib/zatca/ubl/common_aggregate_components/allowance_charge.rb +64 -0
  20. data/lib/zatca/ubl/common_aggregate_components/classified_tax_category.rb +25 -0
  21. data/lib/zatca/ubl/common_aggregate_components/delivery.rb +27 -0
  22. data/lib/zatca/ubl/common_aggregate_components/invoice_line.rb +63 -0
  23. data/lib/zatca/ubl/common_aggregate_components/item.rb +21 -0
  24. data/lib/zatca/ubl/common_aggregate_components/legal_monetary_total.rb +59 -0
  25. data/lib/zatca/ubl/common_aggregate_components/party.rb +28 -0
  26. data/lib/zatca/ubl/common_aggregate_components/party_identification.rb +25 -0
  27. data/lib/zatca/ubl/common_aggregate_components/party_legal_entity.rb +19 -0
  28. data/lib/zatca/ubl/common_aggregate_components/party_tax_scheme.rb +30 -0
  29. data/lib/zatca/ubl/common_aggregate_components/postal_address.rb +59 -0
  30. data/lib/zatca/ubl/common_aggregate_components/price.rb +20 -0
  31. data/lib/zatca/ubl/common_aggregate_components/tax_category.rb +56 -0
  32. data/lib/zatca/ubl/common_aggregate_components/tax_total.rb +58 -0
  33. data/lib/zatca/ubl/common_aggregate_components.rb +2 -0
  34. data/lib/zatca/ubl/invoice.rb +481 -0
  35. data/lib/zatca/ubl/invoice_subtype_builder.rb +50 -0
  36. data/lib/zatca/ubl/signing/cert.rb +48 -0
  37. data/lib/zatca/ubl/signing/invoice_signed_data_reference.rb +44 -0
  38. data/lib/zatca/ubl/signing/key_info.rb +25 -0
  39. data/lib/zatca/ubl/signing/object.rb +20 -0
  40. data/lib/zatca/ubl/signing/qualifying_properties.rb +27 -0
  41. data/lib/zatca/ubl/signing/signature.rb +50 -0
  42. data/lib/zatca/ubl/signing/signature_information.rb +19 -0
  43. data/lib/zatca/ubl/signing/signature_properties_reference.rb +26 -0
  44. data/lib/zatca/ubl/signing/signed_info.rb +21 -0
  45. data/lib/zatca/ubl/signing/signed_properties.rb +81 -0
  46. data/lib/zatca/ubl/signing/signed_signature_properties.rb +23 -0
  47. data/lib/zatca/ubl/signing/ubl_document_signatures.rb +25 -0
  48. data/lib/zatca/ubl/signing/ubl_extension.rb +22 -0
  49. data/lib/zatca/ubl/signing/ubl_extensions.rb +17 -0
  50. data/lib/zatca/ubl/signing.rb +2 -0
  51. data/lib/zatca/ubl.rb +2 -0
  52. data/lib/zatca/version.rb +1 -1
  53. data/lib/zatca.rb +27 -3
  54. data/zatca.gemspec +52 -0
  55. metadata +165 -10
  56. data/Gemfile.lock +0 -100
@@ -0,0 +1,481 @@
1
+ class ZATCA::UBL::Invoice < ZATCA::UBL::BaseComponent
2
+ TYPES = {
3
+ invoice: "388",
4
+ debit: "383",
5
+ credit: "381"
6
+ }.freeze
7
+
8
+ PAYMENT_MEANS = {
9
+ cash: "10",
10
+ credit: "30",
11
+ bank_account: "42",
12
+ bank_card: "48"
13
+ }.freeze
14
+
15
+ attr_reader :signed_hash
16
+ attr_reader :signed_hash_bytes
17
+ attr_reader :public_key_bytes
18
+ attr_reader :certificate_signature
19
+ attr_reader :qualifying_properties
20
+
21
+ attr_accessor :signature, :qr_code
22
+
23
+ option :id, type: Dry::Types["coercible.string"]
24
+ option :uuid, type: Dry::Types["coercible.string"]
25
+ option :note, type: Dry::Types["coercible.string"].optional, optional: true, default: proc {}
26
+ option :instruction_note, type: Dry::Types["coercible.string"].optional, optional: true, default: proc {}
27
+ option :billing_reference, type: Dry::Types["coercible.string"].optional, optional: true, default: proc {}
28
+ option :note_language_id, type: Dry::Types["coercible.string"].optional, optional: true, default: proc {}
29
+ option :issue_date, type: Dry::Types["coercible.string"]
30
+ option :issue_time, type: Dry::Types["coercible.string"]
31
+ option :subtype, type: Dry::Types["coercible.string"]
32
+ option :currency_code, type: Dry::Types["coercible.string"], default: proc { "SAR" }
33
+ option :line_count_numeric, type: Dry::Types["coercible.string"], optional: true
34
+ option :qr_code, type: Dry::Types["coercible.string"].optional, optional: true, default: proc {}
35
+ option :payment_means_code, type: Dry::Types["coercible.string"]
36
+
37
+ option :type, type: Dry::Types["coercible.string"]
38
+ option :invoice_counter_value, type: Dry::Types["coercible.string"]
39
+ option :previous_invoice_hash, type: Dry::Types["coercible.string"], optional: true
40
+
41
+ option :add_ids_to_allowance_charges,
42
+ type: Dry::Types["strict.bool"],
43
+ optional: true,
44
+ default: proc { true }
45
+
46
+ option :accounting_supplier_party, type: ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::Party)
47
+ option :accounting_customer_party, type: ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::Party)
48
+
49
+ option :delivery,
50
+ type: ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::Delivery).optional,
51
+ optional: true,
52
+ default: proc {}
53
+
54
+ option :allowance_charges,
55
+ type: ZATCA::Types::Array.of(ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::AllowanceCharge)),
56
+ optional: true,
57
+ default: proc { [] }
58
+
59
+ option :tax_totals,
60
+ type: ZATCA::Types::Array.of(ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::TaxTotal)),
61
+ default: proc { [] }
62
+
63
+ option :legal_monetary_total, type: ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::LegalMonetaryTotal)
64
+
65
+ option :invoice_lines,
66
+ type: ZATCA::Types::Array.of(ZATCA::Types.Instance(ZATCA::UBL::CommonAggregateComponents::InvoiceLine))
67
+
68
+ option :signature,
69
+ type: ZATCA::Types.Instance(ZATCA::UBL::Signing::Signature).optional,
70
+ optional: true,
71
+ default: proc {}
72
+
73
+ def name
74
+ "Invoice"
75
+ end
76
+
77
+ def attributes
78
+ {
79
+ "xmlns" => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
80
+ "xmlns:cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
81
+ "xmlns:cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
82
+ "xmlns:ext" => "urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"
83
+ }
84
+ end
85
+
86
+ def elements
87
+ add_sequential_ids
88
+
89
+ [
90
+ # Invoice signature
91
+ ubl_extensions_element,
92
+
93
+ # Metadata
94
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ProfileID", value: "reporting:1.0"),
95
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: id),
96
+ ZATCA::UBL::BaseComponent.new(name: "cbc:UUID", value: uuid),
97
+ ZATCA::UBL::BaseComponent.new(name: "cbc:IssueDate", value: issue_date),
98
+ ZATCA::UBL::BaseComponent.new(name: "cbc:IssueTime", value: issue_time),
99
+
100
+ # Invoice type
101
+ ZATCA::UBL::BaseComponent.new(
102
+ name: "cbc:InvoiceTypeCode",
103
+ attributes: {"name" => subtype},
104
+ value: type
105
+ ),
106
+
107
+ # Note
108
+ note_element,
109
+
110
+ # Currency codes
111
+ ZATCA::UBL::BaseComponent.new(name: "cbc:DocumentCurrencyCode", value: currency_code),
112
+ ZATCA::UBL::BaseComponent.new(name: "cbc:TaxCurrencyCode", value: currency_code),
113
+
114
+ # Billing reference for debit and credit notes
115
+ billing_reference_element,
116
+
117
+ # Line Count Numeric (Standard Invoice only)
118
+ line_count_numeric_element,
119
+
120
+ # Additional document references
121
+ # Invoice counter value (ICV)
122
+ ZATCA::UBL::BaseComponent.new(name: "cac:AdditionalDocumentReference", elements: [
123
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: "ICV"),
124
+ ZATCA::UBL::BaseComponent.new(name: "cbc:UUID", value: invoice_counter_value)
125
+ ]),
126
+
127
+ # Previous invoice hash (PIH)
128
+ previous_invoice_hash_document_reference,
129
+
130
+ # QR code
131
+ qr_code_document_reference,
132
+
133
+ # Static: signature
134
+ static_signature_element,
135
+
136
+ # AccountingSupplierParty
137
+ ZATCA::UBL::BaseComponent.new(name: "cac:AccountingSupplierParty", elements: [
138
+ accounting_supplier_party
139
+ ]),
140
+
141
+ # AccountingCustomerParty
142
+ ZATCA::UBL::BaseComponent.new(name: "cac:AccountingCustomerParty", elements: [
143
+ accounting_customer_party
144
+ ]),
145
+
146
+ # Delivery
147
+ delivery,
148
+
149
+ # PaymentMeans
150
+ ZATCA::UBL::BaseComponent.new(name: "cac:PaymentMeans", elements: [
151
+ ZATCA::UBL::BaseComponent.new(name: "cbc:PaymentMeansCode", value: payment_means_code),
152
+ instruction_note_element
153
+ ]),
154
+
155
+ # AllowanceCharges
156
+ # TODO: Figure out how this ties to invoice lines
157
+ *allowance_charges,
158
+
159
+ # TaxTotals
160
+ *tax_totals,
161
+
162
+ # LegalMonetaryTotal
163
+ legal_monetary_total,
164
+
165
+ # InvoiceLines
166
+ *invoice_lines
167
+ ]
168
+ end
169
+
170
+ def generate_hash
171
+ # We don't need to apply the hacks here because they only apply to the
172
+ # QualifyingProperties block which is not present when generating the hash
173
+ canonicalized_xml = generate_unsigned_xml(
174
+ canonicalized: true,
175
+ apply_invoice_hacks: false,
176
+ remove_root_xml_tag: true
177
+ )
178
+
179
+ File.write("xml_for_signing.xml", canonicalized_xml)
180
+
181
+ ZATCA::Hashing.generate_hashes(canonicalized_xml)[:base64]
182
+ end
183
+
184
+ # When submitting to ZATCA, we need to submit the XML in Base64 format, and it
185
+ # needs to be pretty-printed matching their indentation style.
186
+ # The canonicalized option here is left only for debugging purposes.
187
+ def to_base64(canonicalized: true)
188
+ canonicalized_xml_with_hacks_applied = generate_xml(
189
+ canonicalized: canonicalized,
190
+ apply_invoice_hacks: true,
191
+ remove_root_xml_tag: false
192
+ )
193
+
194
+ Base64.strict_encode64(canonicalized_xml_with_hacks_applied)
195
+ end
196
+
197
+ # HACK:
198
+ # Override this method because dry-initializer isn't helping us by having
199
+ # an after_initialize callback. We just need to set the qualifying properties
200
+ # at any point before generating the XML.
201
+ def generate_xml(
202
+ canonicalized: true,
203
+ spaces: 4,
204
+ apply_invoice_hacks: true,
205
+ remove_root_xml_tag: false
206
+ )
207
+ set_qualifying_properties(
208
+ signing_time: @signature&.signing_time,
209
+ cert_digest_value: @signature&.cert_digest_value,
210
+ cert_issuer_name: @signature&.cert_issuer_name,
211
+ cert_serial_number: @signature&.cert_serial_number
212
+ )
213
+
214
+ super(
215
+ canonicalized: canonicalized,
216
+ spaces: spaces,
217
+ apply_invoice_hacks: apply_invoice_hacks,
218
+ remove_root_xml_tag: remove_root_xml_tag
219
+ )
220
+ end
221
+
222
+ def generate_unsigned_xml(
223
+ canonicalized: true,
224
+ apply_invoice_hacks: false,
225
+ remove_root_xml_tag: false
226
+ )
227
+ # HACK: Set signature and QR code to nil temporarily so they get removed
228
+ # from the XML before generating the unsigned XML. An unsigned einvoice
229
+ # should not have a signature or QR code, we additionally remove the qualifying
230
+ # properties because it is a replacement that happens on the generated XML and
231
+ # we only want that replacement on the version we submit to ZATCA.
232
+ original_signature = signature
233
+ original_qr_code = qr_code
234
+ original_qualifying_properties = @qualifying_properties
235
+
236
+ self.signature = nil
237
+ self.qr_code = nil
238
+ @qualifying_properties = nil
239
+
240
+ unsigned_xml = generate_xml(
241
+ canonicalized: canonicalized,
242
+ apply_invoice_hacks: apply_invoice_hacks,
243
+ remove_root_xml_tag: remove_root_xml_tag
244
+ )
245
+
246
+ self.signature = original_signature
247
+ self.qr_code = original_qr_code
248
+ @qualifying_properties = original_qualifying_properties
249
+
250
+ unsigned_xml
251
+ end
252
+
253
+ def sign(
254
+ private_key_path:,
255
+ certificate_path:,
256
+ signing_time: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S"),
257
+ decode_private_key_from_base64: false
258
+ )
259
+ # ZATCA does not like signing_times ending with Z
260
+ signing_time = signing_time.delete_suffix("Z")
261
+
262
+ canonicalized_xml = generate_unsigned_xml(canonicalized: true)
263
+ generated_hashes = ZATCA::Hashing.generate_hashes(canonicalized_xml)
264
+
265
+ # Sign the invoice hash using the private key
266
+ signature = ZATCA::Signing::ECDSA.sign(
267
+ content: generated_hashes[:hexdigest],
268
+ private_key_path: private_key_path,
269
+ decode_from_base64: decode_private_key_from_base64
270
+ )
271
+
272
+ @signed_hash = signature[:base64]
273
+ @signed_hash_bytes = signature[:bytes]
274
+
275
+ # Parse and hash the certificate
276
+ parsed_certificate = ZATCA::Signing::Certificate.read_certificate(certificate_path)
277
+ @public_key_bytes = parsed_certificate.public_key_bytes
278
+
279
+ # Current Version
280
+ @certificate_signature = parsed_certificate.signature
281
+
282
+ # ZATCA requires a different set of attributes when hashing the SignedProperties
283
+ # attributes and does not want those attributes present in the actual XML.
284
+ # So we'll have two sets of signed properties for this purpose, one just
285
+ # to generate a hash out of, and one to actually include in the XML.
286
+ # See: https://zatca1.discourse.group/t/what-do-signed-properties-look-like-when-hashing/717
287
+ #
288
+ # The other SignedProperties that's in the XML is generated when we construct
289
+ # the Signature element below
290
+
291
+ signed_properties_for_hashing = ZATCA::UBL::Signing::SignedProperties.new(
292
+ signing_time: signing_time,
293
+ cert_digest_value: parsed_certificate.hash,
294
+ cert_issuer_name: parsed_certificate.issuer_name,
295
+ cert_serial_number: parsed_certificate.serial_number
296
+ )
297
+
298
+ set_qualifying_properties(
299
+ signing_time: signing_time,
300
+ cert_digest_value: parsed_certificate.hash,
301
+ cert_issuer_name: parsed_certificate.issuer_name,
302
+ cert_serial_number: parsed_certificate.serial_number
303
+ )
304
+
305
+ # ZATCA uses very specific whitespace also for the version of this block
306
+ # that we need to submit to their servers, so we will keep a copy of the XML
307
+ # as it should be spaced, and then after building the XML we will replace
308
+ # the QualifyingProperties block with this one.
309
+ #
310
+ # See: https://zatca1.discourse.group/t/what-do-signed-properties-look-like-when-hashing/717
311
+ #
312
+ # If their server is ever updated to format the block before hashing it on their
313
+ # end, we can safely remove this behavior.
314
+ @qualifying_properties = ZATCA::Hacks.zatca_indented_qualifying_properties(
315
+ signing_time: signing_time,
316
+ cert_digest_value: parsed_certificate.hash,
317
+ cert_issuer_name: parsed_certificate.issuer_name,
318
+ cert_serial_number: parsed_certificate.serial_number
319
+ )
320
+
321
+ signed_properties_hash = signed_properties_for_hashing.generate_hash
322
+
323
+ # Create the signature element using the certficiate, invoice hash, and signed
324
+ # properties hash
325
+ signature_element = ZATCA::UBL::Signing::Signature.new(
326
+ invoice_hash: generated_hashes[:base64],
327
+ signed_properties_hash: signed_properties_hash,
328
+
329
+ # Current Version
330
+ signature_value: @signed_hash,
331
+
332
+ # GPT4 Version
333
+ # signature_value: @signed_hash[:base64],
334
+
335
+ certificate: parsed_certificate.cert_content_without_headers,
336
+ signing_time: signing_time,
337
+ cert_digest_value: parsed_certificate.hash,
338
+ cert_issuer_name: parsed_certificate.issuer_name,
339
+ cert_serial_number: parsed_certificate.serial_number
340
+ )
341
+
342
+ self.signature = signature_element
343
+ end
344
+
345
+ private
346
+
347
+ def note_element
348
+ return nil if note.blank? && note_language_id.blank?
349
+
350
+ ZATCA::UBL::BaseComponent.new(
351
+ name: "cbc:Note",
352
+ attributes: {"languageID" => note_language_id},
353
+ value: note
354
+ )
355
+ end
356
+
357
+ def previous_invoice_hash_document_reference
358
+ return nil if previous_invoice_hash.nil?
359
+
360
+ ZATCA::UBL::BaseComponent.new(name: "cac:AdditionalDocumentReference", elements: [
361
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: "PIH"),
362
+ ZATCA::UBL::BaseComponent.new(name: "cac:Attachment", elements: [
363
+ ZATCA::UBL::BaseComponent.new(
364
+ name: "cbc:EmbeddedDocumentBinaryObject",
365
+ attributes: {"mimeCode" => "text/plain"},
366
+ value: previous_invoice_hash
367
+ )
368
+ ])
369
+ ])
370
+ end
371
+
372
+ def qr_code_document_reference
373
+ return nil if qr_code.blank?
374
+
375
+ ZATCA::UBL::BaseComponent.new(name: "cac:AdditionalDocumentReference", elements: [
376
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: "QR"),
377
+ ZATCA::UBL::BaseComponent.new(name: "cac:Attachment", elements: [
378
+ ZATCA::UBL::BaseComponent.new(
379
+ name: "cbc:EmbeddedDocumentBinaryObject",
380
+ attributes: {"mimeCode" => "text/plain"},
381
+ value: qr_code
382
+ )
383
+ ])
384
+ ])
385
+ end
386
+
387
+ def line_count_numeric_element
388
+ return nil if line_count_numeric.blank?
389
+
390
+ ZATCA::UBL::BaseComponent.new(name: "cbc:LineCountNumeric", value: line_count_numeric)
391
+ end
392
+
393
+ def ubl_extensions_element
394
+ return nil if signature.blank?
395
+
396
+ ZATCA::UBL::Signing::UBLExtensions.new(signature: signature)
397
+ end
398
+
399
+ def static_signature_element
400
+ return nil if signature.blank?
401
+
402
+ ZATCA::UBL::BaseComponent.new(name: "cac:Signature", elements: [
403
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: "urn:oasis:names:specification:ubl:signature:Invoice"),
404
+ ZATCA::UBL::BaseComponent.new(name: "cbc:SignatureMethod", value: "urn:oasis:names:specification:ubl:dsig:enveloped:xades")
405
+ ])
406
+ end
407
+
408
+ def instruction_note_element
409
+ return nil if instruction_note.blank?
410
+
411
+ ZATCA::UBL::BaseComponent.new(name: "cbc:InstructionNote", value: instruction_note)
412
+ end
413
+
414
+ def billing_reference_element
415
+ return nil if billing_reference.blank?
416
+
417
+ ZATCA::UBL::BaseComponent.new(name: "cac:BillingReference", elements: [
418
+ ZATCA::UBL::BaseComponent.new(name: "cac:InvoiceDocumentReference", elements: [
419
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: billing_reference)
420
+ ])
421
+ ])
422
+ end
423
+
424
+ def add_sequential_ids
425
+ add_sequential_ids_to_allowance_charges
426
+ add_sequential_ids_to_invoice_lines
427
+ end
428
+
429
+ # Allowance charges might need to have sequential IDs, this method uses the
430
+ # array index of each element to do that.
431
+ def add_sequential_ids_to_allowance_charges
432
+ return unless add_ids_to_allowance_charges
433
+
434
+ @_added_sequential_ids_to_allowance_charges ||= false
435
+
436
+ return if @_added_sequential_ids_to_allowance_charges
437
+
438
+ allowance_charges.each_with_index do |allowance_charge, index|
439
+ allowance_charge.index = index + 1
440
+ end
441
+
442
+ @_added_sequential_ids_to_allowance_charges = true
443
+ end
444
+
445
+ # Invoice lines must have sequential IDs, this method uses the array index
446
+ # of each element to do that.
447
+ def add_sequential_ids_to_invoice_lines
448
+ @_added_sequential_ids_to_invoice_lines ||= false
449
+
450
+ return if @_added_sequential_ids_to_invoice_lines
451
+
452
+ invoice_lines.each_with_index do |invoice_line, index|
453
+ invoice_line.index = index + 1
454
+ end
455
+
456
+ @_added_sequential_ids_to_invoice_lines = true
457
+ end
458
+
459
+ # ZATCA uses very specific whitespace also for the version of this block
460
+ # that we need to submit to their servers, so we will keep a copy of the XML
461
+ # as it should be spaced, and then after building the XML we will replace
462
+ # the QualifyingProperties block with this one.
463
+ #
464
+ # See: https://zatca1.discourse.group/t/what-do-signed-properties-look-like-when-hashing/717
465
+ #
466
+ # If their server is ever updated to format the block before hashing it on their
467
+ # end, we can safely remove this behavior.
468
+ def set_qualifying_properties(
469
+ signing_time:,
470
+ cert_digest_value:,
471
+ cert_issuer_name:,
472
+ cert_serial_number:
473
+ )
474
+ @qualifying_properties = ZATCA::Hacks.zatca_indented_qualifying_properties(
475
+ signing_time: signing_time,
476
+ cert_digest_value: cert_digest_value,
477
+ cert_issuer_name: cert_issuer_name,
478
+ cert_serial_number: cert_serial_number
479
+ )
480
+ end
481
+ end
@@ -0,0 +1,50 @@
1
+ module ZATCA::UBL::InvoiceSubtypeBuilder
2
+ extend self
3
+
4
+ # Builds the invoice subtype code based on the provided parameters.
5
+ #
6
+ # @param simplified [Boolean] Specifies whether the invoice is a simplified tax invoice.
7
+ # @param third_party [Boolean] Specifies whether the invoice is a third-party invoice transaction.
8
+ # @param nominal [Boolean] Specifies whether the invoice is a nominal invoice transaction.
9
+ # @param exports [Boolean] Specifies whether the invoice is an exports invoice transaction.
10
+ # @param summary [Boolean] Specifies whether the invoice is a summary invoice transaction.
11
+ # @param self_billed [Boolean] Specifies whether the invoice is a self-billed invoice.
12
+ # @return [String] The generated invoice subtype code.
13
+ #
14
+ # @example
15
+ # InvoiceSubtypeBuilder.build(
16
+ # simplified: true,
17
+ # third_party: false,
18
+ # nominal: true,
19
+ # exports: false,
20
+ # summary: false,
21
+ # self_billed: true
22
+ # )
23
+ # # => "0201001"
24
+ #
25
+ # InvoiceSubtypeBuilder.build(
26
+ # simplified: false,
27
+ # third_party: true,
28
+ # nominal: true,
29
+ # exports: true,
30
+ # summary: true,
31
+ # self_billed: false
32
+ # )
33
+ # # => "0111110"
34
+ #
35
+ def build(
36
+ simplified:,
37
+ third_party:,
38
+ nominal:,
39
+ exports:,
40
+ summary:,
41
+ self_billed:
42
+ )
43
+ subtype_prefix = simplified ? "02" : "01"
44
+
45
+ values = [third_party, nominal, exports, summary, self_billed]
46
+ values = values.map { |v| v ? "1" : "0" }
47
+
48
+ subtype_prefix + values.join
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ class ZATCA::UBL::Signing::Cert < ZATCA::UBL::BaseComponent
2
+ # <xades:Cert>
3
+ # <xades:CertDigest>
4
+ # <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
5
+ # <ds:DigestValue>NjlhOTVmYzIzN2I0MjcxNGRjNDQ1N2EzM2I5NGNjNDUyZmQ5ZjExMDUwNGM2ODNjNDAxMTQ0ZDk1NDQ4OTRmYg==</ds:DigestValue>
6
+ # </xades:CertDigest>
7
+ # <xades:IssuerSerial>
8
+ # <ds:X509IssuerName>CN=TSZEINVOICE-SubCA-1, DC=extgazt, DC=gov, DC=local</ds:X509IssuerName>
9
+ # <ds:X509SerialNumber>2475382876776561391517206651645660279462721580</ds:X509SerialNumber>
10
+ # </xades:IssuerSerial>
11
+ # </xades:Cert>
12
+
13
+ def initialize(cert_digest_value:, cert_issuer_name:, cert_serial_number:)
14
+ super()
15
+
16
+ @cert_digest_value = cert_digest_value
17
+ @cert_issuer_name = cert_issuer_name
18
+ @cert_serial_number = cert_serial_number
19
+ end
20
+
21
+ def name
22
+ "xades:Cert"
23
+ end
24
+
25
+ def elements
26
+ [
27
+ ZATCA::UBL::BaseComponent.new(name: "xades:CertDigest", elements: [
28
+ ZATCA::UBL::BaseComponent.new(
29
+ name: "ds:DigestMethod",
30
+ attributes: {
31
+ "Algorithm" => "http://www.w3.org/2001/04/xmlenc#sha256"
32
+ }
33
+ ),
34
+ ZATCA::UBL::BaseComponent.new(name: "ds:DigestValue", value: @cert_digest_value)
35
+ ]),
36
+ ZATCA::UBL::BaseComponent.new(name: "xades:IssuerSerial", elements: [
37
+ ZATCA::UBL::BaseComponent.new(
38
+ name: "ds:X509IssuerName",
39
+ value: @cert_issuer_name
40
+ ),
41
+ ZATCA::UBL::BaseComponent.new(
42
+ name: "ds:X509SerialNumber",
43
+ value: @cert_serial_number
44
+ )
45
+ ])
46
+ ]
47
+ end
48
+ end
@@ -0,0 +1,44 @@
1
+ class ZATCA::UBL::Signing::InvoiceSignedDataReference < ZATCA::UBL::BaseComponent
2
+ attr_accessor :digest_value
3
+
4
+ def initialize(digest_value:)
5
+ super()
6
+ @digest_value = digest_value
7
+ end
8
+
9
+ def name
10
+ "ds:Reference"
11
+ end
12
+
13
+ def attributes
14
+ {
15
+ "Id" => "invoiceSignedData",
16
+ "URI" => ""
17
+ }
18
+ end
19
+
20
+ def elements
21
+ [
22
+ ZATCA::UBL::BaseComponent.new(name: "ds:Transforms", elements: transform_elements),
23
+ ZATCA::UBL::BaseComponent.new(name: "ds:DigestMethod", attributes: {"Algorithm" => "http://www.w3.org/2001/04/xmlenc#sha256"}),
24
+ ZATCA::UBL::BaseComponent.new(name: "ds:DigestValue", value: @digest_value)
25
+ ]
26
+ end
27
+
28
+ private
29
+
30
+ def nested_transform_element(xpath:)
31
+ ZATCA::UBL::BaseComponent.new(name: "ds:Transform", attributes: {"Algorithm" => "http://www.w3.org/TR/1999/REC-xpath-19991116"}, elements: [
32
+ ZATCA::UBL::BaseComponent.new(name: "ds:XPath", value: xpath)
33
+ ])
34
+ end
35
+
36
+ def transform_elements
37
+ [
38
+ nested_transform_element(xpath: "not(//ancestor-or-self::ext:UBLExtensions)"),
39
+ nested_transform_element(xpath: "not(//ancestor-or-self::cac:Signature)"),
40
+ nested_transform_element(xpath: "not(//ancestor-or-self::cac:AdditionalDocumentReference[cbc:ID='QR'])"),
41
+ ZATCA::UBL::BaseComponent.new(name: "ds:Transform", attributes: {"Algorithm" => "http://www.w3.org/2006/12/xml-c14n11"})
42
+ ]
43
+ end
44
+ end
@@ -0,0 +1,25 @@
1
+ class ZATCA::UBL::Signing::KeyInfo < ZATCA::UBL::BaseComponent
2
+ # <ds:KeyInfo>
3
+ # <ds:X509Data>
4
+ # <ds:X509Certificate>MIID9jCCA5ugAwIBAgITbwAAeCy9aKcLA99HrAABAAB4LDAKBggqhkjOPQQDAjBjMRUwEwYKCZImiZPyLGQBGRYFbG9jYWwxEzARBgoJkiaJk/IsZAEZFgNnb3YxFzAVBgoJkiaJk/IsZAEZFgdleHRnYXp0MRwwGgYDVQQDExNUU1pFSU5WT0lDRS1TdWJDQS0xMB4XDTIyMDQxOTIwNDkwOVoXDTI0MDQxODIwNDkwOVowWTELMAkGA1UEBhMCU0ExEzARBgNVBAoTCjMxMjM0NTY3ODkxDDAKBgNVBAsTA1RTVDEnMCUGA1UEAxMeVFNULS05NzA1NjAwNDAtMzEyMzQ1Njc4OTAwMDAzMFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEYYMMoOaFYAhMO/steotfZyavr6p11SSlwsK9azmsLY7b1b+FLhqMArhB2dqHKboxqKNfvkKDePhpqjui5hcn0aOCAjkwggI1MIGaBgNVHREEgZIwgY+kgYwwgYkxOzA5BgNVBAQMMjEtVFNUfDItVFNUfDMtNDdmMTZjMjYtODA2Yi00ZTE1LWIyNjktN2E4MDM4ODRiZTljMR8wHQYKCZImiZPyLGQBAQwPMzEyMzQ1Njc4OTAwMDAzMQ0wCwYDVQQMDAQxMTAwMQwwCgYDVQQaDANUU1QxDDAKBgNVBA8MA1RTVDAdBgNVHQ4EFgQUO5ZiU7NakU3eejVa3I2S1B2sDwkwHwYDVR0jBBgwFoAUdmCM+wagrGdXNZ3PmqynK5k1tS8wTgYDVR0fBEcwRTBDoEGgP4Y9aHR0cDovL3RzdGNybC56YXRjYS5nb3Yuc2EvQ2VydEVucm9sbC9UU1pFSU5WT0lDRS1TdWJDQS0xLmNybDCBrQYIKwYBBQUHAQEEgaAwgZ0wbgYIKwYBBQUHMAGGYmh0dHA6Ly90c3RjcmwuemF0Y2EuZ292LnNhL0NlcnRFbnJvbGwvVFNaRWludm9pY2VTQ0ExLmV4dGdhenQuZ292LmxvY2FsX1RTWkVJTlZPSUNFLVN1YkNBLTEoMSkuY3J0MCsGCCsGAQUFBzABhh9odHRwOi8vdHN0Y3JsLnphdGNhLmdvdi5zYS9vY3NwMA4GA1UdDwEB/wQEAwIHgDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwMwJwYJKwYBBAGCNxUKBBowGDAKBggrBgEFBQcDAjAKBggrBgEFBQcDAzAKBggqhkjOPQQDAgNJADBGAiEA7mHT6yg85jtQGWp3M7tPT7Jk2+zsvVHGs3bU5Z7YE68CIQD60ebQamYjYvdebnFjNfx4X4dop7LsEBFCNSsLY0IFaQ==</ds:X509Certificate>
5
+ # </ds:X509Data>
6
+ # </ds:KeyInfo>
7
+
8
+ def initialize(certificate:)
9
+ super()
10
+
11
+ @certificate = certificate
12
+ end
13
+
14
+ def name
15
+ "ds:KeyInfo"
16
+ end
17
+
18
+ def elements
19
+ [
20
+ ZATCA::UBL::BaseComponent.new(name: "ds:X509Data", elements: [
21
+ ZATCA::UBL::BaseComponent.new(name: "ds:X509Certificate", value: @certificate)
22
+ ])
23
+ ]
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ class ZATCA::UBL::Signing::Object < ZATCA::UBL::BaseComponent
2
+ def initialize(signing_time:, cert_digest_value:, cert_issuer_name:, cert_serial_number:)
3
+ super()
4
+
5
+ @signing_time = signing_time
6
+ @cert_digest_value = cert_digest_value
7
+ @cert_issuer_name = cert_issuer_name
8
+ @cert_serial_number = cert_serial_number
9
+ end
10
+
11
+ def name
12
+ "ds:Object"
13
+ end
14
+
15
+ def elements
16
+ [
17
+ ZATCA::UBL::Signing::QualifyingProperties.new(signing_time: @signing_time, cert_digest_value: @cert_digest_value, cert_issuer_name: @cert_issuer_name, cert_serial_number: @cert_serial_number)
18
+ ]
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ class ZATCA::UBL::Signing::QualifyingProperties < ZATCA::UBL::BaseComponent
2
+ def initialize(signing_time:, cert_digest_value:, cert_issuer_name:, cert_serial_number:)
3
+ super()
4
+
5
+ @signing_time = signing_time
6
+ @cert_digest_value = cert_digest_value
7
+ @cert_issuer_name = cert_issuer_name
8
+ @cert_serial_number = cert_serial_number
9
+ end
10
+
11
+ def name
12
+ "xades:QualifyingProperties"
13
+ end
14
+
15
+ def attributes
16
+ {
17
+ "Target" => "signature",
18
+ "xmlns:xades" => "http://uri.etsi.org/01903/v1.3.2#"
19
+ }
20
+ end
21
+
22
+ def elements
23
+ [
24
+ ZATCA::UBL::Signing::SignedProperties.new(signing_time: @signing_time, cert_digest_value: @cert_digest_value, cert_issuer_name: @cert_issuer_name, cert_serial_number: @cert_serial_number)
25
+ ]
26
+ end
27
+ end