Skip to content

sa: add Saudi Arabia tax regime#740

Open
enrique-fernandez-polo wants to merge 28 commits intoinvopop:mainfrom
enrique-fernandez-polo:main
Open

sa: add Saudi Arabia tax regime#740
enrique-fernandez-polo wants to merge 28 commits intoinvopop:mainfrom
enrique-fernandez-polo:main

Conversation

@enrique-fernandez-polo
Copy link
Copy Markdown

@enrique-fernandez-polo enrique-fernandez-polo commented Feb 25, 2026

Why Saudi Arabia

Saudi Arabia is the strongest next candidate for GOBL regime expansion, scoring highest across commercial urgency, technical fit, documentation quality, and strategic leverage.

Market opportunity. The ZATCA e-invoicing market is valued at $143M (2024), projected to reach $594M by 2033 at 15.3% CAGR. Phase 2 integration has reached Wave 24 (SAR 375K threshold, June 2026), approaching universal coverage of all VAT-registered businesses.

Technical fit. ZATCA uses UBL 2.1 XML based on EN 16931 — GOBL's existing gobl.ubl converter handles the bulk of XML generation. The regime sits cleanly in GOBL's architecture: 15% VAT, 15-digit TIN validation, ZATCA-specific identity types, and well-documented business rules (BR-KSA series).

Strategic multiplier. Every GCC country is following Saudi Arabia's lead: UAE mandates e-invoicing from January 2027, Oman launches its Fawtara pilot in August 2026, Bahrain has active consultations, and Qatar has issued platform tenders. Building the SA regime directly accelerates five additional country implementations.

Documentation quality. All specifications are published in English at zatca.gov.sa, including the XML Implementation Standard v1.2, Data Dictionary, and Developer Portal Manual. The sandbox environment provides full API testing.

Changes

This PR adds the foundational Saudi Arabia tax regime (regimes/sa/) covering:

  • VAT tax categories: 15% standard rate (effective July 2020, previously 5% from January 2018), plus zero-rated, exempt, reverse-charge, export, and outside-scope categories mapped to standard GOBL tax keys
  • TIN validation: 15-digit format with Luhn check digit verification per ZATCA requirements
  • Seller identity types (BT-29-1): CRN (Commercial Registration), MOM (MOMRAH License), MLS (MHRSD License), 700 (Unified Number), SAG (MISA License), OTH (Other)
  • Buyer identity types (BT-46-1): TIN, NAT (National ID), IQA (Iqama), PAS (Passport), GCC (GCC ID), OTH — plus all seller codes per BR-KSA-14
  • Invoice validation rules: Supplier TaxID required (BR-KSA-39), supplier name required (BR-06), customer name and identification required on standard invoices (BR-KSA-42, BR-KSA-81), simplified invoices skip customer requirements
  • Invoice scenarios: Automatic correction type mapping for credit/debit notes
  • 5 example invoices: standard, simplified, credit note, exempt, and reverse-charge — with calculated output envelopes

Out of Scope

The following ZATCA Phase 2 compliance areas are intentionally not covered by this regime PR:

  1. ZATCA tax category mappings and exemption reason codes — UNTDID 5305 tax category codes, VATEX-SA exemption codes, and document type scenarios
  2. UBL 2.1 XML generation — ZATCA-compliant UBL 2.1 output with KSA extensions and schematron validation
  3. Cryptographic signing — XAdES-BES digital signatures, CSID certificate management, and SHA-256 invoice hash chaining
  4. QR code generation — TLV-encoded Base64 QR codes with the 9 mandatory ZATCA tags
  5. Fatoora platform API integration — EGS onboarding, CSID lifecycle, clearance (B2B) and reporting (B2C) API submission
  6. Additional compliance requirements — Archival, sandbox certification, and wave-based rollout compliance

Sources

Test coverage

  • regimes/sa/: 94.0% statement coverage
  • All tests pass: go test ./regimes/sa/...
  • Full suite: go test ./... passes (no regressions)

Pre-Review Checklist

  • Opened this PR as a draft
  • Read the CONTRIBUTING.md guide.
  • Performed a self-review of my code.
  • Added thorough tests with at least 90% code coverage.
  • Modified or created example GOBL documents to show my changes in use, if appropriate.
  • Added links to the source of the changes in tax regimes or addons, either structured or in the comments.
  • Run go generate . to ensure that the Schemas and Regime data are up to date.
  • Reviewed and fixed all linter warnings.
  • Been obsessive with pointer nil checks to avoid panics.
  • Updated the CHANGELOG.md with an overview of my changes.
  • Requested a review from Copilot and fixed or dismissed (with a reason) all the feedback raised.

Only after checking off all the previous items:

  • Marked this PR as ready for review and requested one from @samlown.

enrique-fernandez-polo and others added 13 commits February 25, 2026 20:17
TIN: 15 digits starting and ending with 3 per ZATCA BR-KSA-40.

Includes a minimal regime definition (SA, SAR, Asia/Riyadh) to expose
the Validate/Normalize entry points used by tests.

Sources:
- https://zatca.gov.sa
- https://lookuptax.com/docs/tax-identification-number/saudi-arabia-tax-id-guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VAT rates: 15% since Jul 2020 (Royal Decree A/638), 5% since Jan 2018
(GCC Unified VAT Agreement). Zero/exempt handled via tax.GlobalVATKeys().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Seller IDs (BT-29-1): CRN, MOM, MLS, 700, SAG.
Buyer IDs (BT-46-1): NAT, IQA, PAS, GCC, OTH.

Format validation for numeric types per ZATCA E-Invoice XML
Implementation Standard v1.2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scenarios auto-generate legal notes for reverse charge and simplified
invoices, with Arabic translations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invoice validation per ZATCA rules:
- BR-KSA-39: seller name required
- BR-KSA-40: seller TIN required
- BR-KSA-42: buyer name required on standard invoices

Simplified invoices (B2C) skip customer requirement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5 YAML examples exercising the full regime:
- invoice-sa-standard: B2B with standard + zero-rated lines
- invoice-sa-simplified: B2C POS, no customer
- invoice-sa-credit-note: credit note referencing preceding invoice
- invoice-sa-reverse-charge: B2B with reverse charge tag
- invoice-sa-exempt: invoice with VAT-exempt lines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
README covers VAT rates, identity types, validation rules, and ZATCA
references. Generated sa.json regime data and updated regime-code schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Correct ministry names based on fact-check against ZATCA documentation:
- MOMRA → MOMRAH (Ministry of Municipalities and Housing, post-2024 rename)
- MHRSD Arabic: add full name "والتنمية الاجتماعية" (and Social Development)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add missing TIN (Tax Identification Number) identity type for buyer
identification per ZATCA BR-KSA-14. TIN is a valid buyer-only scheme ID
in the ZATCA e-invoicing specification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement BR-KSA-81: standard invoice buyers must have either a VAT
registration number (TaxID) or an alternative buyer identification
(org.Identity). Also correct validation rule comments to reference
the right ZATCA rules (BR-KSA-39, BR-06, BR-KSA-42).

Document BR-KSA-25 (education/healthcare simplified invoice exception)
as handled by the gobl.zatca addon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Royal Decree → Royal Order No. A/638
- GCC Unified VAT Agreement → GCC VAT Framework Agreement
- MOMRA → MOMRAH in seller identity table
- Add TIN to buyer identity table
- Add note that seller codes are also valid for buyers (BR-KSA-14)
- Fix rule references: BR-KSA-39, BR-06, BR-KSA-42, BR-KSA-81

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix gofmt formatting in sa.go and remove unnecessary string conversion
in sa_test.go.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@enrique-fernandez-polo enrique-fernandez-polo marked this pull request as draft February 25, 2026 21:34
Replace the standalone README.md with a comprehensive Description field
in the regime definition, following the project convention. Covers tax
identification, VAT rates, identity types, invoice validation rules,
and ZATCA Phase 2 addon reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace custom validation functions with inline validation.Match calls
for identity code validation, and remove unnecessary Retained zero value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@enrique-fernandez-polo enrique-fernandez-polo marked this pull request as ready for review February 26, 2026 08:08
Replace ValidateStruct wrapper with direct validation.Match().Validate()
calls for consistency with tax_identity.go pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 26, 2026

Codecov Report

❌ Patch coverage is 96.07843% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.89%. Comparing base (8ca0f6c) to head (5901a1d).

Files with missing lines Patch % Lines
regimes/sa/invoices.go 93.22% 2 Missing and 2 partials ⚠️
regimes/sa/identities.go 90.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #740      +/-   ##
==========================================
+ Coverage   92.86%   92.89%   +0.02%     
==========================================
  Files         331      335       +4     
  Lines       17260    17413     +153     
==========================================
+ Hits        16029    16176     +147     
- Misses        867      870       +3     
- Partials      364      367       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Saudi Arabia (SA) tax regime to GOBL, including VAT category definitions, identity types, invoice validation rules, and example documents/data artifacts to support regime expansion.

Changes:

  • Introduces regimes/sa/ with regime registration, VAT category/rates, identity definitions/normalization, scenario notes, and invoice validation rules.
  • Adds SA to the regimes registry and to the tax regime code schema; generates data/regimes/sa.json.
  • Adds SA invoice examples (YAML inputs + generated JSON envelopes) and updates CHANGELOG.md.

Reviewed changes

Copilot reviewed 21 out of 26 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
regimes/sa/sa.go Defines and registers the SA regime (currency, timezone, validators/normalizers, scenarios, corrections).
regimes/sa/tax_categories.go Adds SA VAT category using global VAT keys and historical standard rates (15%/5%).
regimes/sa/tax_categories_test.go Tests SA VAT rate definitions and effective dates.
regimes/sa/tax_identity.go Adds SA TIN format validation (currently regex-only).
regimes/sa/tax_identity_test.go Tests SA TIN validation and normalization behaviors.
regimes/sa/identities.go Defines seller/buyer identity type codes, normalization, and type-specific validation.
regimes/sa/identities_test.go Tests identity type constants, validation, and normalization.
regimes/sa/invoices.go Implements SA invoice validation rules for supplier/customer requirements and simplified behavior.
regimes/sa/invoices_test.go Tests invoice validation scenarios (supplier/customer requirements; simplified vs standard; zero/exempt lines).
regimes/sa/scenarios.go Adds scenario-driven legal notes for reverse-charge and simplified invoices.
regimes/sa/scenarios_test.go Tests scenario notes are applied as expected.
regimes/regimes.go Registers the SA regime via side-effect import.
data/schemas/tax/regime-code.json Adds "SA" to the allowed regime codes schema.
data/regimes/sa.json Generated SA regime definition data.
examples/sa/invoice-sa-*.yaml Adds SA example invoice inputs (standard, simplified, reverse-charge, exempt, credit-note).
examples/sa/out/invoice-sa-*.json Adds generated envelopes for the SA example invoices.
CHANGELOG.md Notes addition of the new SA regime.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread regimes/sa/sa.go Outdated
Comment thread regimes/sa/invoices.go Outdated
Comment thread regimes/sa/tax_identity_test.go
Comment thread regimes/sa/tax_identity.go
Copy link
Copy Markdown
Collaborator

@samlown samlown left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this! Looks good, although I think the sources of data need to be double checked. For example, where can the latest tax rates be found? What is the source for the list of identity types? Copilot also came up with a few suggestions.

Comment thread regimes/sa/tax_categories.go Outdated
Code: tax.CategoryVAT,
Name: i18n.String{
i18n.EN: "VAT",
i18n.AR: "ضريبة القيمة المضافة",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no short version of the name?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there is one "ض.ق.م" Updated

Comment thread regimes/sa/tax_identity.go Outdated

// validateTaxIdentity checks to ensure the SA TIN format is correct.
func validateTaxIdentity(tID *tax.Identity) error {
return validation.Match(tinRegex).Error("must be a 15-digit number starting and ending with 3").Validate(&tID.Code)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks away from the standard pattern of using ValidateStruct and looses the field attribution. Also, as stated by Copilot, the it appears to be missing a Luhn check.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood a comment in another PR about how to use validation.Match My mistake

Comment thread regimes/sa/invoices.go
// BR-KSA-39: Supplier VAT registration number required on all invoices.
validation.Field(&p.TaxID,
validation.Required,
tax.RequireIdentityCode,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you completely sure about this? Some countries often apply thresholds before registering for a VAT number.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I struggled with this one. There is a mandatory VAT registration threshold of SAR 375,000 (and SAR 187,500 for voluntary), so not all businesses issuing invoices have a TIN.

At the same time, BR-KSA-39 requires the supplier's VAT registration number on all e-invoices.

The required TaxID should reside in the ZATCA add-on that verifies its presence when creating the e-invoice, not in the base regime. I think I mixed law and e-invoicing in some other requirements

Comment thread regimes/sa/identities.go Outdated
Comment on lines +33 to +34
// IdentityTypeTIN is a Tax Identification Number (buyer-only).
IdentityTypeTIN cbc.Code = "TIN"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I understand this... this code is not the same as the Tax ID?

Comment thread regimes/sa/tax_categories.go Outdated
i18n.EN: "Zakat, Tax and Customs Authority - VAT",
i18n.AR: "هيئة الزكاة والضريبة والجمارك - ضريبة القيمة المضافة",
},
URL: "https://zatca.gov.sa/en/E-Invoicing/Introduction/Pages/What-is-e-invoicing.aspx",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't see any details in this page about VAT tax rates specifically. The objective here is to know where to look exactly when a tax rate change is expected.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, that page was about e-invoicing and didn't have VAT rate details. Updated the source URL to the ZATCA VAT Rules & Regulations page

Comment thread regimes/sa/identities.go Outdated
"github.com/invopop/validation"
)

// Party identification scheme codes for seller (BT-29-1) and buyer (BT-46-1).
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in general here, this codes feel very specific for an implementation as opposed to a tax Regime which is mostly focussed on tax law. Are these all standard Type codes? I don't see a source document to be able to verify. Passport for example already has a specific key (passport) inside the org.Identity definition.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok now I get it. These codes (TIN, NAT, IQA, PAS, GCC, OTH) are ZATCA e-invoicing scheme IDs for BT-29-1/BT-46-1 They're implementation-specific, not tax law

Remove the supplier TaxID validation (BR-KSA-39) from the base regime.
VAT registration is only mandatory for businesses with annual taxable
supplies exceeding SAR 375,000, so requiring a TaxID is an e-invoicing
concern that belongs in the ZATCA addon, not the base tax regime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Point the VAT category source to the ZATCA VAT rules and regulations
page where tax rate changes are published, instead of the e-invoicing
introduction page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Refactor validateTaxIdentity to use validation.ValidateStruct with
field attribution, matching the pattern used by other regimes (DE, BR,
ES).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove buyer-only identity types (TIN, NAT, IQA, PAS, GCC, OTH) that
are ZATCA e-invoicing scheme codes (BT-29-1/BT-46-1), not tax law
concepts. Several also duplicate GOBL built-ins (org.IdentityKeyPassport,
org.IdentityKeyNational, etc.).

Keep only seller business registration types (CRN, MOM, MLS, 700, SAG)
which represent real Saudi government-issued registrations rooted in tax
law. The ZATCA scheme ID mappings belong in the e-invoicing addon.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Identity scheme codes (CRN, MOM, MLS, 700, SAG) are ZATCA e-invoicing
concepts (BT-29-1/BT-46-1), not tax law requirements. These belong in
a future ZATCA addon, not the base tax regime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use ض.ق.م (short form) in the Name field, keeping the full
ضريبة القيمة المضافة in the Title field, consistent with how
other regimes use abbreviated names (e.g., IVA for Spanish).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There is no public evidence that ZATCA specifies a check digit
algorithm for position 10 of the TIN.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Invoice-level validations (supplier name, customer name/identification,
simplified invoice rules) belong in the ZATCA e-invoicing addon scope,
not in the base regime definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants