Skip to content

Constructing a Simplified Invoice

Omar Bahareth edited this page Jun 14, 2023 · 17 revisions

Constructing and Signing the Invoice

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
#===============================================================================

Custom Signing (only for simplified invoices)

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

Submitting to APIs

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

Rendering the QR Code

See this page