@formfeed/mcp 0.3.16 → 0.3.17

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.
@@ -6257,9 +6257,9 @@ var require_parser2 = __commonJS({
6257
6257
  this.yytext = "";
6258
6258
  this.match = "";
6259
6259
  }
6260
- var rules = this._currentRules();
6261
- for (var i = 0; i < rules.length; i++) {
6262
- tempMatch = this._input.match(this.rules[rules[i]]);
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, rules[index], this.conditionStack[this.conditionStack.length - 1]);
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;
@@ -16041,10 +16041,10 @@ var require_nunjucks = __commonJS({
16041
16041
  return typeof value === "function";
16042
16042
  }
16043
16043
  exports2.callable = callable;
16044
- function defined(value) {
16044
+ function defined2(value) {
16045
16045
  return value !== void 0;
16046
16046
  }
16047
- exports2.defined = 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());
@@ -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,7 @@ 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 SERVER_INFO = { name: "formfeed", version: "0.3.16" };
83844
+ var SERVER_INFO = { name: "formfeed", version: "0.3.17" };
82707
83845
  var READS = {
82708
83846
  readOnlyHint: true,
82709
83847
  destructiveHint: false,
@@ -82741,8 +83879,15 @@ var renderSummary = (r) => ({
82741
83879
  error: r.error,
82742
83880
  // the validator's verdict of a render whose template declares PDF/UA-1 (plan 21): whether the
82743
83881
  // file passed, and for each failed rule what to change and in which elements
82744
- ...r.accessibility ? { accessibility: r.accessibility } : {}
83882
+ ...r.accessibility ? { accessibility: r.accessibility } : {},
83883
+ // the report of an e-invoice (spec 17): valid, the findings, the XML's link, the hashes it checked
83884
+ ...r.einvoice ? { einvoice: r.einvoice } : {}
82745
83885
  });
83886
+ var EINVOICE_PROBLEMS = /* @__PURE__ */ new Set([
83887
+ "einvoice_data_invalid",
83888
+ "einvoice_display_mismatch",
83889
+ "pdfa_conversion_failed"
83890
+ ]);
82746
83891
  function text3(data) {
82747
83892
  return {
82748
83893
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -82752,12 +83897,19 @@ function text3(data) {
82752
83897
  function failure(e) {
82753
83898
  const message2 = e instanceof FormfeedError ? `${e.code}: ${e.message}` : e instanceof Error ? e.message : String(e);
82754
83899
  const verdict = e instanceof FormfeedError && e.code === "pdfua_validation_failed" ? e.problem?.["accessibility"] : void 0;
83900
+ const invoice = e instanceof FormfeedError && EINVOICE_PROBLEMS.has(e.code) ? Object.fromEntries(
83901
+ ["errors", "einvoice"].filter((key) => e.problem?.[key] !== void 0).map((key) => [key, e.problem?.[key]])
83902
+ ) : {};
83903
+ const details = {
83904
+ ...verdict ? { accessibility: verdict } : {},
83905
+ ...invoice
83906
+ };
82755
83907
  return {
82756
83908
  content: [
82757
83909
  {
82758
83910
  type: "text",
82759
- text: verdict ? `${message2}
82760
- ${JSON.stringify({ accessibility: verdict }, null, 2)}` : message2
83911
+ text: Object.keys(details).length > 0 ? `${message2}
83912
+ ${JSON.stringify(details, null, 2)}` : message2
82761
83913
  }
82762
83914
  ],
82763
83915
  isError: true
@@ -82775,7 +83927,7 @@ function createFormfeedServer(options) {
82775
83927
  ...options.fetch ? { fetch: options.fetch } : {}
82776
83928
  });
82777
83929
  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."
83930
+ 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. 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
83931
  });
82780
83932
  server.registerTool(
82781
83933
  "list_templates",
@@ -82911,11 +84063,38 @@ function createFormfeedServer(options) {
82911
84063
  }
82912
84064
  }
82913
84065
  );
84066
+ server.registerTool(
84067
+ "check_invoice",
84068
+ {
84069
+ title: "Check an e-invoice",
84070
+ 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.",
84071
+ inputSchema: {
84072
+ invoice: z3.record(z3.string(), z3.unknown()).describe(
84073
+ "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"
84074
+ ),
84075
+ profile: z3.enum(["en16931", "basic"]).optional().describe(
84076
+ "The profile the render will ask for; en16931 when left out. basic cannot carry a rounding amount and is refused between two French businesses"
84077
+ )
84078
+ },
84079
+ annotations: READS
84080
+ },
84081
+ async ({ invoice, profile }) => {
84082
+ const check = checkInvoice(invoice, { profile: profile ?? "en16931" });
84083
+ return text3({
84084
+ ok: check.ok,
84085
+ problems: check.problems.map((problem) => ({
84086
+ path: problem.path ? `_invoice.${problem.path}` : "_invoice",
84087
+ ...problem.rule ? { rule: problem.rule } : {},
84088
+ message: problem.message
84089
+ }))
84090
+ });
84091
+ }
84092
+ );
82914
84093
  server.registerTool(
82915
84094
  "render",
82916
84095
  {
82917
84096
  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.",
84097
+ 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. 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
84098
  inputSchema: {
82920
84099
  template: z3.string().optional().describe("Slug or tpl_ id of a stored template; pass this or html"),
82921
84100
  html: z3.string().optional().describe(
@@ -82936,6 +84115,25 @@ function createFormfeedServer(options) {
82936
84115
  locale: z3.string().optional().describe("BCP 47 tag for helpers and translations, e.g. de-DE"),
82937
84116
  wait: z3.boolean().default(true).describe(
82938
84117
  "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"
84118
+ ),
84119
+ einvoice: z3.union([
84120
+ z3.boolean(),
84121
+ z3.object({
84122
+ profile: z3.enum(["en16931", "basic"]).optional().describe(
84123
+ "en16931 (default) carries the whole European standard; basic is the smallest full invoice and is refused between two French businesses"
84124
+ ),
84125
+ flavour: z3.enum(["factur-x", "zugferd"]).optional().describe(
84126
+ "One specification under two names; the default factur-x and zugferd make the same file"
84127
+ ),
84128
+ xml: z3.enum(["embedded", "both"]).optional().describe(
84129
+ "both also stores the XML as a file of its own, linked as einvoice.xml_url"
84130
+ ),
84131
+ display_check: z3.enum(["warn", "strict"]).optional().describe(
84132
+ "What a value of the XML that the PDF does not show does: a warning in the report (default) or a failed render"
84133
+ )
84134
+ })
84135
+ ]).optional().describe(
84136
+ "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
84137
  )
82940
84138
  },
82941
84139
  annotations: CREATES
@@ -82948,7 +84146,8 @@ function createFormfeedServer(options) {
82948
84146
  output,
82949
84147
  filename,
82950
84148
  locale,
82951
- wait
84149
+ wait,
84150
+ einvoice
82952
84151
  }) => {
82953
84152
  try {
82954
84153
  if (!template && !html)
@@ -82961,6 +84160,7 @@ function createFormfeedServer(options) {
82961
84160
  ...output ? { output } : {},
82962
84161
  ...filename ? { filename } : {},
82963
84162
  ...locale ? { locale } : {},
84163
+ ...einvoice !== void 0 ? { post: { einvoice: einvoice === true ? {} : einvoice } } : {},
82964
84164
  meta: { source: "mcp" }
82965
84165
  });
82966
84166
  if (wait && render.status !== "succeeded" && render.status !== "failed")
@@ -83128,11 +84328,110 @@ function createFormfeedServer(options) {
83128
84328
 
83129
84329
  // packages/mcp/src/lib/web.ts
83130
84330
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
84331
+
84332
+ // packages/mcp/src/lib/server-card.ts
84333
+ import { SUPPORTED_PROTOCOL_VERSIONS } from "@modelcontextprotocol/sdk/types.js";
84334
+ var SERVER_CARD_TYPE = "application/mcp-server-card+json";
84335
+ var SERVER_CARD_SCHEMA = "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json";
84336
+ function serverCard(origin, signIn) {
84337
+ const endpoint = `${origin.replace(/\/$/, "")}/mcp`;
84338
+ const supportedProtocolVersions = [...SUPPORTED_PROTOCOL_VERSIONS];
84339
+ return {
84340
+ $schema: SERVER_CARD_SCHEMA,
84341
+ name: "io.github.formfeed-dev/formfeed",
84342
+ version: SERVER_INFO.version,
84343
+ description: "Render PDFs and images from templates, validate data and convert Office files to PDF.",
84344
+ title: "Formfeed",
84345
+ websiteUrl: "https://docs.formfeed.dev/integrations/mcp",
84346
+ repository: {
84347
+ url: "https://github.com/formfeed-dev/formfeed",
84348
+ source: "github",
84349
+ subfolder: "packages/mcp"
84350
+ },
84351
+ icons: [
84352
+ { src: "https://formfeed.dev/favicon.svg", mimeType: "image/svg+xml" },
84353
+ {
84354
+ src: "https://formfeed.dev/brand/formfeed-512.png",
84355
+ mimeType: "image/png",
84356
+ sizes: ["512x512"]
84357
+ }
84358
+ ],
84359
+ remotes: [
84360
+ {
84361
+ type: "streamable-http",
84362
+ url: endpoint,
84363
+ headers: [
84364
+ signIn ? {
84365
+ name: "Authorization",
84366
+ 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",
84367
+ isRequired: false,
84368
+ isSecret: true
84369
+ } : {
84370
+ name: "Authorization",
84371
+ description: "Bearer followed by a Formfeed API key (ff_test_\u2026 or ff_live_\u2026)",
84372
+ isRequired: true,
84373
+ isSecret: true
84374
+ }
84375
+ ],
84376
+ supportedProtocolVersions
84377
+ },
84378
+ ...signIn ? [
84379
+ {
84380
+ type: "streamable-http",
84381
+ url: `${endpoint}/{workspace_id}`,
84382
+ variables: {
84383
+ workspace_id: {
84384
+ description: "Workspace id from the Formfeed app, for a connection that should work in another workspace than the one chosen at sign-in (OAuth)",
84385
+ isRequired: true
84386
+ }
84387
+ },
84388
+ supportedProtocolVersions
84389
+ }
84390
+ ] : []
84391
+ ]
84392
+ };
84393
+ }
84394
+ var CORS = {
84395
+ "access-control-allow-origin": "*",
84396
+ "access-control-allow-methods": "GET, HEAD, OPTIONS",
84397
+ "access-control-allow-headers": "Content-Type, If-None-Match",
84398
+ "access-control-expose-headers": "ETag"
84399
+ };
84400
+ async function serverCardResponse(request, card) {
84401
+ if (request.method === "OPTIONS")
84402
+ return new Response(null, { status: 204, headers: CORS });
84403
+ if (request.method !== "GET" && request.method !== "HEAD")
84404
+ return new Response(null, {
84405
+ status: 405,
84406
+ headers: { ...CORS, allow: "GET, HEAD, OPTIONS" }
84407
+ });
84408
+ const body = JSON.stringify(card, null, 2);
84409
+ const digest = await crypto.subtle.digest(
84410
+ "SHA-256",
84411
+ new TextEncoder().encode(body)
84412
+ );
84413
+ const etag = `"${[...new Uint8Array(digest).slice(0, 16)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}"`;
84414
+ const headers = {
84415
+ ...CORS,
84416
+ "cache-control": "public, max-age=3600",
84417
+ etag
84418
+ };
84419
+ const known = (request.headers.get("if-none-match") ?? "").split(",").map((tag) => tag.trim().replace(/^W\//, ""));
84420
+ if (known.includes(etag) || known.includes("*"))
84421
+ return new Response(null, { status: 304, headers });
84422
+ return new Response(request.method === "HEAD" ? null : body, {
84423
+ status: 200,
84424
+ headers: { ...headers, "content-type": SERVER_CARD_TYPE }
84425
+ });
84426
+ }
84427
+
84428
+ // packages/mcp/src/lib/web.ts
83131
84429
  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
84430
  var API_KEY = /^ff_(live|test)_/;
83133
84431
  var JWT = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
83134
84432
  var MCP_PATH = "/mcp";
83135
84433
  var RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
84434
+ var SERVER_CARD_PATH = `${MCP_PATH}/server-card`;
83136
84435
  function resourceMetadata(origin, workspaceId, authorizationServer2) {
83137
84436
  return {
83138
84437
  resource: `${origin}${MCP_PATH}/${workspaceId}`,
@@ -83208,6 +84507,11 @@ function mcpFetchHandler(options = {}) {
83208
84507
  200
83209
84508
  );
83210
84509
  }
84510
+ if (path === SERVER_CARD_PATH)
84511
+ return serverCardResponse(
84512
+ request,
84513
+ serverCard(origin, Boolean(authorizationServer2))
84514
+ );
83211
84515
  if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`))
83212
84516
  return new Response(null, { status: 404 });
83213
84517
  const workspaceId = path === MCP_PATH ? null : path.slice(MCP_PATH.length + 1);