@formfeed/mcp 0.3.16 → 0.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -5
- package/bin/formfeed-mcp.mjs +1511 -49
- package/package.json +5 -2
package/bin/formfeed-mcp.mjs
CHANGED
|
@@ -6257,9 +6257,9 @@ var require_parser2 = __commonJS({
|
|
|
6257
6257
|
this.yytext = "";
|
|
6258
6258
|
this.match = "";
|
|
6259
6259
|
}
|
|
6260
|
-
var
|
|
6261
|
-
for (var i = 0; i <
|
|
6262
|
-
tempMatch = this._input.match(this.rules[
|
|
6260
|
+
var rules2 = this._currentRules();
|
|
6261
|
+
for (var i = 0; i < rules2.length; i++) {
|
|
6262
|
+
tempMatch = this._input.match(this.rules[rules2[i]]);
|
|
6263
6263
|
if (tempMatch && (!match7 || tempMatch[0].length > match7[0].length)) {
|
|
6264
6264
|
match7 = tempMatch;
|
|
6265
6265
|
index = i;
|
|
@@ -6285,7 +6285,7 @@ var require_parser2 = __commonJS({
|
|
|
6285
6285
|
this._more = false;
|
|
6286
6286
|
this._input = this._input.slice(match7[0].length);
|
|
6287
6287
|
this.matched += match7[0];
|
|
6288
|
-
token = this.performAction.call(this, this.yy, this,
|
|
6288
|
+
token = this.performAction.call(this, this.yy, this, rules2[index], this.conditionStack[this.conditionStack.length - 1]);
|
|
6289
6289
|
if (this.done && this._input) this.done = false;
|
|
6290
6290
|
if (token) return token;
|
|
6291
6291
|
else return;
|
|
@@ -7573,7 +7573,7 @@ var require_base64 = __commonJS({
|
|
|
7573
7573
|
var require_base64_vlq = __commonJS({
|
|
7574
7574
|
"node_modules/.pnpm/source-map@0.6.1/node_modules/source-map/lib/base64-vlq.js"(exports) {
|
|
7575
7575
|
"use strict";
|
|
7576
|
-
var
|
|
7576
|
+
var base642 = require_base64();
|
|
7577
7577
|
var VLQ_BASE_SHIFT = 5;
|
|
7578
7578
|
var VLQ_BASE = 1 << VLQ_BASE_SHIFT;
|
|
7579
7579
|
var VLQ_BASE_MASK = VLQ_BASE - 1;
|
|
@@ -7596,7 +7596,7 @@ var require_base64_vlq = __commonJS({
|
|
|
7596
7596
|
if (vlq > 0) {
|
|
7597
7597
|
digit |= VLQ_CONTINUATION_BIT;
|
|
7598
7598
|
}
|
|
7599
|
-
encoded +=
|
|
7599
|
+
encoded += base642.encode(digit);
|
|
7600
7600
|
} while (vlq > 0);
|
|
7601
7601
|
return encoded;
|
|
7602
7602
|
};
|
|
@@ -7609,7 +7609,7 @@ var require_base64_vlq = __commonJS({
|
|
|
7609
7609
|
if (aIndex >= strLen) {
|
|
7610
7610
|
throw new Error("Expected more digits in base 64 VLQ value.");
|
|
7611
7611
|
}
|
|
7612
|
-
digit =
|
|
7612
|
+
digit = base642.decode(aStr.charCodeAt(aIndex++));
|
|
7613
7613
|
if (digit === -1) {
|
|
7614
7614
|
throw new Error("Invalid base64 digit: " + aStr.charAt(aIndex - 1));
|
|
7615
7615
|
}
|
|
@@ -14656,7 +14656,7 @@ var require_nunjucks = __commonJS({
|
|
|
14656
14656
|
});
|
|
14657
14657
|
return result;
|
|
14658
14658
|
};
|
|
14659
|
-
_proto.fetch = function
|
|
14659
|
+
_proto.fetch = function fetch2(url, cb) {
|
|
14660
14660
|
if (typeof window === "undefined") {
|
|
14661
14661
|
throw new Error("WebLoader can only by used in a browser");
|
|
14662
14662
|
}
|
|
@@ -16041,10 +16041,10 @@ var require_nunjucks = __commonJS({
|
|
|
16041
16041
|
return typeof value === "function";
|
|
16042
16042
|
}
|
|
16043
16043
|
exports2.callable = callable;
|
|
16044
|
-
function
|
|
16044
|
+
function defined2(value) {
|
|
16045
16045
|
return value !== void 0;
|
|
16046
16046
|
}
|
|
16047
|
-
exports2.defined =
|
|
16047
|
+
exports2.defined = defined2;
|
|
16048
16048
|
function divisibleby(one, two) {
|
|
16049
16049
|
return one % two === 0;
|
|
16050
16050
|
}
|
|
@@ -70747,6 +70747,14 @@ function epcPayload(input) {
|
|
|
70747
70747
|
""
|
|
70748
70748
|
].join("\n");
|
|
70749
70749
|
}
|
|
70750
|
+
var normaliseIban = (iban2) => iban2.replace(/\s/g, "").toUpperCase();
|
|
70751
|
+
function ibanChecksum(iban2) {
|
|
70752
|
+
const rearranged = normaliseIban(iban2).slice(4) + normaliseIban(iban2).slice(0, 4);
|
|
70753
|
+
const digits = [...rearranged].map((c) => c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c).join("");
|
|
70754
|
+
let remainder = 0;
|
|
70755
|
+
for (const digit of digits) remainder = (remainder * 10 + Number(digit)) % 97;
|
|
70756
|
+
return remainder === 1;
|
|
70757
|
+
}
|
|
70750
70758
|
|
|
70751
70759
|
// packages/engine/src/lib/codes/qr.ts
|
|
70752
70760
|
var import_qrcode = __toESM(require_lib());
|
|
@@ -70849,11 +70857,11 @@ var codeHelpers = [
|
|
|
70849
70857
|
aliases: ["epc_qr", "girocode"],
|
|
70850
70858
|
doc: {
|
|
70851
70859
|
signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
|
|
70852
|
-
description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment. Give the image an `alt`, and print the bank details as text as well: the code is a picture.",
|
|
70860
|
+
description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment. Put an invoice number into `text` (the remittance line); `reference` is only for a structured creditor reference (RF\u2026). Give the image an `alt`, and print the bank details as text as well: the code is a picture.",
|
|
70853
70861
|
examples: {
|
|
70854
|
-
jinja2: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total,
|
|
70855
|
-
liquid: '<img src="{{ company.iban | epcQr: name: company.name, amount: invoice.total,
|
|
70856
|
-
handlebars: '<img src="{{epcQr name=company.name iban=company.iban amount=invoice.total
|
|
70862
|
+
jinja2: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, text: invoice.number }) }}" alt="QR code to pay invoice {{ invoice.number }}">',
|
|
70863
|
+
liquid: '<img src="{{ company.iban | epcQr: name: company.name, amount: invoice.total, text: invoice.number }}" alt="QR code to pay invoice {{ invoice.number }}">',
|
|
70864
|
+
handlebars: '<img src="{{epcQr name=company.name iban=company.iban amount=invoice.total text=invoice.number}}" alt="QR code to pay invoice {{invoice.number}}">'
|
|
70857
70865
|
},
|
|
70858
70866
|
category: "code"
|
|
70859
70867
|
},
|
|
@@ -81793,6 +81801,49 @@ var paymentMeansCodes = list2(
|
|
|
81793
81801
|
"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 74 75 76 77 78 91 92 93 94 95 96 97 98 ZZZ"
|
|
81794
81802
|
);
|
|
81795
81803
|
|
|
81804
|
+
// packages/engine/src/lib/einvoice/decimal.ts
|
|
81805
|
+
function decimalText(value) {
|
|
81806
|
+
if (typeof value === "string") {
|
|
81807
|
+
const text5 = value.trim();
|
|
81808
|
+
return /^-?\d+(\.\d+)?$/.test(text5) ? trimZeros(text5) : null;
|
|
81809
|
+
}
|
|
81810
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return null;
|
|
81811
|
+
const text4 = value.toPrecision(15);
|
|
81812
|
+
return /^-?\d+(\.\d+)?$/.test(text4) ? trimZeros(text4) : null;
|
|
81813
|
+
}
|
|
81814
|
+
function trimZeros(text4) {
|
|
81815
|
+
const trimmed = text4.includes(".") ? text4.replace(/0+$/, "").replace(/\.$/, "") : text4;
|
|
81816
|
+
return trimmed === "-0" ? "0" : trimmed;
|
|
81817
|
+
}
|
|
81818
|
+
function decimalPlaces(text4) {
|
|
81819
|
+
const dot = text4.indexOf(".");
|
|
81820
|
+
return dot === -1 ? 0 : text4.length - dot - 1;
|
|
81821
|
+
}
|
|
81822
|
+
function toUnits(text4, scale) {
|
|
81823
|
+
const negative = text4.startsWith("-");
|
|
81824
|
+
const [whole = "0", fraction = ""] = (negative ? text4.slice(1) : text4).split(
|
|
81825
|
+
"."
|
|
81826
|
+
);
|
|
81827
|
+
const units = BigInt(whole + fraction.padEnd(scale, "0").slice(0, scale));
|
|
81828
|
+
return negative ? -units : units;
|
|
81829
|
+
}
|
|
81830
|
+
function fromUnits(units, scale, places = scale) {
|
|
81831
|
+
const negative = units < 0n;
|
|
81832
|
+
const digits = (negative ? -units : units).toString().padStart(scale + 1, "0");
|
|
81833
|
+
const whole = digits.slice(0, digits.length - scale);
|
|
81834
|
+
const fraction = digits.slice(digits.length - scale).padEnd(places, "0");
|
|
81835
|
+
const text4 = places > 0 ? `${whole}.${fraction.slice(0, places)}` : whole;
|
|
81836
|
+
return negative && /[1-9]/.test(text4) ? `-${text4}` : text4;
|
|
81837
|
+
}
|
|
81838
|
+
function divideRounded(value, divisor) {
|
|
81839
|
+
const negative = value < 0n !== divisor < 0n;
|
|
81840
|
+
const a = value < 0n ? -value : value;
|
|
81841
|
+
const b2 = divisor < 0n ? -divisor : divisor;
|
|
81842
|
+
const quotient = (a * 2n + b2) / (b2 * 2n);
|
|
81843
|
+
return negative ? -quotient : quotient;
|
|
81844
|
+
}
|
|
81845
|
+
var abs = (value) => value < 0n ? -value : value;
|
|
81846
|
+
|
|
81796
81847
|
// packages/engine/src/lib/einvoice/schema.ts
|
|
81797
81848
|
import { z as z2 } from "zod";
|
|
81798
81849
|
var numeric = z2.union([z2.number(), z2.string()]);
|
|
@@ -81965,6 +82016,1093 @@ var einvoiceOptionsSchema = z2.object({
|
|
|
81965
82016
|
display_check: z2.enum(["warn", "strict"]).optional()
|
|
81966
82017
|
}).strict();
|
|
81967
82018
|
|
|
82019
|
+
// packages/engine/src/lib/einvoice/check.ts
|
|
82020
|
+
var REQUIRED = {
|
|
82021
|
+
number: ["BR-02", "BT-1"],
|
|
82022
|
+
issue_date: ["BR-03", "BT-2"],
|
|
82023
|
+
currency: ["BR-05", "BT-5"],
|
|
82024
|
+
seller: ["BR-06", "BG-4"],
|
|
82025
|
+
"seller.name": ["BR-06", "BT-27"],
|
|
82026
|
+
"seller.address": ["BR-08", "BG-5"],
|
|
82027
|
+
"seller.address.country": ["BR-09", "BT-40"],
|
|
82028
|
+
buyer: ["BR-07", "BG-7"],
|
|
82029
|
+
"buyer.name": ["BR-07", "BT-44"],
|
|
82030
|
+
"buyer.address": ["BR-10", "BG-8"],
|
|
82031
|
+
"buyer.address.country": ["BR-11", "BT-55"],
|
|
82032
|
+
lines: ["BR-16", "BG-25"],
|
|
82033
|
+
"lines[].id": ["BR-21", "BT-126"],
|
|
82034
|
+
"lines[].quantity": ["BR-22", "BT-129"],
|
|
82035
|
+
"lines[].net_amount": ["BR-24", "BT-131"],
|
|
82036
|
+
"lines[].name": ["BR-25", "BT-153"],
|
|
82037
|
+
"lines[].net_price": ["BR-26", "BT-146"],
|
|
82038
|
+
"lines[].tax": ["BR-CO-04", "BT-151"],
|
|
82039
|
+
"lines[].tax.category": ["BR-CO-04", "BT-151"],
|
|
82040
|
+
tax: ["BR-CO-18", "BG-23"],
|
|
82041
|
+
"tax.breakdown": ["BR-CO-18", "BG-23"],
|
|
82042
|
+
"tax.breakdown[].basis": ["BR-45", "BT-116"],
|
|
82043
|
+
"tax.breakdown[].amount": ["BR-46", "BT-117"],
|
|
82044
|
+
"tax.breakdown[].category": ["BR-47", "BT-118"],
|
|
82045
|
+
totals: ["BR-12", "BG-22"],
|
|
82046
|
+
"totals.line_net": ["BR-12", "BT-106"],
|
|
82047
|
+
"totals.tax_basis": ["BR-13", "BT-109"],
|
|
82048
|
+
"totals.tax_total": ["BR-CO-14", "BT-110"],
|
|
82049
|
+
"totals.grand": ["BR-14", "BT-112"],
|
|
82050
|
+
"totals.due": ["BR-15", "BT-115"],
|
|
82051
|
+
"allowances[].amount": ["BR-31", "BT-92"],
|
|
82052
|
+
"allowances[].tax": ["BR-32", "BT-95"],
|
|
82053
|
+
"allowances[].tax.category": ["BR-32", "BT-95"],
|
|
82054
|
+
"charges[].amount": ["BR-36", "BT-99"],
|
|
82055
|
+
"charges[].tax": ["BR-37", "BT-102"],
|
|
82056
|
+
"charges[].tax.category": ["BR-37", "BT-102"],
|
|
82057
|
+
"lines[].allowances[].amount": ["BR-41", "BT-136"],
|
|
82058
|
+
"lines[].charges[].amount": ["BR-43", "BT-141"],
|
|
82059
|
+
"delivery.address.country": ["BR-57", "BT-80"],
|
|
82060
|
+
"preceding_invoices[].number": ["BR-55", "BT-25"]
|
|
82061
|
+
};
|
|
82062
|
+
var RULE_PREFIX = {
|
|
82063
|
+
S: "BR-S",
|
|
82064
|
+
Z: "BR-Z",
|
|
82065
|
+
E: "BR-E",
|
|
82066
|
+
AE: "BR-AE",
|
|
82067
|
+
K: "BR-IC",
|
|
82068
|
+
G: "BR-G",
|
|
82069
|
+
O: "BR-O"
|
|
82070
|
+
};
|
|
82071
|
+
var UNIT_SUGGESTIONS = {
|
|
82072
|
+
piece: "C62",
|
|
82073
|
+
pieces: "C62",
|
|
82074
|
+
pcs: "C62",
|
|
82075
|
+
pc: "C62",
|
|
82076
|
+
stk: "C62",
|
|
82077
|
+
st\u00FCck: "C62",
|
|
82078
|
+
unit: "C62",
|
|
82079
|
+
units: "C62",
|
|
82080
|
+
each: "C62",
|
|
82081
|
+
hour: "HUR",
|
|
82082
|
+
hours: "HUR",
|
|
82083
|
+
h: "HUR",
|
|
82084
|
+
std: "HUR",
|
|
82085
|
+
stunde: "HUR",
|
|
82086
|
+
stunden: "HUR",
|
|
82087
|
+
day: "DAY",
|
|
82088
|
+
days: "DAY",
|
|
82089
|
+
tag: "DAY",
|
|
82090
|
+
tage: "DAY",
|
|
82091
|
+
month: "MON",
|
|
82092
|
+
months: "MON",
|
|
82093
|
+
monat: "MON",
|
|
82094
|
+
year: "ANN",
|
|
82095
|
+
min: "MIN",
|
|
82096
|
+
minute: "MIN",
|
|
82097
|
+
minutes: "MIN",
|
|
82098
|
+
kg: "KGM",
|
|
82099
|
+
g: "GRM",
|
|
82100
|
+
t: "TNE",
|
|
82101
|
+
l: "LTR",
|
|
82102
|
+
litre: "LTR",
|
|
82103
|
+
liter: "LTR",
|
|
82104
|
+
m: "MTR",
|
|
82105
|
+
km: "KMT",
|
|
82106
|
+
m2: "MTK",
|
|
82107
|
+
qm: "MTK",
|
|
82108
|
+
m3: "MTQ",
|
|
82109
|
+
kwh: "KWH",
|
|
82110
|
+
flat: "LS",
|
|
82111
|
+
pauschal: "LS"
|
|
82112
|
+
};
|
|
82113
|
+
var FRENCH_FRAMEWORKS = /* @__PURE__ */ new Set([
|
|
82114
|
+
"B1",
|
|
82115
|
+
"S1",
|
|
82116
|
+
"M1",
|
|
82117
|
+
"B2",
|
|
82118
|
+
"S2",
|
|
82119
|
+
"M2",
|
|
82120
|
+
"S3",
|
|
82121
|
+
"B4",
|
|
82122
|
+
"S4",
|
|
82123
|
+
"M4",
|
|
82124
|
+
"S5",
|
|
82125
|
+
"S6",
|
|
82126
|
+
"B7",
|
|
82127
|
+
"S7",
|
|
82128
|
+
"B8",
|
|
82129
|
+
"S8",
|
|
82130
|
+
"M8"
|
|
82131
|
+
]);
|
|
82132
|
+
var FRENCH_OPERATION = { goods: "B1", services: "S1", mixed: "M1" };
|
|
82133
|
+
var FRENCH_NOTES = [
|
|
82134
|
+
["PMT", "the fixed compensation of \u20AC40 for recovery costs"],
|
|
82135
|
+
["PMD", "the penalties for late payment"],
|
|
82136
|
+
["AAB", "the discount for early payment, or that none is given"]
|
|
82137
|
+
];
|
|
82138
|
+
var CREDIT_TRANSFER = /* @__PURE__ */ new Set(["30", "58"]);
|
|
82139
|
+
function checkInvoice(data, options = {}) {
|
|
82140
|
+
const problems = [];
|
|
82141
|
+
const add2 = (code, path, message2, extra = {}) => {
|
|
82142
|
+
problems.push({
|
|
82143
|
+
code,
|
|
82144
|
+
path,
|
|
82145
|
+
...extra.rule ? { rule: extra.rule } : {},
|
|
82146
|
+
message: extra.rule ? `${message2} (${extra.rule})` : message2,
|
|
82147
|
+
args: {
|
|
82148
|
+
path,
|
|
82149
|
+
...extra.rule ? { rule: extra.rule } : {},
|
|
82150
|
+
...extra.args
|
|
82151
|
+
}
|
|
82152
|
+
});
|
|
82153
|
+
};
|
|
82154
|
+
const parsed = invoiceSchema.safeParse(data);
|
|
82155
|
+
if (!parsed.success) {
|
|
82156
|
+
for (const issue of parsed.error.issues) {
|
|
82157
|
+
const path = pathText(issue.path);
|
|
82158
|
+
const required = REQUIRED[path.replace(/\[\d+\]/g, "[]")];
|
|
82159
|
+
const absent = valueAt(data, issue.path) === void 0;
|
|
82160
|
+
if (absent && required)
|
|
82161
|
+
add2("missing", path, `_invoice.${path} is missing (${required[1]})`, {
|
|
82162
|
+
rule: required[0],
|
|
82163
|
+
args: { term: required[1] }
|
|
82164
|
+
});
|
|
82165
|
+
else if (absent) add2("missing", path, `_invoice.${path} is missing`);
|
|
82166
|
+
else if (issue.code === "too_small" && path === "lines")
|
|
82167
|
+
add2(
|
|
82168
|
+
"missing",
|
|
82169
|
+
path,
|
|
82170
|
+
"_invoice.lines is empty: an invoice has at least one line (BG-25)",
|
|
82171
|
+
{
|
|
82172
|
+
rule: "BR-16",
|
|
82173
|
+
args: { term: "BG-25" }
|
|
82174
|
+
}
|
|
82175
|
+
);
|
|
82176
|
+
else if (issue.code === "too_small" && path === "tax.breakdown")
|
|
82177
|
+
add2(
|
|
82178
|
+
"missing",
|
|
82179
|
+
path,
|
|
82180
|
+
"_invoice.tax.breakdown is empty: an invoice has at least one VAT breakdown (BG-23)",
|
|
82181
|
+
{
|
|
82182
|
+
rule: "BR-CO-18",
|
|
82183
|
+
args: { term: "BG-23" }
|
|
82184
|
+
}
|
|
82185
|
+
);
|
|
82186
|
+
else
|
|
82187
|
+
add2("invalid", path, `_invoice.${path}: ${issue.message}`, {
|
|
82188
|
+
args: { detail: issue.message }
|
|
82189
|
+
});
|
|
82190
|
+
}
|
|
82191
|
+
return { ok: false, invoice: null, problems };
|
|
82192
|
+
}
|
|
82193
|
+
const invoice = normalise(parsed.data, add2, options);
|
|
82194
|
+
if (problems.length > 0) return { ok: false, invoice: null, problems };
|
|
82195
|
+
rules(invoice, add2, options);
|
|
82196
|
+
return problems.length > 0 ? { ok: false, invoice: null, problems } : { ok: true, invoice, problems: [] };
|
|
82197
|
+
}
|
|
82198
|
+
function pathText(path) {
|
|
82199
|
+
let out = "";
|
|
82200
|
+
for (const key of path)
|
|
82201
|
+
out += typeof key === "number" ? `[${key}]` : out ? `.${String(key)}` : String(key);
|
|
82202
|
+
return out;
|
|
82203
|
+
}
|
|
82204
|
+
function valueAt(data, path) {
|
|
82205
|
+
let value = data;
|
|
82206
|
+
for (const key of path) {
|
|
82207
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
82208
|
+
value = value[key];
|
|
82209
|
+
}
|
|
82210
|
+
return value === null ? void 0 : value;
|
|
82211
|
+
}
|
|
82212
|
+
function normalise(input, add2, options) {
|
|
82213
|
+
const amount = (value, path) => {
|
|
82214
|
+
const text4 = decimalText(value);
|
|
82215
|
+
if (text4 === null) {
|
|
82216
|
+
add2("not_a_number", path, `_invoice.${path} is not a number`);
|
|
82217
|
+
return "0";
|
|
82218
|
+
}
|
|
82219
|
+
if (decimalPlaces(text4) > 2) {
|
|
82220
|
+
add2(
|
|
82221
|
+
"decimals",
|
|
82222
|
+
path,
|
|
82223
|
+
`_invoice.${path} has more than 2 decimals; an amount of the invoice is exact to the cent`,
|
|
82224
|
+
{
|
|
82225
|
+
args: { max: 2, value: text4 }
|
|
82226
|
+
}
|
|
82227
|
+
);
|
|
82228
|
+
return "0";
|
|
82229
|
+
}
|
|
82230
|
+
return text4;
|
|
82231
|
+
};
|
|
82232
|
+
const number = (value, path, max) => {
|
|
82233
|
+
const text4 = decimalText(value);
|
|
82234
|
+
if (text4 === null) {
|
|
82235
|
+
add2("not_a_number", path, `_invoice.${path} is not a number`);
|
|
82236
|
+
return "0";
|
|
82237
|
+
}
|
|
82238
|
+
if (decimalPlaces(text4) > max) {
|
|
82239
|
+
add2("decimals", path, `_invoice.${path} has more than ${max} decimals`, {
|
|
82240
|
+
args: { max, value: text4 }
|
|
82241
|
+
});
|
|
82242
|
+
return "0";
|
|
82243
|
+
}
|
|
82244
|
+
return text4;
|
|
82245
|
+
};
|
|
82246
|
+
const optionalAmount = (value, path) => value === void 0 ? void 0 : amount(value, path);
|
|
82247
|
+
const date = (value, path) => {
|
|
82248
|
+
if (value === void 0) return void 0;
|
|
82249
|
+
const [year = 0, month = 0, day = 0] = value.split("-").map(Number);
|
|
82250
|
+
const real = new Date(Date.UTC(year, month - 1, day));
|
|
82251
|
+
if (real.getUTCFullYear() !== year || real.getUTCMonth() !== month - 1 || real.getUTCDate() !== day) {
|
|
82252
|
+
add2("date", path, `_invoice.${path} is not a date of the calendar`, {
|
|
82253
|
+
args: { value }
|
|
82254
|
+
});
|
|
82255
|
+
}
|
|
82256
|
+
return value;
|
|
82257
|
+
};
|
|
82258
|
+
const code = (value, path, list3, name, rule2) => {
|
|
82259
|
+
const upper = value.trim().toUpperCase();
|
|
82260
|
+
if (!list3.has(upper))
|
|
82261
|
+
add2(
|
|
82262
|
+
"code_list",
|
|
82263
|
+
path,
|
|
82264
|
+
`_invoice.${path}: "${value}" is not a code of ${name}`,
|
|
82265
|
+
{
|
|
82266
|
+
rule: rule2,
|
|
82267
|
+
args: { value, list: name }
|
|
82268
|
+
}
|
|
82269
|
+
);
|
|
82270
|
+
return upper;
|
|
82271
|
+
};
|
|
82272
|
+
const period2 = (value, path, rule2) => {
|
|
82273
|
+
if (!value || value.start === void 0 && value.end === void 0)
|
|
82274
|
+
return void 0;
|
|
82275
|
+
const start = date(value.start, `${path}.start`);
|
|
82276
|
+
const end = date(value.end, `${path}.end`);
|
|
82277
|
+
if (start && end && end < start)
|
|
82278
|
+
add2("date_order", path, `_invoice.${path} ends before it starts`, {
|
|
82279
|
+
rule: rule2
|
|
82280
|
+
});
|
|
82281
|
+
return { ...start ? { start } : {}, ...end ? { end } : {} };
|
|
82282
|
+
};
|
|
82283
|
+
const identifier3 = (value) => {
|
|
82284
|
+
if (value === void 0) return void 0;
|
|
82285
|
+
if (typeof value === "string") return { value };
|
|
82286
|
+
return {
|
|
82287
|
+
value: value.value,
|
|
82288
|
+
...value.scheme ? { scheme: value.scheme } : {}
|
|
82289
|
+
};
|
|
82290
|
+
};
|
|
82291
|
+
const address2 = (value, path) => ({
|
|
82292
|
+
...defined({
|
|
82293
|
+
street: value.street,
|
|
82294
|
+
street2: value.street2,
|
|
82295
|
+
street3: value.street3,
|
|
82296
|
+
postcode: value.postcode,
|
|
82297
|
+
city: value.city,
|
|
82298
|
+
subdivision: value.subdivision
|
|
82299
|
+
}),
|
|
82300
|
+
country: code(
|
|
82301
|
+
value.country,
|
|
82302
|
+
`${path}.country`,
|
|
82303
|
+
countryCodes,
|
|
82304
|
+
"ISO 3166-1 (two letters, as in DE)",
|
|
82305
|
+
"BR-CL-14"
|
|
82306
|
+
)
|
|
82307
|
+
});
|
|
82308
|
+
const vatId = (value, path) => {
|
|
82309
|
+
if (value === void 0) return void 0;
|
|
82310
|
+
const id = value.replace(/\s/g, "").toUpperCase();
|
|
82311
|
+
const prefix = id.slice(0, 2);
|
|
82312
|
+
if (!(countryCodes.has(prefix) || prefix === "EL") || !/^[A-Z]{2}/.test(id))
|
|
82313
|
+
add2(
|
|
82314
|
+
"vat_id_prefix",
|
|
82315
|
+
path,
|
|
82316
|
+
`_invoice.${path} must start with the country code of the VAT ID, as in DE123456789`,
|
|
82317
|
+
{
|
|
82318
|
+
rule: "BR-CO-09",
|
|
82319
|
+
args: { value }
|
|
82320
|
+
}
|
|
82321
|
+
);
|
|
82322
|
+
return id;
|
|
82323
|
+
};
|
|
82324
|
+
const electronicAddress = (value, path, rule2) => {
|
|
82325
|
+
const id = identifier3(value);
|
|
82326
|
+
if (!id) return void 0;
|
|
82327
|
+
const scheme = id.scheme?.trim().toUpperCase() ?? (/^[^@\s]+@[^@\s]+$/.test(id.value) ? "EM" : void 0);
|
|
82328
|
+
if (!scheme) {
|
|
82329
|
+
add2(
|
|
82330
|
+
"scheme_required",
|
|
82331
|
+
path,
|
|
82332
|
+
`_invoice.${path} needs its scheme: write { "value": \u2026, "scheme": \u2026 } with a code of the EAS list, or an e-mail address`,
|
|
82333
|
+
{ rule: rule2 }
|
|
82334
|
+
);
|
|
82335
|
+
return void 0;
|
|
82336
|
+
}
|
|
82337
|
+
if (!electronicAddressSchemes.has(scheme))
|
|
82338
|
+
add2(
|
|
82339
|
+
"code_list",
|
|
82340
|
+
`${path}.scheme`,
|
|
82341
|
+
`_invoice.${path}.scheme: "${scheme}" is not a code of the EAS list (EM is an e-mail address)`,
|
|
82342
|
+
{
|
|
82343
|
+
rule: "BR-CL-25",
|
|
82344
|
+
args: { value: scheme, list: "the EAS list" }
|
|
82345
|
+
}
|
|
82346
|
+
);
|
|
82347
|
+
return { value: id.value, scheme };
|
|
82348
|
+
};
|
|
82349
|
+
const party2 = (value, path, addressRule) => {
|
|
82350
|
+
const id = identifier3(value.id);
|
|
82351
|
+
const registration = identifier3(value.legal_registration);
|
|
82352
|
+
const electronic = electronicAddress(
|
|
82353
|
+
value.electronic_address,
|
|
82354
|
+
`${path}.electronic_address`,
|
|
82355
|
+
addressRule
|
|
82356
|
+
);
|
|
82357
|
+
const vat = vatId(value.vat_id, `${path}.vat_id`);
|
|
82358
|
+
return {
|
|
82359
|
+
name: value.name,
|
|
82360
|
+
...defined({ trading_name: value.trading_name, vat_id: vat }),
|
|
82361
|
+
...id ? { id } : {},
|
|
82362
|
+
...registration ? { legal_registration: registration } : {},
|
|
82363
|
+
address: address2(value.address, `${path}.address`),
|
|
82364
|
+
...value.contact && Object.values(value.contact).some((v2) => v2 !== void 0) ? { contact: defined(value.contact) } : {},
|
|
82365
|
+
...electronic ? { electronic_address: electronic } : {}
|
|
82366
|
+
};
|
|
82367
|
+
};
|
|
82368
|
+
const tax = (value, path) => ({
|
|
82369
|
+
category: value.category,
|
|
82370
|
+
...value.rate === void 0 || value.rate === null ? {} : { rate: number(value.rate, `${path}.rate`, 4) }
|
|
82371
|
+
});
|
|
82372
|
+
const allowance = (value, path) => ({
|
|
82373
|
+
amount: amount(value.amount, `${path}.amount`),
|
|
82374
|
+
...defined({
|
|
82375
|
+
reason: value.reason,
|
|
82376
|
+
reason_code: value.reason_code?.trim().toUpperCase(),
|
|
82377
|
+
base_amount: optionalAmount(value.base_amount, `${path}.base_amount`),
|
|
82378
|
+
percent: value.percent === void 0 ? void 0 : number(value.percent, `${path}.percent`, 4)
|
|
82379
|
+
})
|
|
82380
|
+
});
|
|
82381
|
+
const seller = party2(input.seller, "seller", "BR-62");
|
|
82382
|
+
const buyer = party2(input.buyer, "buyer", "BR-63");
|
|
82383
|
+
if (input.buyer.siren !== void 0) {
|
|
82384
|
+
const siren = input.buyer.siren.replace(/\s/g, "");
|
|
82385
|
+
if (buyer.legal_registration && buyer.legal_registration.value.replace(/\s/g, "") !== siren)
|
|
82386
|
+
add2(
|
|
82387
|
+
"siren_mismatch",
|
|
82388
|
+
"buyer.siren",
|
|
82389
|
+
"_invoice.buyer.siren and _invoice.buyer.legal_registration name different registrations; state one of them"
|
|
82390
|
+
);
|
|
82391
|
+
else
|
|
82392
|
+
buyer.legal_registration = {
|
|
82393
|
+
value: siren,
|
|
82394
|
+
scheme: buyer.legal_registration?.scheme ?? "0002"
|
|
82395
|
+
};
|
|
82396
|
+
}
|
|
82397
|
+
const notes = [
|
|
82398
|
+
...input.note ? [{ text: input.note }] : [],
|
|
82399
|
+
...(input.notes ?? []).map((note2) => ({
|
|
82400
|
+
text: note2.text,
|
|
82401
|
+
...note2.subject_code ? { subject_code: note2.subject_code.trim().toUpperCase() } : {}
|
|
82402
|
+
}))
|
|
82403
|
+
];
|
|
82404
|
+
const delivery = input.delivery ? defined({
|
|
82405
|
+
name: input.delivery.name,
|
|
82406
|
+
date: date(input.delivery.date, "delivery.date"),
|
|
82407
|
+
address: input.delivery.address ? address2(input.delivery.address, "delivery.address") : void 0
|
|
82408
|
+
}) : void 0;
|
|
82409
|
+
let businessProcess;
|
|
82410
|
+
if (input.operation_code !== void 0)
|
|
82411
|
+
businessProcess = input.operation_code.trim().toUpperCase();
|
|
82412
|
+
else if (input.operation !== void 0)
|
|
82413
|
+
businessProcess = FRENCH_OPERATION[input.operation];
|
|
82414
|
+
const payment2 = input.payment ? defined({
|
|
82415
|
+
means_code: input.payment.means_code === void 0 ? void 0 : code(
|
|
82416
|
+
String(input.payment.means_code),
|
|
82417
|
+
"payment.means_code",
|
|
82418
|
+
paymentMeansCodes,
|
|
82419
|
+
"UNTDID 4461 (58 is a SEPA credit transfer)",
|
|
82420
|
+
"BR-CL-16"
|
|
82421
|
+
),
|
|
82422
|
+
iban: iban(input.payment.iban, "payment.iban", add2),
|
|
82423
|
+
bic: input.payment.bic?.replace(/\s/g, "").toUpperCase(),
|
|
82424
|
+
account_name: input.payment.account_name,
|
|
82425
|
+
reference: input.payment.reference,
|
|
82426
|
+
terms: input.payment.terms,
|
|
82427
|
+
due_date: date(input.payment.due_date, "payment.due_date"),
|
|
82428
|
+
mandate_id: input.payment.mandate_id,
|
|
82429
|
+
creditor_id: input.payment.creditor_id,
|
|
82430
|
+
debited_iban: iban(
|
|
82431
|
+
input.payment.debited_iban,
|
|
82432
|
+
"payment.debited_iban",
|
|
82433
|
+
add2
|
|
82434
|
+
)
|
|
82435
|
+
}) : void 0;
|
|
82436
|
+
const rounding = optionalAmount(input.totals.rounding, "totals.rounding");
|
|
82437
|
+
if (options.profile === "basic" && rounding !== void 0 && toUnits(rounding, 2) !== 0n)
|
|
82438
|
+
add2(
|
|
82439
|
+
"profile",
|
|
82440
|
+
"totals.rounding",
|
|
82441
|
+
"_invoice.totals.rounding cannot be carried by the profile basic, which has no rounding amount (BT-114); use en16931",
|
|
82442
|
+
{
|
|
82443
|
+
args: { profile: "basic", term: "BT-114" }
|
|
82444
|
+
}
|
|
82445
|
+
);
|
|
82446
|
+
return {
|
|
82447
|
+
number: input.number,
|
|
82448
|
+
issue_date: date(input.issue_date, "issue_date") ?? input.issue_date,
|
|
82449
|
+
type_code: code(
|
|
82450
|
+
String(input.type_code),
|
|
82451
|
+
"type_code",
|
|
82452
|
+
documentTypeCodes,
|
|
82453
|
+
"UNTDID 1001 (380 is an invoice, 381 a credit note)",
|
|
82454
|
+
"BR-CL-01"
|
|
82455
|
+
),
|
|
82456
|
+
currency: code(
|
|
82457
|
+
input.currency,
|
|
82458
|
+
"currency",
|
|
82459
|
+
currencyCodes,
|
|
82460
|
+
"ISO 4217 (three letters, as in EUR)",
|
|
82461
|
+
"BR-CL-04"
|
|
82462
|
+
),
|
|
82463
|
+
...defined({
|
|
82464
|
+
accounting_currency: input.accounting_currency === void 0 ? void 0 : code(
|
|
82465
|
+
input.accounting_currency,
|
|
82466
|
+
"accounting_currency",
|
|
82467
|
+
currencyCodes,
|
|
82468
|
+
"ISO 4217 (three letters, as in EUR)",
|
|
82469
|
+
"BR-CL-05"
|
|
82470
|
+
),
|
|
82471
|
+
buyer_reference: input.buyer_reference,
|
|
82472
|
+
order_reference: input.order_reference,
|
|
82473
|
+
contract_reference: input.contract_reference,
|
|
82474
|
+
period: period2(input.period, "period", "BR-29"),
|
|
82475
|
+
business_process: businessProcess
|
|
82476
|
+
}),
|
|
82477
|
+
notes,
|
|
82478
|
+
preceding_invoices: (input.preceding_invoices ?? []).map(
|
|
82479
|
+
(reference, i) => ({
|
|
82480
|
+
number: reference.number,
|
|
82481
|
+
...defined({
|
|
82482
|
+
issue_date: date(
|
|
82483
|
+
reference.issue_date,
|
|
82484
|
+
`preceding_invoices[${i}].issue_date`
|
|
82485
|
+
)
|
|
82486
|
+
})
|
|
82487
|
+
})
|
|
82488
|
+
),
|
|
82489
|
+
seller: {
|
|
82490
|
+
...seller,
|
|
82491
|
+
...defined({
|
|
82492
|
+
tax_number: input.seller.tax_number,
|
|
82493
|
+
legal_info: input.seller.legal_info,
|
|
82494
|
+
vat_on_debits: input.seller.vat_on_debits
|
|
82495
|
+
})
|
|
82496
|
+
},
|
|
82497
|
+
buyer,
|
|
82498
|
+
...delivery && Object.keys(delivery).length > 0 ? { delivery } : {},
|
|
82499
|
+
lines: input.lines.map((line2, i) => {
|
|
82500
|
+
const path = `lines[${i}]`;
|
|
82501
|
+
const unit = line2.unit_code.trim();
|
|
82502
|
+
const known = unitCodes.has(unit.toUpperCase()) || unitCodes.has(unit);
|
|
82503
|
+
if (!known) {
|
|
82504
|
+
const suggestion = UNIT_SUGGESTIONS[unit.toLowerCase()];
|
|
82505
|
+
add2(
|
|
82506
|
+
"code_list",
|
|
82507
|
+
`${path}.unit_code`,
|
|
82508
|
+
`_invoice.${path}.unit_code: "${unit}" is not a code of UN/ECE Recommendation 20${suggestion ? `; ${suggestion} is the code for it` : " (C62 is a piece, HUR an hour, DAY a day)"}`,
|
|
82509
|
+
{
|
|
82510
|
+
rule: "BR-CL-23",
|
|
82511
|
+
args: {
|
|
82512
|
+
value: unit,
|
|
82513
|
+
list: "UN/ECE Recommendation 20",
|
|
82514
|
+
...suggestion ? { suggestion } : {}
|
|
82515
|
+
}
|
|
82516
|
+
}
|
|
82517
|
+
);
|
|
82518
|
+
}
|
|
82519
|
+
const price = number(line2.net_price, `${path}.net_price`, 6);
|
|
82520
|
+
if (price.startsWith("-"))
|
|
82521
|
+
add2(
|
|
82522
|
+
"negative",
|
|
82523
|
+
`${path}.net_price`,
|
|
82524
|
+
`_invoice.${path}.net_price must not be negative; a credit is a negative quantity`,
|
|
82525
|
+
{ rule: "BR-27" }
|
|
82526
|
+
);
|
|
82527
|
+
const standard = identifier3(line2.standard_item_id);
|
|
82528
|
+
if (standard && !standard.scheme)
|
|
82529
|
+
add2(
|
|
82530
|
+
"scheme_required",
|
|
82531
|
+
`${path}.standard_item_id`,
|
|
82532
|
+
`_invoice.${path}.standard_item_id needs its scheme: write { "value": \u2026, "scheme": \u2026 }, 0160 for a GTIN`,
|
|
82533
|
+
{ rule: "BR-64" }
|
|
82534
|
+
);
|
|
82535
|
+
return {
|
|
82536
|
+
id: String(line2.id),
|
|
82537
|
+
name: line2.name,
|
|
82538
|
+
quantity: number(line2.quantity, `${path}.quantity`, 6),
|
|
82539
|
+
unit_code: unitCodes.has(unit) ? unit : unit.toUpperCase(),
|
|
82540
|
+
net_price: price,
|
|
82541
|
+
net_amount: amount(line2.net_amount, `${path}.net_amount`),
|
|
82542
|
+
tax: tax(line2.tax, `${path}.tax`),
|
|
82543
|
+
allowances: (line2.allowances ?? []).map(
|
|
82544
|
+
(a, j2) => allowance(a, `${path}.allowances[${j2}]`)
|
|
82545
|
+
),
|
|
82546
|
+
charges: (line2.charges ?? []).map(
|
|
82547
|
+
(c, j2) => allowance(c, `${path}.charges[${j2}]`)
|
|
82548
|
+
),
|
|
82549
|
+
...defined({
|
|
82550
|
+
description: line2.description,
|
|
82551
|
+
price_base_quantity: line2.price_base_quantity === void 0 ? void 0 : number(
|
|
82552
|
+
line2.price_base_quantity,
|
|
82553
|
+
`${path}.price_base_quantity`,
|
|
82554
|
+
6
|
|
82555
|
+
),
|
|
82556
|
+
period: period2(line2.period, `${path}.period`, "BR-30"),
|
|
82557
|
+
buyer_accounting_reference: line2.buyer_accounting_reference,
|
|
82558
|
+
note: line2.note,
|
|
82559
|
+
seller_item_id: line2.seller_item_id,
|
|
82560
|
+
buyer_item_id: line2.buyer_item_id,
|
|
82561
|
+
standard_item_id: standard,
|
|
82562
|
+
order_line_reference: line2.order_line_reference
|
|
82563
|
+
})
|
|
82564
|
+
};
|
|
82565
|
+
}),
|
|
82566
|
+
allowances: (input.allowances ?? []).map((a, i) => ({
|
|
82567
|
+
...allowance(a, `allowances[${i}]`),
|
|
82568
|
+
tax: tax(a.tax, `allowances[${i}].tax`)
|
|
82569
|
+
})),
|
|
82570
|
+
charges: (input.charges ?? []).map((c, i) => ({
|
|
82571
|
+
...allowance(c, `charges[${i}]`),
|
|
82572
|
+
tax: tax(c.tax, `charges[${i}].tax`)
|
|
82573
|
+
})),
|
|
82574
|
+
breakdown: input.tax.breakdown.map((row, i) => ({
|
|
82575
|
+
...tax(row, `tax.breakdown[${i}]`),
|
|
82576
|
+
basis: amount(row.basis, `tax.breakdown[${i}].basis`),
|
|
82577
|
+
amount: amount(row.amount, `tax.breakdown[${i}].amount`),
|
|
82578
|
+
...defined({
|
|
82579
|
+
exemption_reason: row.exemption_reason,
|
|
82580
|
+
exemption_reason_code: row.exemption_reason_code?.trim()
|
|
82581
|
+
})
|
|
82582
|
+
})),
|
|
82583
|
+
totals: {
|
|
82584
|
+
line_net: amount(input.totals.line_net, "totals.line_net"),
|
|
82585
|
+
tax_basis: amount(input.totals.tax_basis, "totals.tax_basis"),
|
|
82586
|
+
tax_total: amount(input.totals.tax_total, "totals.tax_total"),
|
|
82587
|
+
grand: amount(input.totals.grand, "totals.grand"),
|
|
82588
|
+
due: amount(input.totals.due, "totals.due"),
|
|
82589
|
+
...defined({
|
|
82590
|
+
allowances: optionalAmount(
|
|
82591
|
+
input.totals.allowances,
|
|
82592
|
+
"totals.allowances"
|
|
82593
|
+
),
|
|
82594
|
+
charges: optionalAmount(input.totals.charges, "totals.charges"),
|
|
82595
|
+
prepaid: optionalAmount(input.totals.prepaid, "totals.prepaid"),
|
|
82596
|
+
tax_total_accounting: optionalAmount(
|
|
82597
|
+
input.totals.tax_total_accounting,
|
|
82598
|
+
"totals.tax_total_accounting"
|
|
82599
|
+
),
|
|
82600
|
+
rounding
|
|
82601
|
+
})
|
|
82602
|
+
},
|
|
82603
|
+
...payment2 && Object.keys(payment2).length > 0 ? { payment: payment2 } : {}
|
|
82604
|
+
};
|
|
82605
|
+
}
|
|
82606
|
+
function iban(value, path, add2) {
|
|
82607
|
+
if (value === void 0) return void 0;
|
|
82608
|
+
const normalised = normaliseIban(value);
|
|
82609
|
+
if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(normalised) || !ibanChecksum(normalised))
|
|
82610
|
+
add2(
|
|
82611
|
+
"iban",
|
|
82612
|
+
path,
|
|
82613
|
+
`_invoice.${path} is not a valid IBAN: its check digits do not match`
|
|
82614
|
+
);
|
|
82615
|
+
return normalised;
|
|
82616
|
+
}
|
|
82617
|
+
function defined(value) {
|
|
82618
|
+
return Object.fromEntries(
|
|
82619
|
+
Object.entries(value).filter(([, v2]) => v2 !== void 0)
|
|
82620
|
+
);
|
|
82621
|
+
}
|
|
82622
|
+
var cents = (text4) => text4 === void 0 ? 0n : toUnits(text4, 2);
|
|
82623
|
+
var money = (value) => fromUnits(value, 2);
|
|
82624
|
+
var rateKey = (tax) => `${tax.category}|${tax.rate ?? ""}`;
|
|
82625
|
+
function rules(invoice, add2, options) {
|
|
82626
|
+
const { totals: totals2 } = invoice;
|
|
82627
|
+
const sum = (path, stated, expected, rule2, formula) => {
|
|
82628
|
+
if (stated !== expected)
|
|
82629
|
+
add2(
|
|
82630
|
+
"sum",
|
|
82631
|
+
path,
|
|
82632
|
+
`_invoice.${path} is ${money(stated)}, but ${formula} ${money(expected)}`,
|
|
82633
|
+
{
|
|
82634
|
+
rule: rule2,
|
|
82635
|
+
args: { value: money(stated), expected: money(expected) }
|
|
82636
|
+
}
|
|
82637
|
+
);
|
|
82638
|
+
};
|
|
82639
|
+
const lineNet = invoice.lines.reduce(
|
|
82640
|
+
(total, line2) => total + cents(line2.net_amount),
|
|
82641
|
+
0n
|
|
82642
|
+
);
|
|
82643
|
+
const allowanceSum = invoice.allowances.reduce(
|
|
82644
|
+
(total, a) => total + cents(a.amount),
|
|
82645
|
+
0n
|
|
82646
|
+
);
|
|
82647
|
+
const chargeSum = invoice.charges.reduce(
|
|
82648
|
+
(total, c) => total + cents(c.amount),
|
|
82649
|
+
0n
|
|
82650
|
+
);
|
|
82651
|
+
const taxSum = invoice.breakdown.reduce(
|
|
82652
|
+
(total, row) => total + cents(row.amount),
|
|
82653
|
+
0n
|
|
82654
|
+
);
|
|
82655
|
+
sum(
|
|
82656
|
+
"totals.line_net",
|
|
82657
|
+
cents(totals2.line_net),
|
|
82658
|
+
lineNet,
|
|
82659
|
+
"BR-CO-10",
|
|
82660
|
+
"the net amounts of the lines add up to"
|
|
82661
|
+
);
|
|
82662
|
+
sum(
|
|
82663
|
+
"totals.allowances",
|
|
82664
|
+
cents(totals2.allowances),
|
|
82665
|
+
allowanceSum,
|
|
82666
|
+
"BR-CO-11",
|
|
82667
|
+
"the allowances of the document add up to"
|
|
82668
|
+
);
|
|
82669
|
+
sum(
|
|
82670
|
+
"totals.charges",
|
|
82671
|
+
cents(totals2.charges),
|
|
82672
|
+
chargeSum,
|
|
82673
|
+
"BR-CO-12",
|
|
82674
|
+
"the charges of the document add up to"
|
|
82675
|
+
);
|
|
82676
|
+
sum(
|
|
82677
|
+
"totals.tax_basis",
|
|
82678
|
+
cents(totals2.tax_basis),
|
|
82679
|
+
cents(totals2.line_net) - cents(totals2.allowances) + cents(totals2.charges),
|
|
82680
|
+
"BR-CO-13",
|
|
82681
|
+
"line_net less allowances plus charges is"
|
|
82682
|
+
);
|
|
82683
|
+
sum(
|
|
82684
|
+
"totals.tax_total",
|
|
82685
|
+
cents(totals2.tax_total),
|
|
82686
|
+
taxSum,
|
|
82687
|
+
"BR-CO-14",
|
|
82688
|
+
"the VAT amounts of the breakdown add up to"
|
|
82689
|
+
);
|
|
82690
|
+
sum(
|
|
82691
|
+
"totals.grand",
|
|
82692
|
+
cents(totals2.grand),
|
|
82693
|
+
cents(totals2.tax_basis) + cents(totals2.tax_total),
|
|
82694
|
+
"BR-CO-15",
|
|
82695
|
+
"tax_basis plus tax_total is"
|
|
82696
|
+
);
|
|
82697
|
+
sum(
|
|
82698
|
+
"totals.due",
|
|
82699
|
+
cents(totals2.due),
|
|
82700
|
+
cents(totals2.grand) - cents(totals2.prepaid) + cents(totals2.rounding),
|
|
82701
|
+
"BR-CO-16",
|
|
82702
|
+
"grand less prepaid plus rounding is"
|
|
82703
|
+
);
|
|
82704
|
+
if (invoice.accounting_currency !== void 0 && totals2.tax_total_accounting === void 0)
|
|
82705
|
+
add2(
|
|
82706
|
+
"missing",
|
|
82707
|
+
"totals.tax_total_accounting",
|
|
82708
|
+
`_invoice.totals.tax_total_accounting is missing: with accounting_currency ${invoice.accounting_currency} the invoice states its VAT total in that currency (BT-111)`,
|
|
82709
|
+
{
|
|
82710
|
+
rule: "BR-53",
|
|
82711
|
+
args: { term: "BT-111" }
|
|
82712
|
+
}
|
|
82713
|
+
);
|
|
82714
|
+
if (invoice.accounting_currency === void 0 && totals2.tax_total_accounting !== void 0)
|
|
82715
|
+
add2(
|
|
82716
|
+
"missing",
|
|
82717
|
+
"accounting_currency",
|
|
82718
|
+
"_invoice.accounting_currency is missing: totals.tax_total_accounting needs the currency it is stated in (BT-6)",
|
|
82719
|
+
{
|
|
82720
|
+
rule: "BR-53",
|
|
82721
|
+
args: { term: "BT-6" }
|
|
82722
|
+
}
|
|
82723
|
+
);
|
|
82724
|
+
const used = /* @__PURE__ */ new Map();
|
|
82725
|
+
const use = (tax, value, path) => {
|
|
82726
|
+
const entry = used.get(rateKey(tax)) ?? {
|
|
82727
|
+
tax,
|
|
82728
|
+
basis: 0n,
|
|
82729
|
+
items: 0,
|
|
82730
|
+
first: path
|
|
82731
|
+
};
|
|
82732
|
+
entry.basis += value;
|
|
82733
|
+
entry.items += 1;
|
|
82734
|
+
used.set(rateKey(tax), entry);
|
|
82735
|
+
};
|
|
82736
|
+
invoice.lines.forEach(
|
|
82737
|
+
(line2, i) => use(line2.tax, cents(line2.net_amount), `lines[${i}]`)
|
|
82738
|
+
);
|
|
82739
|
+
invoice.allowances.forEach(
|
|
82740
|
+
(a, i) => use(a.tax, -cents(a.amount), `allowances[${i}]`)
|
|
82741
|
+
);
|
|
82742
|
+
invoice.charges.forEach(
|
|
82743
|
+
(c, i) => use(c.tax, cents(c.amount), `charges[${i}]`)
|
|
82744
|
+
);
|
|
82745
|
+
const seen = /* @__PURE__ */ new Set();
|
|
82746
|
+
invoice.breakdown.forEach((row, i) => {
|
|
82747
|
+
const path = `tax.breakdown[${i}]`;
|
|
82748
|
+
const prefix = RULE_PREFIX[row.category];
|
|
82749
|
+
const shown = row.rate === void 0 ? row.category : `${row.category} at ${row.rate} %`;
|
|
82750
|
+
if (seen.has(rateKey(row)))
|
|
82751
|
+
add2(
|
|
82752
|
+
"breakdown_duplicate",
|
|
82753
|
+
path,
|
|
82754
|
+
`_invoice.tax.breakdown lists category ${shown} twice; one entry per category and rate`,
|
|
82755
|
+
{
|
|
82756
|
+
args: { category: row.category, rate: row.rate ?? "" }
|
|
82757
|
+
}
|
|
82758
|
+
);
|
|
82759
|
+
seen.add(rateKey(row));
|
|
82760
|
+
const entry = used.get(rateKey(row));
|
|
82761
|
+
const expected = entry?.basis ?? 0n;
|
|
82762
|
+
if (cents(row.basis) !== expected)
|
|
82763
|
+
add2(
|
|
82764
|
+
"sum",
|
|
82765
|
+
`${path}.basis`,
|
|
82766
|
+
`_invoice.${path}.basis is ${money(cents(row.basis))}, but the lines, charges and allowances of category ${shown} come to ${money(expected)}`,
|
|
82767
|
+
{
|
|
82768
|
+
rule: `${prefix}-08`,
|
|
82769
|
+
args: {
|
|
82770
|
+
value: money(cents(row.basis)),
|
|
82771
|
+
expected: money(expected),
|
|
82772
|
+
category: row.category,
|
|
82773
|
+
rate: row.rate ?? ""
|
|
82774
|
+
}
|
|
82775
|
+
}
|
|
82776
|
+
);
|
|
82777
|
+
if (row.rate !== void 0) {
|
|
82778
|
+
const computed = divideRounded(
|
|
82779
|
+
cents(row.basis) * toUnits(row.rate, 4),
|
|
82780
|
+
1000000n
|
|
82781
|
+
);
|
|
82782
|
+
const items = BigInt(Math.max(1, entry?.items ?? 1));
|
|
82783
|
+
const tolerance = (items + 1n) / 2n > 100n ? 100n : (items + 1n) / 2n;
|
|
82784
|
+
if (abs(cents(row.amount) - computed) > tolerance)
|
|
82785
|
+
add2(
|
|
82786
|
+
"sum",
|
|
82787
|
+
`${path}.amount`,
|
|
82788
|
+
`_invoice.${path}.amount is ${money(cents(row.amount))}, but ${row.rate} % of ${money(cents(row.basis))} is ${money(computed)}`,
|
|
82789
|
+
{
|
|
82790
|
+
rule: "BR-CO-17",
|
|
82791
|
+
args: {
|
|
82792
|
+
value: money(cents(row.amount)),
|
|
82793
|
+
expected: money(computed),
|
|
82794
|
+
rate: row.rate,
|
|
82795
|
+
basis: money(cents(row.basis))
|
|
82796
|
+
}
|
|
82797
|
+
}
|
|
82798
|
+
);
|
|
82799
|
+
}
|
|
82800
|
+
});
|
|
82801
|
+
for (const [key, entry] of used) {
|
|
82802
|
+
if (seen.has(key)) continue;
|
|
82803
|
+
const shown = entry.tax.rate === void 0 ? entry.tax.category : `${entry.tax.category} at ${entry.tax.rate} %`;
|
|
82804
|
+
add2(
|
|
82805
|
+
"breakdown_missing",
|
|
82806
|
+
"tax.breakdown",
|
|
82807
|
+
`_invoice.tax.breakdown has no entry for category ${shown}, which _invoice.${entry.first} falls under`,
|
|
82808
|
+
{
|
|
82809
|
+
rule: `${RULE_PREFIX[entry.tax.category]}-01`,
|
|
82810
|
+
args: {
|
|
82811
|
+
category: entry.tax.category,
|
|
82812
|
+
rate: entry.tax.rate ?? "",
|
|
82813
|
+
used: entry.first
|
|
82814
|
+
}
|
|
82815
|
+
}
|
|
82816
|
+
);
|
|
82817
|
+
}
|
|
82818
|
+
const rate = (tax, path, at2) => {
|
|
82819
|
+
const prefix = RULE_PREFIX[tax.category];
|
|
82820
|
+
const number = at2 === "line" || at2 === "breakdown" ? "05" : at2 === "allowance" ? "06" : "07";
|
|
82821
|
+
if (tax.category === "O") {
|
|
82822
|
+
if (tax.rate !== void 0)
|
|
82823
|
+
add2(
|
|
82824
|
+
"rate_forbidden",
|
|
82825
|
+
`${path}.rate`,
|
|
82826
|
+
`_invoice.${path}.rate must be left out: category O is not subject to VAT and has no rate`,
|
|
82827
|
+
{ rule: `${prefix}-${number}` }
|
|
82828
|
+
);
|
|
82829
|
+
return;
|
|
82830
|
+
}
|
|
82831
|
+
if (tax.rate === void 0) {
|
|
82832
|
+
add2(
|
|
82833
|
+
"rate_required",
|
|
82834
|
+
`${path}.rate`,
|
|
82835
|
+
`_invoice.${path}.rate is missing: category ${tax.category} states its rate${tax.category === "S" ? "" : ", which is 0"}`,
|
|
82836
|
+
{
|
|
82837
|
+
rule: at2 === "breakdown" ? "BR-48" : `${prefix}-${number}`,
|
|
82838
|
+
args: { category: tax.category }
|
|
82839
|
+
}
|
|
82840
|
+
);
|
|
82841
|
+
return;
|
|
82842
|
+
}
|
|
82843
|
+
const value = toUnits(tax.rate, 4);
|
|
82844
|
+
if (tax.category === "S" && value <= 0n)
|
|
82845
|
+
add2(
|
|
82846
|
+
"rate_positive",
|
|
82847
|
+
`${path}.rate`,
|
|
82848
|
+
`_invoice.${path}.rate is ${tax.rate}: category S is standard rated and needs a rate above 0; a rate of 0 is category Z or E`,
|
|
82849
|
+
{
|
|
82850
|
+
rule: `${prefix}-${number}`,
|
|
82851
|
+
args: { rate: tax.rate }
|
|
82852
|
+
}
|
|
82853
|
+
);
|
|
82854
|
+
if (tax.category !== "S" && value !== 0n)
|
|
82855
|
+
add2(
|
|
82856
|
+
"rate_zero",
|
|
82857
|
+
`${path}.rate`,
|
|
82858
|
+
`_invoice.${path}.rate is ${tax.rate}: category ${tax.category} carries a rate of 0`,
|
|
82859
|
+
{
|
|
82860
|
+
rule: `${prefix}-${number}`,
|
|
82861
|
+
args: { rate: tax.rate, category: tax.category }
|
|
82862
|
+
}
|
|
82863
|
+
);
|
|
82864
|
+
};
|
|
82865
|
+
invoice.lines.forEach((line2, i) => rate(line2.tax, `lines[${i}].tax`, "line"));
|
|
82866
|
+
invoice.allowances.forEach(
|
|
82867
|
+
(a, i) => rate(a.tax, `allowances[${i}].tax`, "allowance")
|
|
82868
|
+
);
|
|
82869
|
+
invoice.charges.forEach((c, i) => rate(c.tax, `charges[${i}].tax`, "charge"));
|
|
82870
|
+
const categories = new Set(
|
|
82871
|
+
[...used.values()].map((entry) => entry.tax.category)
|
|
82872
|
+
);
|
|
82873
|
+
invoice.breakdown.forEach((row, i) => {
|
|
82874
|
+
const path = `tax.breakdown[${i}]`;
|
|
82875
|
+
const prefix = RULE_PREFIX[row.category];
|
|
82876
|
+
rate(row, path, "breakdown");
|
|
82877
|
+
categories.add(row.category);
|
|
82878
|
+
const reason = row.exemption_reason !== void 0 || row.exemption_reason_code !== void 0;
|
|
82879
|
+
if (row.category === "S" || row.category === "Z") {
|
|
82880
|
+
if (reason)
|
|
82881
|
+
add2(
|
|
82882
|
+
"exemption_reason_forbidden",
|
|
82883
|
+
`${path}.exemption_reason`,
|
|
82884
|
+
`_invoice.${path} must not state an exemption reason: category ${row.category} is not an exemption`,
|
|
82885
|
+
{
|
|
82886
|
+
rule: `${prefix}-10`,
|
|
82887
|
+
args: { category: row.category }
|
|
82888
|
+
}
|
|
82889
|
+
);
|
|
82890
|
+
} else if (!reason) {
|
|
82891
|
+
add2(
|
|
82892
|
+
"exemption_reason_required",
|
|
82893
|
+
`${path}.exemption_reason`,
|
|
82894
|
+
`_invoice.${path}.exemption_reason is missing: category ${row.category} says why no VAT is charged, as text or as exemption_reason_code`,
|
|
82895
|
+
{
|
|
82896
|
+
rule: `${prefix}-10`,
|
|
82897
|
+
args: { category: row.category }
|
|
82898
|
+
}
|
|
82899
|
+
);
|
|
82900
|
+
}
|
|
82901
|
+
if (row.category !== "S" && cents(row.amount) !== 0n)
|
|
82902
|
+
add2(
|
|
82903
|
+
"amount_zero",
|
|
82904
|
+
`${path}.amount`,
|
|
82905
|
+
`_invoice.${path}.amount is ${money(cents(row.amount))}: category ${row.category} carries no VAT, so the amount is 0`,
|
|
82906
|
+
{
|
|
82907
|
+
rule: `${prefix}-09`,
|
|
82908
|
+
args: { value: money(cents(row.amount)), category: row.category }
|
|
82909
|
+
}
|
|
82910
|
+
);
|
|
82911
|
+
});
|
|
82912
|
+
const sellerTaxId = invoice.seller.vat_id !== void 0 || invoice.seller.tax_number !== void 0;
|
|
82913
|
+
for (const category of categories) {
|
|
82914
|
+
const prefix = RULE_PREFIX[category];
|
|
82915
|
+
if (category === "O") {
|
|
82916
|
+
if (invoice.seller.vat_id !== void 0)
|
|
82917
|
+
add2(
|
|
82918
|
+
"vat_id_forbidden",
|
|
82919
|
+
"seller.vat_id",
|
|
82920
|
+
"_invoice.seller.vat_id must be left out: an invoice of category O is not subject to VAT; the seller is named by tax_number or legal_registration",
|
|
82921
|
+
{ rule: "BR-O-02" }
|
|
82922
|
+
);
|
|
82923
|
+
if (invoice.buyer.vat_id !== void 0)
|
|
82924
|
+
add2(
|
|
82925
|
+
"vat_id_forbidden",
|
|
82926
|
+
"buyer.vat_id",
|
|
82927
|
+
"_invoice.buyer.vat_id must be left out: an invoice of category O is not subject to VAT",
|
|
82928
|
+
{ rule: "BR-O-02" }
|
|
82929
|
+
);
|
|
82930
|
+
if (categories.size > 1)
|
|
82931
|
+
add2(
|
|
82932
|
+
"only_category",
|
|
82933
|
+
"tax.breakdown",
|
|
82934
|
+
"_invoice mixes category O with others: an invoice that is not subject to VAT has that one category only",
|
|
82935
|
+
{ rule: "BR-O-11" }
|
|
82936
|
+
);
|
|
82937
|
+
continue;
|
|
82938
|
+
}
|
|
82939
|
+
if (category === "K" || category === "G") {
|
|
82940
|
+
if (invoice.seller.vat_id === void 0)
|
|
82941
|
+
add2(
|
|
82942
|
+
"seller_tax_id_required",
|
|
82943
|
+
"seller.vat_id",
|
|
82944
|
+
`_invoice.seller.vat_id is missing: category ${category} names the seller's VAT ID`,
|
|
82945
|
+
{
|
|
82946
|
+
rule: `${prefix}-02`,
|
|
82947
|
+
args: { category }
|
|
82948
|
+
}
|
|
82949
|
+
);
|
|
82950
|
+
} else if (!sellerTaxId) {
|
|
82951
|
+
add2(
|
|
82952
|
+
"seller_tax_id_required",
|
|
82953
|
+
"seller.vat_id",
|
|
82954
|
+
`_invoice.seller.vat_id is missing: category ${category} names the seller's VAT ID, or tax_number where there is none`,
|
|
82955
|
+
{
|
|
82956
|
+
rule: `${prefix}-02`,
|
|
82957
|
+
args: { category }
|
|
82958
|
+
}
|
|
82959
|
+
);
|
|
82960
|
+
}
|
|
82961
|
+
if (category === "AE" && invoice.buyer.vat_id === void 0 && invoice.buyer.legal_registration === void 0)
|
|
82962
|
+
add2(
|
|
82963
|
+
"buyer_tax_id_required",
|
|
82964
|
+
"buyer.vat_id",
|
|
82965
|
+
"_invoice.buyer.vat_id is missing: with reverse charge the buyer owes the VAT, so the invoice names the buyer's VAT ID or legal_registration",
|
|
82966
|
+
{
|
|
82967
|
+
rule: "BR-AE-02",
|
|
82968
|
+
args: { category }
|
|
82969
|
+
}
|
|
82970
|
+
);
|
|
82971
|
+
if (category === "K") {
|
|
82972
|
+
if (invoice.buyer.vat_id === void 0)
|
|
82973
|
+
add2(
|
|
82974
|
+
"buyer_tax_id_required",
|
|
82975
|
+
"buyer.vat_id",
|
|
82976
|
+
"_invoice.buyer.vat_id is missing: an intra-community supply names the buyer's VAT ID",
|
|
82977
|
+
{
|
|
82978
|
+
rule: "BR-IC-02",
|
|
82979
|
+
args: { category }
|
|
82980
|
+
}
|
|
82981
|
+
);
|
|
82982
|
+
if (invoice.delivery?.date === void 0 && invoice.period === void 0)
|
|
82983
|
+
add2(
|
|
82984
|
+
"delivery_required",
|
|
82985
|
+
"delivery.date",
|
|
82986
|
+
"_invoice.delivery.date is missing: an intra-community supply states when it was delivered, or the invoicing period",
|
|
82987
|
+
{ rule: "BR-IC-11" }
|
|
82988
|
+
);
|
|
82989
|
+
if (invoice.delivery?.address === void 0)
|
|
82990
|
+
add2(
|
|
82991
|
+
"delivery_required",
|
|
82992
|
+
"delivery.address.country",
|
|
82993
|
+
"_invoice.delivery.address.country is missing: an intra-community supply states the country it was delivered to",
|
|
82994
|
+
{ rule: "BR-IC-12" }
|
|
82995
|
+
);
|
|
82996
|
+
}
|
|
82997
|
+
}
|
|
82998
|
+
if (invoice.seller.id === void 0 && invoice.seller.legal_registration === void 0 && invoice.seller.vat_id === void 0)
|
|
82999
|
+
add2(
|
|
83000
|
+
"seller_identifier_required",
|
|
83001
|
+
"seller.vat_id",
|
|
83002
|
+
"_invoice.seller needs an identifier the buyer can match: vat_id, legal_registration or id",
|
|
83003
|
+
{ rule: "BR-CO-26" }
|
|
83004
|
+
);
|
|
83005
|
+
const payment2 = invoice.payment;
|
|
83006
|
+
const account = payment2?.iban !== void 0 || payment2?.bic !== void 0 || payment2?.account_name !== void 0;
|
|
83007
|
+
if (payment2 && payment2.means_code === void 0 && (account || payment2.mandate_id !== void 0 || payment2.debited_iban !== void 0))
|
|
83008
|
+
add2(
|
|
83009
|
+
"means_code_required",
|
|
83010
|
+
"payment.means_code",
|
|
83011
|
+
"_invoice.payment.means_code is missing: payment details say how the invoice is paid, 58 for a SEPA credit transfer, 59 for a SEPA direct debit",
|
|
83012
|
+
{ rule: "BR-49" }
|
|
83013
|
+
);
|
|
83014
|
+
if (payment2?.means_code !== void 0 && CREDIT_TRANSFER.has(payment2.means_code) && payment2.iban === void 0)
|
|
83015
|
+
add2(
|
|
83016
|
+
"account_required",
|
|
83017
|
+
"payment.iban",
|
|
83018
|
+
`_invoice.payment.iban is missing: a payment by credit transfer (code ${payment2.means_code}) names the account`,
|
|
83019
|
+
{
|
|
83020
|
+
rule: "BR-61",
|
|
83021
|
+
args: { means: payment2.means_code }
|
|
83022
|
+
}
|
|
83023
|
+
);
|
|
83024
|
+
if (cents(totals2.due) > 0n && payment2?.due_date === void 0 && payment2?.terms === void 0)
|
|
83025
|
+
add2(
|
|
83026
|
+
"due_or_terms_required",
|
|
83027
|
+
"payment.due_date",
|
|
83028
|
+
"_invoice.payment.due_date is missing: an invoice with an amount due states when it is due, or its payment terms",
|
|
83029
|
+
{ rule: "BR-CO-25" }
|
|
83030
|
+
);
|
|
83031
|
+
if (invoice.seller.address.country === "FR" && invoice.buyer.address.country === "FR") {
|
|
83032
|
+
if (options.profile === "basic")
|
|
83033
|
+
add2(
|
|
83034
|
+
"fr_profile",
|
|
83035
|
+
"",
|
|
83036
|
+
"_invoice is an invoice between French businesses, which France's e-invoicing reform does not accept in the profile basic (AFNOR XP Z12-012 takes EN 16931 and EXTENDED-CTC-FR); use en16931",
|
|
83037
|
+
{ args: { profile: "basic" } }
|
|
83038
|
+
);
|
|
83039
|
+
const digits = (id) => (id?.value ?? "").replace(/\s/g, "");
|
|
83040
|
+
if (!/^\d{9}$/.test(digits(invoice.seller.legal_registration)))
|
|
83041
|
+
add2(
|
|
83042
|
+
"fr_seller_siren",
|
|
83043
|
+
"seller.legal_registration",
|
|
83044
|
+
"_invoice.seller.legal_registration is missing or not nine digits: an invoice between French businesses names the seller's SIREN (BT-30)",
|
|
83045
|
+
{ rule: "BR-FR-10" }
|
|
83046
|
+
);
|
|
83047
|
+
if (!/^\d{9}$/.test(digits(invoice.buyer.legal_registration)))
|
|
83048
|
+
add2(
|
|
83049
|
+
"fr_buyer_siren",
|
|
83050
|
+
"buyer.siren",
|
|
83051
|
+
"_invoice.buyer.siren is missing or not nine digits: an invoice between French businesses names the buyer's SIREN (BT-47)",
|
|
83052
|
+
{ rule: "BR-FR-11" }
|
|
83053
|
+
);
|
|
83054
|
+
if (invoice.business_process === void 0)
|
|
83055
|
+
add2(
|
|
83056
|
+
"fr_operation",
|
|
83057
|
+
"operation",
|
|
83058
|
+
"_invoice.operation is missing: an invoice between French businesses says whether it is for goods, services or mixed (BT-23)",
|
|
83059
|
+
{ rule: "BR-FR-08" }
|
|
83060
|
+
);
|
|
83061
|
+
else if (!FRENCH_FRAMEWORKS.has(invoice.business_process))
|
|
83062
|
+
add2(
|
|
83063
|
+
"code_list",
|
|
83064
|
+
"operation_code",
|
|
83065
|
+
`_invoice.operation_code: "${invoice.business_process}" is not a billing framework of the French specifications (B1, S1, M1 and their variants)`,
|
|
83066
|
+
{
|
|
83067
|
+
rule: "BR-FR-08",
|
|
83068
|
+
args: {
|
|
83069
|
+
value: invoice.business_process,
|
|
83070
|
+
list: "the French billing frameworks"
|
|
83071
|
+
}
|
|
83072
|
+
}
|
|
83073
|
+
);
|
|
83074
|
+
if (invoice.seller.vat_on_debits === void 0)
|
|
83075
|
+
add2(
|
|
83076
|
+
"fr_vat_on_debits",
|
|
83077
|
+
"seller.vat_on_debits",
|
|
83078
|
+
"_invoice.seller.vat_on_debits is missing: an invoice between French businesses says whether the seller pays VAT on invoicing (true) or on payment (false)"
|
|
83079
|
+
);
|
|
83080
|
+
const subjects = new Set(invoice.notes.map((note2) => note2.subject_code));
|
|
83081
|
+
for (const [code, what] of FRENCH_NOTES)
|
|
83082
|
+
if (!subjects.has(code))
|
|
83083
|
+
add2(
|
|
83084
|
+
"fr_note",
|
|
83085
|
+
"notes",
|
|
83086
|
+
`_invoice.notes has no note with the subject_code ${code}: an invoice between French businesses states ${what} as a note (BT-21, BT-22)`,
|
|
83087
|
+
{ rule: "BR-FR-05", args: { subject_code: code } }
|
|
83088
|
+
);
|
|
83089
|
+
if (!invoice.seller.electronic_address)
|
|
83090
|
+
add2(
|
|
83091
|
+
"fr_electronic_address",
|
|
83092
|
+
"seller.electronic_address",
|
|
83093
|
+
"_invoice.seller.electronic_address is missing: an invoice between French businesses names the seller's electronic address (BT-34)",
|
|
83094
|
+
{ rule: "BR-FR-13", args: { term: "BT-34" } }
|
|
83095
|
+
);
|
|
83096
|
+
if (!invoice.buyer.electronic_address)
|
|
83097
|
+
add2(
|
|
83098
|
+
"fr_electronic_address",
|
|
83099
|
+
"buyer.electronic_address",
|
|
83100
|
+
"_invoice.buyer.electronic_address is missing: an invoice between French businesses names the buyer's electronic address (BT-49)",
|
|
83101
|
+
{ rule: "BR-FR-12", args: { term: "BT-49" } }
|
|
83102
|
+
);
|
|
83103
|
+
}
|
|
83104
|
+
}
|
|
83105
|
+
|
|
81968
83106
|
// packages/engine/src/lib/office/errors.ts
|
|
81969
83107
|
var officeLimits = {
|
|
81970
83108
|
/** Entries in one container. */
|
|
@@ -82703,7 +83841,44 @@ var encoder2 = new TextEncoder();
|
|
|
82703
83841
|
// packages/mcp/src/lib/server.ts
|
|
82704
83842
|
import { z as z3 } from "zod";
|
|
82705
83843
|
var CONVERT_LIMIT_BYTES = 20 * 1024 * 1024;
|
|
82706
|
-
var
|
|
83844
|
+
var NO_KEY = "This tool needs a Formfeed API key. Set FORMFEED_API_KEY to a key from https://app.formfeed.dev/keys (a test key, ff_test_\u2026, renders for free), or connect to https://mcp.formfeed.dev/mcp and sign in. Without a key, validate_template (with html and engine) and check_invoice work.";
|
|
83845
|
+
var NO_KEY_INSTRUCTION = "No API key is set, so only validate_template with html and engine, and check_invoice, work here; every other tool answers with how to get a key.";
|
|
83846
|
+
var IMAGE_TYPES = {
|
|
83847
|
+
png: "image/png",
|
|
83848
|
+
jpg: "image/jpeg",
|
|
83849
|
+
webp: "image/webp"
|
|
83850
|
+
};
|
|
83851
|
+
var IMAGE_LIMIT_BYTES = 2 * 1024 * 1024;
|
|
83852
|
+
async function readCapped(res, limit) {
|
|
83853
|
+
if (!res.body) return null;
|
|
83854
|
+
const reader = res.body.getReader();
|
|
83855
|
+
const chunks = [];
|
|
83856
|
+
let size = 0;
|
|
83857
|
+
for (; ; ) {
|
|
83858
|
+
const { done, value } = await reader.read();
|
|
83859
|
+
if (done) break;
|
|
83860
|
+
size += value.length;
|
|
83861
|
+
if (size > limit) {
|
|
83862
|
+
await reader.cancel();
|
|
83863
|
+
return null;
|
|
83864
|
+
}
|
|
83865
|
+
chunks.push(value);
|
|
83866
|
+
}
|
|
83867
|
+
const bytes = new Uint8Array(size);
|
|
83868
|
+
let offset = 0;
|
|
83869
|
+
for (const chunk of chunks) {
|
|
83870
|
+
bytes.set(chunk, offset);
|
|
83871
|
+
offset += chunk.length;
|
|
83872
|
+
}
|
|
83873
|
+
return bytes;
|
|
83874
|
+
}
|
|
83875
|
+
function base64(bytes) {
|
|
83876
|
+
let binary = "";
|
|
83877
|
+
for (let i = 0; i < bytes.length; i += 32768)
|
|
83878
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
83879
|
+
return btoa(binary);
|
|
83880
|
+
}
|
|
83881
|
+
var SERVER_INFO = { name: "formfeed", version: "0.3.18" };
|
|
82707
83882
|
var READS = {
|
|
82708
83883
|
readOnlyHint: true,
|
|
82709
83884
|
destructiveHint: false,
|
|
@@ -82718,6 +83893,37 @@ var CREATES = {
|
|
|
82718
83893
|
var engineSchema = z3.enum(["jinja2", "liquid", "handlebars"]);
|
|
82719
83894
|
var outputSchema = z3.enum(outputFormats);
|
|
82720
83895
|
var kindSchema = z3.enum(["pdf", "image", "docx", "pptx"]);
|
|
83896
|
+
var renderOutput = {
|
|
83897
|
+
id: z3.string().describe("rnd_ id of the render"),
|
|
83898
|
+
status: z3.enum(["queued", "rendering", "succeeded", "failed"]),
|
|
83899
|
+
download_url: z3.string().nullable().describe("Signed link to the file; null until the render succeeded"),
|
|
83900
|
+
expires_at: z3.string().nullable().describe("When download_url stops working; get_render issues a fresh one"),
|
|
83901
|
+
page_count: z3.number().nullable(),
|
|
83902
|
+
units: z3.number().describe("Units the render counted"),
|
|
83903
|
+
environment: z3.enum(["live", "test"]),
|
|
83904
|
+
template: z3.object({
|
|
83905
|
+
id: z3.string(),
|
|
83906
|
+
slug: z3.string(),
|
|
83907
|
+
version: z3.number(),
|
|
83908
|
+
channel: z3.string().nullable().optional(),
|
|
83909
|
+
canary: z3.boolean().optional()
|
|
83910
|
+
}).nullable().describe(
|
|
83911
|
+
"The template and version rendered; null for html and conversions"
|
|
83912
|
+
),
|
|
83913
|
+
error: z3.object({ code: z3.string().optional(), message: z3.string().optional() }).nullable().describe("Why a failed render failed"),
|
|
83914
|
+
accessibility: z3.record(z3.string(), z3.unknown()).optional().describe("The PDF/UA-1 verdict, when the template declares it"),
|
|
83915
|
+
einvoice: z3.record(z3.string(), z3.unknown()).optional().describe("The e-invoice report, when the render made one")
|
|
83916
|
+
};
|
|
83917
|
+
var diagnosticOutput = z3.object({
|
|
83918
|
+
severity: z3.enum(["error", "warning", "info"]),
|
|
83919
|
+
code: z3.string(),
|
|
83920
|
+
message: z3.string(),
|
|
83921
|
+
path: z3.string().optional().describe("Where in the data, for data errors"),
|
|
83922
|
+
line: z3.number().optional(),
|
|
83923
|
+
column: z3.number().optional(),
|
|
83924
|
+
part: z3.string().optional().describe("Word and PowerPoint templates: the file part of the tag"),
|
|
83925
|
+
paragraph: z3.number().optional()
|
|
83926
|
+
});
|
|
82721
83927
|
var templateSummary = (t) => ({
|
|
82722
83928
|
id: t.id,
|
|
82723
83929
|
slug: t.slug,
|
|
@@ -82741,8 +83947,15 @@ var renderSummary = (r) => ({
|
|
|
82741
83947
|
error: r.error,
|
|
82742
83948
|
// the validator's verdict of a render whose template declares PDF/UA-1 (plan 21): whether the
|
|
82743
83949
|
// file passed, and for each failed rule what to change and in which elements
|
|
82744
|
-
...r.accessibility ? { accessibility: r.accessibility } : {}
|
|
83950
|
+
...r.accessibility ? { accessibility: r.accessibility } : {},
|
|
83951
|
+
// the report of an e-invoice (spec 17): valid, the findings, the XML's link, the hashes it checked
|
|
83952
|
+
...r.einvoice ? { einvoice: r.einvoice } : {}
|
|
82745
83953
|
});
|
|
83954
|
+
var EINVOICE_PROBLEMS = /* @__PURE__ */ new Set([
|
|
83955
|
+
"einvoice_data_invalid",
|
|
83956
|
+
"einvoice_display_mismatch",
|
|
83957
|
+
"pdfa_conversion_failed"
|
|
83958
|
+
]);
|
|
82746
83959
|
function text3(data) {
|
|
82747
83960
|
return {
|
|
82748
83961
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -82752,19 +83965,26 @@ function text3(data) {
|
|
|
82752
83965
|
function failure(e) {
|
|
82753
83966
|
const message2 = e instanceof FormfeedError ? `${e.code}: ${e.message}` : e instanceof Error ? e.message : String(e);
|
|
82754
83967
|
const verdict = e instanceof FormfeedError && e.code === "pdfua_validation_failed" ? e.problem?.["accessibility"] : void 0;
|
|
83968
|
+
const invoice = e instanceof FormfeedError && EINVOICE_PROBLEMS.has(e.code) ? Object.fromEntries(
|
|
83969
|
+
["errors", "einvoice"].filter((key) => e.problem?.[key] !== void 0).map((key) => [key, e.problem?.[key]])
|
|
83970
|
+
) : {};
|
|
83971
|
+
const details = {
|
|
83972
|
+
...verdict ? { accessibility: verdict } : {},
|
|
83973
|
+
...invoice
|
|
83974
|
+
};
|
|
82755
83975
|
return {
|
|
82756
83976
|
content: [
|
|
82757
83977
|
{
|
|
82758
83978
|
type: "text",
|
|
82759
|
-
text:
|
|
82760
|
-
${JSON.stringify(
|
|
83979
|
+
text: Object.keys(details).length > 0 ? `${message2}
|
|
83980
|
+
${JSON.stringify(details, null, 2)}` : message2
|
|
82761
83981
|
}
|
|
82762
83982
|
],
|
|
82763
83983
|
isError: true
|
|
82764
83984
|
};
|
|
82765
83985
|
}
|
|
82766
83986
|
function createFormfeedServer(options) {
|
|
82767
|
-
const client = new Formfeed({
|
|
83987
|
+
const client = options.apiKey ? new Formfeed({
|
|
82768
83988
|
apiKey: options.apiKey,
|
|
82769
83989
|
headers: {
|
|
82770
83990
|
"user-agent": `formfeed-mcp/${SERVER_INFO.version}`,
|
|
@@ -82773,15 +83993,39 @@ function createFormfeedServer(options) {
|
|
|
82773
83993
|
...options.region ? { region: options.region } : {},
|
|
82774
83994
|
...options.baseUrl ? { baseUrl: options.baseUrl } : {},
|
|
82775
83995
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
82776
|
-
});
|
|
83996
|
+
}) : null;
|
|
83997
|
+
const api = () => {
|
|
83998
|
+
if (!client) throw new Error(NO_KEY);
|
|
83999
|
+
return client;
|
|
84000
|
+
};
|
|
84001
|
+
const fetchImpl = options.fetch ?? ((input, init) => fetch(input, init));
|
|
82777
84002
|
const server = new McpServer(SERVER_INFO, {
|
|
82778
|
-
instructions: "Formfeed renders PDFs and images from HTML templates, and fills Word and PowerPoint templates (as DOCX, PPTX or PDF). Start with list_templates, read the data shape with get_template_schema, check data with validate_template, then call render and hand the download_url to the user. convert_to_pdf turns office documents into PDFs. get_workspace says which workspace this server works in and what the period has left."
|
|
84003
|
+
instructions: (client ? "" : `${NO_KEY_INSTRUCTION} `) + "Formfeed renders PDFs and images from HTML templates, and fills Word and PowerPoint templates (as DOCX, PPTX or PDF). Start with list_templates, read the data shape with get_template_schema, check data with validate_template, then call render and hand the download_url to the user. An image render also returns the picture itself, so look at it before handing it on. For an e-invoice (ZUGFeRD / Factur-X), put the invoice into data._invoice, check it with check_invoice, and call render with einvoice set; the template prints the same block. convert_to_pdf turns office documents into PDFs. get_workspace says which workspace this server works in and what the period has left."
|
|
82779
84004
|
});
|
|
84005
|
+
const picture = async (render) => {
|
|
84006
|
+
const mimeType = IMAGE_TYPES[render.output];
|
|
84007
|
+
if (render.status !== "succeeded" || !mimeType || !render.download_url)
|
|
84008
|
+
return [];
|
|
84009
|
+
try {
|
|
84010
|
+
const res = await fetchImpl(render.download_url);
|
|
84011
|
+
const bytes = res.ok ? await readCapped(res, IMAGE_LIMIT_BYTES) : null;
|
|
84012
|
+
return bytes ? [{ type: "image", data: base64(bytes), mimeType }] : [];
|
|
84013
|
+
} catch {
|
|
84014
|
+
return [];
|
|
84015
|
+
}
|
|
84016
|
+
};
|
|
84017
|
+
const renderResult = async (render) => {
|
|
84018
|
+
const result = text3(renderSummary(render));
|
|
84019
|
+
return {
|
|
84020
|
+
...result,
|
|
84021
|
+
content: [...result.content, ...await picture(render)]
|
|
84022
|
+
};
|
|
84023
|
+
};
|
|
82780
84024
|
server.registerTool(
|
|
82781
84025
|
"list_templates",
|
|
82782
84026
|
{
|
|
82783
84027
|
title: "List templates",
|
|
82784
|
-
description: "Lists the templates of the workspace the
|
|
84028
|
+
description: "Lists the templates of the workspace this server works in (the one chosen at sign-in, or the API key's), most recently changed first: id, slug, name, kind (pdf, image, docx for Word, pptx for PowerPoint), engine, tags and published_version. Call it first to find the slug that get_template_schema, validate_template and render take; skip it when the user already named the template. Returns every match in one answer, without archived templates, and an empty list when nothing matches. A template whose published_version is null has never been published, so render cannot use it yet. Read-only and free.",
|
|
82785
84029
|
inputSchema: {
|
|
82786
84030
|
kind: kindSchema.optional().describe("Only templates that produce this kind of document"),
|
|
82787
84031
|
engine: engineSchema.optional().describe("Only templates written in this template language"),
|
|
@@ -82792,11 +84036,26 @@ function createFormfeedServer(options) {
|
|
|
82792
84036
|
"Text to find in the name or slug, case-insensitive (e.g. invoice)"
|
|
82793
84037
|
)
|
|
82794
84038
|
},
|
|
84039
|
+
outputSchema: {
|
|
84040
|
+
templates: z3.array(
|
|
84041
|
+
z3.object({
|
|
84042
|
+
id: z3.string().describe("tpl_ id"),
|
|
84043
|
+
slug: z3.string(),
|
|
84044
|
+
name: z3.string(),
|
|
84045
|
+
kind: z3.string().describe("pdf, image, docx (Word) or pptx (PowerPoint)"),
|
|
84046
|
+
engine: z3.string().describe("jinja2, liquid or handlebars"),
|
|
84047
|
+
description: z3.string().nullable(),
|
|
84048
|
+
tags: z3.array(z3.string()),
|
|
84049
|
+
published_version: z3.number().nullable().describe("null: never published, so render cannot use it yet"),
|
|
84050
|
+
updated_at: z3.string()
|
|
84051
|
+
})
|
|
84052
|
+
)
|
|
84053
|
+
},
|
|
82795
84054
|
annotations: READS
|
|
82796
84055
|
},
|
|
82797
84056
|
async (args2) => {
|
|
82798
84057
|
try {
|
|
82799
|
-
const templates = await
|
|
84058
|
+
const templates = await api().templates.all(args2);
|
|
82800
84059
|
return text3({ templates: templates.map(templateSummary) });
|
|
82801
84060
|
} catch (e) {
|
|
82802
84061
|
return failure(e);
|
|
@@ -82813,13 +84072,22 @@ function createFormfeedServer(options) {
|
|
|
82813
84072
|
"Slug (e.g. invoice) or tpl_ id, as list_templates returns them"
|
|
82814
84073
|
)
|
|
82815
84074
|
},
|
|
84075
|
+
outputSchema: {
|
|
84076
|
+
template: z3.string(),
|
|
84077
|
+
version: z3.number().describe("The version the schema comes from"),
|
|
84078
|
+
status: z3.string().describe("published, or draft when nothing is published yet"),
|
|
84079
|
+
schema: z3.record(z3.string(), z3.unknown()).describe(
|
|
84080
|
+
"JSON Schema of data; inferred from sample_data when none is stored"
|
|
84081
|
+
),
|
|
84082
|
+
sample_data: z3.record(z3.string(), z3.unknown())
|
|
84083
|
+
},
|
|
82816
84084
|
annotations: READS
|
|
82817
84085
|
},
|
|
82818
84086
|
async ({ template }) => {
|
|
82819
84087
|
try {
|
|
82820
|
-
const version = await
|
|
84088
|
+
const version = await api().templates.versions.get(template, "published").catch(async (e) => {
|
|
82821
84089
|
if (e.status === 404)
|
|
82822
|
-
return
|
|
84090
|
+
return api().templates.versions.get(template, "latest");
|
|
82823
84091
|
throw e;
|
|
82824
84092
|
});
|
|
82825
84093
|
const schema = version.data_schema ?? inferSchema(version.sample_data ?? {});
|
|
@@ -82850,12 +84118,17 @@ function createFormfeedServer(options) {
|
|
|
82850
84118
|
"The data the document will be rendered with, shaped as get_template_schema describes. Left out, a stored template is checked with its sample data"
|
|
82851
84119
|
)
|
|
82852
84120
|
},
|
|
84121
|
+
outputSchema: {
|
|
84122
|
+
ok: z3.boolean().describe("false when a diagnostic is an error"),
|
|
84123
|
+
template: z3.object({ id: z3.string(), slug: z3.string(), version: z3.number() }).optional().describe("The stored version checked; absent for html"),
|
|
84124
|
+
diagnostics: z3.array(diagnosticOutput)
|
|
84125
|
+
},
|
|
82853
84126
|
annotations: READS
|
|
82854
84127
|
},
|
|
82855
84128
|
async ({ template, html, engine, data }) => {
|
|
82856
84129
|
try {
|
|
82857
84130
|
if (template) {
|
|
82858
|
-
const check = (version) =>
|
|
84131
|
+
const check = (version) => api().templates.validate(template, {
|
|
82859
84132
|
version,
|
|
82860
84133
|
...data ? { data } : {}
|
|
82861
84134
|
});
|
|
@@ -82911,11 +84184,48 @@ function createFormfeedServer(options) {
|
|
|
82911
84184
|
}
|
|
82912
84185
|
}
|
|
82913
84186
|
);
|
|
84187
|
+
server.registerTool(
|
|
84188
|
+
"check_invoice",
|
|
84189
|
+
{
|
|
84190
|
+
title: "Check an e-invoice",
|
|
84191
|
+
description: "Checks the _invoice block of an e-invoice offline, as the API does before it renders one: the fields EN 16931 asks for, the totals to the cent, VAT categories and code lists, and France's rules for an invoice between two French businesses. Free, no units, nothing sent. Returns ok and problems, each with the field, the rule (BR-\u2026) where one applies and a message. Nothing is computed for you: change the values a problem names, check again, then call render with einvoice and the block as data._invoice. The validator that runs during the render holds the file to the full rule set and to PDF/A-3b as well.",
|
|
84192
|
+
inputSchema: {
|
|
84193
|
+
invoice: z3.record(z3.string(), z3.unknown()).describe(
|
|
84194
|
+
"The _invoice block as it goes into data._invoice: number, issue_date, currency, seller, buyer, lines, tax.breakdown, totals and payment, as https://docs.formfeed.dev/templates/e-invoices#invoice-block describes"
|
|
84195
|
+
),
|
|
84196
|
+
profile: z3.enum(["en16931", "basic"]).optional().describe(
|
|
84197
|
+
"The profile the render will ask for; en16931 when left out. basic cannot carry a rounding amount and is refused between two French businesses"
|
|
84198
|
+
)
|
|
84199
|
+
},
|
|
84200
|
+
outputSchema: {
|
|
84201
|
+
ok: z3.boolean(),
|
|
84202
|
+
problems: z3.array(
|
|
84203
|
+
z3.object({
|
|
84204
|
+
path: z3.string().describe("The field, e.g. _invoice.seller.vat_id"),
|
|
84205
|
+
rule: z3.string().optional().describe("The EN 16931 rule, e.g. BR-03"),
|
|
84206
|
+
message: z3.string()
|
|
84207
|
+
})
|
|
84208
|
+
)
|
|
84209
|
+
},
|
|
84210
|
+
annotations: READS
|
|
84211
|
+
},
|
|
84212
|
+
async ({ invoice, profile }) => {
|
|
84213
|
+
const check = checkInvoice(invoice, { profile: profile ?? "en16931" });
|
|
84214
|
+
return text3({
|
|
84215
|
+
ok: check.ok,
|
|
84216
|
+
problems: check.problems.map((problem) => ({
|
|
84217
|
+
path: problem.path ? `_invoice.${problem.path}` : "_invoice",
|
|
84218
|
+
...problem.rule ? { rule: problem.rule } : {},
|
|
84219
|
+
message: problem.message
|
|
84220
|
+
}))
|
|
84221
|
+
});
|
|
84222
|
+
}
|
|
84223
|
+
);
|
|
82914
84224
|
server.registerTool(
|
|
82915
84225
|
"render",
|
|
82916
84226
|
{
|
|
82917
84227
|
title: "Render a document",
|
|
82918
|
-
description: "Creates a document by filling a stored template, or ad-hoc html, with data: a PDF or image, or DOCX, PPTX or PDF from Word and PowerPoint templates. Returns the render with status and download_url. Use it to make a document from data; to turn an existing office file into a PDF, use convert_to_pdf. Stored templates render their published version, so one whose published_version is null fails with template_not_found. Every call is a new render: a live key consumes units, a test key is free (watermarked on the Free plan). A render that fails returns status failed and an error with code and message; validate_template finds most causes beforehand. The link expires at expires_at; get_render issues a fresh one.",
|
|
84228
|
+
description: "Creates a document by filling a stored template, or ad-hoc html, with data: a PDF or image, or DOCX, PPTX or PDF from Word and PowerPoint templates. Returns the render with status and download_url; for an image (png, jpg or webp) up to 2 MB the result also carries the picture itself, so look at what you made before handing it on (a PDF comes as its link). Use it to make a document from data; to turn an existing office file into a PDF, use convert_to_pdf. Stored templates render their published version, so one whose published_version is null fails with template_not_found. Every call is a new render: a live key or a signed-in agent consumes units, a test key is free (watermarked on the Free plan). A render that fails returns status failed and an error with code and message; validate_template finds most causes beforehand. With einvoice the PDF becomes a ZUGFeRD / Factur-X e-invoice built from data._invoice (check the block with check_invoice first): the render then carries einvoice, the validator's report, and a refused invoice names the fields and rules to fix. The link expires at expires_at; get_render issues a fresh one.",
|
|
82919
84229
|
inputSchema: {
|
|
82920
84230
|
template: z3.string().optional().describe("Slug or tpl_ id of a stored template; pass this or html"),
|
|
82921
84231
|
html: z3.string().optional().describe(
|
|
@@ -82936,8 +84246,28 @@ function createFormfeedServer(options) {
|
|
|
82936
84246
|
locale: z3.string().optional().describe("BCP 47 tag for helpers and translations, e.g. de-DE"),
|
|
82937
84247
|
wait: z3.boolean().default(true).describe(
|
|
82938
84248
|
"Most renders are finished when the call returns; true also waits for one that continues in the background, false returns it as it is (queued or rendering) to check later with get_render"
|
|
84249
|
+
),
|
|
84250
|
+
einvoice: z3.union([
|
|
84251
|
+
z3.boolean(),
|
|
84252
|
+
z3.object({
|
|
84253
|
+
profile: z3.enum(["en16931", "basic"]).optional().describe(
|
|
84254
|
+
"en16931 (default) carries the whole European standard; basic is the smallest full invoice and is refused between two French businesses"
|
|
84255
|
+
),
|
|
84256
|
+
flavour: z3.enum(["factur-x", "zugferd"]).optional().describe(
|
|
84257
|
+
"One specification under two names; the default factur-x and zugferd make the same file"
|
|
84258
|
+
),
|
|
84259
|
+
xml: z3.enum(["embedded", "both"]).optional().describe(
|
|
84260
|
+
"both also stores the XML as a file of its own, linked as einvoice.xml_url"
|
|
84261
|
+
),
|
|
84262
|
+
display_check: z3.enum(["warn", "strict"]).optional().describe(
|
|
84263
|
+
"What a value of the XML that the PDF does not show does: a warning in the report (default) or a failed render"
|
|
84264
|
+
)
|
|
84265
|
+
})
|
|
84266
|
+
]).optional().describe(
|
|
84267
|
+
"Makes the PDF a ZUGFeRD / Factur-X e-invoice whose XML is built from data._invoice: true for the defaults, an object to choose, false to switch off what the template declares. PDF only; live renders from the Starter plan, test keys on Free make 20 marked ones a day"
|
|
82939
84268
|
)
|
|
82940
84269
|
},
|
|
84270
|
+
outputSchema: renderOutput,
|
|
82941
84271
|
annotations: CREATES
|
|
82942
84272
|
},
|
|
82943
84273
|
async ({
|
|
@@ -82948,12 +84278,13 @@ function createFormfeedServer(options) {
|
|
|
82948
84278
|
output,
|
|
82949
84279
|
filename,
|
|
82950
84280
|
locale,
|
|
82951
|
-
wait
|
|
84281
|
+
wait,
|
|
84282
|
+
einvoice
|
|
82952
84283
|
}) => {
|
|
82953
84284
|
try {
|
|
82954
84285
|
if (!template && !html)
|
|
82955
84286
|
return failure(new Error("Pass template or html"));
|
|
82956
|
-
let render = await
|
|
84287
|
+
let render = await api().renders.create({
|
|
82957
84288
|
...template ? { template } : {},
|
|
82958
84289
|
...html ? { html } : {},
|
|
82959
84290
|
...engine ? { engine } : {},
|
|
@@ -82961,11 +84292,12 @@ function createFormfeedServer(options) {
|
|
|
82961
84292
|
...output ? { output } : {},
|
|
82962
84293
|
...filename ? { filename } : {},
|
|
82963
84294
|
...locale ? { locale } : {},
|
|
84295
|
+
...einvoice !== void 0 ? { post: { einvoice: einvoice === true ? {} : einvoice } } : {},
|
|
82964
84296
|
meta: { source: "mcp" }
|
|
82965
84297
|
});
|
|
82966
84298
|
if (wait && render.status !== "succeeded" && render.status !== "failed")
|
|
82967
|
-
render = await
|
|
82968
|
-
return
|
|
84299
|
+
render = await api().renders.waitFor(render.id);
|
|
84300
|
+
return await renderResult(render);
|
|
82969
84301
|
} catch (e) {
|
|
82970
84302
|
return failure(e);
|
|
82971
84303
|
}
|
|
@@ -83000,6 +84332,7 @@ function createFormfeedServer(options) {
|
|
|
83000
84332
|
single_page_sheets: z3.boolean().optional().describe("Each spreadsheet sheet on one page"),
|
|
83001
84333
|
filename: z3.string().optional().describe("Name of the PDF behind download_url, e.g. report.pdf")
|
|
83002
84334
|
},
|
|
84335
|
+
outputSchema: renderOutput,
|
|
83003
84336
|
annotations: CREATES
|
|
83004
84337
|
},
|
|
83005
84338
|
async (args2) => {
|
|
@@ -83032,7 +84365,7 @@ function createFormfeedServer(options) {
|
|
|
83032
84365
|
};
|
|
83033
84366
|
let render;
|
|
83034
84367
|
if (render_id) {
|
|
83035
|
-
render = await
|
|
84368
|
+
render = await api().pdf.convert(render_id, convert);
|
|
83036
84369
|
} else {
|
|
83037
84370
|
let data;
|
|
83038
84371
|
let name;
|
|
@@ -83063,7 +84396,7 @@ function createFormfeedServer(options) {
|
|
|
83063
84396
|
);
|
|
83064
84397
|
name = file_name;
|
|
83065
84398
|
}
|
|
83066
|
-
render = await
|
|
84399
|
+
render = await api().pdf.convert(
|
|
83067
84400
|
{ file: { data, name } },
|
|
83068
84401
|
{
|
|
83069
84402
|
...convert,
|
|
@@ -83072,8 +84405,8 @@ function createFormfeedServer(options) {
|
|
|
83072
84405
|
);
|
|
83073
84406
|
}
|
|
83074
84407
|
if (render.status !== "succeeded" && render.status !== "failed")
|
|
83075
|
-
render = await
|
|
83076
|
-
return
|
|
84408
|
+
render = await api().renders.waitFor(render.id);
|
|
84409
|
+
return await renderResult(render);
|
|
83077
84410
|
} catch (e) {
|
|
83078
84411
|
return failure(e);
|
|
83079
84412
|
}
|
|
@@ -83083,17 +84416,18 @@ function createFormfeedServer(options) {
|
|
|
83083
84416
|
"get_render",
|
|
83084
84417
|
{
|
|
83085
84418
|
title: "Get a render",
|
|
83086
|
-
description: "Returns the current state of a render: status (queued, rendering, succeeded or failed), download_url with expires_at, page_count, units and error. Use it to follow a render started with wait false, or to get a fresh link once the old one has expired; render and convert_to_pdf already return this for the render they just made. Read-only and free; an unknown id returns an error.",
|
|
84419
|
+
description: "Returns the current state of a render: status (queued, rendering, succeeded or failed), download_url with expires_at, page_count, units and error, and the picture of a finished image render up to 2 MB. Use it to follow a render started with wait false, or to get a fresh link once the old one has expired; render and convert_to_pdf already return this for the render they just made. Read-only and free; an unknown id returns an error.",
|
|
83087
84420
|
inputSchema: {
|
|
83088
84421
|
id: z3.string().describe(
|
|
83089
84422
|
"rnd_ id of the render, as render or convert_to_pdf returned it"
|
|
83090
84423
|
)
|
|
83091
84424
|
},
|
|
84425
|
+
outputSchema: renderOutput,
|
|
83092
84426
|
annotations: READS
|
|
83093
84427
|
},
|
|
83094
84428
|
async ({ id }) => {
|
|
83095
84429
|
try {
|
|
83096
|
-
return
|
|
84430
|
+
return await renderResult(await api().renders.get(id));
|
|
83097
84431
|
} catch (e) {
|
|
83098
84432
|
return failure(e);
|
|
83099
84433
|
}
|
|
@@ -83105,11 +84439,36 @@ function createFormfeedServer(options) {
|
|
|
83105
84439
|
title: "Which workspace this server works in",
|
|
83106
84440
|
description: "Returns the workspace and organisation this server acts in, the plan, the environment (live or test) and the units of the current period: included, used and the balance of prepaid units. Call it when the user asks where a document will land or how many renders are left, or when list_templates returns nothing you expected. A signed-in agent works in the workspace chosen when it was connected, which the person changes in Formfeed under Account \u2192 Connected agents; an API key works in the workspace it was made for. Read-only and free.",
|
|
83107
84441
|
inputSchema: {},
|
|
84442
|
+
outputSchema: {
|
|
84443
|
+
workspace: z3.object({
|
|
84444
|
+
id: z3.string(),
|
|
84445
|
+
slug: z3.string(),
|
|
84446
|
+
name: z3.string(),
|
|
84447
|
+
region: z3.string().describe("eu, us or apac")
|
|
84448
|
+
}),
|
|
84449
|
+
organization: z3.object({
|
|
84450
|
+
id: z3.string(),
|
|
84451
|
+
slug: z3.string(),
|
|
84452
|
+
name: z3.string()
|
|
84453
|
+
}),
|
|
84454
|
+
plan: z3.object({
|
|
84455
|
+
id: z3.string(),
|
|
84456
|
+
name: z3.string(),
|
|
84457
|
+
monthly_units: z3.number().nullable().optional().describe("Units included a month; null where the plan sets none")
|
|
84458
|
+
}),
|
|
84459
|
+
environment: z3.enum(["live", "test"]).optional().describe("live renders count units; test renders are free"),
|
|
84460
|
+
period: z3.object({ start: z3.string(), end: z3.string() }).describe("The current billing period"),
|
|
84461
|
+
units: z3.object({
|
|
84462
|
+
included: z3.number().nullable().optional(),
|
|
84463
|
+
used: z3.number(),
|
|
84464
|
+
overage_balance: z3.number().optional().describe("Prepaid units left beyond the included ones")
|
|
84465
|
+
})
|
|
84466
|
+
},
|
|
83108
84467
|
annotations: READS
|
|
83109
84468
|
},
|
|
83110
84469
|
async () => {
|
|
83111
84470
|
try {
|
|
83112
|
-
const { workspace, organization, plan, environment, period: period2, units } = await
|
|
84471
|
+
const { workspace, organization, plan, environment, period: period2, units } = await api().account.get();
|
|
83113
84472
|
return text3({
|
|
83114
84473
|
workspace,
|
|
83115
84474
|
organization,
|
|
@@ -83128,11 +84487,110 @@ function createFormfeedServer(options) {
|
|
|
83128
84487
|
|
|
83129
84488
|
// packages/mcp/src/lib/web.ts
|
|
83130
84489
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
84490
|
+
|
|
84491
|
+
// packages/mcp/src/lib/server-card.ts
|
|
84492
|
+
import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
|
|
84493
|
+
var SERVER_CARD_TYPE = "application/mcp-server-card+json";
|
|
84494
|
+
var SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
|
|
84495
|
+
function serverCard(origin, signIn) {
|
|
84496
|
+
const endpoint = `${origin.replace(/\/$/, "")}/mcp`;
|
|
84497
|
+
const supportedProtocolVersions = [...SUPPORTED_PROTOCOL_VERSIONS];
|
|
84498
|
+
return {
|
|
84499
|
+
$schema: SERVER_CARD_SCHEMA,
|
|
84500
|
+
name: "io.github.formfeed-dev/formfeed",
|
|
84501
|
+
version: SERVER_INFO.version,
|
|
84502
|
+
description: "Render PDFs and images from templates, validate data and convert Office files to PDF.",
|
|
84503
|
+
title: "Formfeed",
|
|
84504
|
+
websiteUrl: "https://docs.formfeed.dev/integrations/mcp",
|
|
84505
|
+
repository: {
|
|
84506
|
+
url: "https://github.com/formfeed-dev/formfeed",
|
|
84507
|
+
source: "github",
|
|
84508
|
+
subfolder: "packages/mcp"
|
|
84509
|
+
},
|
|
84510
|
+
icons: [
|
|
84511
|
+
{ src: "https://formfeed.dev/favicon.svg", mimeType: "image/svg+xml" },
|
|
84512
|
+
{
|
|
84513
|
+
src: "https://formfeed.dev/brand/formfeed-512.png",
|
|
84514
|
+
mimeType: "image/png",
|
|
84515
|
+
sizes: ["512x512"]
|
|
84516
|
+
}
|
|
84517
|
+
],
|
|
84518
|
+
remotes: [
|
|
84519
|
+
{
|
|
84520
|
+
type: "streamable-http",
|
|
84521
|
+
url: endpoint,
|
|
84522
|
+
headers: [
|
|
84523
|
+
signIn ? {
|
|
84524
|
+
name: "Authorization",
|
|
84525
|
+
description: "Bearer followed by a Formfeed API key (ff_test_\u2026 or ff_live_\u2026); without it the client signs in with your Formfeed account (OAuth) and you choose the workspace",
|
|
84526
|
+
isRequired: false,
|
|
84527
|
+
isSecret: true
|
|
84528
|
+
} : {
|
|
84529
|
+
name: "Authorization",
|
|
84530
|
+
description: "Bearer followed by a Formfeed API key (ff_test_\u2026 or ff_live_\u2026)",
|
|
84531
|
+
isRequired: true,
|
|
84532
|
+
isSecret: true
|
|
84533
|
+
}
|
|
84534
|
+
],
|
|
84535
|
+
supportedProtocolVersions
|
|
84536
|
+
},
|
|
84537
|
+
...signIn ? [
|
|
84538
|
+
{
|
|
84539
|
+
type: "streamable-http",
|
|
84540
|
+
url: `${endpoint}/{workspace_id}`,
|
|
84541
|
+
variables: {
|
|
84542
|
+
workspace_id: {
|
|
84543
|
+
description: "Workspace id from the Formfeed app, for a connection that should work in another workspace than the one chosen at sign-in (OAuth)",
|
|
84544
|
+
isRequired: true
|
|
84545
|
+
}
|
|
84546
|
+
},
|
|
84547
|
+
supportedProtocolVersions
|
|
84548
|
+
}
|
|
84549
|
+
] : []
|
|
84550
|
+
]
|
|
84551
|
+
};
|
|
84552
|
+
}
|
|
84553
|
+
var CORS = {
|
|
84554
|
+
"access-control-allow-origin": "*",
|
|
84555
|
+
"access-control-allow-methods": "GET, HEAD, OPTIONS",
|
|
84556
|
+
"access-control-allow-headers": "Content-Type, If-None-Match",
|
|
84557
|
+
"access-control-expose-headers": "ETag"
|
|
84558
|
+
};
|
|
84559
|
+
async function serverCardResponse(request, card) {
|
|
84560
|
+
if (request.method === "OPTIONS")
|
|
84561
|
+
return new Response(null, { status: 204, headers: CORS });
|
|
84562
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
84563
|
+
return new Response(null, {
|
|
84564
|
+
status: 405,
|
|
84565
|
+
headers: { ...CORS, allow: "GET, HEAD, OPTIONS" }
|
|
84566
|
+
});
|
|
84567
|
+
const body = JSON.stringify(card, null, 2);
|
|
84568
|
+
const digest = await crypto.subtle.digest(
|
|
84569
|
+
"SHA-256",
|
|
84570
|
+
new TextEncoder().encode(body)
|
|
84571
|
+
);
|
|
84572
|
+
const etag = `"${[...new Uint8Array(digest).slice(0, 16)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
|
|
84573
|
+
const headers = {
|
|
84574
|
+
...CORS,
|
|
84575
|
+
"cache-control": "public, max-age=3600",
|
|
84576
|
+
etag
|
|
84577
|
+
};
|
|
84578
|
+
const known = (request.headers.get("if-none-match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, ""));
|
|
84579
|
+
if (known.includes(etag) || known.includes("*"))
|
|
84580
|
+
return new Response(null, { status: 304, headers });
|
|
84581
|
+
return new Response(request.method === "HEAD" ? null : body, {
|
|
84582
|
+
status: 200,
|
|
84583
|
+
headers: { ...headers, "content-type": SERVER_CARD_TYPE }
|
|
84584
|
+
});
|
|
84585
|
+
}
|
|
84586
|
+
|
|
84587
|
+
// packages/mcp/src/lib/web.ts
|
|
83131
84588
|
var WORKSPACE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
83132
84589
|
var API_KEY = /^ff_(live|test)_/;
|
|
83133
84590
|
var JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
83134
84591
|
var MCP_PATH = "/mcp";
|
|
83135
84592
|
var RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
84593
|
+
var SERVER_CARD_PATH = `${MCP_PATH}/server-card`;
|
|
83136
84594
|
function resourceMetadata(origin, workspaceId, authorizationServer2) {
|
|
83137
84595
|
return {
|
|
83138
84596
|
resource: `${origin}${MCP_PATH}/${workspaceId}`,
|
|
@@ -83208,6 +84666,11 @@ function mcpFetchHandler(options = {}) {
|
|
|
83208
84666
|
200
|
|
83209
84667
|
);
|
|
83210
84668
|
}
|
|
84669
|
+
if (path === SERVER_CARD_PATH)
|
|
84670
|
+
return serverCardResponse(
|
|
84671
|
+
request,
|
|
84672
|
+
serverCard(origin, Boolean(authorizationServer2))
|
|
84673
|
+
);
|
|
83211
84674
|
if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`))
|
|
83212
84675
|
return new Response(null, { status: 404 });
|
|
83213
84676
|
const workspaceId = path === MCP_PATH ? null : path.slice(MCP_PATH.length + 1);
|
|
@@ -83366,7 +84829,7 @@ var authorizationServer = flag("--auth-server") ?? process.env["FORMFEED_AUTH_SE
|
|
|
83366
84829
|
var publicOrigin = flag("--public-origin") ?? process.env["FORMFEED_PUBLIC_ORIGIN"];
|
|
83367
84830
|
if (args.includes("--help") || args.includes("-h")) {
|
|
83368
84831
|
process.stdout.write(
|
|
83369
|
-
"formfeed-mcp: Model Context Protocol server for Formfeed\n\n formfeed-mcp stdio;
|
|
84832
|
+
"formfeed-mcp: Model Context Protocol server for Formfeed\n\n formfeed-mcp stdio; FORMFEED_API_KEY, without it the offline checks only\n formfeed-mcp --http Streamable HTTP on --port (8790) and --host (127.0.0.1); the key comes from Authorization: Bearer\n --auth-server <issuer> (or FORMFEED_AUTH_SERVER) also takes OAuth access tokens on /mcp and /mcp/<workspace id>;\n --public-origin <url> (or FORMFEED_PUBLIC_ORIGIN) is the origin clients see behind a proxy\n"
|
|
83370
84833
|
);
|
|
83371
84834
|
} else if (args.includes("--http")) {
|
|
83372
84835
|
startHttp({
|
|
@@ -83379,17 +84842,16 @@ if (args.includes("--help") || args.includes("-h")) {
|
|
|
83379
84842
|
}).then((h) => process.stderr.write(`formfeed-mcp listening on ${h.url}
|
|
83380
84843
|
`));
|
|
83381
84844
|
} else {
|
|
83382
|
-
const apiKey = process.env["FORMFEED_API_KEY"];
|
|
83383
|
-
if (!apiKey)
|
|
83384
|
-
process.stderr.write(
|
|
83385
|
-
|
|
83386
|
-
|
|
83387
|
-
|
|
83388
|
-
|
|
84845
|
+
const apiKey = process.env["FORMFEED_API_KEY"] || void 0;
|
|
84846
|
+
if (!apiKey)
|
|
84847
|
+
process.stderr.write(
|
|
84848
|
+
"FORMFEED_API_KEY is not set: only validate_template (with html) and check_invoice work\n"
|
|
84849
|
+
);
|
|
84850
|
+
startStdio({ apiKey, baseUrl, region }).catch((e) => {
|
|
84851
|
+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}
|
|
83389
84852
|
`);
|
|
83390
|
-
|
|
83391
|
-
|
|
83392
|
-
}
|
|
84853
|
+
process.exitCode = 1;
|
|
84854
|
+
});
|
|
83393
84855
|
}
|
|
83394
84856
|
/*! Bundled license information:
|
|
83395
84857
|
|