ZuloOne
English
Sign in
Menu

21 · Saudi Arabia — VAT + ZATCA/FATOORA (`LocalizationSaudiArabia`)

Updated

Customization layer. One of the two implemented country packages. It seeds Saudi VAT data (SA jurisdiction, 5%/15% rates, codes S/Z/E/O) on top of the common Tax model and implements the ZATCA/FATOORA e-invoice (Phase 2, Integration Phase) for Sales documents. The common tax objects and tax engine are not duplicated. The package only seeds data, extends three documents owned by other models, and adds its own services/channel/job.

← General specification · Package template

Contour fact. Three transaction scripts (SaudiVatTx on SalesRealization, SaudiCreditVatTx on SalesCreditNote, SaudiDebitVatTx on SalesDebitNote) read the rate from document.TaxRateApplied, a field on the document itself, not from the TaxRule engine. The package creates no TaxRule records at all. Rate applicability in KSA is determined by the value that Tax writes to the document, not by a separate package rule (see Localization · invariants). The live sales process is Sales and purchasing.

What is created (object map)#

The package does not create new kinds of Tax objects. It seeds data into Tax dictionaries, extends two dictionaries and three documents owned by other models, and adds one own dictionary, one own register, and the e-invoice service set.

Kind Name Purpose
Model LocalizationSaudiArabia country package, Customization(3) layer
DataPackage DataPackages/vat-SA.json TaxAuthority ZATCA, TaxJurisdiction SA, Tax VAT-SA, 5×TaxCategory, 4×TaxRate (5%/15%, standard and reverse-charge), 5×TaxCode (S/Z/E/O/RC-SA), 8×TaxReportMapping
Dictionary LocalizationSaudiArabiaSettings singleton switch: EInvoiceEnabled, VatRegistrationNumber
Dictionary FatooraStubDelivery dev Fatoora stub state as data: accepted payloads by idempotency key
DictionaryExtension LegalEntity.LocalizationSaudiArabia CommercialRegistration (seller CRN, UBL fallback scheme)
DictionaryExtension TaxCategory.LocalizationSaudiArabia ExemptionReasonCode/ExemptionReasonText (VATEX-SA-* for UBL, E/O categories)
DocumentExtension ×3 SalesRealization.LocalizationSaudiArabia (→SalesRealization), SalesCreditNote.LocalizationSaudiArabia, SalesDebitNote.LocalizationSaudiArabia the same 5 e-invoice envelope fields (UUID/type/hash/PIH/QR)
Transaction scripts ×3 SaudiVatTx, SaudiCreditVatTx, SaudiDebitVatTx VAT posting into VatPayable
Register VatPayable VAT payable by customer/outlet/contract
Service SaudiEInvoice (ISaudiEInvoice) central orchestrator: UBL, hash, PIH/ICV, queue, outcome application
Service EInvoiceRelease (CoC link of IEInvoiceRelease from Tax) blocks buyer release of a Standard invoice until Cleared
Service SaudiTaxDetermination (ISaudiTaxDetermination) writes the per-entity TaxMapping pin; PinLegalEntitiesAsync for the job, PinLegalEntityAsync for one entity now
Service (host seam) ZatcaQr, ZatcaXades, ZatcaCsr, FatooraSigner (IOutboundSigner, profile fatoora) QR-TLV, XAdES hash/signature, CSR/key pair, HTTP channel signature
OutboundChannel ×5 fatoora-clearance, fatoora-reporting, fatoora-compliance, fatoora-compliance-invoices, fatoora-production-csid same baseUrl, different only by path
Job ApplyFatooraOutcomes (every minute) takes completed channel outcomes and moves TaxDocument to Cleared/Reported/Rejected
Job PinSaudiTaxCodes (every five minutes) pins each Saudi legal entity without a mapping of its own to code S of VAT-SA, via ISaudiTaxDetermination
WebServices ×10 5 FatooraStub* dev stubs (isDevOnly:true) + 5 Zatca* (CSR/Compliance/ComplianceInvoices/ComplianceSamples/ProductionCsid, onboarding, also isDevOnly:true) manual CSID onboarding from the development stand
Menu Menu/menu.json Saudi Arabia group → Registers/Settings subgroups
— reused — all Tax engine and objects, including TaxDocument/TaxAuthorityConnection/TaxSubmission; SalesRealization and paired Sales documents not created here

model.json#

{
  "modelType": "Custom", "modelVersion": "1.35.0", "layerId": 3,
  "metaId": "8d2f5a41-9c6b-4e3f-8a7d-1b4c6e2f9a50", "name": "LocalizationSaudiArabia"
}

Dependencies: Inventory (≥1.9.0), Organization (≥1.4.0), Sales (≥1.33.0), Tax (≥1.11.0). There is no Accounting dependency. This package does not write VAT accounts or GL postings; KSA VAT lives only in its own register VatPayable (see below).

Reference data (DataPackages/vat-SA.json)#

Manifest DataPackages/index.json: when: "country", country: "SA", sort: 25. The common IDataPackageService applies it when installing a package for a legal entity whose country is SA. Mechanism: Localization · DataPackages.

Object Values
TaxAuthority ZATCA (CountryCode SA)
TaxJurisdiction SA
Tax VAT-SA (EffectiveFrom 2018-01-01)
TaxCategory ×5 STANDARD, ZERO_RATED, EXEMPT, OUT_OF_SCOPE, REVERSE_CHARGE (Treatment = category code)
TaxRate ×4 VAT5 — 0.05, 2018-01-01 → 2020-06-30; VAT15 — 0.15, 2020-07-01 → indefinitely, both under category STANDARD. VAT5-RC and VAT15-RC repeat the same two windows under REVERSE_CHARGE
TaxCode ×5 S, Z, E, O, RC-SA; each references its own TaxCategory
TaxReportMapping ×8 ReturnType VAT-SA: S/OUTPUT→1, Z/OUTPUT→3, E/OUTPUT→5, S/INPUT→7, RC-SA/INPUT→9, RC-SA/OUTPUT→9, Z/INPUT→10, E/INPUT→11

Why the reverse charge has its own rates. Rate resolution narrows to the code's category, and both standard rows are pinned to STANDARD. A code under REVERSE_CHARGE would therefore find no rate at all and the calculation would refuse, so the package ships the same two windows a second time. The code is named RC-SA, not RC: TaxCode's business key is Code globally, and a second RC would upsert over the base vat package's own and rebind it to VAT-SA.

The return and the filing sheet#

KSA filing happens in the ZATCA portal under the taxpayer's credentials — this package signs and submits nothing. What it produces is the box-by-box figure sheet (SaTaxFiling.ExportAsync → SaTaxFilingExport, a semicolon CSV), built from a universal TaxReturn whose lines were stamped with a box by the mappings above.

Ten of the sixteen boxes are sourced from the ledger, four are computed by the form, and two have no source in ZuloOne. Every box is printed with a Source column — ledger, computed or manual — because a zero the system vouches for and a zero nobody filled are different zeros:

Box Source Note
1, 3, 5 ledger standard / zero-rated / exempt sales
2, 4 manual healthcare-education to citizens, exports
6 computed sum of 1–5
7, 10, 11 ledger standard / zero-rated / exempt purchases
8 manual import VAT paid at customs — still unsourced. ZuloOne has no customs document, so the figure comes off the clearance papers by hand
9 ledger imports under the reverse charge
12 computed sum of 7–11
13 computed box 6 VAT + the self-assessed half of box 9 − box 12 VAT
14, 15 manual prior-period corrections, credit carried forward
16 computed 13 + 14 − 15

Box 9 with numbers. Sales 1 000 at 15% (box 1: 1 000 / 150), a standard purchase 200 at 15% (box 7: 200 / 30), and an import of 500 under RC-SA (box 9: 500 / 75). The import is written by one calculation as two lines — an INPUT deduction and its OUTPUT mirror (Tax · reverse charge) — and the sheet shows it once, on the purchases side: box 12 = 700 / 105, which already deducts the 75. Box 13 adds the owed half back: 150 + 75 − 105 = 120, exactly TaxReturn.NetPayable (225 − 105). Without that middle term the sheet would claim 45 and the business would under-declare the import.

Rate change on 2020-07-01, with numbers. A realization dated 2020-06-30 uses VAT5 (5%): base 100.00 SAR → VAT 5.00 SAR. A realization dated 2020-07-01 at the same price uses VAT15 (15%): base 100.00 SAR → VAT 15.00 SAR. Rate selection uses the EffectiveFrom/EffectiveTo window of the TaxRate record valid on the document TaxPointDate. Historical TaxCalculation records of already posted documents are not recalculated retroactively. See Tax · invariants.

Dictionaries#

LocalizationSaudiArabiaSettings (singleton)#

The only package switch. SaudiEInvoice checks it first in every operation and exits silently if the record is missing or EInvoiceEnabled is not set.

Fields: see LocalizationSaudiArabiaSettings in Objects below.

Field Business note
EInvoiceEnabled Enables the whole e-invoice pipeline; unset means off.
VatRegistrationNumber Seller VAT number used for setup and reference.

FatooraStubDelivery#

Data for the Fatoora dev stub: what it has already accepted by idempotency key (IdempotencyKey, RequestBody, ResponseBody, StatusCode). It replaces the static in-process dictionary that the stand controller used before. Stub state survives a restart and is visible in the grid, not only in process memory.

Dictionary extensions#

Extension Field Meaning
LegalEntity.LocalizationSaudiArabia CommercialRegistration (String 32) seller CRN; UBL fallback identifier when the seller has no VAT number: sellerScheme = VAT number not empty ? "VAT" : "CRN"
TaxCategory.LocalizationSaudiArabia ExemptionReasonCode (String 32), ExemptionReasonText (String 256) VATEX-SA-* code/text for categories EXEMPT/OUT_OF_SCOPE; read only for letters E/O (see below)

The package has no separate “exemption reason” dictionary. These are two fields on the TaxCategory extension, not a standalone object.

Sales document extensions#

Three extensions have the same set of five fields. The meanings below are specific to the Saudi e-invoice flow.

Extension folder Whose document?
SalesRealization.LocalizationSaudiArabia SalesRealization
SalesCreditNote.LocalizationSaudiArabia SalesCreditNote
SalesDebitNote.LocalizationSaudiArabia SalesDebitNote

The extension folder is SalesRealization.LocalizationSaudiArabia. Scripts inside bind "objectName": "SalesRealization". The extension identity is its metaId.

Field Meaning
InvoiceUuid envelope UUID: copy of TaxDocument.Uuid, for printing
ZatcaInvoiceType Standard (B2B) / Simplified (B2C)
InvoiceHash hash of the current invoice
PreviousInvoiceHash PIH: hash of the previous invoice in the chain
QrCode TLV in Base64

SaudiEInvoice writes all five fields by raw dictionary-record update through IDocumentManager.UpdateDocumentAsync, not by typed property assignment. A code search for document.InvoiceHash = finds nothing. Print forms for invoices and notes read these fields.

Register VatPayable#

isDoubleEntry: false, useBalanceTable: true, allowNegativeBalance: true (unlike, for example, LoyaltyPoints in CRM, which forbids a negative balance). Resource: Amount. Analytics: all three are required, isRequired: true.

Analytic Source
Customer document.Customer
CustomerOutlet document.Outlet
SalesContract document.Contract

Exactly three transaction scripts write to it: SaudiVatTx (SalesRealization, +VAT), SaudiCreditVatTx (SalesCreditNote, −VAT), and SaudiDebitVatTx (SalesDebitNote, +VAT). All three return without a movement when document.TaxRateApplied <= 0: a zero or missing rate does not create a VatPayable line. SaudiVatTx calculates the base with pricing.LineAmount(qty, unitPrice, discount), including the line discount. Credit and debit notes use the same signature without the discount parameter.

Services#

SaudiEInvoice (ISaudiEInvoice) — central orchestrator#

The only point that builds an e-invoice. EnsureAsync(sourceDocumentId) is called once from document OnAfterPostAsync, after the transition to Issued, and performs the whole path:

  1. Reads LocalizationSaudiArabiaSettings.EInvoiceEnabled; missing/off means exit with no side effects.
  2. Best-effort, swallowing and logging exceptions, calls ApplyChannelOutcomesAsync: the same method as the separate job ApplyFatooraOutcomes, called here because the orchestrator is already running.
  3. Idempotency: looks for an existing TaxDocument by SourceDocumentId. If it exists, returns its MetaId; a repeated call recreates nothing.
  4. Resolves customer and legal entity by source type (SalesRealization/ SalesCreditNote/SalesDebitNote).
  5. Chooses the branch: !string.IsNullOrWhiteSpace(customer.TaxRegistrationNumber) ? "Standard" : "Simplified" — not Customer.CustomerType.
  6. Takes ICV+PIH through IKeyedCounterService.TakeAsync("zatca-icv", legalEntity); the counter is per legal entity. For the legal entity's first invoice, PIH = Base64(SHA256("")), the hash of an empty string.
  7. Builds UBL 2.1 (BuildUblAsync), calculates InvoiceHash through IZatcaXades, stamps QR and XAdES signature if a certificate is configured (StampAsync), and seals the envelope with xades.Seal. Sealing does not change InvoiceHash.
  8. Saves TaxDocument and stores the just-calculated InvoiceHash as the PIH for the next invoice of the same legal entity.
  9. Moves TaxDocument to subtype Issued through IDocumentPostingService.SetSubtypeAsync.
  10. Stamps the same five fields back to the source document by raw dictionary record; see Sales document extensions.
  11. Enqueues the payload to fatoora-clearance or fatoora-reporting through IOutboundGateway.EnqueueAsync, with IdempotencyKey = "zatca:" + uuid.
  12. Does not fill TaxSubmission directly. It is filled only by the mock rejection path when TaxAuthorityConnection.IsMockReject is enabled; in the normal path the log remains empty until the real channel/job response.

ApplyChannelOutcomesAsync reads completed packages from both real channels (fatoora-clearance/fatoora-reporting) through IOutboundGateway.TakeCompletedAsync and maps statuses: Succeeded → Cleared (clearance) or Reported (reporting); Failed/Abandoned/Cancelled → Rejected. InDoubt and other statuses are intentionally not confirmed. A code comment warns that confirmation would “bury” a request that reached the network without a response; such packages stay questionable until a person resolves them. The outcome is applied through ITaxDocumentOutcome.ApplyAsync.

BuyerReleaseBlockAsync(sourceId) returns null (do not block) except when EInvoiceEnabled is true, an envelope exists for the source, InvoiceType == "Standard", and Subtype != "Cleared". In that case it returns an explicit block reason. Simplified invoices, disabled e-invoices, and missing envelopes never block release.

NoteReferenceXmlAsync (credit/debit note) builds cac:BillingReference/cac:InvoiceDocumentReference to the original SalesRealization (note.OriginalInvoice/debit.OriginalInvoice). It reads the original InvoiceUuid through a raw bag lookup with IDataService.GetByIdAsync, not through typed access, and adds cac:PaymentMeans/InstructionNote with reason CANCELLATION for a credit note or ADDITIONAL_CHARGES for a debit note.

Category letter in UBL#

LetterOf(taxCalculation):

TaxCategory.Code Letter
EXEMPT E
OUT_OF_SCOPE O
ZERO_RATED Z
STANDARD or REVERSE_CHARGE S
no TaxCalculation (fallback) S if rate > 0; otherwise Z

For E/O, UBL also includes TaxExemptionReasonCode/ TaxExemptionReason: values of the TaxCategory.LocalizationSaudiArabia extension fields (ExemptionReasonCode/ExemptionReasonText). Code format is VATEX-SA-*. The code list is only the official ZATCA Code List; it is not seeded in the package.

UBL 2.1 envelope identifiers (important clarification)#

The document cbc:UUID is the UUID of the envelope itself (envelope.Uuid, the same GUID as TaxDocument.Uuid and the InvoiceUuid extension field), not the ordinal ICV counter. ICV and PIH are two separate cac:AdditionalDocumentReference blocks:

cbc:ID Content
ICV cbc:UUID = counter number (IKeyedCounterService value)
PIH nested binary object: hash of the previous invoice

The XML root and UBL type code depend on document kind: Invoice/CreditNote/ DebitNote, codes 388/381/383. Saudi subtype is the name attribute on cbc:InvoiceTypeCode: 0100000 (Standard) / 0200000 (Simplified).

VAT rounding in the header, with numbers#

The header VAT total is the sum of already-rounded line VAT amounts, not a rounding of the header base. Rounding the header separately would make the header total differ from the sum of its own lines. Example: three lines of 0.10 SAR at 15%.

Calculation Result
By line: 0.10 × 0.15 = 0.015 → round → 0.02 per line, ×3 lines 0.06 SAR (what the code does)
By header base: 0.30 × 0.15 = 0.045 → round → 0.05 0.05 SAR (not used; differs from the line sum by 0.01 SAR)

Rounding everywhere uses GlobalConstants.Get<int?>("AmountScale") ?? 2 (Tax · global constants), not a hardcoded 2. A tenant with another AmountScale gets the same number of decimal places in code and in the print form.

Two more code-confirmed invariants in the same category: UBL IssueDate is the date of the document itself, not UtcNow at posting time; only IssueTime, which is not on the document, comes from the posting clock. Seller country and buyer country are resolved separately, side by side, not by one shared variable; otherwise the seller country leaks into the buyer address on export invoices.

QR-TLV (ZatcaQr), XAdES (ZatcaXades), CSR (ZatcaCsr), channel signature (FatooraSigner)#

  • QR: tags 1–6 are unsigned: seller name, VAT number, timestamp, invoice total, VAT total, invoice hash. Tags 7–9 are added only when a certificate is configured: ECDSA signature, DER public key, certificate signature. Base64 is limited to 500 characters; only the seller name is shortened for that limit, other tags are not cut.
  • XAdES: InvoiceHash(xml) = Base64(SHA-256(C14N 1.0 XML minus UBLExtensions/Signature/the QR reference itself)). SignedInfoDigest is calculated separately and signed independently. Seal always embeds QR; XAdES signature/certificate are embedded only when both are present. Sealing does not change the already calculated InvoiceHash.
  • CSR: mints a P-256 ECDSA key pair plus PKCS#10 CSR. Template PREZATCA-Code-Signing is for Sandbox; ZATCA-Code-Signing is for Simulation/Production. The two non-final environments, Sandbox and Simulation, use different templates, while Simulation and Production share ZATCA-Code-Signing.
  • FatooraSigner implements the platform IOutboundSigner. The channel finds it by profile name "fatoora" (the channel signer field), not by service name. It builds Accept-Version: V2, conditional Clearance-Status: 1, and Authorization: Basic base64(token:secret). The OTP header is intentionally not included. It is a one-time onboarding header; including it in the signature for repeated sending would risk replay.

Outbound channels and job#

All five OutboundChannel records have the same baseUrl (https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal) and differ only by path. All use signer: "fatoora", authMode: "Signer". Environment (Sandbox/Simulation/Production) is not stored on the channel. It is stored on TaxAuthorityConnection; see below.

Channel path
fatoora-clearance /invoices/clearance/single
fatoora-reporting /invoices/reporting/single
fatoora-compliance /compliance
fatoora-compliance-invoices /compliance/invoices
fatoora-production-csid /production/csids

Job ApplyFatooraOutcomes (cronExpression: "* * * * *", once per minute, executeSingle: true) runs the same ApplyChannelOutcomesAsync as the side call inside EnsureAsync. The separate pass is required for envelopes whose channel outcome arrived after the source document had already posted and EnsureAsync had long finished.

Determination — SaudiTaxDetermination + PinSaudiTaxCodes#

The package seeds the tax, the rates, the codes and the box layout; none of that decides WHICH code a document gets. Determination runs rule → TaxMapping → the seller country's own standard code → the stand-wide TaxSettings.DefaultTaxCode. The last step is one row for the whole stand, and a country package cannot own it — a stand carrying two countries has exactly one of it. Before the country step existed, a Saudi invoice without a per-entity row was computed with whatever the stand default is: it posts, it balances, and it reaches the return, where the VAT-SA layout maps S/Z/E and has nothing to say about the code that actually arrived — every box comes out empty.

ISaudiTaxDetermination writes that row.

Method Does
PinLegalEntitiesAsync() every legal entity whose country has CodeISO2 = 'SA' and no TaxMapping of its own gets one pointing at code S of VAT-SA; returns the number written
PinLegalEntityAsync(Guid) the same for one entity, for a test or an implementer who has just created the card

The written row is SourceType = 'LegalEntity', SourceId = the entity, TaxCode = S of VAT-SA, Priority = 100 (the weakest mapping on purpose — item, item group and counterparty rows must still beat it), EffectiveFrom = the code's own start date, IsDisabled = false. Any existing mapping for the entity, enabled or disabled, stops the write: a disabled row is an answer too. The tax is looked up by code, so on a stand without the vat-SA package the run returns 0 and writes nothing.

Job PinSaudiTaxCodes (cronExpression: "*/5 * * * *", executeSingle: true, queueTtlMinutes: 15) calls PinLegalEntitiesAsync and logs pinned=N.

Why a job and not a LegalEntity after-insert handler. Tax reads and the TaxMapping insert after next() sit inside the entity insert's lock window, next to the country-pack installer writing the same tables in the same transaction. The platform swallows an after-event failure, so the caller's transaction is already rolled back and every following statement dies. A job owns its own transaction. The pin is not instant — the window is the job's period — and SaudiTaxDeterminationTest asserts that window (zero mappings immediately after the card is saved).

The seller's country answers before the stand default (the country step), so a Saudi entity with no mapping of its own resolves to the one standard-treatment code of VAT-SA — the same S this pin writes — from the moment the card exists. The pin is kept because a TaxMapping row is the decision visible as data: it can be read, overridden or disabled, while the engine fallback is invisible and goes quiet the moment a country names more than one standard code. Should the package ever seed a second Saudi tax, the fallback stops answering and this pin is the only thing between the tenant and a foreign code.

WebServices — onboarding (all isDevOnly: true)#

Five Zatca* services manually obtain CSID from the development stand. The minted key pair and CSR/OTP exchange must never go through the production UI as a normal flow; only these service endpoints on a development stand are used: ZatcaCsr (mints CSR + private key; the secret is not saved), ZatcaCompliance (OTP+CSR → Compliance CSID, returns the secret), ZatcaComplianceInvoices (sends one compliance invoice sample), ZatcaComplianceSamples (all six ZATCA compliance-kind samples), and ZatcaProductionCsid (Compliance CSID → Production CSID, returns the secret). Five FatooraStub* services are mirror dev stubs that answer instead of real gw-fatoora and write to FatooraStubDelivery.

Connection setup (TaxAuthorityConnection)#

The connection object is universal and defined in Tax. KSA does not create a new object. It only fills a record (legal entity × ZATCA) and connects the channel/signer "fatoora". The record Environment (Sandbox/Simulation/Production) chooses the Fatoora environment logically. The channels themselves, as above, use the same baseUrl for all environments; the split is the Environment value, not separate OutboundChannel records. Secrets (private key, CSID) are not form fields; only CredentialRef points to secure platform storage.

The Saudi Arabia group (displayOrder: 11) → Registers (VatPayable) and Settings (LocalizationSaudiArabiaSettings). There are no reports in the menu: the package has no CustomReport and no TaxReportMapping.

Business logic and invariants#

The Clearance/Reporting branch is selected by the buyer VAT number, not by CustomerType. Non-empty Customer.TaxRegistrationNumber ⇒ Standard → Clearance. The invoice is not valid for the buyer until status Cleared, and BuyerReleaseBlockAsync blocks release. Empty VAT number ⇒ Simplified → Reporting. The invoice is issued immediately and reported after the fact; it is never blocked.

The PIH+ICV chain is per legal entity, not global. IKeyedCounterService.TakeAsync("zatca-icv", legalEntity) gives each legal entity an independent counter. The first invoice of a legal entity carries PIH = hash of an empty string; each following invoice carries the hash of the previous invoice of the same legal entity. A chain break, such as missed or substituted PIH, is a reason for rejection during ZATCA clearance. The package only writes a correct chain on its side.

cbc:UUID is not a sequence number. UUID is the envelope identity. ICV (sequence number) and PIH (chain hash) are separate AdditionalDocumentReference blocks; see above. Confusing them in an integration means submitting XML that is invalid for ZATCA.

KSA VAT is not posted to the general ledger by this package. There is no Accounting dependency. VAT reaches the ledger through the universal TaxCalculation (TaxCalculationGLTx in Tax), not through this package. See Localization · direct entry into another model's register. Unlike Ukraine, KSA writes to no foreign register at all.

Idempotency has two levels. EnsureAsync is idempotent by SourceDocumentId, so a repeated call does not create a second TaxDocument. Channel sending uses IdempotencyKey = "zatca:" + uuid, so a repeated network send does not create a duplicate on the ZATCA side.

InDoubt outcomes are not confirmed. See ApplyChannelOutcomesAsync above. This is a deliberate invariant, not a missing feature: confirming a status that is not really known would hide the real delivery problem.

Package tests#

Package integration tests (Tests/) cover the mandatory scenarios from the package template plus KSA-specific checks: rate change 5%→15% on 2020-07-01; Standard branch (buyer VAT number) vs Simplified branch (no VAT number); category letters S/Z/E/O, including fallback without TaxCalculation; PIH chain across several consecutive invoices of one legal entity; blocking Standard-invoice release until Cleared and unblocking afterward; VAT rounding in the header as the sum of rounded lines (the 3×0.10 SAR at 15% case above). Separate unit-level checks cover ZatcaQr/ ZatcaXades hash definition and TLV correctness.

  • Depends on: Inventory, Organization, Sales, Tax.
  • Extends: LegalEntity (Organization), TaxCategory (Tax), SalesRealization/SalesCreditNote/SalesDebitNote (Sales).
  • Does not depend on: Accounting, HR. This package has no GL postings and no contributions/payroll logic, unlike Ukraine, which depends on both.
  • Incoming dependencies: none. This is a leaf package; no model depends on it.

Objects#

Generated from workspace metadata by node tools/gen-reference.mjs; edit the metadata, not this block.

Dictionaries#

FatooraStubDelivery — Fatoora stub delivery

Field Caption Type Required
ID Code String(50) Yes
IdempotencyKey Idempotency key String(200) No
RequestBody Request body String(4000) No
ResponseBody Response body String(4000) No
StatusCode Status code Integer No

LocalizationSaudiArabiaSettings — LocalizationSaudiArabia settings

Field Caption Type Required
EInvoiceEnabled E-invoice (ZATCA) enabled Boolean No
ID Code String(50) Yes
VatRegistrationNumber VAT registration number String(128) No

SaTaxFilingExport — VAT return export

The ZATCA VAT return as figures ready to be typed into the Fatoora portal. Payload is a semicolon CSV of the official boxes. This is not a signed submission and not an upload: filing stays in the portal.

Field Caption Type Required
CreatedOn Created on DateTime No
ID Code String(50) Yes
LegalEntity Legal entity → LegalEntity Yes
Payload Payload LargeText No
PeriodFrom Period from DateTime Yes
PeriodTo Period to DateTime Yes
ReturnType Return type String(32) Yes

Registers#

VatPayable — VAT payable

Resources:

Resource Caption Type Required
Amount Amount Money —

Analytics:

Analytic Caption Type Required
VatPayable-Customer VatPayable-Customer — Yes
VatPayable-CustomerOutlet VatPayable-CustomerOutlet — Yes
VatPayable-SalesContract VatPayable-SalesContract — Yes
Was this page helpful?