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,59 @@
1
+ require "starkbank-ecdsa"
2
+
3
+ class ZATCA::Signing::ECDSA
4
+ def self.sign(content:, private_key: nil, private_key_path: nil, decode_from_base64: false)
5
+ private_key = parse_private_key(key: private_key, key_path: private_key_path, decode_from_base64: decode_from_base64)
6
+
7
+ ecdsa_signature = EllipticCurve::Ecdsa.sign(content, private_key)
8
+
9
+ {
10
+ base64: ecdsa_signature.toBase64,
11
+ bytes: ecdsa_signature.toDer
12
+ }
13
+ end
14
+
15
+ def self.add_header_blocks(key_content)
16
+ # If key is missing header blocks, add them, otherwise return it as is
17
+ header = "-----BEGIN EC PRIVATE KEY-----"
18
+ footer = "-----END EC PRIVATE KEY-----"
19
+
20
+ unless key_content.include?(header) && key_content.include?(footer)
21
+ key_content = "#{header}\n#{key_content}\n#{footer}"
22
+ end
23
+
24
+ key_content
25
+ end
26
+
27
+ def self.read_private_key_from_pem(pem)
28
+ EllipticCurve::PrivateKey.fromPem(add_header_blocks(pem))
29
+ end
30
+
31
+ # Returns a parsed private key
32
+ # If decode_from_base64 is set to true, the key will be decoded from base64
33
+ # before passing to OpenSSL to read it. # This is necessary because that's how
34
+ # ZATCA's sample key is provided.
35
+ def self.parse_private_key(key: nil, key_path: nil, decode_from_base64: false)
36
+ parsed_key = if key.is_a?(EllipticCurve::PrivateKey)
37
+ key
38
+ elsif key.is_a?(String)
39
+ key_content = if decode_from_base64
40
+ Base64.decode64(key)
41
+ else
42
+ key
43
+ end
44
+
45
+ read_private_key_from_pem(key_content)
46
+ elsif key_path.present?
47
+ key_content = File.read(key_path)
48
+ key_content = Base64.decode64(key_content) if decode_from_base64
49
+
50
+ read_private_key_from_pem(key_content)
51
+ end
52
+
53
+ if parsed_key.blank?
54
+ raise ArgumentError.new("private_key or private_key_path is required")
55
+ end
56
+
57
+ parsed_key
58
+ end
59
+ end
data/lib/zatca/tag.rb CHANGED
@@ -5,9 +5,21 @@ module ZATCA
5
5
  vat_registration_number: 2,
6
6
  timestamp: 3,
7
7
  invoice_total: 4,
8
- vat_total: 5
8
+ vat_total: 5,
9
+ xml_invoice_hash: 6,
10
+ ecdsa_signature: 7,
11
+ ecdsa_public_key: 8,
12
+ ecdsa_stamp_signature: 9 # TODO: is this needed ?
9
13
  }.freeze
10
14
 
15
+ PHASE_1_TAGS = [
16
+ :seller_name,
17
+ :vat_registration_number,
18
+ :timestamp,
19
+ :invoice_total,
20
+ :vat_total
21
+ ].freeze
22
+
11
23
  attr_accessor :id, :key, :value
12
24
 
13
25
  def initialize(key:, value:)
@@ -20,14 +32,12 @@ module ZATCA
20
32
  {id: @id, key: @key, value: @value}
21
33
  end
22
34
 
23
- def to_tlv
24
- # TLV should be concatenated together without any separator in the following
25
- # format: character_value_of_id character_value_of_value_length value_itself
26
- # All of this should be in 8-bit ASCII.
27
- tlv = @id.chr + @value.bytesize.chr + value
35
+ def should_be_utf8_encoded?
36
+ PHASE_1_TAGS.include?(key)
37
+ end
28
38
 
29
- # We need to use force_encoding because encode will raise errors when
30
- # trying to encode a string with utf-8 characters.
39
+ def to_tlv
40
+ tlv = @id.chr + @value.bytesize.chr + @value
31
41
  tlv.force_encoding("ASCII-8BIT")
32
42
  end
33
43
  end
data/lib/zatca/tags.rb CHANGED
@@ -33,8 +33,12 @@ module ZATCA
33
33
  Base64.strict_encode64(to_tlv)
34
34
  end
35
35
 
36
- private
36
+ # This is helpful for debugging only, for ZATCA's requirements just call `to_base64`
37
+ def to_hex_tlv
38
+ to_tlv.unpack1("H*")
39
+ end
37
40
 
41
+ # This is helpful for debugging only, for ZATCA's requirements just call `to_base64`
38
42
  def to_tlv
39
43
  @tags.map(&:to_tlv).join("")
40
44
  end
@@ -13,10 +13,10 @@ module ZATCA
13
13
  required(:invoice_total).filled(:string)
14
14
  required(:vat_total).filled(:string)
15
15
 
16
- # TODO: Data types required by 1 January 2023
17
- # - Hash of XML Invoice
18
- # - ECDSA signature
19
- # - ECDSA public key
20
- # - ECDSA signature of the cryptographic stamp’s public key by ZATCA’s technical CA
16
+ # Data types required for Phase 2 by 1 January 2023
17
+ optional(:xml_invoice_hash).filled(:string)
18
+ optional(:ecdsa_signature).filled(:string)
19
+ optional(:ecdsa_public_key).filled(:string)
20
+ optional(:ecdsa_stamp_signature).filled(:string)
21
21
  end
22
22
  end
@@ -0,0 +1,7 @@
1
+ require "dry-types"
2
+
3
+ module ZATCA
4
+ module Types
5
+ include Dry.Types()
6
+ end
7
+ end
@@ -0,0 +1,142 @@
1
+ require "dry-initializer"
2
+ require_relative "../types"
3
+
4
+ class ZATCA::UBL::BaseComponent
5
+ extend Dry::Initializer
6
+ attr_accessor :elements, :attributes, :name, :value, :index
7
+
8
+ # def self.guard_dig(obj)
9
+ # unless obj.respond_to?(:dig)
10
+ # raise TypeError, "#{obj.class.name} does not have #dig method"
11
+ # end
12
+ # end
13
+
14
+ ArrayOfBaseComponentOrNil = ZATCA::Types::Array.of(ZATCA::Types.Instance(ZATCA::UBL::BaseComponent))
15
+ .default { [] }
16
+ .constructor { |value| value.blank? ? [] : value.compact }
17
+
18
+ option :elements,
19
+ type: ArrayOfBaseComponentOrNil,
20
+ default: proc { [] },
21
+ optional: true
22
+
23
+ option :attributes, type: ZATCA::Types::Strict::Hash, default: proc { {} }, optional: true
24
+ option :value, type: ZATCA::Types::Coercible::String, default: proc { "" }, optional: true
25
+ option :name, type: ZATCA::Types::Strict::String, default: proc { "" }, optional: true
26
+ option :index, type: ZATCA::Types::Coercible::Integer.optional, default: proc {}, optional: true
27
+
28
+ # def initialize(elements: [], attributes: {}, value: "", name: "", index: nil)
29
+ # @elements = elements
30
+ # @attributes = attributes
31
+ # @value = value
32
+ # @name = name
33
+
34
+ # # HACK: Add a nil index property to be used for cases where we need
35
+ # # sequential IDs, this list can be populated after the array is built
36
+ # @index = index
37
+ # end
38
+
39
+ # There are cases where we end up constructing elements with no content
40
+ # and we don't want to include them in the final XML.
41
+ #
42
+ # This method helps us to return nil if the element has no attributes,
43
+ # elements or value.
44
+ #
45
+ # which is then caught in the `build_xml` method (using `elements.compact`)
46
+ # and ignored.
47
+ def self.build(elements: [], attributes: {}, value: "", name: "", index: nil)
48
+ return nil if elements.blank? && attributes.blank? && value.blank?
49
+
50
+ new(elements: elements, attributes: attributes, value: value, name: name, index: index)
51
+ end
52
+
53
+ def [](name)
54
+ elements.find { |element| element.name == name }
55
+ end
56
+
57
+ def dig(key, *args)
58
+ value = self[key]
59
+ return value if args.length == 0 || value.nil?
60
+ # DigRb.guard_dig(value)
61
+ value.dig(*args)
62
+ end
63
+
64
+ # TODO: Under Active Development
65
+ def find_nested_element_by_path(path)
66
+ path_parts = path.split("/")
67
+ nested_element = self
68
+
69
+ path_parts.each_with_index do |path_part, index|
70
+ # byebug
71
+ # next_path_part = path_parts[index + 1]
72
+ # found_element = nested_element[path_part]
73
+ # found_next_path_part = found_element[next_path_part]
74
+
75
+ # element_with_next_path_part = found_element.find do |child_element|
76
+ # child_element.name == next_path_part
77
+ # end
78
+ # byebug
79
+ # nested_element.elements.each do |element|
80
+ # byebug
81
+ # next_element = element[path_part]
82
+
83
+ # if next_element && next_element[next_path_part]
84
+ # nested_element = next_element[next_path_part]
85
+ # break
86
+ # elsif next_element.present?
87
+ # nested_element = next_element
88
+ # end
89
+ # end
90
+
91
+ # nested_element = found_element if found_element.present?
92
+ end
93
+
94
+ nested_element
95
+ end
96
+
97
+ def schema
98
+ self.class.schema
99
+ end
100
+
101
+ def to_h
102
+ {
103
+ name => {
104
+ attributes: attributes,
105
+ **elements.map(&:to_h),
106
+ value: value
107
+ }
108
+ }
109
+ end
110
+
111
+ def to_xml
112
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
113
+ xml.root { build_xml(xml) }
114
+ end
115
+
116
+ builder.to_xml
117
+ end
118
+
119
+ def build_xml(xml)
120
+ xml.send(name, attributes) do
121
+ if elements.length > 0
122
+ elements.compact.each { |element| element.build_xml(xml) }
123
+ elsif value.present?
124
+ xml.text(value)
125
+ end
126
+ end
127
+ end
128
+
129
+ def generate_xml(
130
+ canonicalized: false,
131
+ spaces: 2,
132
+ apply_invoice_hacks: false,
133
+ remove_root_xml_tag: false
134
+ )
135
+ ZATCA::UBL::Builder.new(element: self).build(
136
+ canonicalized: canonicalized,
137
+ spaces: spaces,
138
+ apply_invoice_hacks: apply_invoice_hacks,
139
+ remove_root_xml_tag: remove_root_xml_tag
140
+ )
141
+ end
142
+ end
@@ -0,0 +1,166 @@
1
+ class ZATCA::UBL::Builder
2
+ extend Dry::Initializer
3
+
4
+ option :element, type: ZATCA::Types.Instance(ZATCA::UBL::BaseComponent)
5
+
6
+ def build(
7
+ canonicalized: false,
8
+ spaces: 4,
9
+ apply_invoice_hacks: false,
10
+ remove_root_xml_tag: false
11
+ )
12
+ @remove_root_xml_tag = remove_root_xml_tag
13
+
14
+ builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
15
+ element.build_xml(xml)
16
+ end
17
+
18
+ xml = if canonicalized
19
+ canonicalized_xml(builder: builder)
20
+ else
21
+ uncanonicalized_xml(builder: builder, spaces: spaces)
22
+ end
23
+
24
+ xml = apply_hacks_to_invoice(element, xml) if apply_invoice_hacks
25
+
26
+ xml
27
+ end
28
+
29
+ private
30
+
31
+ # ZATCA sadly requires very specific and unconventional indentation in the XML
32
+ # when it is pretty (uncanonicalized), the only way we can accomplish this is
33
+ # to find and replace blocks manually.
34
+ def apply_hacks_to_invoice(element, xml)
35
+ return xml unless element.is_a?(ZATCA::UBL::Invoice)
36
+
37
+ apply_qualifying_properties_hacks(element, xml)
38
+ end
39
+
40
+ def apply_qualifying_properties_hacks(invoice, xml)
41
+ return xml if invoice.qualifying_properties.blank?
42
+
43
+ regex = ZATCA::Hacks.qualifying_properties_regex
44
+
45
+ xml.gsub(regex, invoice.qualifying_properties)
46
+ end
47
+
48
+ # This function does not produce canonicalization matching C14N 1.1, it applies
49
+ # C14N 1.1 then manually adds back the whitespace in the format that ZATCA
50
+ # expects.
51
+ def canonicalized_xml(builder:)
52
+ builder.doc.canonicalize(Nokogiri::XML::XML_C14N_1_1)
53
+
54
+ # TODO: In case ZATCA ever asks us to use their whitespace format again.
55
+ # In some meetings they say we have to use it, in some meetings they say
56
+ # we don't. The simpler approach is that we don't use it.
57
+ #
58
+ # ZATCA's docs specifically state we must use C14N 1.1 canonicalization.
59
+ # xml = uncanonicalized_xml(builder: builder, spaces: 4)
60
+ # xml_doc = Nokogiri::XML(xml)
61
+
62
+ # canonical_xml = xml_doc.canonicalize(Nokogiri::XML::XML_C14N_1_1)
63
+
64
+ # canonical_xml
65
+ end
66
+
67
+ def uncanonicalized_xml(builder:, spaces: 4)
68
+ builder.to_xml(indent: spaces.to_i)
69
+
70
+ # xml = builder.to_xml(indent: spaces.to_i)
71
+ # xml = match_xml_string_to_zatca_whitespaces(xml)
72
+ # xml
73
+ end
74
+
75
+ def match_xml_string_to_zatca_whitespaces(xml)
76
+ # ZATCA has elements that are not spaced by multiples of 4, and random new
77
+ # lines with trailing whitespaces, so we need to manually adjust our
78
+ # indentation to match ZATCA's.
79
+ zatca_weird_whitespaces.each do |whitespace_hash|
80
+ xml.gsub!(whitespace_hash[:our_version], whitespace_hash[:zatca_version])
81
+ end
82
+
83
+ # Canonicalization already removes the root XML tag for us, but since we had
84
+ # to create a new uncanonicalized document for ZATCA's invoice hacks, we
85
+ # have to remove it manually.
86
+ if @remove_root_xml_tag
87
+ xml.gsub!("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n", "")
88
+ end
89
+
90
+ # ZATCA Removes the final newline character, so we do the same
91
+ xml.chomp
92
+
93
+ # This part is not clear, ZATCA shared documents with us that use CRLF
94
+ # but the samples in the SDK use LF, so we're not sure which one is correct.
95
+ # ZATCA wants CRLF (\r\n) in their canonicalized form instead of just LF (\n)
96
+ xml.gsub!("\n", "\r\n")
97
+
98
+ xml
99
+ end
100
+
101
+ # Not sure if this is needed, in some meetings ZATCA says you have to match
102
+ # their whitspace exactly and in some meetings they say you don't.
103
+ # HACK: This is really hacky, using regexes or XPaths would be better, but
104
+ # that wasn't easy to build and maintain, so we're using this if/until we run
105
+ # into issues.
106
+ #
107
+ # We may eventually go to an approach where we just have hardcoded XML in each
108
+ # element's file and we just add in the values instead of generating our own
109
+ # XML if this gets too hard to maintain.
110
+ def zatca_weird_whitespaces
111
+ @_zatca_weird_whitespaces ||= [
112
+ {
113
+ our_version: "<cbc:ProfileID>",
114
+ zatca_version: "\n <cbc:ProfileID>"
115
+ },
116
+ {
117
+ our_version: "<cac:AccountingSupplierParty>",
118
+ zatca_version: "\n \n <cac:AccountingSupplierParty>"
119
+ },
120
+ {
121
+ our_version: " <cbc:CompanyID>",
122
+ zatca_version: " <cbc:CompanyID>"
123
+ },
124
+ {
125
+ our_version: " <cac:TaxCategory>",
126
+ zatca_version: " <cac:TaxCategory>"
127
+ },
128
+ {
129
+ our_version: " </cac:TaxCategory>",
130
+ zatca_version: " </cac:TaxCategory>"
131
+ },
132
+ {
133
+ our_version: ' <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>',
134
+ zatca_version: ' <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>'
135
+ },
136
+ {
137
+ our_version: "<cac:TaxScheme>\n <cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5153\">VAT</cbc:ID>",
138
+ zatca_version: "<cac:TaxScheme>\n <cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5153\">VAT</cbc:ID>"
139
+ },
140
+ {
141
+ our_version: "<cac:TaxTotal>\n <cbc:TaxAmount currencyID=\"SAR\">",
142
+ zatca_version: "<cac:TaxTotal>\n <cbc:TaxAmount currencyID=\"SAR\">"
143
+ },
144
+ {
145
+ our_version: " <cbc:RoundingAmount currencyID=\"SAR\">",
146
+ zatca_version: " <cbc:RoundingAmount currencyID=\"SAR\">"
147
+ },
148
+ {
149
+ our_version: " <cbc:ChargeIndicator>",
150
+ zatca_version: " <cbc:ChargeIndicator>"
151
+ },
152
+ {
153
+ our_version: " <cbc:AllowanceChargeReason>",
154
+ zatca_version: " <cbc:AllowanceChargeReason>"
155
+ },
156
+ {
157
+ our_version: " <cbc:Amount currencyID=\"SAR\">",
158
+ zatca_version: " <cbc:Amount currencyID=\"SAR\">"
159
+ },
160
+ {
161
+ our_version: "<cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5305\">S</cbc:ID>\n <cbc:Percent>",
162
+ zatca_version: "<cbc:ID schemeAgencyID=\"6\" schemeID=\"UN/ECE 5305\">S</cbc:ID>\n <cbc:Percent>"
163
+ }
164
+ ]
165
+ end
166
+ end
@@ -0,0 +1,64 @@
1
+ class ZATCA::UBL::CommonAggregateComponents::AllowanceCharge < ZATCA::UBL::BaseComponent
2
+ attr_accessor :id, :charge_indicator, :allowance_charge_reason, :amount
3
+
4
+ # <cac:AllowanceCharge>
5
+ # <cbc:ID>1</cbc:ID>
6
+ # <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
7
+ # <cbc:AllowanceChargeReason>discount</cbc:AllowanceChargeReason>
8
+ # <cbc:Amount currencyID="SAR">2</cbc:Amount>
9
+ # <cac:TaxCategory>
10
+ # <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5305">S</cbc:ID>
11
+ # <cbc:Percent>15</cbc:Percent>
12
+ # <cac:TaxScheme>
13
+ # <cbc:ID schemeAgencyID="6" schemeID="UN/ECE 5153">VAT</cbc:ID>
14
+ # </cac:TaxScheme>
15
+ # </cac:TaxCategory>
16
+ # </cac:AllowanceCharge>
17
+
18
+ def initialize(
19
+ charge_indicator:, allowance_charge_reason:, amount:, currency_id: "SAR",
20
+ tax_category: nil, add_tax_category: true, add_id: true, tax_categories: []
21
+ )
22
+ super()
23
+
24
+ @charge_indicator = charge_indicator.to_s
25
+
26
+ @allowance_charge_reason = allowance_charge_reason
27
+ @amount = amount
28
+ @currency_id = currency_id
29
+
30
+ @add_tax_category = add_tax_category
31
+ @tax_category = tax_category
32
+ @add_id = add_id
33
+
34
+ if add_tax_category && @tax_category.blank?
35
+ @tax_category = ZATCA::UBL::CommonAggregateComponents::TaxCategory.new
36
+ end
37
+
38
+ @tax_categories = if @tax_category.present? && tax_categories.blank?
39
+ [@tax_category]
40
+ else
41
+ tax_categories
42
+ end
43
+ end
44
+
45
+ def name
46
+ "cac:AllowanceCharge"
47
+ end
48
+
49
+ def id_element
50
+ if @add_id && index.present?
51
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: index)
52
+ end
53
+ end
54
+
55
+ def elements
56
+ [
57
+ id_element,
58
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ChargeIndicator", value: @charge_indicator),
59
+ ZATCA::UBL::BaseComponent.new(name: "cbc:AllowanceChargeReason", value: @allowance_charge_reason),
60
+ ZATCA::UBL::BaseComponent.new(name: "cbc:Amount", value: @amount, attributes: {"currencyID" => @currency_id}),
61
+ *@tax_categories
62
+ ]
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ class ZATCA::UBL::CommonAggregateComponents::ClassifiedTaxCategory < ZATCA::UBL::BaseComponent
2
+ attr_reader :scheme_id, :id
3
+
4
+ def initialize(id: "S", percent: "15.00", tax_scheme_id: "VAT")
5
+ super()
6
+
7
+ @id = id
8
+ @percent = percent
9
+ @tax_scheme_id = tax_scheme_id
10
+ end
11
+
12
+ def name
13
+ "cac:ClassifiedTaxCategory"
14
+ end
15
+
16
+ def elements
17
+ [
18
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: @id),
19
+ ZATCA::UBL::BaseComponent.new(name: "cbc:Percent", value: @percent),
20
+ ZATCA::UBL::BaseComponent.new(name: "cac:TaxScheme", elements: [
21
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: @tax_scheme_id)
22
+ ])
23
+ ]
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ class ZATCA::UBL::CommonAggregateComponents::Delivery < ZATCA::UBL::BaseComponent
2
+ attr_reader :scheme_id, :id
3
+
4
+ def initialize(actual_delivery_date:, latest_delivery_date: nil)
5
+ super()
6
+
7
+ @latest_delivery_date = latest_delivery_date
8
+ @actual_delivery_date = actual_delivery_date
9
+ end
10
+
11
+ def name
12
+ "cac:Delivery"
13
+ end
14
+
15
+ def latest_delivery_date_element
16
+ return nil if @latest_delivery_date.nil?
17
+
18
+ ZATCA::UBL::BaseComponent.new(name: "cbc:LatestDeliveryDate", value: @latest_delivery_date)
19
+ end
20
+
21
+ def elements
22
+ [
23
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ActualDeliveryDate", value: @actual_delivery_date),
24
+ latest_delivery_date_element
25
+ ]
26
+ end
27
+ end
@@ -0,0 +1,63 @@
1
+ class ZATCA::UBL::CommonAggregateComponents::InvoiceLine < ZATCA::UBL::BaseComponent
2
+ # <cac:InvoiceLine>
3
+ # <cbc:ID>1</cbc:ID>
4
+ # <cbc:InvoicedQuantity unitCode="PCE">44.000000</cbc:InvoicedQuantity>
5
+ # <cbc:LineExtensionAmount currencyID="SAR">966.00</cbc:LineExtensionAmount>
6
+ # <cac:TaxTotal>
7
+ # <cbc:TaxAmount currencyID="SAR">144.90</cbc:TaxAmount>
8
+ # <cbc:RoundingAmount currencyID="SAR">1110.90</cbc:RoundingAmount>
9
+
10
+ # </cac:TaxTotal>
11
+ # <cac:Item>
12
+ # <cbc:Name>dsd</cbc:Name>
13
+ # <cac:ClassifiedTaxCategory>
14
+ # <cbc:ID>S</cbc:ID>
15
+ # <cbc:Percent>15.00</cbc:Percent>
16
+ # <cac:TaxScheme>
17
+ # <cbc:ID>VAT</cbc:ID>
18
+ # </cac:TaxScheme>
19
+ # </cac:ClassifiedTaxCategory>
20
+ # </cac:Item>
21
+ # <cac:Price>
22
+ # <cbc:PriceAmount currencyID="SAR">22.00</cbc:PriceAmount>
23
+ # <cac:AllowanceCharge>
24
+ # <cbc:ChargeIndicator>false</cbc:ChargeIndicator>
25
+ # <cbc:AllowanceChargeReason>discount</cbc:AllowanceChargeReason>
26
+ # <cbc:Amount currencyID="SAR">2.00</cbc:Amount>
27
+ # </cac:AllowanceCharge>
28
+ # </cac:Price>
29
+ # </cac:InvoiceLine>
30
+
31
+ def initialize(
32
+ invoiced_quantity:, invoiced_quantity_unit_code:,
33
+ line_extension_amount:, tax_total:, item:,
34
+ price:, allowance_charge: nil, currency_id: "SAR"
35
+ )
36
+ super()
37
+
38
+ @invoiced_quantity = invoiced_quantity
39
+ @invoiced_quantity_unit_code = invoiced_quantity_unit_code
40
+ @line_extension_amount = line_extension_amount
41
+ @tax_total = tax_total
42
+ @item = item
43
+ @price = price
44
+ @allowance_charge = allowance_charge
45
+ @currency_id = currency_id
46
+ end
47
+
48
+ def name
49
+ "cac:InvoiceLine"
50
+ end
51
+
52
+ def elements
53
+ [
54
+ ZATCA::UBL::BaseComponent.new(name: "cbc:ID", value: index),
55
+ ZATCA::UBL::BaseComponent.new(name: "cbc:InvoicedQuantity", value: @invoiced_quantity, attributes: {unitCode: @invoiced_quantity_unit_code}),
56
+ ZATCA::UBL::BaseComponent.new(name: "cbc:LineExtensionAmount", value: @line_extension_amount, attributes: {currencyID: @currency_id}),
57
+ @tax_total,
58
+ @item,
59
+ @price,
60
+ @allowance_charge
61
+ ]
62
+ end
63
+ end
@@ -0,0 +1,21 @@
1
+ class ZATCA::UBL::CommonAggregateComponents::Item < ZATCA::UBL::BaseComponent
2
+ attr_reader :scheme_id, :id
3
+
4
+ def initialize(name:, classified_tax_category: nil)
5
+ super()
6
+
7
+ @name = name
8
+ @classified_tax_category = classified_tax_category || ZATCA::UBL::CommonAggregateComponents::ClassifiedTaxCategory.new
9
+ end
10
+
11
+ def name
12
+ "cac:Item"
13
+ end
14
+
15
+ def elements
16
+ [
17
+ ZATCA::UBL::BaseComponent.new(name: "cbc:Name", value: @name),
18
+ @classified_tax_category
19
+ ]
20
+ end
21
+ end