-
Notifications
You must be signed in to change notification settings - Fork 16
Constructing a Simplified Invoice
Omar Bahareth edited this page Jun 14, 2023
·
17 revisions
For simplified invoices(B2C), you need to sign and create the QR-code yourself. For standard invoices (B2B), you use ZATCA's clearance API which signs and adds the QR Code for you.
#===============================================================================
# 1. Setup the Invoice Values
#===============================================================================
invoice_id = "SME00010"
invoice_uuid = "8e6000cf-1a98-4174-b3e7-b5d5954bc10d"
note = "ABC"
note_language_id = "ar"
issue_date = "2022-08-17"
issue_time = "17:41:08"
invoice_subtype = ZATCA::UBL::InvoiceSubtypeBuilder.build(
simplified: true,
third_party: false,
nominal: false,
exports: false,
summary: false,
self_billed: false
) # => "0200000"
payment_means_code = ZATCA::UBL::Invoice::PAYMENT_MEANS[:bank_card] # => "48"
invoice_type_code_value = ZATCA::UBL::Invoice::INVOICE_TYPE_CODES[:invoice] # => "388"
invoice_counter_value = "10"
previous_invoice_hash = "NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ=="
currency_code = "SAR"
vat_registration_number = "311111111101113"
#===============================================================================
# 2. Setup the Invoice's Nested Properties
#===============================================================================
# Create the supplier party (the party issuing the invoice, e.g. the seller)
accounting_supplier_party = ZATCA::UBL::CommonAggregateComponents::Party.new(
party_identification: ZATCA::UBL::CommonAggregateComponents::PartyIdentification.new(
id: "324223432432432"
),
postal_address: ZATCA::UBL::CommonAggregateComponents::PostalAddress.new(
street_name: "الامير سلطان",
additional_street_name: nil,
building_number: "3242",
plot_identification: "4323",
city_subdivision_name: "32423423",
city_name: "الرياض | Riyadh",
postal_zone: "32432",
country_subentity: nil,
country_identification_code: "SA"
),
party_tax_scheme: ZATCA::UBL::CommonAggregateComponents::PartyTaxScheme.new(
company_id: vat_registration_number
),
party_legal_entity: ZATCA::UBL::CommonAggregateComponents::PartyLegalEntity.new(
registration_name: "Acme Widgets LTD"
)
)
# Create the customer party (the party receiving the invoice, e.g. the buyer)
accounting_customer_party = ZATCA::UBL::CommonAggregateComponents::Party.new(
# party_identification: ZATCA::UBL::CommonAggregateComponents::PartyIdentification.new(
# id: "2345",
# scheme_id: "NAT"
# ),
party_identification: nil,
postal_address: ZATCA::UBL::CommonAggregateComponents::PostalAddress.new(
street_name: nil,
additional_street_name: nil,
building_number: nil,
plot_identification: nil,
city_subdivision_name: "32423423",
city_name: nil,
postal_zone: nil,
country_subentity: nil,
country_identification_code: "SA"
),
party_tax_scheme: ZATCA::UBL::CommonAggregateComponents::PartyTaxScheme.new,
party_legal_entity: nil
)
# Create the (optional) delivery object (detailing the delivery date and time)
# delivery = ZATCA::UBL::CommonAggregateComponents::Delivery.new(
# actual_delivery_date: "2022-03-13",
# latest_delivery_date: "2022-03-15"
# )
delivery = nil
# Create the allowance charges (e.g. discounts) for the invoice
allowance_charges = [
ZATCA::UBL::CommonAggregateComponents::AllowanceCharge.new(
charge_indicator: false,
amount: "0.00",
allowance_charge_reason: "discount",
currency_id: "SAR",
tax_categories: [
# Yes, ZATCA's official valid sample duplicates these, not sure why
ZATCA::UBL::CommonAggregateComponents::TaxCategory.new(
tax_percent: "15"
),
ZATCA::UBL::CommonAggregateComponents::TaxCategory.new(
tax_percent: "15"
)
]
)
]
# Create the tax totals for the invoice
# ZATCA's official valid sample has two of these, not sure why
tax_totals = [
ZATCA::UBL::CommonAggregateComponents::TaxTotal.new(
tax_amount: "30.15"
),
ZATCA::UBL::CommonAggregateComponents::TaxTotal.new(
tax_amount: "30.15",
tax_subtotal_amount: "30.15",
taxable_amount: "201.00",
tax_category: ZATCA::UBL::CommonAggregateComponents::TaxCategory.new(
tax_percent: "15.00"
)
)
]
# Create the legal monetary total for the invoice
legal_monetary_total = ZATCA::UBL::CommonAggregateComponents::LegalMonetaryTotal.new(
line_extension_amount: "201.00",
tax_exclusive_amount: "201.00",
tax_inclusive_amount: "231.15",
allowance_total_amount: "0.00",
prepaid_amount: "0.00",
payable_amount: "231.15"
)
# Create the invoice lines for the invoice (the list of items that were sold)
invoice_lines = [
# Book
ZATCA::UBL::CommonAggregateComponents::InvoiceLine.new(
invoiced_quantity: "33.000000",
invoiced_quantity_unit_code: "PCE",
line_extension_amount: "99.00",
tax_total: ZATCA::UBL::CommonAggregateComponents::TaxTotal.new(
tax_amount: "14.85",
rounding_amount: "113.85"
),
item: ZATCA::UBL::CommonAggregateComponents::Item.new(
name: "كتاب"
),
price: ZATCA::UBL::CommonAggregateComponents::Price.new(
price_amount: "3.00",
allowance_charge: ZATCA::UBL::CommonAggregateComponents::AllowanceCharge.new(
charge_indicator: false,
allowance_charge_reason: "discount",
amount: "0.00",
add_tax_category: false,
# ZATCA's samples can sometimes have a nested tax scheme with an ID
# and sometimes they omit it. Setting this boolean controls whether it is
# present or not
add_id: false
)
)
),
# Pen
ZATCA::UBL::CommonAggregateComponents::InvoiceLine.new(
invoiced_quantity: "3.000000",
invoiced_quantity_unit_code: "PCE",
line_extension_amount: "102.00",
tax_total: ZATCA::UBL::CommonAggregateComponents::TaxTotal.new(
tax_amount: "15.30",
rounding_amount: "117.30"
),
item: ZATCA::UBL::CommonAggregateComponents::Item.new(
name: "قلم"
),
price: ZATCA::UBL::CommonAggregateComponents::Price.new(
price_amount: "34.00",
allowance_charge: ZATCA::UBL::CommonAggregateComponents::AllowanceCharge.new(
charge_indicator: false,
allowance_charge_reason: "discount",
amount: "0.00",
add_tax_category: false,
add_id: false
)
)
)
]
#===============================================================================
# 3. Construct the Invoice
#===============================================================================
# Construct the invoice using all of the above
invoice = ZATCA::UBL::Invoice.new(
add_ids_to_allowance_charges: false,
id: invoice_id,
uuid: invoice_uuid,
issue_date: issue_date,
issue_time: issue_time,
invoice_type_mask: invoice_type_mask,
invoice_type_code_value: invoice_type_code_value,
invoice_counter_value: invoice_counter_value,
previous_invoice_hash: previous_invoice_hash,
accounting_supplier_party: accounting_supplier_party,
accounting_customer_party: accounting_customer_party,
delivery: delivery,
payment_means_code: payment_means_code,
allowance_charges: allowance_charges,
tax_totals: tax_totals,
legal_monetary_total: legal_monetary_total,
invoice_lines: invoice_lines,
currency_code: currency_code,
note: note,
note_language_id: note_language_id
)
#===============================================================================
# 4. Sign the Invoice (ONLY FOR SIMPLIFIED INVOICES)
#===============================================================================
# Must not be encoded in Base64 and must have header blocks
private_key_path = "path/to_your/private.key"
# Must be in pem format with header blocks
certificate_path = "path/to_your/certificate.pem"
# You can omit this if you don't need to customize it,
# the default value is the same as you see here.
signing_time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
# Sign the invoice (this adds a couple of new elements to the invoice)
invoice.sign(
private_key_path: private_key_path,
certificate_path: certificate_path,
signing_time: signing_time
)
# See the section under this code block if you need more
# control over the signing process.
#===============================================================================
# 5. Create the QR Code (ONLY FOR SIMPLIFIED INVOICES)
#===============================================================================
invoice_hash = invoice.generate_hash
# Create the tags
tags = ZATCA::Tags.new({
seller_name: "Acme Widgets LTD",
vat_registration_number: "311111111101113",
timestamp: "2022-08-17T17:41:08Z",
vat_total: "30.15",
invoice_total: "231.15",
xml_invoice_hash: invoice_hash[:base64],
# These 3 properties on the invoice assume you have signed it before.
# They are nil unless signed.
ecdsa_signature: invoice.signed_hash,
ecdsa_public_key: invoice.certificate_public_key_bytes,
ecdsa_stamp_signature: invoice.certificate_signature
})
# Turn the tags to TLV and then encode that TLV to base64
invoice.qr_code = tags.to_base64
#===============================================================================
# Done! You now have an invoice constructed and signed
#===============================================================================
Use this approach if you need more control over signing the invoice or reading keys and certificates
# Returns a hash with the invoice's SHA-256 hash and the Base64 version of it
# in the format {hash: "SHA-256 hash", base64: "Base64 version of the hash"}
invoice_hash = invoice.generate_hash
# Parse the private key
# ZATCA samples have the private key encoded in Base64, you can pass
# decode_from_base64: true if your key is in a similar format to theirs.
# Otherwise if your key is in a PEM format (with header blocks), you can omit
# the option or set it to false.
private_key_path = "path/to_your/private.key"
private_key = ZATCA::Signing::Encrypting.parse_private_key(
key_path: private_key_path,
decode_from_base64: false
)
# Sign the invoice hash using the private key
signature = ZATCA::Signing::Encrypting.encrypt_with_ecdsa(
content: invoice_hash[:hash],
private_key: private_key
)
# signing_time = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
signing_time = "2022-09-15T00:41:21Z"
# Parse and hash the certificate
# The certificate needs to be in a PEM format (with header blocks)
certificate_path = "path/to_your/certificate.pem"
parsed_certificate = ZATCA::Signing::Certificate.read_certificate(certificate_path)
# Hash signed properties
signed_properties = ZATCA::UBL::Signing::SignedProperties.new(
signing_time: signing_time,
cert_digest_value: parsed_certificate.hash,
cert_issuer_name: parsed_certificate.issuer_name,
cert_serial_number: parsed_certificate.serial_number
)
# Fix: Make sure we follow the same logic as invoice hash
signature_properties_digest = signed_properties.generate_hash
# Create the signature element using the certficiate, invoice hash, and signed
# properties hash
signature_element = ZATCA::UBL::Signing::Signature.new(
invoice_digest_value: invoice_hash[:base64],
signature_properties_digest: signature_properties_digest,
signature_value: signature,
certificate: parsed_certificate.cert_content_without_headers,
signing_time: signing_time,
cert_digest_value: parsed_certificate.hash,
cert_issuer_name: parsed_certificate.issuer_name,
cert_serial_number: parsed_certificate.serial_number
)
invoice.signature = signature_element
When submitting to APIs (e.g. compliance)
# Later on when you need to submit it, you can access its properties like so:
invoice.uuid
invoice.to_base64 # This is what maps to `invoice` in ZATCA's APIs
invoice.generate_hash[:base64] # This is what maps to `invoiceHash` in ZATCA's APIs
See this page