@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.
@@ -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;
@@ -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 base64 = require_base64();
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 += base64.encode(digit);
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 = base64.decode(aStr.charCodeAt(aIndex++));
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 fetch(url, cb) {
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 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());
@@ -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, reference: invoice.number }) }}" alt="QR code to pay invoice {{ invoice.number }}">',
70855
- liquid: '<img src="{{ company.iban | epcQr: name: company.name, amount: invoice.total, reference: invoice.number }}" alt="QR code to pay invoice {{ invoice.number }}">',
70856
- handlebars: '<img src="{{epcQr name=company.name iban=company.iban amount=invoice.total reference=invoice.number}}" alt="QR code to pay invoice {{invoice.number}}">'
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 SERVER_INFO = { name: "formfeed", version: "0.3.16" };
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: verdict ? `${message2}
82760
- ${JSON.stringify({ accessibility: verdict }, null, 2)}` : message2
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 API key belongs to, 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.",
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 client.templates.all(args2);
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 client.templates.versions.get(template, "published").catch(async (e) => {
84088
+ const version = await api().templates.versions.get(template, "published").catch(async (e) => {
82821
84089
  if (e.status === 404)
82822
- return client.templates.versions.get(template, "latest");
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) => client.templates.validate(template, {
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 client.renders.create({
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 client.renders.waitFor(render.id);
82968
- return text3(renderSummary(render));
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 client.pdf.convert(render_id, convert);
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 client.pdf.convert(
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 client.renders.waitFor(render.id);
83076
- return text3(renderSummary(render));
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 text3(renderSummary(await client.renders.get(id)));
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 client.account.get();
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; needs FORMFEED_API_KEY\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"
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("FORMFEED_API_KEY is required for the stdio server\n");
83385
- process.exitCode = 3;
83386
- } else {
83387
- startStdio({ apiKey, baseUrl, region }).catch((e) => {
83388
- process.stderr.write(`${e instanceof Error ? e.message : String(e)}
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
- process.exitCode = 1;
83391
- });
83392
- }
84853
+ process.exitCode = 1;
84854
+ });
83393
84855
  }
83394
84856
  /*! Bundled license information:
83395
84857