Skip to content

grunt-it/fiscalize

Repository files navigation

@grunt-it/fiscalize

An open-source engine for national e-invoicing and fiscal clearance in the EU: the country-specific, real-time submission of invoices to a member state's tax authority (certificates, protective marks, gap-free sequencing, response authentication).

Document generation (EN16931 → UBL / CII / Factur-X) is handled by the upstream library @e-invoice-eu/core, which fiscalize builds on rather than forks. fiscalize adds the missing layer above it: a pluggable per-country adapter interface over EN16931, with Slovenia (e-SLOG 2.0 + FURS) as the first complete pilot adapter. The architecture is designed to generalise across the EU as the ViDA mandate takes effect (cross-border B2B 2030, full harmonisation 2035).

It is framework-agnostic: build an invoice, get conformant XML out, and clear it with the national authority. Host-agnostic consumers wrap it as a leaf dependency.

License: engine + adapters Apache-2.0.

Status, P1 + P2. Covers the e-invoice core (generate + validate an EN16931 core invoice as e-SLOG 2.0 / UBL / CII, with official-XSD validation) and FURS fiscal verification (ZOI/EOR, unit-verified; live test-env round-trip deferred, see below). The deployable clearance service (blagajnica) and the invoicing UI are later phases, see ROADMAP.md.

Why this shape

Slovenia's e-SLOG 2.0 is EN16931-compliant but is its own XML syntax (UN/EDIFACT INVOIC-derived, namespace urn:eslog:2.00), not UBL. So fiscalize:

  • depends on @e-invoice-eu/core (WTFPL) for the EN16931 model, validation, and UBL / CII / Peppol / Factur-X serialization, the hard, maintained part; and
  • owns the thin Slovenian delta: an e-SLOG 2.0 serializer.

Staying on upstream is deliberate, EN16931 / Peppol rules move (the SI B2B mandate lands Jan 2028), and we want to inherit those updates rather than fork away from them. Rule-change monitoring is tracked via upkeep.

Install

Published to GitHub Packages under the @grunt-it scope. Auth follows the house standard: a committed bunfig.toml that reads the token from the REGISTRY_TOKEN env var (the same config app-template / utility-belt use; CI provides REGISTRY_TOKEN as a secret). In the consuming repo:

# bunfig.toml, no secret in the file, safe to commit
[install.scopes]
"@grunt-it" = { token = "$REGISTRY_TOKEN", url = "https://npm.pkg.github.com" }
export REGISTRY_TOKEN="ghp_…"   # locally: a PAT with read:packages
bun add @grunt-it/fiscalize

Quick start

Promise-friendly, for any host (never throws):

import { createEInvoice, type Invoice } from "@grunt-it/fiscalize";

const invoice: Invoice = {
  invoiceNumber: "2026-000123",
  issueDate: "2026-05-25",
  dueDate: "2026-06-08",
  seller: {
    name: "Zelena Japka d.o.o.",
    vatId: "SI12345678",
    address: { street: "Slovenska cesta 1", city: "Ljubljana", postalZone: "1000", countryCode: "SI" },
    iban: "SI56020170014356205",
    bic: "LJBASI2X",
  },
  buyer: {
    name: "Kupec d.o.o.",
    vatId: "SI87654321",
    address: { city: "Ljubljana", postalZone: "1000", countryCode: "SI" },
  },
  lines: [
    { id: "1", name: "Detergent 1L", quantity: 10, netPrice: 5.0, lineNetAmount: 50.0, taxRate: 22 },
  ],
  taxBreakdown: [{ taxableAmount: 50.0, taxAmount: 11.0, taxRate: 22 }],
  totals: { lineExtensionAmount: 50.0, taxExclusiveAmount: 50.0, taxAmount: 11.0, taxInclusiveAmount: 61.0 },
};

const result = await createEInvoice(invoice, { format: "eslog" });
if (result.ok) {
  console.log(result.data); // e-SLOG 2.0 XML
} else {
  console.error(result.error.message, result.error.status);
}

format is "eslog" | "ubl" | "cii". For ubl/cii, EN16931 validation runs before generation by default (validate: false to skip).

Effect-native API

import { Effect } from "effect";
import { parseInvoice, validateEn16931, generateEInvoice, serializeEslog } from "@grunt-it/fiscalize";

const program = Effect.gen(function* () {
  const invoice = yield* parseInvoice(rawInput);   // valibot, structural gate
  yield* validateEn16931(invoice);                 // Ajv vs EN16931 schema
  return yield* generateEInvoice(invoice, { format: "ubl" });
});

serializeEslog(invoice) is a pure synchronous escape hatch for e-SLOG only.

Validate produced e-SLOG against the official XSD

import { generateEInvoice, validateEslogXml } from "@grunt-it/fiscalize";

// validate on the way out…
const xml = await Effect.runPromise(
  generateEInvoice(invoice, { format: "eslog", validateOutput: true }),
);

// …or validate any e-SLOG XML string standalone
yield* validateEslogXml(someEslogXml);

validateEslogXml checks the XML against the official e-SLOG 2.0 XSD (eSLOG20_INVOIC_v200.xsd + xmldsig-core-schema.xsd, from the epos.si Aug-2020 package), using xmllint compiled to WebAssembly, no native bindings.

FURS fiscal verification (P2)

For Slovenian cash-register fiscalization (distinct from the e-invoice document): compute the ZOI and obtain the EOR from FURS.

import { Effect } from "effect";
import { makeFursClient } from "@grunt-it/fiscalize/furs";

const program = Effect.gen(function* () {
  const furs = yield* makeFursClient({ p12, passphrase, production: false });
  const { zoi, eor, printable } = yield* furs.reportInvoice({
    taxNumber: 10489185,
    issueDateTime: new Date(),
    invoiceNumber: "11",
    businessPremiseId: "BP101",
    electronicDeviceId: "0001",
    invoiceAmount: 19.15,
    vat: [{ taxRate: 22, taxableAmount: 15.7, taxAmount: 3.45 }],
  });
  return { zoi, eor, printable }; // ZOI + FURS EOR + the QR/PDF417 string
});

p12 is the taxpayer's certificate (a FURS test cert for the test env; the shop's eDavki cert in production). ZOI is MD5(RSA-SHA256/PKCS#1 v1.5(…)); messages are RS256 JWS over mutual TLS.

Pass fursResponseCertPem (FURS's response-signing cert) to authenticate FURS's replies, the client then verifies each response's JWS signature before trusting the EOR (raising FursResponseSignatureError on a spoofed/tampered response). Strongly recommended for production.

⚠ Runtime requirement, live FURS needs Node, not bun. Outbound mutual-TLS client certs do not work under bun 1.3.6, so the live FURS submission path (echo / reportInvoice / registerBusinessPremise) must run under a Node runtime on a non-proxied network. The ZOI / JWS / verification crypto runs anywhere (incl. bun). This is a real deployment constraint, documented in full with the opt-in path: docs/FURS-RUNTIME.md.

The model

Invoice is a clean, EN16931-aligned core invoice: header (BT-1/2/3/5/9/72), seller & buyer (BG-4/BG-7, with VAT, address, bank), lines (BG-25), VAT breakdown (BG-23), and totals (BG-22). Every field is annotated with its Business Term so the mapping to each syntax stays auditable. Validated with valibot.

What's covered vs deferred

P1 maps the mandatory + common core, and produced e-SLOG XML is validated against the official e-SLOG 2.0 XSD. Deferred (see ROADMAP.md): business-rule (schematron-equivalent) validation, the official package ships no .sch, so those rules are spec prose, plus document/line-level allowances, contacts, multiple payment means, and the long tail of optional BTs. The e-SLOG mapping is grounded in the official spec (epos.si) and cross-checked against the MIT-licensed reference generator Media24si/eslog2.

Examples

Runnable end-to-end scripts in examples/:

bun run examples/e-invoice.ts     # model → e-SLOG 2.0 (XSD-valid) + UBL
bun run examples/furs-offline.ts  # FURS ZOI + request-JWS + response-verify (offline)

examples/furs-offline.ts shows the FURS crypto without a live call (runs under bun). For a live submission, see docs/FURS-RUNTIME.md.

Development

bun install
bun test
bunx tsc --noEmit

License

Apache-2.0 (engine + country adapters). (Bundled third-party schema files keep their own terms, see src/lib/eslog/schema/PROVENANCE.md.)

About

Open-source engine for national e-invoicing and fiscal clearance in the EU, built on EN16931 with a pluggable per-country adapter interface. Piloted in Slovenia (e-SLOG 2.0 + FURS). Builds on @e-invoice-eu/core.

Topics

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors