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.
- checksums.yaml +4 -4
- data/README.md +11 -2
- data/bin/console +0 -0
- data/bin/setup +0 -0
- data/lib/zatca/client.rb +173 -0
- data/lib/zatca/hacks.rb +45 -0
- data/lib/zatca/hashing.rb +18 -0
- data/lib/zatca/qr_code_extractor.rb +31 -0
- data/lib/zatca/qr_code_generator.rb +9 -2
- data/lib/zatca/signing/certificate.rb +78 -0
- data/lib/zatca/signing/csr.rb +220 -0
- data/lib/zatca/signing/ecdsa.rb +59 -0
- data/lib/zatca/tag.rb +18 -8
- data/lib/zatca/tags.rb +5 -1
- data/lib/zatca/tags_schema.rb +5 -5
- data/lib/zatca/types.rb +7 -0
- data/lib/zatca/ubl/base_component.rb +142 -0
- data/lib/zatca/ubl/builder.rb +166 -0
- data/lib/zatca/ubl/common_aggregate_components/allowance_charge.rb +64 -0
- data/lib/zatca/ubl/common_aggregate_components/classified_tax_category.rb +25 -0
- data/lib/zatca/ubl/common_aggregate_components/delivery.rb +27 -0
- data/lib/zatca/ubl/common_aggregate_components/invoice_line.rb +63 -0
- data/lib/zatca/ubl/common_aggregate_components/item.rb +21 -0
- data/lib/zatca/ubl/common_aggregate_components/legal_monetary_total.rb +59 -0
- data/lib/zatca/ubl/common_aggregate_components/party.rb +28 -0
- data/lib/zatca/ubl/common_aggregate_components/party_identification.rb +25 -0
- data/lib/zatca/ubl/common_aggregate_components/party_legal_entity.rb +19 -0
- data/lib/zatca/ubl/common_aggregate_components/party_tax_scheme.rb +30 -0
- data/lib/zatca/ubl/common_aggregate_components/postal_address.rb +59 -0
- data/lib/zatca/ubl/common_aggregate_components/price.rb +20 -0
- data/lib/zatca/ubl/common_aggregate_components/tax_category.rb +56 -0
- data/lib/zatca/ubl/common_aggregate_components/tax_total.rb +58 -0
- data/lib/zatca/ubl/common_aggregate_components.rb +2 -0
- data/lib/zatca/ubl/invoice.rb +481 -0
- data/lib/zatca/ubl/invoice_subtype_builder.rb +50 -0
- data/lib/zatca/ubl/signing/cert.rb +48 -0
- data/lib/zatca/ubl/signing/invoice_signed_data_reference.rb +44 -0
- data/lib/zatca/ubl/signing/key_info.rb +25 -0
- data/lib/zatca/ubl/signing/object.rb +20 -0
- data/lib/zatca/ubl/signing/qualifying_properties.rb +27 -0
- data/lib/zatca/ubl/signing/signature.rb +50 -0
- data/lib/zatca/ubl/signing/signature_information.rb +19 -0
- data/lib/zatca/ubl/signing/signature_properties_reference.rb +26 -0
- data/lib/zatca/ubl/signing/signed_info.rb +21 -0
- data/lib/zatca/ubl/signing/signed_properties.rb +81 -0
- data/lib/zatca/ubl/signing/signed_signature_properties.rb +23 -0
- data/lib/zatca/ubl/signing/ubl_document_signatures.rb +25 -0
- data/lib/zatca/ubl/signing/ubl_extension.rb +22 -0
- data/lib/zatca/ubl/signing/ubl_extensions.rb +17 -0
- data/lib/zatca/ubl/signing.rb +2 -0
- data/lib/zatca/ubl.rb +2 -0
- data/lib/zatca/version.rb +1 -1
- data/lib/zatca.rb +27 -3
- data/zatca.gemspec +52 -0
- metadata +165 -10
- 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
|