Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 121 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,125 @@

Open-source Slovenian **fiscalization + e-invoicing** toolkit on the grunt-it
TS/Effect stack — the compliance-hard sliver of what Minimax does, as a reusable,
framework-agnostic engine (not a full accounting suite).
**framework-agnostic** engine (not a full accounting suite).

> Scaffold. Implementation lands in the P1 PR (e-invoice core). See `ROADMAP.md`.
It knows nothing about Medusa, HTTP frameworks, or any host: build an invoice,
get conformant XML out. Consumers (e.g. a Medusa fiscalization plugin) wrap it as
a leaf dependency.

> **Status — P1.** This release covers the **e-invoice core**: generate +
> validate an EN16931 core invoice as **e-SLOG 2.0** (Slovenian) and **UBL / CII**.
> FURS fiscal verification (ZOI/EOR), Medusa integration, and the service/MCP
> surface are later phases — see [`ROADMAP.md`](./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`](https://github.com/gflohr/e-invoice-eu)
(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. Create a `bunfig.toml`:

```toml
[install.scopes]
"@grunt-it" = { token = "$REGISTRY_TOKEN", url = "https://npm.pkg.github.com" }
```

```bash
export REGISTRY_TOKEN="ghp_…" # a PAT with read:packages
bun add @grunt-it/fiscalize
```

## Quick start

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

```ts
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

```ts
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.

## 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**. Document-level allowances/charges,
line-level allowances, contacts, multiple payment means, e-SLOG XSD/schematron
output validation, and the long tail of optional BTs are deferred — see
[`ROADMAP.md`](./ROADMAP.md). The e-SLOG mapping is grounded in the official
spec (epos.si) and cross-checked against the MIT-licensed reference generator
`Media24si/eslog2`.

## Development

```bash
bun install
bun test
bunx tsc --noEmit
```

## License

MIT — see [`LICENSE`](./LICENSE).
54 changes: 54 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Roadmap

Phasing mirrors the ticket (`grunt-it/tickets/fiscalization-einvoicing-toolkit.md`).

## P1 — e-invoice core ✅ (this release)

EN16931 core invoice → **e-SLOG 2.0** + **UBL / CII**, with validation.

- Clean EN16931-aligned `Invoice` domain model (valibot), BT-annotated.
- Owned **e-SLOG 2.0** serializer (`urn:eslog:2.00`, UN/EDIFACT-INVOIC structure):
header, dates, parties (+ bank + VAT), currency, payment terms, lines, document
totals, VAT breakdown.
- **UBL / CII** via `@e-invoice-eu/core` (model → internal `ubl:Invoice` JSON).
- EN16931 structural validation via the lib's JSON Schema (Ajv 2019-09).
- Effect-native API + Promise boundary (`createEInvoice`).

### Deferred within the e-invoice core (next P1.x slices)

- **e-SLOG XSD + schematron validation** of *output*. P1 validates the EN16931
model structurally (UBL schema); it does not yet validate the produced e-SLOG
XML against the official e-SLOG 2.0 XSD or business-rule schematron. Vendor the
XSD (incl. `xmldsig-core-schema.xsd`) and add an output-validation pass.
- **Document-level allowances/charges** (BG-20/BG-21) → e-SLOG `G_SG16`, UBL
`cac:AllowanceCharge`. Model has the totals (BT-107/108) but not the detail.
- **Line-level allowances** (BG-27/BG-28) → e-SLOG `G_SG39`.
- **Contacts** (BG-6/BG-9), **delivery address** (BG-15), **multiple payment
means**, item identifiers (EAN/buyer/seller), `cbc:Note` granularity.
- **Cross-border**: EAS scheme defaults beyond SI VAT (`9949`); a country→EAS map.
- **XML digital signature** (XAdES over e-SLOG) — required for some exchange paths.
- **Credit notes / corrected invoices** end-to-end (type codes are modelled;
the negative-amount + reference-to-original rules are not yet exercised).

## P2 — FURS fiscal verification

Cash-register receipts → ZOI/EOR. Derive from `node-furs-fiscal-verification`
(TS-ify; cert handling, FURS tax-API calls). Cross-language refs: SLOTax (.NET),
jurgenwerk/furs_fiscal_verification (Ruby).

## P3 — integration surface

`order.placed` → fiscalize (FURS) + e-invoice (e-SLOG/UBL). Consumed by a
separate `@grunt-it/medusa-plugin-si-fiscalization` track — fiscalize stays
framework-agnostic; the plugin wraps it.

## P4 — service + MCP

Optional long-running service + MCP-accessible surface (sibling to secret-tap,
upkeep). Package for reuse.

## Ongoing — compliance tracking

FURS / e-SLOG / EN16931 / Peppol rules change (incl. the moving Jan-2028 SI B2B
mandate). Tracked via `upkeep`'s AI compliance check; fiscalize is its first
consumer. Rule changes become mapping updates here.
114 changes: 114 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Effect } from "effect";
import { generateEInvoice, type GenerateOptions } from "./lib/einvoice/generate";
import { validateEn16931 } from "./lib/einvoice/validate";
import { type EffectResult, runSafe } from "./lib/foundation/runtime";
import { parseInvoice } from "./lib/invoice/validate";

export interface CreateEInvoiceOptions extends GenerateOptions {
/**
* Run EN16931 business-rule validation before generating. Defaults to `true`
* for `ubl`/`cii` and `false` for `eslog` (e-SLOG has its own XSD/schematron
* validation path, deferred — see ROADMAP.md).
*/
validate?: boolean;
}

/**
* The full pipeline as an Effect: parse unknown input against the invoice model
* → optionally validate against EN16931 → generate XML in the requested format.
*/
export const createEInvoiceEffect = (input: unknown, options: CreateEInvoiceOptions) =>
Effect.gen(function* () {
const invoice = yield* parseInvoice(input);
const shouldValidate = options.validate ?? options.format !== "eslog";
if (shouldValidate) yield* validateEn16931(invoice);
return yield* generateEInvoice(invoice, options);
});

/**
* Promise-friendly wrapper of {@link createEInvoiceEffect} for non-Effect hosts
* (e.g. a Medusa plugin). Never throws — returns `{ ok: true, data }` with the
* XML or `{ ok: false, error: { message, status } }`.
*/
export function createEInvoice(
input: unknown,
options: CreateEInvoiceOptions,
): Promise<EffectResult<string>> {
return runSafe(createEInvoiceEffect(input, options));
}

export * from "./lib/foundation";
export * from "./lib/invoice";
export * from "./lib/einvoice";
export { type EslogOptions, serializeEslog } from "./lib/eslog/serialize";
export * as eslogCodes from "./lib/eslog/codes";
16 changes: 16 additions & 0 deletions src/lib/einvoice/formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** Output formats fiscalize can produce. */
export const FORMATS = ["eslog", "ubl", "cii"] as const;
export type Format = (typeof FORMATS)[number];

/**
* Map a fiscalize format to the `@e-invoice-eu/core` format string. `eslog` is
* handled by our own serializer and is intentionally absent here.
*/
export const LIB_FORMAT: Record<Exclude<Format, "eslog">, string> = {
ubl: "UBL",
cii: "CII",
};

export function isFormat(value: string): value is Format {
return (FORMATS as readonly string[]).includes(value);
}
Loading
Loading