npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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 → pipeline

Getting started

npm install uae-einvoice-engine

Or, 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 currencyExchangeRate is present.

  • PINT-AE UBL XML generation for both Invoice and CreditNote roots.

  • ERP adapter patternErpAdapter interface 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.ts only 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.ts models 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.ts covers 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-dev resource 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, whose ibr-cl-01 only 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.