uae-einvoice-engine
v0.1.0
Published
TypeScript engine for UAE PINT-AE Self-Billing (Self-Billed Invoice 389, Self-Billed Credit Note 261): canonical invoice model, Layer 1-4 validation, VAT/totals calculation, PINT-AE UBL XML generation, and an ERP/POS adapter pattern. Does not implement st
Maintainers
Readme
uae-einvoice-engine
TypeScript engine for UAE PINT-AE Self-Billing — generating and validating Self-Billed Invoice (389) and Self-Billed Credit Note (261) documents, plus the applicable out-of-scope invoice variant (480), in TypeScript, in-process, with no backend to stand up.
Scope: PINT AE Self-Billing only. This engine implements the PINT AE Self-Billing profile (document type codes 389 and 261), not the standard PINT AE Billing profile (380/381). See "What this engine implements" and "What this engine does not do" below for the exact boundary, and "Verification status" for what's actually been checked against official artifacts.
The UAE is rolling out mandatory e-invoicing under the PINT-AE (Peppol International Invoice model, UAE jurisdiction) format. This library covers the Self-Billing slice of that format — where the buyer, not the seller, issues the invoice — implementing the canonical invoice model, VAT category and transaction-code logic, code-list validation, a chain of business rules, and UBL XML generation for that profile. It does not implement the standard (seller-issued) PINT AE Billing profile.
import { processInvoice } from "uae-einvoice-engine";
import type { CanonicalInvoice } from "uae-einvoice-engine";
const invoice: CanonicalInvoice = { /* ... */ };
const result = processInvoice(invoice);
if (!result.valid) {
for (const issue of result.validation.issues) {
console.log(issue.layer, issue.ruleId, issue.path, issue.message);
}
} else {
console.log(result.xml); // PINT-AE UBL XML, ready to hand to an ASP
}No hosted service, no account, no vendor lock-in — you call a function, you get back a validation result and XML. Bring your own ERP/POS data and your own ASP submission.
Why this exists
Most UAE e-invoicing tooling is either a full hosted platform (pick a vendor, integrate their API, pay per document) or nothing at all. This is the middle ground: a small, dependency-light library that does the actual compliance work — canonical model, validation, calculation, XML generation — and stays out of your infrastructure decisions. Run it inside whatever backend, framework, or ERP integration you already have.
It's deliberately scoped as the compliance engine only — see "What this engine does not do" below for what's intentionally out of scope, and why.
Architecture
ERP / POS SYSTEMS
│
▼
ErpAdapter adapters/ (you write one per source system)
│ .toCanonical()
▼
CANONICAL INVOICE core/models/canonicalInvoice.ts (types)
core/models/schema.ts (Zod structural schema)
│
▼
processInvoice() pipeline/processInvoice.ts
│
├─ resolve profile + transaction code uae/profile.ts, businessRules.ts
├─ calculate lines / VAT / totals core/calculation/calculator.ts
├─ Layer 1: schema validation core/models/schema.ts
├─ Layer 2: code-list validation core/validation/codeListValidator.ts
├─ Layer 3/4: PINT + UAE business rules core/validation/businessRules.ts
└─ generate PINT-AE UBL XML uae/xml/generator.ts
│
▼
{ valid, validation, document, xml }ERP/POS-specific logic never leaks into the compliance core, and the core
never knows about any particular ERP — the only contract between them is
the ErpAdapter interface and the canonical invoice shape.
Directory layout:
src/
core/
models/ canonical TypeScript types + Zod schema (Layer 1)
validation/ code-list validator (Layer 2), business-rule validator (Layers 3-4)
calculation/ line/VAT/totals calculator
uae/
codeLists/ registry.ts — the code lists the engine actively validates against
profile.ts profile + document-type-code resolution
endpointResolver.ts special buyer endpoint derivation
xml/ PINT-AE UBL XML generator
adapters/
common/ ErpAdapter interface every adapter implements
windward/ reference adapter — a worked example, not a dependency
pipeline/ processInvoice() orchestrator
index.ts public API barrel
examples/run-demo.ts
tests/
unit/ calculator, businessRules, codeListValidator
integration/ full pipeline, reference adapter → pipelineGetting started
npm install uae-einvoice-engineOr, working from this repo directly:
npm install
npm test # vitest — unit + integration tests
npm run demo # builds a sample invoice, runs the pipeline, prints XML
npm run build # tsc -> dist/Usage
import { processInvoice } from "uae-einvoice-engine";
import type { CanonicalInvoice } from "uae-einvoice-engine";
const invoice: CanonicalInvoice = { /* ... */ };
const result = processInvoice(invoice);
if (!result.valid) {
for (const issue of result.validation.issues) {
console.log(issue.layer, issue.ruleId, issue.path, issue.message);
}
} else {
console.log(result.xml); // PINT-AE UBL XML
}Plugging in your own ERP/POS
The engine doesn't ship an opinion about where your invoice data comes
from. You write a small adapter that implements ErpAdapter<TRaw>
(src/adapters/common/erpAdapter.ts):
export interface ErpAdapter<TRawInvoice = unknown> {
readonly name: string;
getInvoice(externalId: string): Promise<TRawInvoice>;
toCanonical(raw: TRawInvoice): CanonicalInvoice;
listChangedInvoices?(since: Date): Promise<TRawInvoice[]>;
}src/adapters/windward/ is a full reference implementation of that
interface — client, type-mapper, and adapter class — included so you can
see the pattern end-to-end rather than working from the interface alone.
It's an example, not a required dependency; nothing in core/ or uae/
knows it exists.
import { WindwardAdapter } from "uae-einvoice-engine";
const adapter = new WindwardAdapter(
{ baseUrl: "https://windward.example.com/api", apiKey: process.env.WINDWARD_API_KEY! },
{ seller: { legalName: "Acme Trading LLC", trn: "...", addressLine1: "...", city: "Dubai", subdivision: "DXB", countryCode: "AE", electronicAddress: "..." } },
);
const raw = await adapter.getInvoice("WW-2001");
const canonical = adapter.toCanonical(raw);
const result = processInvoice(canonical);To add your own ERP:
adapters/
your-erp/
client.ts talks to the ERP's real API
types.ts ERP-native raw types (adapter-local only)
invoiceMapper.ts toCanonical(raw, tenantConfig) -> CanonicalInvoice
index.ts class YourErpAdapter implements ErpAdapter<YourRawInvoice>The mapper must not import anything from core/ or uae/ beyond the
canonical types — it should have zero knowledge of PINT-AE, UBL, or VAT
category codes. That knowledge is the engine's job, not the adapter's.
What this engine implements
Profile and document types (src/uae/profile.ts,
src/uae/codeLists/registry.ts):
PINT AE Self-Billing 1.0.4 (
urn:peppol:pint:selfbilling-1@ae-1,urn:peppol:bis:selfbilling) — the only profile the engine resolves to; it is not configurable to the Billing profile.Self-Billed Invoice — document type 389.
Self-Billed Credit Note — document type 261.
Applicable out-of-scope scenarios, where supported — Invoice out of scope of tax (480) and the corresponding out-of-scope credit note (81), both under the self-billing profile.
Canonical invoice model (
core/models/canonicalInvoice.ts) — a complete, spec-derived TypeScript model covering mandatory UUIDs, document/line-level allowance-charge VAT category requirements,billingPeriod,currencyExchangeRate, AED total/line fields, payment cards, controlled additional item properties, batch numbers, line invoice period / order-line reference, and supporting-document metadata.Layer 1 schema validation — a Zod re-implementation of the canonical JSON Schema.
Layer 2 code-list validation — AE address subdivisions, VAT category codes, exemption reason codes, credit reason codes, registration types, frequency codes, attachment MIME types (
uae/codeLists/registry.ts).Layers 3-4 business rules, covering seller/buyer TRN (
ibr-132-ae) and TIN (ibr-148-ae) format, TRN/TIN presence and VAT-scheme consistency, registration-type dependencies (PAS → passport country, TL → authority name), the 8-bit transaction type code and its consequences (summary invoice → billing period, e-commerce → delivery address), VAT category behaviour for all six categories (S/E/O/AE/Z/N) including reverse-charge item-id requirements, goods/services classification requirements, allowance/charge base×percentage=amount consistency, gross/discount/net price consistency, payment-means-30 → account requirement, and credit-note preceding-invoice requirements (reason ≠ VD).Calculation engine — line net amounts, per-category VAT amounts, VAT breakdown grouping by (category, rate), and the full BIS document-totals formula chain, plus AED-denominated fields when a
currencyExchangeRateis present.PINT-AE UBL XML generation for both Invoice and CreditNote roots.
ERP adapter pattern —
ErpAdapterinterface plus a full reference adapter (client / mapper / types) modeled on Windward's API shape.Transaction-code and special-endpoint derivation — the engine computes BTAE-02 and the three special buyer endpoint identifiers (deemed supply, export/non-Peppol buyer, buyer not subject to UAE e-invoicing) rather than accepting them as free text.
What this engine does not do
This is a compliance engine, not a compliance platform. Deliberately out of scope:
- Standard PINT AE Billing is not implemented. This engine does not
generate or validate the standard (seller-issued) PINT AE Billing
Invoice (380) or Billing Credit Note (381). There is no code
path, configuration flag, or profile option that produces these document
types —
src/uae/codeLists/registry.tsonly defines codes for the self-billing and out-of-scope variants (389/261/480/81). If your use case needs standard Billing, this engine is not currently a fit for it. - No XSD / Schematron validation. A production pipeline should run the
generated XML through the real UBL XSD and the full generic-PINT + UAE
Schematron assertions before submission. This engine implements a
representative, curated subset of the named business rules as TypeScript
— it does not vendor or execute the actual Schematron files. Treat
processInvoice's validation as a strong pre-check, not a substitute for real Schematron/XSD validation against the official artifacts. - No ASP integration. There's no network submission, status polling, or provider abstraction here — this engine produces the XML; getting it to the UAE network via an Accredited Service Provider is a separate concern you own.
- No persistence, API server, sync engine, or multi-tenant config store. Everything here runs in-process against plain objects you supply. There's no database schema, no HTTP server, no auth.
- The reference adapter is a documented shape, not a verified
integration.
adapters/windward/client.tsmodels a plausible REST client (base URL + bearer token + one GET-by-id / one list-changed call) as a pattern to copy, not a tested Windward integration. Treat it as a template. - Code lists are a curated, actively-validated subset — not the full
set of Genericode files per document type (ISO3166, ISO4217, UNCL*
series, ICD, eas, etc).
uae/codeLists/registry.tscovers what the current validators check and is designed to be swapped for a versioned/external registry later.
If you need the full Schematron/XSD layer, ASP submission, and persistence on top of this, that's a natural place to build — this engine is meant to be the reusable foundation underneath it, not the whole thing.
Verification status
"Validated" can mean several different things. Here's exactly what has and
hasn't been checked for this engine, split by kind of evidence — full
detail and reproducible scripts are in verification/
and CHANGES.md.
1. The engine's own TypeScript validation (npm test). Layer 1
(schema), Layer 2 (code lists), and Layer 3/4 (business rules) are
exercised by this repo's unit and integration tests. This checks internal
consistency against the rules this engine has itself implemented — it does
not independently confirm those rules match the official Schematron.
2. Official-artifact Schematron verification (external to this repo, not run at library runtime). The engine's generated XML (389 invoice, 261 credit note, 480 out-of-scope invoice) was run through the real, vendored OASIS/OpenPeppol PINT AE Billing-track Schematron (Saxon, generic PINT
- UAE jurisdiction) from a supplied
pint-ae-resources-devresource package:- UAE jurisdiction Schematron: 0 failures, all three documents.
- Generic PINT Schematron: 0 failures for the 480 out-of-scope
invoice; 1 failure each (
ibr-cl-01, document-type-code-in-codelist) for the 389 invoice and 261 credit note — because that particular pack is the Billing track, whoseibr-cl-01only allow-lists 380/480 and 81/381, not 389/261. See point 3. - Evidence:
verification/billing-pack-schematron-run/.
3. Direct official-rule verification (Self-Billing track). The
ibr-cl-01 gap in point 2 was resolved by fetching the actual PINT AE
Self-Billing v1.0.4 rule — published directly by OpenPeppol at
https://docs.peppol.eu/poac/ae/upcoming/pint-ae-sb/trn-invoice/rule/ibr-cl-01/
— and executing its exact published XPath test against the engine's
generated XML. Both the 389 invoice and 261 credit note satisfy it. This
was a direct execution of the published assertion, not a full
Schematron-binary run of the Self-Billing package (that package's compiled
.sch/.xslt could not be downloaded into the verification environment).
Evidence: verification/self-billing-v1.0.4-official-rule/.
What this adds up to: every generic-PINT and UAE-jurisdiction assertion
this audit checked (jurisdiction rules across all three sample documents,
generic rules on the 480 document, and ibr-cl-01 specifically for 389/261)
currently passes, using a mix of full-Schematron execution and direct
official-rule execution as described above. This is not a claim of full
rule-for-rule Schematron parity — the business rules in businessRules.ts
remain a curated subset (see "What this engine does not do"), and no full
Schematron-binary run of the official Self-Billing v1.0.4 package has been
performed end-to-end. Treat this engine's validation as a strong pre-check,
and run the real official Schematron/XSD against your own output before
production use.
Tech stack
TypeScript on Node.js 20+, Zod for schema validation,
xmlbuilder2 for XML
generation, Vitest for tests.
Contributing
Issues and PRs welcome — especially around expanding code-list coverage, additional reference adapters, and closing the gap noted above between the curated business rules here and the full official Schematron rule set.
License
MIT — see LICENSE.
