#!/usr/bin/env node
/*
* signet-to-ubl.js
* Projects a SIGNET Canonical Data Model Invoice into a UBL 2.1 Invoice
* conforming to Peppol BIS Billing 3.0 (EN 16931 compliant).
*
* Usage: node tools/signet-to-ubl.js [path/to/invoice.json] > out.xml
* Default input: examples/invoice.json
*
* This is a reference projection, not a validator. It demonstrates that a
* SIGNET invoice carries exactly the EN 16931 Business Terms that UBL / Peppol
* BIS Billing require. Each mapping is annotated with its BT/BG reference.
*
* Licensed CC0 1.0 by Concert Foundation.
*/
const fs = require("fs");
const path = require("path");
/**
* Pure transform: SIGNET canonical Invoice -> Peppol BIS Billing 3.0 UBL string.
* No I/O; safe to import in Node or the browser.
*/
function toUBL(inv) {
// --- helpers ---
const esc = (s) => String(s)
.replace(/&/g, "&").replace(//g, ">")
.replace(/"/g, """).replace(/'/g, "'");
const date = (dt) => (dt ? String(dt).slice(0, 10) : undefined); // ISO date-time -> date (UBL)
const money = (v) => (v == null ? undefined : Number(v.amount).toFixed(2));
const cur = (v) => (v && v.currency) || inv.currency;
// Map a SIGNET Identifier to a UBL party scheme.
// Peppol/ICD: LEI = 0199. DIDs have no ICD code; emit as plain ID (demonstration).
const PARTY_SCHEME = { "gleif:lei": "0199" };
function companyId(idObj) {
const scheme = PARTY_SCHEME[idObj.scheme];
return scheme
? `${esc(idObj.id)}`
: `${esc(idObj.id)}`;
}
const L = []; // line buffer
const w = (s) => L.push(s);
// --- document header ---
w(``);
w(``);
// Peppol BIS Billing 3.0 customization + profile
w(` urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0`);
w(` urn:fdc:peppol.eu:2017:poacc:billing:01:1.0`);
w(` ${esc(inv.id.id)}`); // BT-1
w(` ${date(inv.issueDate)}`); // BT-2
if (inv.paymentDueDate) w(` ${date(inv.paymentDueDate)}`); // BT-9
w(` ${esc(inv.invoiceTypeCode || "380")}`); // BT-3
w(` ${esc(inv.currency)}`); // BT-5
// references
if (inv.order) { // BT-13
w(` ${esc(inv.order.id)}`);
}
if (inv.contract) { // BT-12
w(` ${esc(inv.contract.id)}`);
}
// seller (BG-4)
w(` `);
w(` ${companyId(inv.seller)}`); // BT-27/BT-30
w(` `);
// buyer (BG-7)
w(` `);
w(` ${companyId(inv.buyer)}`); // BT-44/BT-47
w(` `);
// payment terms (BT-20)
if (inv.paymentTerms) {
w(` ${esc(inv.paymentTerms)}`);
}
// tax total (BG-22 / BT-110) with VAT breakdown (BG-23)
const taxTotal = money(inv.taxTotal);
w(` `);
w(` ${taxTotal}`); // BT-110
for (const vb of (inv.vatBreakdown || [])) {
w(` `);
w(` ${money(vb.taxableAmount)}`); // BT-116
w(` ${money(vb.taxAmount)}`); // BT-117
w(` `);
w(` ${esc(vb.categoryCode)}`); // BT-118
w(` ${vb.rate}`); // BT-119
w(` VAT`);
w(` `);
w(` `);
}
w(` `);
// legal monetary total (BG-22)
w(` `);
w(` ${money(inv.lineExtensionTotal)}`); // BT-106
w(` ${money(inv.taxExclusiveTotal)}`); // BT-109
w(` ${money(inv.taxInclusiveTotal)}`); // BT-112
w(` ${money(inv.payableAmount)}`); // BT-115
w(` `);
// invoice lines (BG-25)
for (const ln of inv.lines) {
w(` `);
w(` ${esc(ln.id)}`); // BT-126
if (ln.note) w(` ${esc(ln.note)}`); // BT-127
const uom = ln.unitOfMeasure || "C62";
w(` ${ln.quantity}`); // BT-129/130
w(` ${money(ln.netAmount)}`); // BT-131
w(` `);
w(` ${esc(ln.itemName)}`); // BT-153
if (ln.vatCategoryCode) {
w(` `);
w(` ${esc(ln.vatCategoryCode)}`); // BT-151
if (ln.vatRate != null) w(` ${ln.vatRate}`);
w(` VAT`);
w(` `);
}
if (ln.classification) { // BT-158
const listId = (ln.classification.scheme || "").toUpperCase();
w(` `);
w(` ${esc(ln.classification.id)}`);
w(` `);
}
w(` `);
if (ln.itemNetPrice) {
w(` ${money(ln.itemNetPrice)}`); // BT-146
}
w(` `);
}
w(``);
return L.join("\n") + "\n";
}
// --- CLI wrapper ---
if (require.main === module) {
const inPath = process.argv[2] || path.join(__dirname, "..", "examples", "invoice.json");
const inv = JSON.parse(fs.readFileSync(inPath, "utf8"));
process.stdout.write(toUBL(inv));
}
module.exports = { toUBL };