@getpeppr/cli 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -40
- package/dist/index.js +3148 -147
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -7,6 +7,14 @@ import { Command } from "commander";
|
|
|
7
7
|
// src/utils/file.ts
|
|
8
8
|
import { readFileSync, existsSync } from "fs";
|
|
9
9
|
import { resolve } from "path";
|
|
10
|
+
|
|
11
|
+
// src/utils/errors.ts
|
|
12
|
+
function exitWithError(message, code = 2) {
|
|
13
|
+
process.stderr.write(message + "\n");
|
|
14
|
+
process.exit(code);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// src/utils/file.ts
|
|
10
18
|
function readJsonFile(filePath) {
|
|
11
19
|
const resolved = resolve(filePath);
|
|
12
20
|
if (!existsSync(resolved)) {
|
|
@@ -18,104 +26,2619 @@ function readJsonFile(filePath) {
|
|
|
18
26
|
} catch {
|
|
19
27
|
return { ok: false, error: `Error: could not read file \u2014 ${resolved}` };
|
|
20
28
|
}
|
|
21
|
-
try {
|
|
22
|
-
const data = JSON.parse(content);
|
|
23
|
-
return { ok: true, data };
|
|
24
|
-
} catch {
|
|
25
|
-
return { ok: false, error: `Error: invalid JSON in file \u2014 ${resolved}` };
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(content);
|
|
31
|
+
return { ok: true, data };
|
|
32
|
+
} catch {
|
|
33
|
+
return { ok: false, error: `Error: invalid JSON in file \u2014 ${resolved}` };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function readAndValidateInvoiceJson(filePath) {
|
|
37
|
+
const parseResult = readJsonFile(filePath);
|
|
38
|
+
if (!parseResult.ok) {
|
|
39
|
+
exitWithError(parseResult.error);
|
|
40
|
+
}
|
|
41
|
+
if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
|
|
42
|
+
exitWithError(
|
|
43
|
+
"Error: JSON file must contain an object, not an array or primitive"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return parseResult.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/formatters/validation.ts
|
|
50
|
+
import pc from "picocolors";
|
|
51
|
+
function sectionHeader(title) {
|
|
52
|
+
const pad = 45 - title.length - 4;
|
|
53
|
+
return pc.dim(`\u2500\u2500 ${title} ${"\u2500".repeat(Math.max(pad, 3))}`);
|
|
54
|
+
}
|
|
55
|
+
function formatError(item) {
|
|
56
|
+
const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
|
|
57
|
+
const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
|
|
58
|
+
return ` ${pc.red("\u2717")} ${field}${item.message}${ruleId}`;
|
|
59
|
+
}
|
|
60
|
+
function formatWarning(item) {
|
|
61
|
+
const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
|
|
62
|
+
const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
|
|
63
|
+
return ` ${pc.yellow("\u26A0")} ${field}${item.message}${ruleId}`;
|
|
64
|
+
}
|
|
65
|
+
function formatSection(title, errors, warnings) {
|
|
66
|
+
const lines = [sectionHeader(title)];
|
|
67
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
68
|
+
lines.push(` ${pc.green("\u2713")} All rules passed`);
|
|
69
|
+
return lines.join("\n");
|
|
70
|
+
}
|
|
71
|
+
if (errors.length === 0) {
|
|
72
|
+
lines.push(` ${pc.green("\u2713")} No errors`);
|
|
73
|
+
}
|
|
74
|
+
for (const err of errors) {
|
|
75
|
+
lines.push(formatError(err));
|
|
76
|
+
}
|
|
77
|
+
for (const warn2 of warnings) {
|
|
78
|
+
lines.push(formatWarning(warn2));
|
|
79
|
+
}
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
function formatValidationResult(filename, result) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(`
|
|
85
|
+
Validating: ${pc.bold(filename)}
|
|
86
|
+
`);
|
|
87
|
+
lines.push(
|
|
88
|
+
formatSection(
|
|
89
|
+
"Structure",
|
|
90
|
+
result.structure.errors,
|
|
91
|
+
result.structure.warnings
|
|
92
|
+
)
|
|
93
|
+
);
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(
|
|
96
|
+
formatSection(
|
|
97
|
+
"Business Rules (Peppol BIS 3.0)",
|
|
98
|
+
result.schematron.errors,
|
|
99
|
+
result.schematron.warnings
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(
|
|
104
|
+
formatSection(
|
|
105
|
+
"Country Rules",
|
|
106
|
+
result.countryRules.errors,
|
|
107
|
+
result.countryRules.warnings
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push(sectionHeader("Summary"));
|
|
112
|
+
const { totalErrors, totalWarnings, valid } = result;
|
|
113
|
+
if (valid && totalWarnings === 0) {
|
|
114
|
+
lines.push(` ${pc.green(pc.bold("\u2713 Invoice is valid"))}`);
|
|
115
|
+
} else if (valid) {
|
|
116
|
+
lines.push(
|
|
117
|
+
` ${pc.green(pc.bold("\u2713 Invoice is valid"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
const parts = [];
|
|
121
|
+
parts.push(`${totalErrors} error${totalErrors === 1 ? "" : "s"}`);
|
|
122
|
+
if (totalWarnings > 0) {
|
|
123
|
+
parts.push(`${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`);
|
|
124
|
+
}
|
|
125
|
+
lines.push(
|
|
126
|
+
` ${pc.red(pc.bold(`\u2717 ${parts.join(", ")}`))} \u2014 invoice non-compliant`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
lines.push("");
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ../sdk/dist/core/ubl-builder.js
|
|
134
|
+
var UBL_NS = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
|
|
135
|
+
var CAC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
|
|
136
|
+
var CBC_NS = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
|
|
137
|
+
var CREDIT_NOTE_NS = "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2";
|
|
138
|
+
var PEPPOL_CUSTOMIZATION_ID = "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0";
|
|
139
|
+
var PEPPOL_PROFILE_ID = "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0";
|
|
140
|
+
var DEFAULT_UNIT = "EA";
|
|
141
|
+
var DEFAULT_PAYMENT_MEANS = 30;
|
|
142
|
+
var UNIT_CODE_MAP = {
|
|
143
|
+
each: "EA",
|
|
144
|
+
piece: "EA",
|
|
145
|
+
pieces: "EA",
|
|
146
|
+
hour: "HUR",
|
|
147
|
+
hours: "HUR",
|
|
148
|
+
day: "DAY",
|
|
149
|
+
days: "DAY",
|
|
150
|
+
week: "WEE",
|
|
151
|
+
weeks: "WEE",
|
|
152
|
+
month: "MON",
|
|
153
|
+
months: "MON",
|
|
154
|
+
year: "ANN",
|
|
155
|
+
years: "ANN",
|
|
156
|
+
kilogram: "KGM",
|
|
157
|
+
kg: "KGM",
|
|
158
|
+
meter: "MTR",
|
|
159
|
+
metre: "MTR",
|
|
160
|
+
liter: "LTR",
|
|
161
|
+
litre: "LTR",
|
|
162
|
+
unit: "C62",
|
|
163
|
+
units: "C62",
|
|
164
|
+
set: "SET",
|
|
165
|
+
sets: "SET",
|
|
166
|
+
pack: "PK",
|
|
167
|
+
packs: "PK"
|
|
168
|
+
};
|
|
169
|
+
function resolveUnitCode(unit) {
|
|
170
|
+
return UNIT_CODE_MAP[unit.toLowerCase()] ?? unit;
|
|
171
|
+
}
|
|
172
|
+
function escapeXml(str) {
|
|
173
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
174
|
+
}
|
|
175
|
+
function formatDate(dateStr) {
|
|
176
|
+
if (!dateStr) {
|
|
177
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
178
|
+
}
|
|
179
|
+
return dateStr.split("T")[0];
|
|
180
|
+
}
|
|
181
|
+
function formatAmount(amount) {
|
|
182
|
+
return amount.toFixed(2);
|
|
183
|
+
}
|
|
184
|
+
function round2(n) {
|
|
185
|
+
return Math.round(n * 100) / 100;
|
|
186
|
+
}
|
|
187
|
+
function parsePeppolId(peppolId) {
|
|
188
|
+
const scheme = peppolId.split(":")[0];
|
|
189
|
+
const id = peppolId.split(":").slice(1).join(":");
|
|
190
|
+
return { scheme, id };
|
|
191
|
+
}
|
|
192
|
+
function buildPartyXml(party, role) {
|
|
193
|
+
const { scheme: endpointScheme, id: endpointId } = parsePeppolId(party.peppolId);
|
|
194
|
+
return `
|
|
195
|
+
<cac:${role}>
|
|
196
|
+
<cac:Party>
|
|
197
|
+
<cbc:EndpointID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:EndpointID>
|
|
198
|
+
<cac:PartyIdentification>
|
|
199
|
+
<cbc:ID schemeID="${escapeXml(endpointScheme)}">${escapeXml(endpointId)}</cbc:ID>
|
|
200
|
+
</cac:PartyIdentification>
|
|
201
|
+
<cac:PartyName>
|
|
202
|
+
<cbc:Name>${escapeXml(party.name)}</cbc:Name>
|
|
203
|
+
</cac:PartyName>
|
|
204
|
+
<cac:PostalAddress>
|
|
205
|
+
${party.street ? `<cbc:StreetName>${escapeXml(party.street)}</cbc:StreetName>` : ""}
|
|
206
|
+
${party.city ? `<cbc:CityName>${escapeXml(party.city)}</cbc:CityName>` : ""}
|
|
207
|
+
${party.postalCode ? `<cbc:PostalZone>${escapeXml(party.postalCode)}</cbc:PostalZone>` : ""}
|
|
208
|
+
<cac:Country>
|
|
209
|
+
<cbc:IdentificationCode>${escapeXml(party.country)}</cbc:IdentificationCode>
|
|
210
|
+
</cac:Country>
|
|
211
|
+
</cac:PostalAddress>
|
|
212
|
+
${party.vatNumber ? `<cac:PartyTaxScheme>
|
|
213
|
+
<cbc:CompanyID>${escapeXml(party.vatNumber)}</cbc:CompanyID>
|
|
214
|
+
<cac:TaxScheme>
|
|
215
|
+
<cbc:ID>VAT</cbc:ID>
|
|
216
|
+
</cac:TaxScheme>
|
|
217
|
+
</cac:PartyTaxScheme>` : ""}
|
|
218
|
+
<cac:PartyLegalEntity>
|
|
219
|
+
<cbc:RegistrationName>${escapeXml(party.name)}</cbc:RegistrationName>
|
|
220
|
+
${party.companyId ? `<cbc:CompanyID>${escapeXml(party.companyId)}</cbc:CompanyID>` : ""}
|
|
221
|
+
</cac:PartyLegalEntity>
|
|
222
|
+
${party.contactName || party.phone || party.email ? `<cac:Contact>
|
|
223
|
+
${party.contactName ? `<cbc:Name>${escapeXml(party.contactName)}</cbc:Name>` : ""}
|
|
224
|
+
${party.phone ? `<cbc:Telephone>${escapeXml(party.phone)}</cbc:Telephone>` : ""}
|
|
225
|
+
${party.email ? `<cbc:ElectronicMail>${escapeXml(party.email)}</cbc:ElectronicMail>` : ""}
|
|
226
|
+
</cac:Contact>` : ""}
|
|
227
|
+
</cac:Party>
|
|
228
|
+
</cac:${role}>`;
|
|
229
|
+
}
|
|
230
|
+
function buildPayeePartyXml(party) {
|
|
231
|
+
const { scheme, id } = parsePeppolId(party.peppolId);
|
|
232
|
+
return `
|
|
233
|
+
<cac:PayeeParty>
|
|
234
|
+
<cac:PartyIdentification>
|
|
235
|
+
<cbc:ID schemeID="${escapeXml(scheme)}">${escapeXml(id)}</cbc:ID>
|
|
236
|
+
</cac:PartyIdentification>
|
|
237
|
+
<cac:PartyName>
|
|
238
|
+
<cbc:Name>${escapeXml(party.name)}</cbc:Name>
|
|
239
|
+
</cac:PartyName>
|
|
240
|
+
${party.companyId ? `<cac:PartyLegalEntity>
|
|
241
|
+
<cbc:RegistrationName>${escapeXml(party.name)}</cbc:RegistrationName>
|
|
242
|
+
<cbc:CompanyID>${escapeXml(party.companyId)}</cbc:CompanyID>
|
|
243
|
+
</cac:PartyLegalEntity>` : ""}
|
|
244
|
+
</cac:PayeeParty>`;
|
|
245
|
+
}
|
|
246
|
+
function buildTaxRepresentativePartyXml(party) {
|
|
247
|
+
const parts = [
|
|
248
|
+
" <cac:TaxRepresentativeParty>",
|
|
249
|
+
" <cac:PartyName>",
|
|
250
|
+
` <cbc:Name>${escapeXml(party.name)}</cbc:Name>`,
|
|
251
|
+
" </cac:PartyName>"
|
|
252
|
+
];
|
|
253
|
+
parts.push(" <cac:PostalAddress>");
|
|
254
|
+
if (party.street) {
|
|
255
|
+
parts.push(` <cbc:StreetName>${escapeXml(party.street)}</cbc:StreetName>`);
|
|
256
|
+
}
|
|
257
|
+
if (party.city) {
|
|
258
|
+
parts.push(` <cbc:CityName>${escapeXml(party.city)}</cbc:CityName>`);
|
|
259
|
+
}
|
|
260
|
+
if (party.postalCode) {
|
|
261
|
+
parts.push(` <cbc:PostalZone>${escapeXml(party.postalCode)}</cbc:PostalZone>`);
|
|
262
|
+
}
|
|
263
|
+
parts.push(" <cac:Country>");
|
|
264
|
+
parts.push(` <cbc:IdentificationCode>${escapeXml(party.country)}</cbc:IdentificationCode>`);
|
|
265
|
+
parts.push(" </cac:Country>");
|
|
266
|
+
parts.push(" </cac:PostalAddress>");
|
|
267
|
+
if (party.vatNumber) {
|
|
268
|
+
parts.push(" <cac:PartyTaxScheme>");
|
|
269
|
+
parts.push(` <cbc:CompanyID>${escapeXml(party.vatNumber)}</cbc:CompanyID>`);
|
|
270
|
+
parts.push(" <cac:TaxScheme>");
|
|
271
|
+
parts.push(" <cbc:ID>VAT</cbc:ID>");
|
|
272
|
+
parts.push(" </cac:TaxScheme>");
|
|
273
|
+
parts.push(" </cac:PartyTaxScheme>");
|
|
274
|
+
}
|
|
275
|
+
parts.push(" </cac:TaxRepresentativeParty>");
|
|
276
|
+
return parts.join("\n");
|
|
277
|
+
}
|
|
278
|
+
function buildAttachmentXml(attachment) {
|
|
279
|
+
const parts = [
|
|
280
|
+
"<cac:AdditionalDocumentReference>",
|
|
281
|
+
` <cbc:ID>${escapeXml(attachment.id)}</cbc:ID>`
|
|
282
|
+
];
|
|
283
|
+
if (attachment.description) {
|
|
284
|
+
parts.push(` <cbc:DocumentDescription>${escapeXml(attachment.description)}</cbc:DocumentDescription>`);
|
|
285
|
+
}
|
|
286
|
+
if (attachment.content || attachment.url) {
|
|
287
|
+
parts.push(" <cac:Attachment>");
|
|
288
|
+
if (attachment.content && attachment.mimeType && attachment.filename) {
|
|
289
|
+
parts.push(` <cbc:EmbeddedDocumentBinaryObject mimeCode="${escapeXml(attachment.mimeType)}" filename="${escapeXml(attachment.filename)}">${attachment.content}</cbc:EmbeddedDocumentBinaryObject>`);
|
|
290
|
+
} else if (attachment.url) {
|
|
291
|
+
parts.push(` <cac:ExternalReference>
|
|
292
|
+
<cbc:URI>${escapeXml(attachment.url)}</cbc:URI>
|
|
293
|
+
</cac:ExternalReference>`);
|
|
294
|
+
}
|
|
295
|
+
parts.push(" </cac:Attachment>");
|
|
296
|
+
}
|
|
297
|
+
parts.push("</cac:AdditionalDocumentReference>");
|
|
298
|
+
return parts.join("\n ");
|
|
299
|
+
}
|
|
300
|
+
function buildInvoicePeriodXml(period) {
|
|
301
|
+
const parts = ["<cac:InvoicePeriod>"];
|
|
302
|
+
if (period.startDate) {
|
|
303
|
+
parts.push(` <cbc:StartDate>${formatDate(period.startDate)}</cbc:StartDate>`);
|
|
304
|
+
}
|
|
305
|
+
if (period.endDate) {
|
|
306
|
+
parts.push(` <cbc:EndDate>${formatDate(period.endDate)}</cbc:EndDate>`);
|
|
307
|
+
}
|
|
308
|
+
parts.push("</cac:InvoicePeriod>");
|
|
309
|
+
return parts.join("\n ");
|
|
310
|
+
}
|
|
311
|
+
function buildDeliveryXml(delivery) {
|
|
312
|
+
const parts = ["<cac:Delivery>"];
|
|
313
|
+
if (delivery.date) {
|
|
314
|
+
parts.push(` <cbc:ActualDeliveryDate>${formatDate(delivery.date)}</cbc:ActualDeliveryDate>`);
|
|
315
|
+
}
|
|
316
|
+
if (delivery.locationId || delivery.address) {
|
|
317
|
+
parts.push(" <cac:DeliveryLocation>");
|
|
318
|
+
if (delivery.locationId) {
|
|
319
|
+
parts.push(` <cbc:ID>${escapeXml(delivery.locationId)}</cbc:ID>`);
|
|
320
|
+
}
|
|
321
|
+
if (delivery.address) {
|
|
322
|
+
parts.push(" <cac:Address>");
|
|
323
|
+
if (delivery.address.street) {
|
|
324
|
+
parts.push(` <cbc:StreetName>${escapeXml(delivery.address.street)}</cbc:StreetName>`);
|
|
325
|
+
}
|
|
326
|
+
if (delivery.address.city) {
|
|
327
|
+
parts.push(` <cbc:CityName>${escapeXml(delivery.address.city)}</cbc:CityName>`);
|
|
328
|
+
}
|
|
329
|
+
if (delivery.address.postalCode) {
|
|
330
|
+
parts.push(` <cbc:PostalZone>${escapeXml(delivery.address.postalCode)}</cbc:PostalZone>`);
|
|
331
|
+
}
|
|
332
|
+
parts.push(` <cac:Country>
|
|
333
|
+
<cbc:IdentificationCode>${escapeXml(delivery.address.country)}</cbc:IdentificationCode>
|
|
334
|
+
</cac:Country>`);
|
|
335
|
+
parts.push(" </cac:Address>");
|
|
336
|
+
}
|
|
337
|
+
parts.push(" </cac:DeliveryLocation>");
|
|
338
|
+
}
|
|
339
|
+
parts.push("</cac:Delivery>");
|
|
340
|
+
return parts.join("\n ");
|
|
341
|
+
}
|
|
342
|
+
function buildDocumentAllowanceChargeXml(item, isCharge, currency) {
|
|
343
|
+
const vatCategory = item.vatCategory ?? "S";
|
|
344
|
+
return `
|
|
345
|
+
<cac:AllowanceCharge>
|
|
346
|
+
<cbc:ChargeIndicator>${isCharge}</cbc:ChargeIndicator>
|
|
347
|
+
<cbc:AllowanceChargeReason>${escapeXml(item.reason)}</cbc:AllowanceChargeReason>
|
|
348
|
+
<cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(item.amount)}</cbc:Amount>
|
|
349
|
+
<cac:TaxCategory>
|
|
350
|
+
<cbc:ID>${vatCategory}</cbc:ID>
|
|
351
|
+
<cbc:Percent>${item.vatRate}</cbc:Percent>
|
|
352
|
+
<cac:TaxScheme>
|
|
353
|
+
<cbc:ID>VAT</cbc:ID>
|
|
354
|
+
</cac:TaxScheme>
|
|
355
|
+
</cac:TaxCategory>
|
|
356
|
+
</cac:AllowanceCharge>`;
|
|
357
|
+
}
|
|
358
|
+
function buildLineAllowanceChargeXml(reason, amount, isCharge, currency) {
|
|
359
|
+
return `
|
|
360
|
+
<cac:AllowanceCharge>
|
|
361
|
+
<cbc:ChargeIndicator>${isCharge}</cbc:ChargeIndicator>
|
|
362
|
+
<cbc:AllowanceChargeReason>${escapeXml(reason)}</cbc:AllowanceChargeReason>
|
|
363
|
+
<cbc:Amount currencyID="${escapeXml(currency)}">${formatAmount(amount)}</cbc:Amount>
|
|
364
|
+
</cac:AllowanceCharge>`;
|
|
365
|
+
}
|
|
366
|
+
function calculateLineExtensionAmount(line) {
|
|
367
|
+
const base = line.quantity * line.unitPrice;
|
|
368
|
+
const lineAllowances = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
|
|
369
|
+
const lineCharges = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
|
|
370
|
+
return base - lineAllowances + lineCharges;
|
|
371
|
+
}
|
|
372
|
+
function buildDocumentLineXml(line, index, currency, lineTag, qtyTag) {
|
|
373
|
+
const lineTotal = calculateLineExtensionAmount(line);
|
|
374
|
+
const unit = resolveUnitCode(line.unit ?? DEFAULT_UNIT);
|
|
375
|
+
const vatCategory = line.vatCategory ?? "S";
|
|
376
|
+
const lineAllowancesXml = (line.allowances ?? []).map((a) => buildLineAllowanceChargeXml(a.reason, a.amount, false, currency)).join("");
|
|
377
|
+
const lineChargesXml = (line.charges ?? []).map((c) => buildLineAllowanceChargeXml(c.reason, c.amount, true, currency)).join("");
|
|
378
|
+
return `
|
|
379
|
+
<cac:${lineTag}>
|
|
380
|
+
<cbc:ID>${index + 1}</cbc:ID>
|
|
381
|
+
${line.accountingCost ? `<cbc:AccountingCost>${escapeXml(line.accountingCost)}</cbc:AccountingCost>` : ""}
|
|
382
|
+
<cbc:${qtyTag} unitCode="${escapeXml(unit)}">${Number(line.quantity.toFixed(6))}</cbc:${qtyTag}>
|
|
383
|
+
<cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(lineTotal)}</cbc:LineExtensionAmount>
|
|
384
|
+
${lineAllowancesXml}${lineChargesXml}
|
|
385
|
+
<cac:Item>
|
|
386
|
+
<cbc:Name>${escapeXml(line.description)}</cbc:Name>
|
|
387
|
+
${line.itemId ? `<cac:SellersItemIdentification>
|
|
388
|
+
<cbc:ID>${escapeXml(line.itemId)}</cbc:ID>
|
|
389
|
+
</cac:SellersItemIdentification>` : ""}
|
|
390
|
+
<cac:ClassifiedTaxCategory>
|
|
391
|
+
<cbc:ID>${vatCategory}</cbc:ID>
|
|
392
|
+
<cbc:Percent>${line.vatRate}</cbc:Percent>
|
|
393
|
+
<cac:TaxScheme>
|
|
394
|
+
<cbc:ID>VAT</cbc:ID>
|
|
395
|
+
</cac:TaxScheme>
|
|
396
|
+
</cac:ClassifiedTaxCategory>
|
|
397
|
+
${line.standardItemId ? `<cac:StandardItemIdentification>
|
|
398
|
+
<cbc:ID schemeID="${escapeXml(line.standardItemScheme ?? "0160")}">${escapeXml(line.standardItemId)}</cbc:ID>
|
|
399
|
+
</cac:StandardItemIdentification>` : ""}
|
|
400
|
+
${line.commodityCode && line.commodityScheme ? `<cac:CommodityClassification>
|
|
401
|
+
<cbc:ItemClassificationCode listID="${escapeXml(line.commodityScheme)}">${escapeXml(line.commodityCode)}</cbc:ItemClassificationCode>
|
|
402
|
+
</cac:CommodityClassification>` : ""}
|
|
403
|
+
${(line.properties ?? []).map((p) => `<cac:AdditionalItemProperty>
|
|
404
|
+
<cbc:Name>${escapeXml(p.name)}</cbc:Name>
|
|
405
|
+
<cbc:Value>${escapeXml(p.value)}</cbc:Value>
|
|
406
|
+
</cac:AdditionalItemProperty>`).join("\n ")}
|
|
407
|
+
</cac:Item>
|
|
408
|
+
<cac:Price>
|
|
409
|
+
<cbc:PriceAmount currencyID="${escapeXml(currency)}">${formatAmount(line.unitPrice)}</cbc:PriceAmount>
|
|
410
|
+
${line.baseQuantity != null ? `<cbc:BaseQuantity unitCode="${escapeXml(resolveUnitCode(line.baseQuantityUnit ?? line.unit ?? DEFAULT_UNIT))}">${line.baseQuantity}</cbc:BaseQuantity>` : ""}
|
|
411
|
+
</cac:Price>
|
|
412
|
+
</cac:${lineTag}>`;
|
|
413
|
+
}
|
|
414
|
+
function buildInvoiceLineXml(line, index, currency) {
|
|
415
|
+
return buildDocumentLineXml(line, index, currency, "InvoiceLine", "InvoicedQuantity");
|
|
416
|
+
}
|
|
417
|
+
function calculateTaxSubtotals(lines, allowances, charges) {
|
|
418
|
+
const groups = /* @__PURE__ */ new Map();
|
|
419
|
+
function addToGroup(vatCategory, vatRate, amount) {
|
|
420
|
+
const key = `${vatCategory}-${vatRate}`;
|
|
421
|
+
const lineTax = round2(amount * (vatRate / 100));
|
|
422
|
+
const existing = groups.get(key);
|
|
423
|
+
if (existing) {
|
|
424
|
+
existing.taxableAmount = round2(existing.taxableAmount + amount);
|
|
425
|
+
existing.taxAmount = round2(existing.taxAmount + lineTax);
|
|
426
|
+
} else {
|
|
427
|
+
groups.set(key, {
|
|
428
|
+
vatRate,
|
|
429
|
+
vatCategory,
|
|
430
|
+
taxableAmount: amount,
|
|
431
|
+
taxAmount: lineTax
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const line of lines) {
|
|
436
|
+
addToGroup(line.vatCategory ?? "S", line.vatRate, calculateLineExtensionAmount(line));
|
|
437
|
+
}
|
|
438
|
+
for (const a of allowances ?? []) {
|
|
439
|
+
addToGroup(a.vatCategory ?? "S", a.vatRate, -a.amount);
|
|
440
|
+
}
|
|
441
|
+
for (const c of charges ?? []) {
|
|
442
|
+
addToGroup(c.vatCategory ?? "S", c.vatRate, c.amount);
|
|
443
|
+
}
|
|
444
|
+
return Array.from(groups.values());
|
|
445
|
+
}
|
|
446
|
+
function calculateDocumentTotals(lines, allowances, charges) {
|
|
447
|
+
const taxSubtotals = calculateTaxSubtotals(lines, allowances, charges);
|
|
448
|
+
const lineExtensionAmount = lines.reduce((sum, line) => sum + calculateLineExtensionAmount(line), 0);
|
|
449
|
+
const allowanceTotalAmount = (allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
|
|
450
|
+
const chargeTotalAmount = (charges ?? []).reduce((sum, c) => sum + c.amount, 0);
|
|
451
|
+
const taxExclusiveAmount = round2(lineExtensionAmount - allowanceTotalAmount + chargeTotalAmount);
|
|
452
|
+
const totalTax = round2(taxSubtotals.reduce((sum, st) => sum + st.taxAmount, 0));
|
|
453
|
+
const taxInclusiveAmount = round2(taxExclusiveAmount + totalTax);
|
|
454
|
+
return {
|
|
455
|
+
lineExtensionAmount,
|
|
456
|
+
allowanceTotalAmount,
|
|
457
|
+
chargeTotalAmount,
|
|
458
|
+
taxExclusiveAmount,
|
|
459
|
+
totalTax,
|
|
460
|
+
taxInclusiveAmount,
|
|
461
|
+
payableAmount: taxInclusiveAmount,
|
|
462
|
+
taxSubtotals
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function buildTaxTotalXml(taxSubtotals, totalTax, currency) {
|
|
466
|
+
const subtotalsXml = taxSubtotals.map((st) => `
|
|
467
|
+
<cac:TaxSubtotal>
|
|
468
|
+
<cbc:TaxableAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxableAmount)}</cbc:TaxableAmount>
|
|
469
|
+
<cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(st.taxAmount)}</cbc:TaxAmount>
|
|
470
|
+
<cac:TaxCategory>
|
|
471
|
+
<cbc:ID>${st.vatCategory}</cbc:ID>
|
|
472
|
+
<cbc:Percent>${st.vatRate}</cbc:Percent>
|
|
473
|
+
<cac:TaxScheme>
|
|
474
|
+
<cbc:ID>VAT</cbc:ID>
|
|
475
|
+
</cac:TaxScheme>
|
|
476
|
+
</cac:TaxCategory>
|
|
477
|
+
</cac:TaxSubtotal>`).join("");
|
|
478
|
+
return `<cac:TaxTotal>
|
|
479
|
+
<cbc:TaxAmount currencyID="${escapeXml(currency)}">${formatAmount(totalTax)}</cbc:TaxAmount>
|
|
480
|
+
${subtotalsXml}
|
|
481
|
+
</cac:TaxTotal>`;
|
|
482
|
+
}
|
|
483
|
+
function buildTaxCurrencyTotalXml(totalTax, taxCurrency, rate) {
|
|
484
|
+
const convertedAmount = Math.round(totalTax * rate * 100) / 100;
|
|
485
|
+
return `<cac:TaxTotal>
|
|
486
|
+
<cbc:TaxAmount currencyID="${escapeXml(taxCurrency)}">${formatAmount(convertedAmount)}</cbc:TaxAmount>
|
|
487
|
+
</cac:TaxTotal>`;
|
|
488
|
+
}
|
|
489
|
+
function buildLegalMonetaryTotalXml(totals, currency, options) {
|
|
490
|
+
const prepaid = options?.prepaidAmount;
|
|
491
|
+
const rounding = options?.roundingAmount;
|
|
492
|
+
const payableAmount = totals.taxInclusiveAmount - (prepaid ?? 0) + (rounding ?? 0);
|
|
493
|
+
return `<cac:LegalMonetaryTotal>
|
|
494
|
+
<cbc:LineExtensionAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.lineExtensionAmount)}</cbc:LineExtensionAmount>
|
|
495
|
+
<cbc:TaxExclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxExclusiveAmount)}</cbc:TaxExclusiveAmount>
|
|
496
|
+
<cbc:TaxInclusiveAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.taxInclusiveAmount)}</cbc:TaxInclusiveAmount>
|
|
497
|
+
${totals.allowanceTotalAmount > 0 ? `<cbc:AllowanceTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.allowanceTotalAmount)}</cbc:AllowanceTotalAmount>` : ""}
|
|
498
|
+
${totals.chargeTotalAmount > 0 ? `<cbc:ChargeTotalAmount currencyID="${escapeXml(currency)}">${formatAmount(totals.chargeTotalAmount)}</cbc:ChargeTotalAmount>` : ""}
|
|
499
|
+
${prepaid != null ? `<cbc:PrepaidAmount currencyID="${escapeXml(currency)}">${formatAmount(prepaid)}</cbc:PrepaidAmount>` : ""}
|
|
500
|
+
${rounding != null ? `<cbc:PayableRoundingAmount currencyID="${escapeXml(currency)}">${formatAmount(rounding)}</cbc:PayableRoundingAmount>` : ""}
|
|
501
|
+
<cbc:PayableAmount currencyID="${escapeXml(currency)}">${formatAmount(payableAmount)}</cbc:PayableAmount>
|
|
502
|
+
</cac:LegalMonetaryTotal>`;
|
|
503
|
+
}
|
|
504
|
+
function buildPaymentMeansXml(input) {
|
|
505
|
+
const paymentMeans = input.paymentMeans ?? DEFAULT_PAYMENT_MEANS;
|
|
506
|
+
return `<cac:PaymentMeans>
|
|
507
|
+
<cbc:PaymentMeansCode>${paymentMeans}</cbc:PaymentMeansCode>
|
|
508
|
+
${input.paymentReference ? `<cbc:PaymentID>${escapeXml(input.paymentReference)}</cbc:PaymentID>` : ""}
|
|
509
|
+
${input.paymentIban ? `<cac:PayeeFinancialAccount>
|
|
510
|
+
<cbc:ID>${escapeXml(input.paymentIban)}</cbc:ID>
|
|
511
|
+
${input.paymentBic ? `<cac:FinancialInstitutionBranch>
|
|
512
|
+
<cbc:ID>${escapeXml(input.paymentBic)}</cbc:ID>
|
|
513
|
+
</cac:FinancialInstitutionBranch>` : ""}
|
|
514
|
+
</cac:PayeeFinancialAccount>` : ""}
|
|
515
|
+
</cac:PaymentMeans>`;
|
|
516
|
+
}
|
|
517
|
+
function buildCreditNoteLineXml(line, index, currency) {
|
|
518
|
+
return buildDocumentLineXml(line, index, currency, "CreditNoteLine", "CreditedQuantity");
|
|
519
|
+
}
|
|
520
|
+
function buildOrderReferenceXml(orderReference, salesOrderReference) {
|
|
521
|
+
if (!orderReference && !salesOrderReference)
|
|
522
|
+
return "";
|
|
523
|
+
const parts = ["<cac:OrderReference>"];
|
|
524
|
+
if (orderReference) {
|
|
525
|
+
parts.push(`<cbc:ID>${escapeXml(orderReference)}</cbc:ID>`);
|
|
526
|
+
}
|
|
527
|
+
if (salesOrderReference) {
|
|
528
|
+
parts.push(`<cbc:SalesOrderID>${escapeXml(salesOrderReference)}</cbc:SalesOrderID>`);
|
|
529
|
+
}
|
|
530
|
+
parts.push("</cac:OrderReference>");
|
|
531
|
+
return parts.join("");
|
|
532
|
+
}
|
|
533
|
+
function buildInvoiceXml(input) {
|
|
534
|
+
const currency = input.currency ?? "EUR";
|
|
535
|
+
const date = formatDate(input.date);
|
|
536
|
+
const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
|
|
537
|
+
const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
|
|
538
|
+
const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
|
|
539
|
+
const linesXml = input.lines.map((line, i) => buildInvoiceLineXml(line, i, currency)).join("");
|
|
540
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
541
|
+
<Invoice xmlns="${UBL_NS}"
|
|
542
|
+
xmlns:cac="${CAC_NS}"
|
|
543
|
+
xmlns:cbc="${CBC_NS}">
|
|
544
|
+
<cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
|
|
545
|
+
<cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
|
|
546
|
+
<cbc:ID>${escapeXml(input.number)}</cbc:ID>
|
|
547
|
+
<cbc:IssueDate>${date}</cbc:IssueDate>
|
|
548
|
+
${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
|
|
549
|
+
${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
|
|
550
|
+
<cbc:InvoiceTypeCode>${input.invoiceTypeCode ?? (input.isCreditNote ? 381 : 380)}</cbc:InvoiceTypeCode>
|
|
551
|
+
${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
|
|
552
|
+
${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
|
|
553
|
+
<cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
|
|
554
|
+
${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
|
|
555
|
+
${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
|
|
556
|
+
${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
|
|
557
|
+
${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
|
|
558
|
+
${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
|
|
559
|
+
${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
|
|
560
|
+
${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
|
|
561
|
+
${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
|
|
562
|
+
${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
|
|
563
|
+
${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
|
|
564
|
+
${buildPartyXml(input.to, "AccountingCustomerParty")}
|
|
565
|
+
${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
|
|
566
|
+
${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
|
|
567
|
+
${input.delivery ? buildDeliveryXml(input.delivery) : ""}
|
|
568
|
+
${buildPaymentMeansXml(input)}
|
|
569
|
+
${input.paymentTerms ? `<cac:PaymentTerms>
|
|
570
|
+
<cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
|
|
571
|
+
</cac:PaymentTerms>` : ""}
|
|
572
|
+
${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
|
|
573
|
+
${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
|
|
574
|
+
${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
|
|
575
|
+
${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
|
|
576
|
+
${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
|
|
577
|
+
${linesXml}
|
|
578
|
+
</Invoice>`;
|
|
579
|
+
}
|
|
580
|
+
function buildCreditNoteXml(input) {
|
|
581
|
+
const currency = input.currency ?? "EUR";
|
|
582
|
+
const date = formatDate(input.date);
|
|
583
|
+
const dueDate = input.dueDate ? formatDate(input.dueDate) : void 0;
|
|
584
|
+
const hasTaxCurrency = input.taxCurrency && input.taxCurrency !== currency;
|
|
585
|
+
const totals = calculateDocumentTotals(input.lines, input.allowances, input.charges);
|
|
586
|
+
const linesXml = input.lines.map((line, i) => buildCreditNoteLineXml(line, i, currency)).join("");
|
|
587
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
588
|
+
<CreditNote xmlns="${CREDIT_NOTE_NS}"
|
|
589
|
+
xmlns:cac="${CAC_NS}"
|
|
590
|
+
xmlns:cbc="${CBC_NS}">
|
|
591
|
+
<cbc:CustomizationID>${PEPPOL_CUSTOMIZATION_ID}</cbc:CustomizationID>
|
|
592
|
+
<cbc:ProfileID>${PEPPOL_PROFILE_ID}</cbc:ProfileID>
|
|
593
|
+
<cbc:ID>${escapeXml(input.number)}</cbc:ID>
|
|
594
|
+
<cbc:IssueDate>${date}</cbc:IssueDate>
|
|
595
|
+
${dueDate ? `<cbc:DueDate>${dueDate}</cbc:DueDate>` : ""}
|
|
596
|
+
${input.taxPointDate ? `<cbc:TaxPointDate>${formatDate(input.taxPointDate)}</cbc:TaxPointDate>` : ""}
|
|
597
|
+
<cbc:CreditNoteTypeCode>${input.invoiceTypeCode ?? 381}</cbc:CreditNoteTypeCode>
|
|
598
|
+
${input.note ? `<cbc:Note>${escapeXml(input.note)}</cbc:Note>` : ""}
|
|
599
|
+
${input.accountingCost ? `<cbc:AccountingCost>${escapeXml(input.accountingCost)}</cbc:AccountingCost>` : ""}
|
|
600
|
+
<cbc:DocumentCurrencyCode>${escapeXml(currency)}</cbc:DocumentCurrencyCode>
|
|
601
|
+
${hasTaxCurrency ? `<cbc:TaxCurrencyCode>${escapeXml(input.taxCurrency)}</cbc:TaxCurrencyCode>` : ""}
|
|
602
|
+
${input.buyerReference ? `<cbc:BuyerReference>${escapeXml(input.buyerReference)}</cbc:BuyerReference>` : ""}
|
|
603
|
+
${input.invoicePeriod ? buildInvoicePeriodXml(input.invoicePeriod) : ""}
|
|
604
|
+
${buildOrderReferenceXml(input.orderReference, input.salesOrderReference)}
|
|
605
|
+
<cac:BillingReference><cac:InvoiceDocumentReference><cbc:ID>${escapeXml(input.invoiceReference)}</cbc:ID></cac:InvoiceDocumentReference></cac:BillingReference>
|
|
606
|
+
${input.despatchReference ? `<cac:DespatchDocumentReference><cbc:ID>${escapeXml(input.despatchReference)}</cbc:ID></cac:DespatchDocumentReference>` : ""}
|
|
607
|
+
${input.receiptReference ? `<cac:ReceiptDocumentReference><cbc:ID>${escapeXml(input.receiptReference)}</cbc:ID></cac:ReceiptDocumentReference>` : ""}
|
|
608
|
+
${input.contractReference ? `<cac:ContractDocumentReference><cbc:ID>${escapeXml(input.contractReference)}</cbc:ID></cac:ContractDocumentReference>` : ""}
|
|
609
|
+
${(input.attachments ?? []).map((a) => buildAttachmentXml(a)).join("\n ")}
|
|
610
|
+
${input.projectReference ? `<cac:ProjectReference><cbc:ID>${escapeXml(input.projectReference)}</cbc:ID></cac:ProjectReference>` : ""}
|
|
611
|
+
${input.from ? buildPartyXml(input.from, "AccountingSupplierParty") : ""}
|
|
612
|
+
${buildPartyXml(input.to, "AccountingCustomerParty")}
|
|
613
|
+
${input.payeeParty ? buildPayeePartyXml(input.payeeParty) : ""}
|
|
614
|
+
${input.taxRepresentative ? buildTaxRepresentativePartyXml(input.taxRepresentative) : ""}
|
|
615
|
+
${input.delivery ? buildDeliveryXml(input.delivery) : ""}
|
|
616
|
+
${buildPaymentMeansXml(input)}
|
|
617
|
+
${input.paymentTerms ? `<cac:PaymentTerms>
|
|
618
|
+
<cbc:Note>${escapeXml(input.paymentTerms)}</cbc:Note>
|
|
619
|
+
</cac:PaymentTerms>` : ""}
|
|
620
|
+
${(input.allowances ?? []).map((a) => buildDocumentAllowanceChargeXml(a, false, currency)).join("")}
|
|
621
|
+
${(input.charges ?? []).map((c) => buildDocumentAllowanceChargeXml(c, true, currency)).join("")}
|
|
622
|
+
${hasTaxCurrency && input.taxCurrencyRate ? buildTaxCurrencyTotalXml(totals.totalTax, input.taxCurrency, input.taxCurrencyRate) : ""}
|
|
623
|
+
${buildTaxTotalXml(totals.taxSubtotals, totals.totalTax, currency)}
|
|
624
|
+
${buildLegalMonetaryTotalXml(totals, currency, { prepaidAmount: input.prepaidAmount, roundingAmount: input.roundingAmount })}
|
|
625
|
+
${linesXml}
|
|
626
|
+
</CreditNote>`;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ../sdk/dist/core/country-rules.js
|
|
630
|
+
function warn(field, message, ruleId) {
|
|
631
|
+
return { field, message, ruleId };
|
|
632
|
+
}
|
|
633
|
+
var BE_STRUCTURED_RE = /^\+{3}\d{3}\/\d{4}\/\d{5}\+{3}$/;
|
|
634
|
+
function validateBelgianCheckDigit(reference) {
|
|
635
|
+
const digits = reference.replace(/[^0-9]/g, "");
|
|
636
|
+
if (digits.length !== 12)
|
|
637
|
+
return false;
|
|
638
|
+
const base = parseInt(digits.slice(0, 10), 10);
|
|
639
|
+
const check = parseInt(digits.slice(10, 12), 10);
|
|
640
|
+
const expected = base % 97 === 0 ? 97 : base % 97;
|
|
641
|
+
return check === expected;
|
|
642
|
+
}
|
|
643
|
+
function validateBelgium(input, _errors, warnings) {
|
|
644
|
+
const ref = input.paymentReference;
|
|
645
|
+
if (ref && BE_STRUCTURED_RE.test(ref)) {
|
|
646
|
+
if (!validateBelgianCheckDigit(ref)) {
|
|
647
|
+
warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
|
|
648
|
+
}
|
|
649
|
+
} else if (!ref) {
|
|
650
|
+
warnings.push(warn("paymentReference", "Belgian recipients typically expect a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function validateBelgiumSeller(input, _errors, warnings) {
|
|
654
|
+
const ref = input.paymentReference;
|
|
655
|
+
if (ref && BE_STRUCTURED_RE.test(ref)) {
|
|
656
|
+
if (!validateBelgianCheckDigit(ref)) {
|
|
657
|
+
warnings.push(warn("paymentReference", `Belgian structured communication "${ref}" has an invalid mod-97 checksum. Verify the reference.`, "BE-01"));
|
|
658
|
+
}
|
|
659
|
+
} else if (!ref) {
|
|
660
|
+
warnings.push(warn("paymentReference", "Belgian sellers typically include a structured communication reference (+++NNN/NNNN/NNNNN+++ format).", "BE-02"));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
var FR_SIRET_RE = /^\d{14}$/;
|
|
664
|
+
var FR_VAT_RE = /^FR[A-Z0-9]{2}\d{9}$/;
|
|
665
|
+
function validateFrance(input, _errors, warnings) {
|
|
666
|
+
const { companyId, vatNumber } = input.to ?? {};
|
|
667
|
+
if (companyId && !FR_SIRET_RE.test(companyId)) {
|
|
668
|
+
warnings.push(warn("to.companyId", `French company ID (SIRET) should be exactly 14 digits, got "${companyId}".`, "FR-01"));
|
|
669
|
+
}
|
|
670
|
+
if (vatNumber && !FR_VAT_RE.test(vatNumber)) {
|
|
671
|
+
warnings.push(warn("to.vatNumber", `French VAT number should match format FR + 2 characters + 9 digits (SIREN), got "${vatNumber}".`, "FR-02"));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function validateItaly(input, _errors, warnings) {
|
|
675
|
+
if (!input.buyerReference) {
|
|
676
|
+
warnings.push(warn("buyerReference", "Italian recipients (SDI) typically require a buyer reference (CIG/CUP code). Consider setting buyerReference.", "IT-01"));
|
|
677
|
+
}
|
|
678
|
+
const peppolId = input.to?.peppolId;
|
|
679
|
+
if (peppolId?.startsWith("0201:")) {
|
|
680
|
+
const fiscalCode = peppolId.slice(5);
|
|
681
|
+
if (fiscalCode.length !== 11 && fiscalCode.length !== 16) {
|
|
682
|
+
warnings.push(warn("to.peppolId", `Italian fiscal code (after 0201:) should be 11 digits (partita IVA) or 16 characters (codice fiscale), got ${fiscalCode.length} characters.`, "IT-02"));
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
var NL_KVK_RE = /^\d{8}$/;
|
|
687
|
+
var NL_VAT_RE = /^NL\d{9}B\d{2}$/;
|
|
688
|
+
function validateNetherlands(input, _errors, warnings) {
|
|
689
|
+
const { companyId, vatNumber } = input.to ?? {};
|
|
690
|
+
if (companyId && !NL_KVK_RE.test(companyId)) {
|
|
691
|
+
warnings.push(warn("to.companyId", `Dutch KVK number should be exactly 8 digits, got "${companyId}".`, "NL-01"));
|
|
692
|
+
}
|
|
693
|
+
if (vatNumber && !NL_VAT_RE.test(vatNumber)) {
|
|
694
|
+
warnings.push(warn("to.vatNumber", `Dutch VAT number should match format NL + 9 digits + B + 2 digits, got "${vatNumber}".`, "NL-02"));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
var DE_VAT_RE = /^DE\d{9}$/;
|
|
698
|
+
function validateGermany(input, _errors, warnings) {
|
|
699
|
+
const { vatNumber } = input.to ?? {};
|
|
700
|
+
if (vatNumber && !DE_VAT_RE.test(vatNumber)) {
|
|
701
|
+
warnings.push(warn("to.vatNumber", `German VAT number should match format DE + 9 digits, got "${vatNumber}".`, "DE-01"));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function validateCountryRules(input) {
|
|
705
|
+
const errors = [];
|
|
706
|
+
const warnings = [];
|
|
707
|
+
const buyerCountry = input.to?.country;
|
|
708
|
+
const sellerCountry = input.from?.country;
|
|
709
|
+
if (buyerCountry) {
|
|
710
|
+
switch (buyerCountry) {
|
|
711
|
+
case "BE":
|
|
712
|
+
validateBelgium(input, errors, warnings);
|
|
713
|
+
break;
|
|
714
|
+
case "FR":
|
|
715
|
+
validateFrance(input, errors, warnings);
|
|
716
|
+
break;
|
|
717
|
+
case "IT":
|
|
718
|
+
validateItaly(input, errors, warnings);
|
|
719
|
+
break;
|
|
720
|
+
case "NL":
|
|
721
|
+
validateNetherlands(input, errors, warnings);
|
|
722
|
+
break;
|
|
723
|
+
case "DE":
|
|
724
|
+
validateGermany(input, errors, warnings);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (sellerCountry && sellerCountry !== buyerCountry) {
|
|
729
|
+
switch (sellerCountry) {
|
|
730
|
+
case "BE":
|
|
731
|
+
validateBelgiumSeller(input, errors, warnings);
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return { errors, warnings };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ../sdk/dist/core/validator.js
|
|
739
|
+
function error(field, message, ruleId, suggestion) {
|
|
740
|
+
return { field, message, ruleId, suggestion };
|
|
741
|
+
}
|
|
742
|
+
function warning(field, message, ruleId) {
|
|
743
|
+
return { field, message, ruleId };
|
|
744
|
+
}
|
|
745
|
+
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
746
|
+
function validateParty(party, path) {
|
|
747
|
+
const errors = [];
|
|
748
|
+
if (!party.name?.trim()) {
|
|
749
|
+
errors.push(error(`${path}.name`, "Business name is required", "BR-06"));
|
|
750
|
+
}
|
|
751
|
+
if (!party.peppolId) {
|
|
752
|
+
errors.push(error(`${path}.peppolId`, "Peppol participant ID is required", void 0, 'Format: "scheme:id", e.g. "0208:BE0123456789" for Belgian companies'));
|
|
753
|
+
} else if (!party.peppolId.includes(":")) {
|
|
754
|
+
errors.push(error(`${path}.peppolId`, `Invalid Peppol ID format: "${party.peppolId}"`, void 0, 'Must be "scheme:id" format. Common schemes: 0208 (Belgium), 0009 (France SIRET), 0204 (Germany Leitweg)'));
|
|
755
|
+
}
|
|
756
|
+
if (!party.country) {
|
|
757
|
+
errors.push(error(`${path}.country`, "Country code is required", "BR-11"));
|
|
758
|
+
} else if (party.country.length !== 2) {
|
|
759
|
+
errors.push(error(`${path}.country`, `Invalid country code: "${party.country}"`, void 0, "Must be ISO 3166-1 alpha-2 (e.g., BE, FR, DE, NL)"));
|
|
760
|
+
}
|
|
761
|
+
return errors;
|
|
762
|
+
}
|
|
763
|
+
function validateBuyerAddress(party, path) {
|
|
764
|
+
const errors = [];
|
|
765
|
+
if (!party.street?.trim()) {
|
|
766
|
+
errors.push(error(`${path}.street`, "Street address is required for the buyer", "BR-50", 'e.g. "123 Business Street"'));
|
|
767
|
+
}
|
|
768
|
+
if (!party.city?.trim()) {
|
|
769
|
+
errors.push(error(`${path}.city`, "City is required for the buyer", "BR-51", 'e.g. "Brussels"'));
|
|
770
|
+
}
|
|
771
|
+
if (!party.postalCode?.trim()) {
|
|
772
|
+
errors.push(error(`${path}.postalCode`, "Postal code is required for the buyer", "BR-53", 'e.g. "1000"'));
|
|
773
|
+
}
|
|
774
|
+
return errors;
|
|
775
|
+
}
|
|
776
|
+
function validateLine(line, index, isCreditNote = false) {
|
|
777
|
+
const errors = [];
|
|
778
|
+
const path = `lines[${index}]`;
|
|
779
|
+
if (!line.description?.trim()) {
|
|
780
|
+
errors.push(error(`${path}.description`, "Line item description is required", "BR-25"));
|
|
781
|
+
}
|
|
782
|
+
if (line.quantity === void 0 || line.quantity === null) {
|
|
783
|
+
errors.push(error(`${path}.quantity`, "Quantity is required", "BR-22"));
|
|
784
|
+
} else if (line.quantity <= 0 && !isCreditNote) {
|
|
785
|
+
errors.push(error(`${path}.quantity`, `Quantity must be positive, got ${line.quantity}`, void 0, "For returns/credits, use a credit note instead"));
|
|
786
|
+
}
|
|
787
|
+
if (line.unitPrice === void 0 || line.unitPrice === null) {
|
|
788
|
+
errors.push(error(`${path}.unitPrice`, "Unit price is required", "BR-26"));
|
|
789
|
+
} else if (line.unitPrice < 0) {
|
|
790
|
+
errors.push(error(`${path}.unitPrice`, `Unit price cannot be negative, got ${line.unitPrice}`, void 0, "For discounts, use a negative quantity or a separate discount line"));
|
|
791
|
+
}
|
|
792
|
+
if (line.vatRate === void 0 || line.vatRate === null) {
|
|
793
|
+
errors.push(error(`${path}.vatRate`, "VAT rate is required", "BR-CO-17"));
|
|
794
|
+
} else if (line.vatRate < 0 || line.vatRate > 100) {
|
|
795
|
+
errors.push(error(`${path}.vatRate`, `VAT rate must be between 0 and 100, got ${line.vatRate}`, void 0, "Use 0 for zero-rated, 21 for standard Belgian VAT, etc."));
|
|
796
|
+
}
|
|
797
|
+
return errors;
|
|
798
|
+
}
|
|
799
|
+
function validateInvoice(input) {
|
|
800
|
+
const errors = [];
|
|
801
|
+
const warnings = [];
|
|
802
|
+
if (!input.number?.trim()) {
|
|
803
|
+
errors.push(error("number", "Invoice number is required", "BR-02", "Must be unique per supplier"));
|
|
804
|
+
}
|
|
805
|
+
const VALID_TYPE_CODES = [380, 381, 383, 384, 386, 389, 751];
|
|
806
|
+
if (input.invoiceTypeCode != null && !VALID_TYPE_CODES.includes(input.invoiceTypeCode)) {
|
|
807
|
+
errors.push(error("invoiceTypeCode", `Invalid invoice type code: ${input.invoiceTypeCode}`, void 0, "Valid codes: 380, 381, 383, 384, 386, 389, 751"));
|
|
808
|
+
}
|
|
809
|
+
if (input.isCreditNote && !input.invoiceReference?.trim()) {
|
|
810
|
+
errors.push(error("invoiceReference", "Reference to the original invoice is required for credit notes", void 0, 'Set invoiceReference to the original invoice number (e.g., "INV-001")'));
|
|
811
|
+
}
|
|
812
|
+
if (input.from) {
|
|
813
|
+
warnings.push(warning("from", "Seller info is determined by your API key. The 'from' field is deprecated and ignored."));
|
|
814
|
+
}
|
|
815
|
+
if (!input.to) {
|
|
816
|
+
errors.push(error("to", "Buyer (to) is required", "BR-07"));
|
|
817
|
+
} else {
|
|
818
|
+
errors.push(...validateParty(input.to, "to"));
|
|
819
|
+
errors.push(...validateBuyerAddress(input.to, "to"));
|
|
820
|
+
}
|
|
821
|
+
if (input.payeeParty) {
|
|
822
|
+
if (!input.payeeParty.name?.trim()) {
|
|
823
|
+
errors.push(error("payeeParty.name", "Payee party name is required", "BR-17"));
|
|
824
|
+
}
|
|
825
|
+
if (!input.payeeParty.peppolId) {
|
|
826
|
+
errors.push(error("payeeParty.peppolId", "Payee party Peppol ID is required", void 0, 'Format: "scheme:id", e.g. "0208:BE0123456789"'));
|
|
827
|
+
} else if (!input.payeeParty.peppolId.includes(":")) {
|
|
828
|
+
errors.push(error("payeeParty.peppolId", `Invalid Peppol ID format: "${input.payeeParty.peppolId}"`, void 0, 'Must be "scheme:id" format'));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (!input.lines || input.lines.length === 0) {
|
|
832
|
+
errors.push(error("lines", "At least one line item is required", "BR-16", "Add items to the lines array"));
|
|
833
|
+
} else {
|
|
834
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
835
|
+
errors.push(...validateLine(input.lines[i], i, input.isCreditNote));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
if (input.date) {
|
|
839
|
+
if (!ISO_DATE_RE.test(input.date)) {
|
|
840
|
+
errors.push(error("date", `Invalid date format: "${input.date}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (input.dueDate) {
|
|
844
|
+
if (!ISO_DATE_RE.test(input.dueDate)) {
|
|
845
|
+
errors.push(error("dueDate", `Invalid due date format: "${input.dueDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
if (input.taxPointDate) {
|
|
849
|
+
if (!ISO_DATE_RE.test(input.taxPointDate)) {
|
|
850
|
+
errors.push(error("taxPointDate", `Invalid tax point date format: "${input.taxPointDate}"`, void 0, "Use ISO 8601: YYYY-MM-DD"));
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
if (input.roundingAmount !== void 0 && input.roundingAmount !== null) {
|
|
854
|
+
if (input.roundingAmount < -0.99 || input.roundingAmount > 0.99) {
|
|
855
|
+
errors.push(error("roundingAmount", `Rounding amount must be between -0.99 and 0.99, got ${input.roundingAmount}`, void 0, "B2BRouter stores rounding as integer cents (\xB199). Use values like 0.50 or -0.25."));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (!input.buyerReference && !input.orderReference) {
|
|
859
|
+
warnings.push(warning("buyerReference", "Either buyerReference or orderReference is required by Peppol BIS 3.0 (BT-10).", "BR-10"));
|
|
860
|
+
}
|
|
861
|
+
if (input.taxCurrency && input.taxCurrency !== (input.currency ?? "EUR") && !input.taxCurrencyRate) {
|
|
862
|
+
errors.push(error("taxCurrencyRate", "Tax currency rate is required when taxCurrency differs from document currency", "BR-53", "Set taxCurrencyRate to the exchange rate from document currency to tax currency"));
|
|
863
|
+
}
|
|
864
|
+
if (input.taxCurrency && input.taxCurrency === (input.currency ?? "EUR")) {
|
|
865
|
+
warnings.push(warning("taxCurrency", "Tax currency is the same as document currency \u2014 TaxCurrencyCode will be omitted"));
|
|
866
|
+
}
|
|
867
|
+
if (input.taxCurrencyRate !== void 0 && input.taxCurrencyRate <= 0) {
|
|
868
|
+
errors.push(error("taxCurrencyRate", `Tax currency rate must be positive, got ${input.taxCurrencyRate}`, void 0, "Set to the exchange rate from document currency to tax currency"));
|
|
869
|
+
}
|
|
870
|
+
if (!input.dueDate) {
|
|
871
|
+
warnings.push(warning("dueDate", "No due date specified. Recommended for payment terms.", "BR-09"));
|
|
872
|
+
}
|
|
873
|
+
if (!input.to?.vatNumber) {
|
|
874
|
+
warnings.push(warning("to.vatNumber", "Buyer VAT number not provided. May be required for B2B."));
|
|
875
|
+
}
|
|
876
|
+
if (input.paymentMeans === 30 && !input.paymentIban) {
|
|
877
|
+
warnings.push(warning("paymentIban", "Payment means is credit transfer but no IBAN provided. Buyer won't know where to pay."));
|
|
878
|
+
}
|
|
879
|
+
if (input.from?.peppolId && input.to?.peppolId && input.from.peppolId === input.to.peppolId) {
|
|
880
|
+
errors.push(error("to.peppolId", "Buyer and seller cannot have the same Peppol ID", void 0, "Check that 'from' and 'to' are different parties"));
|
|
881
|
+
}
|
|
882
|
+
const countryResult = validateCountryRules(input);
|
|
883
|
+
warnings.push(...countryResult.warnings);
|
|
884
|
+
return {
|
|
885
|
+
valid: errors.length === 0,
|
|
886
|
+
errors,
|
|
887
|
+
warnings
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ../sdk/dist/version.js
|
|
892
|
+
var SDK_VERSION = "1.3.0";
|
|
893
|
+
|
|
894
|
+
// ../sdk/dist/core/client.js
|
|
895
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([429, 500, 502, 503, 504]);
|
|
896
|
+
function sleep(ms) {
|
|
897
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
898
|
+
}
|
|
899
|
+
function calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs) {
|
|
900
|
+
if (retryAfterMs !== void 0)
|
|
901
|
+
return Math.min(retryAfterMs, maxDelayMs);
|
|
902
|
+
const exponentialDelay = initialDelayMs * Math.pow(2, attempt);
|
|
903
|
+
const jitter = Math.random() * initialDelayMs;
|
|
904
|
+
return Math.min(exponentialDelay + jitter, maxDelayMs);
|
|
905
|
+
}
|
|
906
|
+
function parseRetryAfter(headerValue) {
|
|
907
|
+
if (!headerValue)
|
|
908
|
+
return void 0;
|
|
909
|
+
const seconds = Number(headerValue);
|
|
910
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
911
|
+
return seconds * 1e3;
|
|
912
|
+
}
|
|
913
|
+
const dateMs = Date.parse(headerValue);
|
|
914
|
+
if (!Number.isNaN(dateMs)) {
|
|
915
|
+
const delayMs = dateMs - Date.now();
|
|
916
|
+
return delayMs > 0 ? delayMs : 0;
|
|
917
|
+
}
|
|
918
|
+
return void 0;
|
|
919
|
+
}
|
|
920
|
+
function isRetryableError(error2) {
|
|
921
|
+
if (error2 instanceof PeppolApiError) {
|
|
922
|
+
return RETRYABLE_STATUS_CODES.has(error2.statusCode);
|
|
923
|
+
}
|
|
924
|
+
if (error2 instanceof Error && error2.name === "AbortError") {
|
|
925
|
+
return true;
|
|
926
|
+
}
|
|
927
|
+
if (error2 instanceof TypeError && /fetch failed|network|ECONNREFUSED|ECONNRESET|ENOTFOUND|ETIMEDOUT/i.test(error2.message)) {
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
return false;
|
|
931
|
+
}
|
|
932
|
+
var DEFAULT_BASE_URL = "https://api.getpeppr.dev/v1";
|
|
933
|
+
var GetpepprAdapter = class {
|
|
934
|
+
name = "getpeppr";
|
|
935
|
+
baseUrl;
|
|
936
|
+
apiKey;
|
|
937
|
+
timeout;
|
|
938
|
+
retryConfig;
|
|
939
|
+
onRequest;
|
|
940
|
+
onResponse;
|
|
941
|
+
constructor(config) {
|
|
942
|
+
this.apiKey = config.apiKey;
|
|
943
|
+
this.timeout = config.timeout ?? 3e4;
|
|
944
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
945
|
+
this.retryConfig = {
|
|
946
|
+
maxRetries: config.retry?.maxRetries ?? 3,
|
|
947
|
+
initialDelayMs: config.retry?.initialDelayMs ?? 500,
|
|
948
|
+
maxDelayMs: config.retry?.maxDelayMs ?? 3e4
|
|
949
|
+
};
|
|
950
|
+
this.onRequest = config.onRequest;
|
|
951
|
+
this.onResponse = config.onResponse;
|
|
952
|
+
}
|
|
953
|
+
async request(method, path, body, extraHeaders) {
|
|
954
|
+
const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
|
|
955
|
+
let lastError;
|
|
956
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
957
|
+
try {
|
|
958
|
+
return await this.doRequest(method, path, body, extraHeaders);
|
|
959
|
+
} catch (err) {
|
|
960
|
+
lastError = err;
|
|
961
|
+
const is429 = err instanceof PeppolApiError && err.statusCode === 429;
|
|
962
|
+
const isSafeMethod = /^(GET|DELETE|HEAD)$/i.test(method);
|
|
963
|
+
const hasIdempotencyKey = !!extraHeaders?.["Idempotency-Key"];
|
|
964
|
+
const canRetry = is429 || isSafeMethod || hasIdempotencyKey;
|
|
965
|
+
if (attempt < maxRetries && canRetry && isRetryableError(err)) {
|
|
966
|
+
const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
|
|
967
|
+
await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
throw err;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
throw lastError;
|
|
974
|
+
}
|
|
975
|
+
async doRequest(method, path, body, extraHeaders) {
|
|
976
|
+
const url = `${this.baseUrl}${path}`;
|
|
977
|
+
const controller = new AbortController();
|
|
978
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
979
|
+
const requestHeaders = {
|
|
980
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
981
|
+
"Content-Type": "application/json",
|
|
982
|
+
Accept: "application/json",
|
|
983
|
+
"User-Agent": `getpeppr-sdk/${SDK_VERSION}`,
|
|
984
|
+
...extraHeaders
|
|
985
|
+
};
|
|
986
|
+
const startTime = Date.now();
|
|
987
|
+
if (this.onRequest) {
|
|
988
|
+
try {
|
|
989
|
+
this.onRequest({
|
|
990
|
+
method,
|
|
991
|
+
url,
|
|
992
|
+
headers: { ...requestHeaders },
|
|
993
|
+
body,
|
|
994
|
+
timestamp: startTime
|
|
995
|
+
});
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
try {
|
|
1000
|
+
const response = await fetch(url, {
|
|
1001
|
+
method,
|
|
1002
|
+
headers: requestHeaders,
|
|
1003
|
+
body: body ? JSON.stringify(body) : void 0,
|
|
1004
|
+
signal: controller.signal
|
|
1005
|
+
});
|
|
1006
|
+
if (!response.ok) {
|
|
1007
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
1008
|
+
const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
|
|
1009
|
+
if (this.onResponse) {
|
|
1010
|
+
try {
|
|
1011
|
+
this.onResponse({
|
|
1012
|
+
status: response.status,
|
|
1013
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1014
|
+
body: errorBody,
|
|
1015
|
+
durationMs: Date.now() - startTime,
|
|
1016
|
+
timestamp: Date.now()
|
|
1017
|
+
});
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
throw new PeppolApiError(`getpeppr API error (${response.status}): ${errorBody}`, response.status, errorBody, retryAfterMs);
|
|
1022
|
+
}
|
|
1023
|
+
if (response.status === 204) {
|
|
1024
|
+
if (this.onResponse) {
|
|
1025
|
+
try {
|
|
1026
|
+
this.onResponse({
|
|
1027
|
+
status: response.status,
|
|
1028
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1029
|
+
body: void 0,
|
|
1030
|
+
durationMs: Date.now() - startTime,
|
|
1031
|
+
timestamp: Date.now()
|
|
1032
|
+
});
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
return void 0;
|
|
1037
|
+
}
|
|
1038
|
+
let responseBody;
|
|
1039
|
+
try {
|
|
1040
|
+
responseBody = await response.json();
|
|
1041
|
+
} catch {
|
|
1042
|
+
throw new PeppolApiError(`getpeppr API error: unexpected response format (status ${response.status})`, response.status, "Response body is not valid JSON");
|
|
1043
|
+
}
|
|
1044
|
+
if (this.onResponse) {
|
|
1045
|
+
try {
|
|
1046
|
+
this.onResponse({
|
|
1047
|
+
status: response.status,
|
|
1048
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1049
|
+
body: responseBody,
|
|
1050
|
+
durationMs: Date.now() - startTime,
|
|
1051
|
+
timestamp: Date.now()
|
|
1052
|
+
});
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return responseBody;
|
|
1057
|
+
} finally {
|
|
1058
|
+
clearTimeout(timeoutId);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
// NOTE: sendInvoice and createInvoice hit the same SDK endpoint (POST /invoices).
|
|
1062
|
+
// The gateway (Tasks 9-10) differentiates them: sendInvoice wraps with
|
|
1063
|
+
// { invoice: {...}, send_after_import: true }, createInvoice omits the flag.
|
|
1064
|
+
async sendInvoice(input, options) {
|
|
1065
|
+
const headers = {};
|
|
1066
|
+
if (options?.idempotencyKey) {
|
|
1067
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
1068
|
+
}
|
|
1069
|
+
if (options?.validateRecipient) {
|
|
1070
|
+
headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
|
|
1071
|
+
}
|
|
1072
|
+
const result = await this.request("POST", "/invoices", input, headers);
|
|
1073
|
+
return parseSendResult(result);
|
|
1074
|
+
}
|
|
1075
|
+
async createInvoice(input, options) {
|
|
1076
|
+
const headers = {};
|
|
1077
|
+
if (options?.idempotencyKey) {
|
|
1078
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
1079
|
+
}
|
|
1080
|
+
if (options?.validateRecipient) {
|
|
1081
|
+
headers["X-Validate-Recipient"] = options.validateRecipient === true ? "warn" : String(options.validateRecipient);
|
|
1082
|
+
}
|
|
1083
|
+
const result = await this.request("POST", "/invoices", { ...input, _draft: true }, headers);
|
|
1084
|
+
return parseSendResult(result);
|
|
1085
|
+
}
|
|
1086
|
+
async sendInvoiceById(id) {
|
|
1087
|
+
await this.request("POST", `/invoices/send/${id}`);
|
|
1088
|
+
}
|
|
1089
|
+
async sendCreditNote(input) {
|
|
1090
|
+
const result = await this.request("POST", "/credit-notes", input);
|
|
1091
|
+
return parseSendResult(result);
|
|
1092
|
+
}
|
|
1093
|
+
async validateDocument(input) {
|
|
1094
|
+
return this.request("POST", "/validate", input);
|
|
1095
|
+
}
|
|
1096
|
+
async listInvoices(options) {
|
|
1097
|
+
const params = new URLSearchParams();
|
|
1098
|
+
if (options?.limit != null)
|
|
1099
|
+
params.set("limit", String(options.limit));
|
|
1100
|
+
if (options?.offset != null)
|
|
1101
|
+
params.set("offset", String(options.offset));
|
|
1102
|
+
if (options?.includeLines)
|
|
1103
|
+
params.set("include", "lines");
|
|
1104
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1105
|
+
const result = await this.request("GET", `/invoices${query}`);
|
|
1106
|
+
const invoices = result.invoices ?? result.data ?? [];
|
|
1107
|
+
const meta = result.meta;
|
|
1108
|
+
return {
|
|
1109
|
+
data: invoices.map((inv) => ({
|
|
1110
|
+
id: String(inv.id ?? ""),
|
|
1111
|
+
number: String(inv.number ?? ""),
|
|
1112
|
+
status: mapStatus(String(inv.state ?? inv.status ?? "submitted")),
|
|
1113
|
+
createdAt: inv.created_at ? String(inv.created_at) : void 0
|
|
1114
|
+
})),
|
|
1115
|
+
meta: {
|
|
1116
|
+
totalCount: Number(meta?.total_count ?? invoices.length),
|
|
1117
|
+
offset: Number(meta?.offset ?? options?.offset ?? 0),
|
|
1118
|
+
limit: Number(meta?.limit ?? options?.limit ?? 25),
|
|
1119
|
+
hasMore: meta ? Number(meta.total_count) > Number(meta.offset) + Number(meta.limit) : false,
|
|
1120
|
+
truncated: Boolean(meta?.truncated ?? false)
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
async getStatus(documentId) {
|
|
1125
|
+
const result = await this.request("GET", `/invoices/${documentId}`);
|
|
1126
|
+
return parseSendResult(result);
|
|
1127
|
+
}
|
|
1128
|
+
async lookupDirectory(scheme, id) {
|
|
1129
|
+
return this.request("GET", `/directory/${scheme}/${id}`);
|
|
1130
|
+
}
|
|
1131
|
+
async searchDirectory(params) {
|
|
1132
|
+
const query = new URLSearchParams(params).toString();
|
|
1133
|
+
return this.request("GET", `/directory/search?${query}`);
|
|
1134
|
+
}
|
|
1135
|
+
async getInvoiceAs(id, format) {
|
|
1136
|
+
const { maxRetries, initialDelayMs, maxDelayMs } = this.retryConfig;
|
|
1137
|
+
let lastError;
|
|
1138
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1139
|
+
try {
|
|
1140
|
+
return await this.doRequestBinary(`/invoices/${id}/as/${format}`);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
lastError = err;
|
|
1143
|
+
if (attempt < maxRetries && isRetryableError(err)) {
|
|
1144
|
+
const retryAfterMs = err instanceof PeppolApiError ? err.retryAfterMs : void 0;
|
|
1145
|
+
await sleep(calculateRetryDelay(attempt, initialDelayMs, maxDelayMs, retryAfterMs));
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
throw err;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
throw lastError;
|
|
1152
|
+
}
|
|
1153
|
+
async doRequestBinary(path) {
|
|
1154
|
+
const url = `${this.baseUrl}${path}`;
|
|
1155
|
+
const controller = new AbortController();
|
|
1156
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
1157
|
+
const requestHeaders = {
|
|
1158
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
1159
|
+
};
|
|
1160
|
+
const startTime = Date.now();
|
|
1161
|
+
if (this.onRequest) {
|
|
1162
|
+
try {
|
|
1163
|
+
this.onRequest({
|
|
1164
|
+
method: "GET",
|
|
1165
|
+
url,
|
|
1166
|
+
headers: { ...requestHeaders },
|
|
1167
|
+
timestamp: startTime
|
|
1168
|
+
});
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
try {
|
|
1173
|
+
const response = await fetch(url, {
|
|
1174
|
+
method: "GET",
|
|
1175
|
+
headers: requestHeaders,
|
|
1176
|
+
signal: controller.signal
|
|
1177
|
+
});
|
|
1178
|
+
if (!response.ok) {
|
|
1179
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
1180
|
+
const retryAfterMs = response.status === 429 ? parseRetryAfter(response.headers.get("Retry-After")) : void 0;
|
|
1181
|
+
if (this.onResponse) {
|
|
1182
|
+
try {
|
|
1183
|
+
this.onResponse({
|
|
1184
|
+
status: response.status,
|
|
1185
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1186
|
+
body: errorBody,
|
|
1187
|
+
durationMs: Date.now() - startTime,
|
|
1188
|
+
timestamp: Date.now()
|
|
1189
|
+
});
|
|
1190
|
+
} catch {
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
throw new PeppolApiError(`getpeppr API error (${response.status}): ${errorBody}`, response.status, errorBody, retryAfterMs);
|
|
1194
|
+
}
|
|
1195
|
+
const responseBody = await response.arrayBuffer();
|
|
1196
|
+
if (this.onResponse) {
|
|
1197
|
+
try {
|
|
1198
|
+
this.onResponse({
|
|
1199
|
+
status: response.status,
|
|
1200
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
1201
|
+
body: `[ArrayBuffer: ${responseBody.byteLength} bytes]`,
|
|
1202
|
+
durationMs: Date.now() - startTime,
|
|
1203
|
+
timestamp: Date.now()
|
|
1204
|
+
});
|
|
1205
|
+
} catch {
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
return responseBody;
|
|
1209
|
+
} finally {
|
|
1210
|
+
clearTimeout(timeoutId);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
async validateDocumentServer(input) {
|
|
1214
|
+
return this.request("POST", "/validate/server", input);
|
|
1215
|
+
}
|
|
1216
|
+
async listEvents(options) {
|
|
1217
|
+
const params = new URLSearchParams();
|
|
1218
|
+
if (options?.limit != null)
|
|
1219
|
+
params.set("limit", String(options.limit));
|
|
1220
|
+
if (options?.offset != null)
|
|
1221
|
+
params.set("offset", String(options.offset));
|
|
1222
|
+
if (options?.invoiceId)
|
|
1223
|
+
params.set("invoiceId", options.invoiceId);
|
|
1224
|
+
if (options?.dateFrom)
|
|
1225
|
+
params.set("dateFrom", options.dateFrom);
|
|
1226
|
+
if (options?.dateTo)
|
|
1227
|
+
params.set("dateTo", options.dateTo);
|
|
1228
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1229
|
+
const result = await this.request("GET", `/events${query}`);
|
|
1230
|
+
const events = result.events ?? result.data ?? [];
|
|
1231
|
+
const meta = result.meta;
|
|
1232
|
+
return {
|
|
1233
|
+
data: events.map((evt) => ({
|
|
1234
|
+
name: String(evt.name ?? ""),
|
|
1235
|
+
text: String(evt.text ?? ""),
|
|
1236
|
+
notes: evt.notes ? String(evt.notes) : void 0,
|
|
1237
|
+
createdAt: String(evt.createdAt ?? evt.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
|
|
1238
|
+
invoice: evt.invoice,
|
|
1239
|
+
contact: evt.contact
|
|
1240
|
+
})),
|
|
1241
|
+
meta: {
|
|
1242
|
+
totalCount: Number(meta?.total_count ?? meta?.totalCount ?? events.length),
|
|
1243
|
+
offset: Number(meta?.offset ?? options?.offset ?? 0),
|
|
1244
|
+
limit: Number(meta?.limit ?? options?.limit ?? 25),
|
|
1245
|
+
hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
|
|
1246
|
+
truncated: Boolean(meta?.truncated ?? false)
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
async acknowledgeInvoice(id) {
|
|
1251
|
+
const result = await this.request("POST", `/invoices/${id}/ack`);
|
|
1252
|
+
return parseSendResult(result);
|
|
1253
|
+
}
|
|
1254
|
+
async updateInvoice(id, input) {
|
|
1255
|
+
const result = await this.request("PUT", `/invoices/${id}`, input);
|
|
1256
|
+
return parseSendResult(result);
|
|
1257
|
+
}
|
|
1258
|
+
async deleteInvoice(id) {
|
|
1259
|
+
const result = await this.request("DELETE", `/invoices/${id}`);
|
|
1260
|
+
return parseSendResult(result);
|
|
1261
|
+
}
|
|
1262
|
+
async markInvoiceAs(id, state, options) {
|
|
1263
|
+
const body = { state };
|
|
1264
|
+
if (options?.commit)
|
|
1265
|
+
body.commit = options.commit;
|
|
1266
|
+
if (options?.reason)
|
|
1267
|
+
body.reason = options.reason;
|
|
1268
|
+
const result = await this.request("POST", `/invoices/${id}/mark-as`, body);
|
|
1269
|
+
return parseSendResult(result);
|
|
1270
|
+
}
|
|
1271
|
+
async listContacts(options) {
|
|
1272
|
+
const params = new URLSearchParams();
|
|
1273
|
+
if (options?.limit != null)
|
|
1274
|
+
params.set("limit", String(options.limit));
|
|
1275
|
+
if (options?.offset != null)
|
|
1276
|
+
params.set("offset", String(options.offset));
|
|
1277
|
+
if (options?.name)
|
|
1278
|
+
params.set("name", options.name);
|
|
1279
|
+
if (options?.isClient != null)
|
|
1280
|
+
params.set("isClient", String(options.isClient));
|
|
1281
|
+
if (options?.isProvider != null)
|
|
1282
|
+
params.set("isProvider", String(options.isProvider));
|
|
1283
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1284
|
+
const result = await this.request("GET", `/contacts${query}`);
|
|
1285
|
+
const contacts = result.contacts ?? result.data ?? [];
|
|
1286
|
+
const meta = result.meta;
|
|
1287
|
+
return {
|
|
1288
|
+
data: contacts.map(parseContact),
|
|
1289
|
+
meta: {
|
|
1290
|
+
totalCount: Number(meta?.total_count ?? meta?.totalCount ?? contacts.length),
|
|
1291
|
+
offset: Number(meta?.offset ?? options?.offset ?? 0),
|
|
1292
|
+
limit: Number(meta?.limit ?? options?.limit ?? 25),
|
|
1293
|
+
hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
|
|
1294
|
+
truncated: Boolean(meta?.truncated ?? false)
|
|
1295
|
+
}
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
async getContact(id) {
|
|
1299
|
+
const result = await this.request("GET", `/contacts/${id}`);
|
|
1300
|
+
return parseContact(result);
|
|
1301
|
+
}
|
|
1302
|
+
async createContact(input) {
|
|
1303
|
+
const result = await this.request("POST", "/contacts", input);
|
|
1304
|
+
return parseContact(result);
|
|
1305
|
+
}
|
|
1306
|
+
async updateContact(id, input) {
|
|
1307
|
+
const result = await this.request("PUT", `/contacts/${id}`, input);
|
|
1308
|
+
return parseContact(result);
|
|
1309
|
+
}
|
|
1310
|
+
async deleteContact(id) {
|
|
1311
|
+
await this.request("DELETE", `/contacts/${id}`);
|
|
1312
|
+
}
|
|
1313
|
+
async listBankAccounts(options) {
|
|
1314
|
+
const params = new URLSearchParams();
|
|
1315
|
+
if (options?.limit != null)
|
|
1316
|
+
params.set("limit", String(options.limit));
|
|
1317
|
+
if (options?.offset != null)
|
|
1318
|
+
params.set("offset", String(options.offset));
|
|
1319
|
+
const query = params.toString() ? `?${params.toString()}` : "";
|
|
1320
|
+
const result = await this.request("GET", `/bank-accounts${query}`);
|
|
1321
|
+
const bankAccounts = result.bankAccounts ?? result.data ?? [];
|
|
1322
|
+
const meta = result.meta;
|
|
1323
|
+
return {
|
|
1324
|
+
data: bankAccounts.map(parseBankAccount),
|
|
1325
|
+
meta: {
|
|
1326
|
+
totalCount: Number(meta?.total_count ?? meta?.totalCount ?? bankAccounts.length),
|
|
1327
|
+
offset: Number(meta?.offset ?? options?.offset ?? 0),
|
|
1328
|
+
limit: Number(meta?.limit ?? options?.limit ?? 25),
|
|
1329
|
+
hasMore: meta ? Number(meta.total_count ?? meta.totalCount) > Number(meta.offset) + Number(meta.limit) : false,
|
|
1330
|
+
truncated: Boolean(meta?.truncated ?? false)
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
async getBankAccount(id) {
|
|
1335
|
+
const result = await this.request("GET", `/bank-accounts/${id}`);
|
|
1336
|
+
return parseBankAccount(result);
|
|
1337
|
+
}
|
|
1338
|
+
async createBankAccount(input) {
|
|
1339
|
+
const result = await this.request("POST", "/bank-accounts", input);
|
|
1340
|
+
return parseBankAccount(result);
|
|
1341
|
+
}
|
|
1342
|
+
async updateBankAccount(id, input) {
|
|
1343
|
+
const result = await this.request("PUT", `/bank-accounts/${id}`, input);
|
|
1344
|
+
return parseBankAccount(result);
|
|
1345
|
+
}
|
|
1346
|
+
async deleteBankAccount(id) {
|
|
1347
|
+
await this.request("DELETE", `/bank-accounts/${id}`);
|
|
1348
|
+
}
|
|
1349
|
+
async importInvoice(options) {
|
|
1350
|
+
const body = {
|
|
1351
|
+
file: arrayBufferToBase64(options.file),
|
|
1352
|
+
filename: options.filename,
|
|
1353
|
+
mimeType: options.mimeType ?? detectMimeType(options.filename)
|
|
1354
|
+
};
|
|
1355
|
+
const result = await this.request("POST", "/invoices/import", body);
|
|
1356
|
+
return parseSendResult(result);
|
|
1357
|
+
}
|
|
1358
|
+
async listTransportTypes() {
|
|
1359
|
+
const result = await this.request("GET", "/transports/types");
|
|
1360
|
+
const types = result.transportTypes ?? result.data ?? [];
|
|
1361
|
+
return types.map((t) => ({
|
|
1362
|
+
code: String(t.code ?? ""),
|
|
1363
|
+
name: String(t.name ?? "")
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
async listTransports() {
|
|
1367
|
+
const result = await this.request("GET", "/transports");
|
|
1368
|
+
const transports = result.transports ?? result.data ?? [];
|
|
1369
|
+
return transports.map(parseTransport);
|
|
1370
|
+
}
|
|
1371
|
+
async getTransport(code) {
|
|
1372
|
+
const result = await this.request("GET", `/transports/${code}`);
|
|
1373
|
+
return parseTransport(result);
|
|
1374
|
+
}
|
|
1375
|
+
async createTransport(input) {
|
|
1376
|
+
const result = await this.request("POST", "/transports", input);
|
|
1377
|
+
return parseTransport(result);
|
|
1378
|
+
}
|
|
1379
|
+
async updateTransport(code, input) {
|
|
1380
|
+
const result = await this.request("PUT", `/transports/${code}`, input);
|
|
1381
|
+
return parseTransport(result);
|
|
1382
|
+
}
|
|
1383
|
+
async deleteTransport(code) {
|
|
1384
|
+
await this.request("DELETE", `/transports/${code}`);
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
function arrayBufferToBase64(buffer) {
|
|
1388
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
1389
|
+
let binary = "";
|
|
1390
|
+
for (const byte of bytes) {
|
|
1391
|
+
binary += String.fromCharCode(byte);
|
|
1392
|
+
}
|
|
1393
|
+
return btoa(binary);
|
|
1394
|
+
}
|
|
1395
|
+
function detectMimeType(filename) {
|
|
1396
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1397
|
+
switch (ext) {
|
|
1398
|
+
case "xml":
|
|
1399
|
+
return "application/xml";
|
|
1400
|
+
case "pdf":
|
|
1401
|
+
return "application/pdf";
|
|
1402
|
+
case "json":
|
|
1403
|
+
return "application/json";
|
|
1404
|
+
default:
|
|
1405
|
+
return "application/octet-stream";
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function parseSendResult(result) {
|
|
1409
|
+
return {
|
|
1410
|
+
id: String(result.id ?? ""),
|
|
1411
|
+
status: mapStatus(String(result.status ?? "submitted")),
|
|
1412
|
+
peppolMessageId: result.peppolMessageId ?? result.peppol_message_id,
|
|
1413
|
+
createdAt: String(result.createdAt ?? result.updatedAt ?? result.created_at ?? (/* @__PURE__ */ new Date()).toISOString()),
|
|
1414
|
+
ublXml: result.ublXml,
|
|
1415
|
+
warnings: Array.isArray(result.warnings) ? result.warnings : void 0
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
function parseContact(raw) {
|
|
1419
|
+
const contact = {
|
|
1420
|
+
id: String(raw.id ?? ""),
|
|
1421
|
+
name: String(raw.name ?? "")
|
|
1422
|
+
};
|
|
1423
|
+
if (raw.peppolId != null)
|
|
1424
|
+
contact.peppolId = String(raw.peppolId);
|
|
1425
|
+
if (raw.vatNumber != null)
|
|
1426
|
+
contact.vatNumber = String(raw.vatNumber);
|
|
1427
|
+
if (raw.companyId != null)
|
|
1428
|
+
contact.companyId = String(raw.companyId);
|
|
1429
|
+
if (raw.street != null)
|
|
1430
|
+
contact.street = String(raw.street);
|
|
1431
|
+
if (raw.city != null)
|
|
1432
|
+
contact.city = String(raw.city);
|
|
1433
|
+
if (raw.postalCode != null)
|
|
1434
|
+
contact.postalCode = String(raw.postalCode);
|
|
1435
|
+
if (raw.country != null)
|
|
1436
|
+
contact.country = String(raw.country);
|
|
1437
|
+
if (raw.email != null)
|
|
1438
|
+
contact.email = String(raw.email);
|
|
1439
|
+
if (raw.phone != null)
|
|
1440
|
+
contact.phone = String(raw.phone);
|
|
1441
|
+
if (raw.isClient != null)
|
|
1442
|
+
contact.isClient = Boolean(raw.isClient);
|
|
1443
|
+
if (raw.isProvider != null)
|
|
1444
|
+
contact.isProvider = Boolean(raw.isProvider);
|
|
1445
|
+
if (raw.createdAt != null)
|
|
1446
|
+
contact.createdAt = String(raw.createdAt);
|
|
1447
|
+
if (raw.updatedAt != null)
|
|
1448
|
+
contact.updatedAt = String(raw.updatedAt);
|
|
1449
|
+
if (raw.directoryVerified != null)
|
|
1450
|
+
contact.directoryVerified = Boolean(raw.directoryVerified);
|
|
1451
|
+
if (raw.directoryLastChecked != null)
|
|
1452
|
+
contact.directoryLastChecked = String(raw.directoryLastChecked);
|
|
1453
|
+
return contact;
|
|
1454
|
+
}
|
|
1455
|
+
function parseBankAccount(raw) {
|
|
1456
|
+
const account = {
|
|
1457
|
+
id: String(raw.id ?? ""),
|
|
1458
|
+
name: String(raw.name ?? ""),
|
|
1459
|
+
type: raw.type === "number" ? "number" : "iban"
|
|
1460
|
+
};
|
|
1461
|
+
if (raw.iban != null)
|
|
1462
|
+
account.iban = String(raw.iban);
|
|
1463
|
+
if (raw.number != null)
|
|
1464
|
+
account.number = String(raw.number);
|
|
1465
|
+
if (raw.bic != null)
|
|
1466
|
+
account.bic = String(raw.bic);
|
|
1467
|
+
if (raw.country != null)
|
|
1468
|
+
account.country = String(raw.country);
|
|
1469
|
+
if (raw.createdAt != null)
|
|
1470
|
+
account.createdAt = String(raw.createdAt);
|
|
1471
|
+
if (raw.updatedAt != null)
|
|
1472
|
+
account.updatedAt = String(raw.updatedAt);
|
|
1473
|
+
return account;
|
|
1474
|
+
}
|
|
1475
|
+
function parseTransport(raw) {
|
|
1476
|
+
return {
|
|
1477
|
+
id: String(raw.id ?? ""),
|
|
1478
|
+
transportTypeCode: String(raw.transportTypeCode ?? ""),
|
|
1479
|
+
name: String(raw.name ?? ""),
|
|
1480
|
+
status: raw.status ? String(raw.status) : void 0
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
1484
|
+
"submitted",
|
|
1485
|
+
"delivered",
|
|
1486
|
+
"accepted",
|
|
1487
|
+
"rejected",
|
|
1488
|
+
"paid",
|
|
1489
|
+
"failed",
|
|
1490
|
+
"cleared",
|
|
1491
|
+
"acknowledged",
|
|
1492
|
+
"in_process",
|
|
1493
|
+
"under_query",
|
|
1494
|
+
"conditionally_accepted",
|
|
1495
|
+
"partially_paid",
|
|
1496
|
+
"no_action"
|
|
1497
|
+
]);
|
|
1498
|
+
function mapStatus(raw) {
|
|
1499
|
+
const s = raw.toLowerCase();
|
|
1500
|
+
return VALID_STATUSES.has(s) ? s : "unknown";
|
|
1501
|
+
}
|
|
1502
|
+
var PeppolError = class extends Error {
|
|
1503
|
+
constructor(message) {
|
|
1504
|
+
super(message);
|
|
1505
|
+
this.name = "PeppolError";
|
|
1506
|
+
}
|
|
1507
|
+
};
|
|
1508
|
+
var PeppolValidationError = class extends PeppolError {
|
|
1509
|
+
validation;
|
|
1510
|
+
constructor(message, validation) {
|
|
1511
|
+
super(message);
|
|
1512
|
+
this.validation = validation;
|
|
1513
|
+
this.name = "PeppolValidationError";
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
var PeppolApiError = class extends PeppolError {
|
|
1517
|
+
statusCode;
|
|
1518
|
+
responseBody;
|
|
1519
|
+
/** Parsed Retry-After delay in milliseconds (present on 429 responses) */
|
|
1520
|
+
retryAfterMs;
|
|
1521
|
+
constructor(message, statusCode, responseBody, retryAfterMs) {
|
|
1522
|
+
super(message);
|
|
1523
|
+
this.statusCode = statusCode;
|
|
1524
|
+
this.responseBody = responseBody;
|
|
1525
|
+
this.name = "PeppolApiError";
|
|
1526
|
+
this.retryAfterMs = retryAfterMs;
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
var Peppol = class {
|
|
1530
|
+
adapter;
|
|
1531
|
+
invoices;
|
|
1532
|
+
creditNotes;
|
|
1533
|
+
directory;
|
|
1534
|
+
events;
|
|
1535
|
+
contacts;
|
|
1536
|
+
bankAccounts;
|
|
1537
|
+
transports;
|
|
1538
|
+
constructor(config) {
|
|
1539
|
+
if (!config.apiKey) {
|
|
1540
|
+
throw new PeppolError("API key is required. Sign up at https://console.getpeppr.dev to get your sandbox key.");
|
|
1541
|
+
}
|
|
1542
|
+
this.adapter = new GetpepprAdapter(config);
|
|
1543
|
+
this.invoices = new InvoiceOperations(this.adapter);
|
|
1544
|
+
this.creditNotes = new CreditNoteOperations(this.adapter);
|
|
1545
|
+
this.directory = new DirectoryOperations(this.adapter);
|
|
1546
|
+
this.events = new EventOperations(this.adapter);
|
|
1547
|
+
this.contacts = new ContactOperations(this.adapter);
|
|
1548
|
+
this.bankAccounts = new BankAccountOperations(this.adapter);
|
|
1549
|
+
this.transports = new TransportOperations(this.adapter);
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Validate an invoice without sending it.
|
|
1553
|
+
* Useful for pre-flight checks in your UI.
|
|
1554
|
+
*/
|
|
1555
|
+
validate(input) {
|
|
1556
|
+
return validateInvoice(input);
|
|
1557
|
+
}
|
|
1558
|
+
/**
|
|
1559
|
+
* Generate UBL XML without sending.
|
|
1560
|
+
* Useful for debugging or manual submission.
|
|
1561
|
+
*/
|
|
1562
|
+
toXml(input) {
|
|
1563
|
+
const validation = validateInvoice(input);
|
|
1564
|
+
if (!validation.valid) {
|
|
1565
|
+
throw new PeppolValidationError(`Invoice validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, validation);
|
|
1566
|
+
}
|
|
1567
|
+
if (input.isCreditNote) {
|
|
1568
|
+
return buildCreditNoteXml(input);
|
|
1569
|
+
}
|
|
1570
|
+
return buildInvoiceXml(input);
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
async function* paginate(fetchPage, options) {
|
|
1574
|
+
const pageSize = options?.limit ?? 25;
|
|
1575
|
+
let offset = 0;
|
|
1576
|
+
while (true) {
|
|
1577
|
+
const page = await fetchPage(offset, pageSize);
|
|
1578
|
+
for (const item of page.data) {
|
|
1579
|
+
yield item;
|
|
1580
|
+
}
|
|
1581
|
+
if (!page.meta.hasMore)
|
|
1582
|
+
break;
|
|
1583
|
+
offset += pageSize;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
var InvoiceOperations = class {
|
|
1587
|
+
adapter;
|
|
1588
|
+
constructor(adapter) {
|
|
1589
|
+
this.adapter = adapter;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Create a draft invoice without sending it.
|
|
1593
|
+
* Validates input client-side, then creates the invoice on B2BRouter.
|
|
1594
|
+
* Use `sendById()` to send the draft when ready.
|
|
1595
|
+
*
|
|
1596
|
+
* @example
|
|
1597
|
+
* ```ts
|
|
1598
|
+
* const draft = await peppol.invoices.create({ number: "INV-001", to, lines });
|
|
1599
|
+
* // Later, when ready:
|
|
1600
|
+
* await peppol.invoices.sendById(draft.id);
|
|
1601
|
+
* ```
|
|
1602
|
+
*/
|
|
1603
|
+
async create(input, options) {
|
|
1604
|
+
const validation = validateInvoice(input);
|
|
1605
|
+
if (!validation.valid) {
|
|
1606
|
+
throw new PeppolValidationError(`Invoice validation failed:
|
|
1607
|
+
${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
|
|
1608
|
+
}
|
|
1609
|
+
const result = await this.adapter.createInvoice(input, options);
|
|
1610
|
+
if (validation.warnings.length > 0) {
|
|
1611
|
+
result.warnings = validation.warnings;
|
|
1612
|
+
}
|
|
1613
|
+
return result;
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Send an existing draft invoice by ID.
|
|
1617
|
+
* The invoice must have been previously created with `create()`.
|
|
1618
|
+
*
|
|
1619
|
+
* @example
|
|
1620
|
+
* ```ts
|
|
1621
|
+
* await peppol.invoices.sendById("inv-1");
|
|
1622
|
+
* ```
|
|
1623
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support draft sending
|
|
1624
|
+
*/
|
|
1625
|
+
async sendById(id) {
|
|
1626
|
+
return this.adapter.sendInvoiceById(id);
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Send an invoice via Peppol.
|
|
1630
|
+
*
|
|
1631
|
+
* @example
|
|
1632
|
+
* ```ts
|
|
1633
|
+
* const result = await peppol.invoices.send({
|
|
1634
|
+
* number: "INV-001",
|
|
1635
|
+
* from: { name: "My Company", peppolId: "0208:BE0123456789", country: "BE" },
|
|
1636
|
+
* to: { name: "Client Co", peppolId: "0208:BE9876543210", country: "BE" },
|
|
1637
|
+
* lines: [
|
|
1638
|
+
* { description: "Consulting", quantity: 10, unitPrice: 150, vatRate: 21 }
|
|
1639
|
+
* ]
|
|
1640
|
+
* });
|
|
1641
|
+
* ```
|
|
1642
|
+
*/
|
|
1643
|
+
async send(input, options) {
|
|
1644
|
+
const validation = validateInvoice(input);
|
|
1645
|
+
if (!validation.valid) {
|
|
1646
|
+
throw new PeppolValidationError(`Invoice validation failed:
|
|
1647
|
+
${validation.errors.map((e) => ` - ${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ""}`).join("\n")}`, validation);
|
|
1648
|
+
}
|
|
1649
|
+
const result = await this.adapter.sendInvoice(input, options);
|
|
1650
|
+
if (validation.warnings.length > 0) {
|
|
1651
|
+
result.warnings = validation.warnings;
|
|
1652
|
+
}
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
/** List invoices with pagination, filtering, and proper metadata */
|
|
1656
|
+
async list(options) {
|
|
1657
|
+
return this.adapter.listInvoices(options);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Async iterator over all invoices, automatically handling pagination.
|
|
1661
|
+
*
|
|
1662
|
+
* @example
|
|
1663
|
+
* ```ts
|
|
1664
|
+
* for await (const invoice of peppol.invoices.listAll()) {
|
|
1665
|
+
* console.log(invoice.id, invoice.status);
|
|
1666
|
+
* }
|
|
1667
|
+
* ```
|
|
1668
|
+
*/
|
|
1669
|
+
listAll(options) {
|
|
1670
|
+
return paginate((offset, limit) => this.adapter.listInvoices({ ...options, offset, limit }), options);
|
|
1671
|
+
}
|
|
1672
|
+
/** Get the status of a sent invoice */
|
|
1673
|
+
async getStatus(documentId) {
|
|
1674
|
+
return this.adapter.getStatus(documentId);
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Export an invoice in a specific format (e.g., PDF, UBL XML).
|
|
1678
|
+
* Returns raw binary data as an ArrayBuffer.
|
|
1679
|
+
*
|
|
1680
|
+
* @example
|
|
1681
|
+
* ```ts
|
|
1682
|
+
* const pdf = await peppol.invoices.getAs("inv-123", "pdf");
|
|
1683
|
+
* fs.writeFileSync("invoice.pdf", Buffer.from(pdf));
|
|
1684
|
+
* ```
|
|
1685
|
+
*/
|
|
1686
|
+
async getAs(id, format) {
|
|
1687
|
+
return this.adapter.getInvoiceAs(id, format);
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Validate an invoice server-side using XSD and Schematron rules.
|
|
1691
|
+
* Builds UBL XML from the input and sends it to the server for validation.
|
|
1692
|
+
* More thorough than client-side validation — catches XML-level issues.
|
|
1693
|
+
*
|
|
1694
|
+
* @example
|
|
1695
|
+
* ```ts
|
|
1696
|
+
* const result = await peppol.invoices.validateServer({
|
|
1697
|
+
* number: "INV-001",
|
|
1698
|
+
* to: { name: "Acme", peppolId: "0208:BE0123456789", country: "BE" },
|
|
1699
|
+
* lines: [{ description: "Item", quantity: 1, unitPrice: 100, vatRate: 21 }]
|
|
1700
|
+
* });
|
|
1701
|
+
* console.log(result.valid, result.schematron.errors);
|
|
1702
|
+
* ```
|
|
1703
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support server-side validation
|
|
1704
|
+
*/
|
|
1705
|
+
async validateServer(input) {
|
|
1706
|
+
return this.adapter.validateDocumentServer(input);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Import an invoice from a file (XML, PDF, JSON).
|
|
1710
|
+
* The file is base64-encoded and sent to the gateway, which forwards it
|
|
1711
|
+
* as multipart/form-data to B2BRouter's import endpoint.
|
|
1712
|
+
*
|
|
1713
|
+
* @example
|
|
1714
|
+
* ```ts
|
|
1715
|
+
* const xmlBytes = fs.readFileSync("invoice.xml");
|
|
1716
|
+
* const result = await peppol.invoices.importFile({
|
|
1717
|
+
* file: xmlBytes,
|
|
1718
|
+
* filename: "invoice.xml",
|
|
1719
|
+
* });
|
|
1720
|
+
* console.log(result.id, result.status);
|
|
1721
|
+
* ```
|
|
1722
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support file import
|
|
1723
|
+
*/
|
|
1724
|
+
async importFile(options) {
|
|
1725
|
+
return this.adapter.importInvoice(options);
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Acknowledge a received invoice.
|
|
1729
|
+
*
|
|
1730
|
+
* @example
|
|
1731
|
+
* ```ts
|
|
1732
|
+
* const result = await peppol.invoices.acknowledge("inv-123");
|
|
1733
|
+
* console.log(result.status); // "accepted"
|
|
1734
|
+
* ```
|
|
1735
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support acknowledgement
|
|
1736
|
+
*/
|
|
1737
|
+
async acknowledge(id) {
|
|
1738
|
+
return this.adapter.acknowledgeInvoice(id);
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Update an existing invoice (draft only).
|
|
1742
|
+
* Only include the fields you want to change — partial updates are supported.
|
|
1743
|
+
*
|
|
1744
|
+
* @example
|
|
1745
|
+
* ```ts
|
|
1746
|
+
* const updated = await peppol.invoices.update("inv-123", {
|
|
1747
|
+
* dueDate: "2026-04-01",
|
|
1748
|
+
* lines: [
|
|
1749
|
+
* { id: "line-1", quantity: 5 },
|
|
1750
|
+
* { id: "line-2", _destroy: true },
|
|
1751
|
+
* ],
|
|
1752
|
+
* });
|
|
1753
|
+
* ```
|
|
1754
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support invoice updates
|
|
1755
|
+
*/
|
|
1756
|
+
async update(id, input) {
|
|
1757
|
+
return this.adapter.updateInvoice(id, input);
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Delete an invoice.
|
|
1761
|
+
*
|
|
1762
|
+
* @example
|
|
1763
|
+
* ```ts
|
|
1764
|
+
* const deleted = await peppol.invoices.delete("inv-123");
|
|
1765
|
+
* ```
|
|
1766
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support invoice deletion
|
|
1767
|
+
*/
|
|
1768
|
+
async delete(id) {
|
|
1769
|
+
return this.adapter.deleteInvoice(id);
|
|
1770
|
+
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Transition an invoice to a new state.
|
|
1773
|
+
* B2BRouter validates the state machine — invalid transitions return an error.
|
|
1774
|
+
*
|
|
1775
|
+
* @example
|
|
1776
|
+
* ```ts
|
|
1777
|
+
* await peppol.invoices.markAs("inv-123", "accepted");
|
|
1778
|
+
* await peppol.invoices.markAs("inv-123", "paid", {
|
|
1779
|
+
* commit: "with_mail",
|
|
1780
|
+
* reason: "Payment received",
|
|
1781
|
+
* });
|
|
1782
|
+
* ```
|
|
1783
|
+
* @throws {PeppolApiError} 501 if the gateway provider does not support state transitions
|
|
1784
|
+
*/
|
|
1785
|
+
async markAs(id, state, options) {
|
|
1786
|
+
return this.adapter.markInvoiceAs(id, state, options);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Send multiple invoices in parallel with controlled concurrency.
|
|
1790
|
+
* Each invoice is validated and sent individually — failures don't affect other invoices
|
|
1791
|
+
* unless `stopOnError: true` is set.
|
|
1792
|
+
*
|
|
1793
|
+
* The SDK's built-in retry logic (including 429 Retry-After) provides automatic
|
|
1794
|
+
* rate-limit handling at the request level.
|
|
1795
|
+
*
|
|
1796
|
+
* @example
|
|
1797
|
+
* ```ts
|
|
1798
|
+
* const result = await peppol.invoices.sendBatch([invoice1, invoice2, invoice3], {
|
|
1799
|
+
* concurrency: 3,
|
|
1800
|
+
* });
|
|
1801
|
+
* console.log(`${result.succeeded.length} sent, ${result.failed.length} failed`);
|
|
1802
|
+
* ```
|
|
1803
|
+
*/
|
|
1804
|
+
async sendBatch(inputs, options) {
|
|
1805
|
+
const concurrency = options?.concurrency ?? 5;
|
|
1806
|
+
const stopOnError = options?.stopOnError ?? false;
|
|
1807
|
+
const succeeded = [];
|
|
1808
|
+
const failed = [];
|
|
1809
|
+
let stopped = false;
|
|
1810
|
+
for (let i = 0; i < inputs.length; i += concurrency) {
|
|
1811
|
+
if (stopped)
|
|
1812
|
+
break;
|
|
1813
|
+
const chunk = inputs.slice(i, i + concurrency);
|
|
1814
|
+
const promises = chunk.map(async (input, j) => {
|
|
1815
|
+
const index = i + j;
|
|
1816
|
+
if (stopped)
|
|
1817
|
+
return;
|
|
1818
|
+
try {
|
|
1819
|
+
const result = await this.send(input);
|
|
1820
|
+
succeeded.push({ index, result });
|
|
1821
|
+
} catch (error2) {
|
|
1822
|
+
failed.push({ index, input, error: error2 });
|
|
1823
|
+
if (stopOnError) {
|
|
1824
|
+
stopped = true;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
});
|
|
1828
|
+
await Promise.all(promises);
|
|
1829
|
+
}
|
|
1830
|
+
return { succeeded, failed, total: inputs.length };
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Poll until an invoice reaches a target status.
|
|
1834
|
+
*
|
|
1835
|
+
* @example
|
|
1836
|
+
* ```ts
|
|
1837
|
+
* const result = await peppol.invoices.waitFor(id, "accepted", { timeout: 60000 });
|
|
1838
|
+
* ```
|
|
1839
|
+
*/
|
|
1840
|
+
async waitFor(documentId, targetStatus, options) {
|
|
1841
|
+
const timeout = options?.timeout ?? 12e4;
|
|
1842
|
+
const interval = options?.interval ?? 5e3;
|
|
1843
|
+
const targets = Array.isArray(targetStatus) ? targetStatus : [targetStatus];
|
|
1844
|
+
const terminalFailures = ["failed", "rejected"];
|
|
1845
|
+
const startTime = Date.now();
|
|
1846
|
+
while (true) {
|
|
1847
|
+
const result = await this.getStatus(documentId);
|
|
1848
|
+
if (targets.includes(result.status)) {
|
|
1849
|
+
return result;
|
|
1850
|
+
}
|
|
1851
|
+
if (terminalFailures.includes(result.status)) {
|
|
1852
|
+
throw new PeppolError(`Document ${documentId} reached terminal status "${result.status}" while waiting for "${targets.join('" or "')}"`);
|
|
1853
|
+
}
|
|
1854
|
+
if (Date.now() - startTime >= timeout) {
|
|
1855
|
+
throw new PeppolError(`Timed out waiting for document ${documentId} to reach status "${targets.join('" or "')}" (last: "${result.status}")`);
|
|
1856
|
+
}
|
|
1857
|
+
await sleep(interval);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
var CreditNoteOperations = class {
|
|
1862
|
+
adapter;
|
|
1863
|
+
constructor(adapter) {
|
|
1864
|
+
this.adapter = adapter;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Send a credit note via Peppol.
|
|
1868
|
+
* @deprecated Use peppol.invoices.send({ ...input, isCreditNote: true }) instead.
|
|
1869
|
+
*/
|
|
1870
|
+
async send(input) {
|
|
1871
|
+
const invoiceInput = {
|
|
1872
|
+
...input,
|
|
1873
|
+
isCreditNote: true,
|
|
1874
|
+
invoiceReference: input.invoiceReference
|
|
1875
|
+
};
|
|
1876
|
+
const validation = validateInvoice(invoiceInput);
|
|
1877
|
+
if (!validation.valid) {
|
|
1878
|
+
throw new PeppolValidationError(`Credit note validation failed:
|
|
1879
|
+
${validation.errors.map((e) => ` - ${e.field}: ${e.message}`).join("\n")}`, validation);
|
|
1880
|
+
}
|
|
1881
|
+
return this.adapter.sendInvoice(invoiceInput);
|
|
1882
|
+
}
|
|
1883
|
+
};
|
|
1884
|
+
var DirectoryOperations = class {
|
|
1885
|
+
adapter;
|
|
1886
|
+
constructor(adapter) {
|
|
1887
|
+
this.adapter = adapter;
|
|
1888
|
+
}
|
|
1889
|
+
/**
|
|
1890
|
+
* Look up a Peppol participant in the directory.
|
|
1891
|
+
*
|
|
1892
|
+
* @example
|
|
1893
|
+
* ```ts
|
|
1894
|
+
* const entry = await peppol.directory.lookup("0208:BE0123456789");
|
|
1895
|
+
* console.log(entry.name, entry.capabilities);
|
|
1896
|
+
* ```
|
|
1897
|
+
*/
|
|
1898
|
+
async lookup(peppolId) {
|
|
1899
|
+
const colonIndex = peppolId.indexOf(":");
|
|
1900
|
+
if (colonIndex === -1) {
|
|
1901
|
+
throw new PeppolError('Invalid Peppol ID format. Expected "scheme:id" (e.g., "0208:BE0123456789")');
|
|
1902
|
+
}
|
|
1903
|
+
const scheme = peppolId.slice(0, colonIndex);
|
|
1904
|
+
const id = peppolId.slice(colonIndex + 1);
|
|
1905
|
+
return this.adapter.lookupDirectory(scheme, id);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Search the Peppol Directory for participants.
|
|
1909
|
+
*
|
|
1910
|
+
* @example
|
|
1911
|
+
* ```ts
|
|
1912
|
+
* const result = await peppol.directory.search({ name: "Acme", country: "BE" });
|
|
1913
|
+
* console.log(result.data); // DirectoryEntry[]
|
|
1914
|
+
* console.log(result.meta.totalCount);
|
|
1915
|
+
* ```
|
|
1916
|
+
*/
|
|
1917
|
+
async search(options) {
|
|
1918
|
+
if (!options.name && !options.country && !options.vatNumber) {
|
|
1919
|
+
throw new PeppolError("At least one search criterion is required (name, country, or vatNumber)");
|
|
1920
|
+
}
|
|
1921
|
+
if (options.name && options.name.length < 3) {
|
|
1922
|
+
throw new PeppolError("Search name must be at least 3 characters");
|
|
1923
|
+
}
|
|
1924
|
+
if (!this.adapter.searchDirectory) {
|
|
1925
|
+
throw new PeppolError("Directory search is not supported by this backend adapter");
|
|
1926
|
+
}
|
|
1927
|
+
const params = {};
|
|
1928
|
+
if (options.name)
|
|
1929
|
+
params.name = options.name;
|
|
1930
|
+
if (options.country)
|
|
1931
|
+
params.country = options.country;
|
|
1932
|
+
if (options.vatNumber)
|
|
1933
|
+
params.vatNumber = options.vatNumber;
|
|
1934
|
+
if (options.limit !== void 0)
|
|
1935
|
+
params.limit = String(options.limit);
|
|
1936
|
+
if (options.offset !== void 0)
|
|
1937
|
+
params.offset = String(options.offset);
|
|
1938
|
+
return this.adapter.searchDirectory(params);
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Search the Peppol Directory by VAT number.
|
|
1942
|
+
* Convenience method — equivalent to `search({ vatNumber })`.
|
|
1943
|
+
*
|
|
1944
|
+
* @example
|
|
1945
|
+
* ```ts
|
|
1946
|
+
* const result = await peppol.directory.searchByVat("BE0123456789");
|
|
1947
|
+
* ```
|
|
1948
|
+
*/
|
|
1949
|
+
async searchByVat(vatNumber) {
|
|
1950
|
+
return this.search({ vatNumber });
|
|
1951
|
+
}
|
|
1952
|
+
};
|
|
1953
|
+
var EventOperations = class {
|
|
1954
|
+
adapter;
|
|
1955
|
+
constructor(adapter) {
|
|
1956
|
+
this.adapter = adapter;
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* List events with optional filtering and pagination.
|
|
1960
|
+
*
|
|
1961
|
+
* @example
|
|
1962
|
+
* ```ts
|
|
1963
|
+
* const result = await peppol.events.list({ limit: 10 });
|
|
1964
|
+
* console.log(result.data, result.meta);
|
|
1965
|
+
*
|
|
1966
|
+
* // Filter by invoice
|
|
1967
|
+
* const invoiceEvents = await peppol.events.list({ invoiceId: "inv-123" });
|
|
1968
|
+
* ```
|
|
1969
|
+
*/
|
|
1970
|
+
async list(options) {
|
|
1971
|
+
return this.adapter.listEvents(options);
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Async iterator over all events, automatically handling pagination.
|
|
1975
|
+
*
|
|
1976
|
+
* @example
|
|
1977
|
+
* ```ts
|
|
1978
|
+
* for await (const event of peppol.events.listAll({ invoiceId: "inv-123" })) {
|
|
1979
|
+
* console.log(event.name, event.createdAt);
|
|
1980
|
+
* }
|
|
1981
|
+
* ```
|
|
1982
|
+
*/
|
|
1983
|
+
listAll(options) {
|
|
1984
|
+
return paginate((offset, limit) => this.adapter.listEvents({ ...options, offset, limit }), options);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
var ContactOperations = class {
|
|
1988
|
+
adapter;
|
|
1989
|
+
constructor(adapter) {
|
|
1990
|
+
this.adapter = adapter;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* List contacts with optional filtering and pagination.
|
|
1994
|
+
*
|
|
1995
|
+
* @example
|
|
1996
|
+
* ```ts
|
|
1997
|
+
* const result = await peppol.contacts.list({ limit: 10, isClient: true });
|
|
1998
|
+
* console.log(result.data, result.meta);
|
|
1999
|
+
* ```
|
|
2000
|
+
*/
|
|
2001
|
+
async list(options) {
|
|
2002
|
+
return this.adapter.listContacts(options);
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* Get a single contact by ID.
|
|
2006
|
+
*
|
|
2007
|
+
* @example
|
|
2008
|
+
* ```ts
|
|
2009
|
+
* const contact = await peppol.contacts.get("123");
|
|
2010
|
+
* console.log(contact.name, contact.peppolId);
|
|
2011
|
+
* ```
|
|
2012
|
+
*/
|
|
2013
|
+
async get(id) {
|
|
2014
|
+
return this.adapter.getContact(id);
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Create a new contact.
|
|
2018
|
+
*
|
|
2019
|
+
* @example
|
|
2020
|
+
* ```ts
|
|
2021
|
+
* const contact = await peppol.contacts.create({
|
|
2022
|
+
* name: "Acme Corp",
|
|
2023
|
+
* peppolId: "0208:BE0123456789",
|
|
2024
|
+
* country: "BE",
|
|
2025
|
+
* isClient: true,
|
|
2026
|
+
* });
|
|
2027
|
+
* ```
|
|
2028
|
+
*/
|
|
2029
|
+
async create(input) {
|
|
2030
|
+
return this.adapter.createContact(input);
|
|
2031
|
+
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Update an existing contact.
|
|
2034
|
+
*
|
|
2035
|
+
* @example
|
|
2036
|
+
* ```ts
|
|
2037
|
+
* const updated = await peppol.contacts.update("123", { email: "new@acme.com" });
|
|
2038
|
+
* ```
|
|
2039
|
+
*/
|
|
2040
|
+
async update(id, input) {
|
|
2041
|
+
return this.adapter.updateContact(id, input);
|
|
2042
|
+
}
|
|
2043
|
+
/**
|
|
2044
|
+
* Delete a contact.
|
|
2045
|
+
*
|
|
2046
|
+
* @example
|
|
2047
|
+
* ```ts
|
|
2048
|
+
* await peppol.contacts.delete("123");
|
|
2049
|
+
* ```
|
|
2050
|
+
*/
|
|
2051
|
+
async delete(id) {
|
|
2052
|
+
return this.adapter.deleteContact(id);
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Async iterator over all contacts, automatically handling pagination.
|
|
2056
|
+
*
|
|
2057
|
+
* @example
|
|
2058
|
+
* ```ts
|
|
2059
|
+
* for await (const contact of peppol.contacts.listAll({ isClient: true })) {
|
|
2060
|
+
* console.log(contact.name, contact.peppolId);
|
|
2061
|
+
* }
|
|
2062
|
+
* ```
|
|
2063
|
+
*/
|
|
2064
|
+
listAll(options) {
|
|
2065
|
+
return paginate((offset, limit) => this.adapter.listContacts({ ...options, offset, limit }));
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
var BankAccountOperations = class {
|
|
2069
|
+
adapter;
|
|
2070
|
+
constructor(adapter) {
|
|
2071
|
+
this.adapter = adapter;
|
|
2072
|
+
}
|
|
2073
|
+
/**
|
|
2074
|
+
* List bank accounts with optional pagination.
|
|
2075
|
+
*
|
|
2076
|
+
* @example
|
|
2077
|
+
* ```ts
|
|
2078
|
+
* const result = await peppol.bankAccounts.list({ limit: 10 });
|
|
2079
|
+
* console.log(result.data, result.meta);
|
|
2080
|
+
* ```
|
|
2081
|
+
*/
|
|
2082
|
+
async list(options) {
|
|
2083
|
+
return this.adapter.listBankAccounts(options);
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Get a single bank account by ID.
|
|
2087
|
+
*
|
|
2088
|
+
* @example
|
|
2089
|
+
* ```ts
|
|
2090
|
+
* const account = await peppol.bankAccounts.get("123");
|
|
2091
|
+
* console.log(account.name, account.iban);
|
|
2092
|
+
* ```
|
|
2093
|
+
*/
|
|
2094
|
+
async get(id) {
|
|
2095
|
+
return this.adapter.getBankAccount(id);
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Create a new bank account.
|
|
2099
|
+
*
|
|
2100
|
+
* @example
|
|
2101
|
+
* ```ts
|
|
2102
|
+
* const account = await peppol.bankAccounts.create({
|
|
2103
|
+
* name: "Main Account",
|
|
2104
|
+
* iban: "BE68539007547034",
|
|
2105
|
+
* bic: "BBRUBEBB",
|
|
2106
|
+
* country: "BE",
|
|
2107
|
+
* });
|
|
2108
|
+
* ```
|
|
2109
|
+
*/
|
|
2110
|
+
async create(input) {
|
|
2111
|
+
return this.adapter.createBankAccount(input);
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Update an existing bank account.
|
|
2115
|
+
*
|
|
2116
|
+
* @example
|
|
2117
|
+
* ```ts
|
|
2118
|
+
* const updated = await peppol.bankAccounts.update("123", { name: "Updated Name" });
|
|
2119
|
+
* ```
|
|
2120
|
+
*/
|
|
2121
|
+
async update(id, input) {
|
|
2122
|
+
return this.adapter.updateBankAccount(id, input);
|
|
2123
|
+
}
|
|
2124
|
+
/**
|
|
2125
|
+
* Delete a bank account.
|
|
2126
|
+
*
|
|
2127
|
+
* @example
|
|
2128
|
+
* ```ts
|
|
2129
|
+
* await peppol.bankAccounts.delete("123");
|
|
2130
|
+
* ```
|
|
2131
|
+
*/
|
|
2132
|
+
async delete(id) {
|
|
2133
|
+
return this.adapter.deleteBankAccount(id);
|
|
2134
|
+
}
|
|
2135
|
+
/**
|
|
2136
|
+
* Async iterator over all bank accounts, automatically handling pagination.
|
|
2137
|
+
*
|
|
2138
|
+
* @example
|
|
2139
|
+
* ```ts
|
|
2140
|
+
* for await (const account of peppol.bankAccounts.listAll()) {
|
|
2141
|
+
* console.log(account.name, account.iban);
|
|
2142
|
+
* }
|
|
2143
|
+
* ```
|
|
2144
|
+
*/
|
|
2145
|
+
listAll(options) {
|
|
2146
|
+
return paginate((offset, limit) => this.adapter.listBankAccounts({ ...options, offset, limit }));
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
var TransportOperations = class {
|
|
2150
|
+
adapter;
|
|
2151
|
+
constructor(adapter) {
|
|
2152
|
+
this.adapter = adapter;
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* List all available transport types in the network.
|
|
2156
|
+
* Returns global transport types (not account-scoped).
|
|
2157
|
+
*
|
|
2158
|
+
* @example
|
|
2159
|
+
* ```ts
|
|
2160
|
+
* const types = await peppol.transports.listTypes();
|
|
2161
|
+
* console.log(types); // [{ code: "peppol", name: "Peppol BIS 3.0" }, ...]
|
|
2162
|
+
* ```
|
|
2163
|
+
*/
|
|
2164
|
+
async listTypes() {
|
|
2165
|
+
return this.adapter.listTransportTypes();
|
|
2166
|
+
}
|
|
2167
|
+
/**
|
|
2168
|
+
* List configured transports for this account.
|
|
2169
|
+
*
|
|
2170
|
+
* @example
|
|
2171
|
+
* ```ts
|
|
2172
|
+
* const transports = await peppol.transports.list();
|
|
2173
|
+
* console.log(transports); // [{ id: "t-1", transportTypeCode: "peppol", name: "..." }, ...]
|
|
2174
|
+
* ```
|
|
2175
|
+
*/
|
|
2176
|
+
async list() {
|
|
2177
|
+
return this.adapter.listTransports();
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Get a single transport by code.
|
|
2181
|
+
*
|
|
2182
|
+
* @example
|
|
2183
|
+
* ```ts
|
|
2184
|
+
* const transport = await peppol.transports.get("peppol");
|
|
2185
|
+
* ```
|
|
2186
|
+
*/
|
|
2187
|
+
async get(code) {
|
|
2188
|
+
return this.adapter.getTransport(code);
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Create a new transport.
|
|
2192
|
+
*
|
|
2193
|
+
* @example
|
|
2194
|
+
* ```ts
|
|
2195
|
+
* const transport = await peppol.transports.create({
|
|
2196
|
+
* transportTypeCode: "peppol",
|
|
2197
|
+
* email: "billing@acme.com",
|
|
2198
|
+
* });
|
|
2199
|
+
* ```
|
|
2200
|
+
*/
|
|
2201
|
+
async create(input) {
|
|
2202
|
+
return this.adapter.createTransport(input);
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* Update an existing transport.
|
|
2206
|
+
*
|
|
2207
|
+
* @example
|
|
2208
|
+
* ```ts
|
|
2209
|
+
* const transport = await peppol.transports.update("peppol", { email: "new@acme.com" });
|
|
2210
|
+
* ```
|
|
2211
|
+
*/
|
|
2212
|
+
async update(code, input) {
|
|
2213
|
+
return this.adapter.updateTransport(code, input);
|
|
2214
|
+
}
|
|
2215
|
+
/**
|
|
2216
|
+
* Delete a transport.
|
|
2217
|
+
*
|
|
2218
|
+
* @example
|
|
2219
|
+
* ```ts
|
|
2220
|
+
* await peppol.transports.delete("peppol");
|
|
2221
|
+
* ```
|
|
2222
|
+
*/
|
|
2223
|
+
async delete(code) {
|
|
2224
|
+
return this.adapter.deleteTransport(code);
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
|
|
2228
|
+
// ../sdk/dist/core/code-lists.js
|
|
2229
|
+
var EAS_SCHEMES = [
|
|
2230
|
+
{ code: "0002", name: "System Information et Repertoire des Entreprises et des Etablissements (SIRENE)", country: "FR" },
|
|
2231
|
+
{ code: "0007", name: "Organisationsnummer", country: "SE" },
|
|
2232
|
+
{ code: "0009", name: "SIRET-CODE", country: "FR" },
|
|
2233
|
+
{ code: "0088", name: "EAN Location Code (GLN)" },
|
|
2234
|
+
{ code: "0096", name: "Danish Chamber of Commerce (P-nummer)", country: "DK" },
|
|
2235
|
+
{ code: "0184", name: "Danish Central Business Register (CVR)", country: "DK" },
|
|
2236
|
+
{ code: "0190", name: "Dutch Chamber of Commerce (KVK)", country: "NL" },
|
|
2237
|
+
{ code: "0191", name: "Organisatie Identificatie Nummer (OIN)", country: "NL" },
|
|
2238
|
+
{ code: "0192", name: "Danish SE-number (Erhvervsstyrelsen)", country: "DK" },
|
|
2239
|
+
{ code: "0195", name: "Singapore Unique Entity Number (UEN)", country: "SG" },
|
|
2240
|
+
{ code: "0196", name: "Icelandic Kennitala", country: "IS" },
|
|
2241
|
+
{ code: "0198", name: "Danish ERST id (Erhvervsstyrelsen)", country: "DK" },
|
|
2242
|
+
{ code: "0200", name: "Lithuanian Legal Entity Register (GRIS)", country: "LT" },
|
|
2243
|
+
{ code: "0201", name: "Italian Codice Destinatario", country: "IT" },
|
|
2244
|
+
{ code: "0202", name: "Italian Fiscal Code (Codice Fiscale)", country: "IT" },
|
|
2245
|
+
{ code: "0204", name: "German Leitweg-ID", country: "DE" },
|
|
2246
|
+
{ code: "0208", name: "Belgian Enterprise Number (KBO/BCE)", country: "BE" },
|
|
2247
|
+
{ code: "0209", name: "German Creditor Identifier (GS1)", country: "DE" },
|
|
2248
|
+
{ code: "0210", name: "Italian Codice Fiscale (per IPA)", country: "IT" },
|
|
2249
|
+
{ code: "0211", name: "Italian Partita IVA (VAT number)", country: "IT" },
|
|
2250
|
+
{ code: "0212", name: "Finnish OVT code", country: "FI" },
|
|
2251
|
+
{ code: "0213", name: "Finnish OP identifier", country: "FI" }
|
|
2252
|
+
];
|
|
2253
|
+
var EAS_BY_CODE = new Map(EAS_SCHEMES.map((s) => [s.code, s]));
|
|
2254
|
+
var UNIT_ALIAS_MAP = {
|
|
2255
|
+
each: "EA",
|
|
2256
|
+
piece: "EA",
|
|
2257
|
+
pieces: "EA",
|
|
2258
|
+
hour: "HUR",
|
|
2259
|
+
hours: "HUR",
|
|
2260
|
+
day: "DAY",
|
|
2261
|
+
days: "DAY",
|
|
2262
|
+
week: "WEE",
|
|
2263
|
+
weeks: "WEE",
|
|
2264
|
+
month: "MON",
|
|
2265
|
+
months: "MON",
|
|
2266
|
+
year: "ANN",
|
|
2267
|
+
years: "ANN",
|
|
2268
|
+
kilogram: "KGM",
|
|
2269
|
+
kg: "KGM",
|
|
2270
|
+
meter: "MTR",
|
|
2271
|
+
metre: "MTR",
|
|
2272
|
+
liter: "LTR",
|
|
2273
|
+
litre: "LTR",
|
|
2274
|
+
unit: "C62",
|
|
2275
|
+
units: "C62",
|
|
2276
|
+
set: "SET",
|
|
2277
|
+
sets: "SET",
|
|
2278
|
+
pack: "PK",
|
|
2279
|
+
packs: "PK",
|
|
2280
|
+
minute: "MIN",
|
|
2281
|
+
minutes: "MIN",
|
|
2282
|
+
second: "SEC",
|
|
2283
|
+
seconds: "SEC",
|
|
2284
|
+
tonne: "TNE",
|
|
2285
|
+
ton: "TNE",
|
|
2286
|
+
"square metre": "MTK",
|
|
2287
|
+
"square meter": "MTK",
|
|
2288
|
+
sqm: "MTK"
|
|
2289
|
+
};
|
|
2290
|
+
var UNIT_CODES = /* @__PURE__ */ new Map([
|
|
2291
|
+
["EA", "Each"],
|
|
2292
|
+
["HUR", "Hour"],
|
|
2293
|
+
["DAY", "Day"],
|
|
2294
|
+
["WEE", "Week"],
|
|
2295
|
+
["MON", "Month"],
|
|
2296
|
+
["ANN", "Year"],
|
|
2297
|
+
["MIN", "Minute"],
|
|
2298
|
+
["SEC", "Second"],
|
|
2299
|
+
["KGM", "Kilogram"],
|
|
2300
|
+
["MTR", "Metre"],
|
|
2301
|
+
["LTR", "Litre"],
|
|
2302
|
+
["MTK", "Square metre"],
|
|
2303
|
+
["TNE", "Tonne"],
|
|
2304
|
+
["C62", "One (unit)"],
|
|
2305
|
+
["SET", "Set"],
|
|
2306
|
+
["PK", "Pack"]
|
|
2307
|
+
]);
|
|
2308
|
+
function resolveUnit(input) {
|
|
2309
|
+
return UNIT_ALIAS_MAP[input.toLowerCase()] ?? input;
|
|
2310
|
+
}
|
|
2311
|
+
function getAllUnits() {
|
|
2312
|
+
return Array.from(UNIT_CODES.entries()).map(([code, name]) => ({ code, name })).sort((a, b) => a.code.localeCompare(b.code));
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// ../sdk/dist/core/schematron.js
|
|
2316
|
+
function violation(ruleId, severity, message, field) {
|
|
2317
|
+
return { ruleId, severity, message, field };
|
|
2318
|
+
}
|
|
2319
|
+
var _knownUnitCodes;
|
|
2320
|
+
function getKnownUnitCodes() {
|
|
2321
|
+
if (!_knownUnitCodes) {
|
|
2322
|
+
_knownUnitCodes = new Set(getAllUnits().map((u) => u.code));
|
|
2323
|
+
}
|
|
2324
|
+
return _knownUnitCodes;
|
|
2325
|
+
}
|
|
2326
|
+
var VALID_VAT_CATEGORIES = /* @__PURE__ */ new Set(["S", "Z", "E", "AE", "K", "G", "O", "L", "M"]);
|
|
2327
|
+
function computeLineNet(line) {
|
|
2328
|
+
const baseQty = line.baseQuantity ?? 1;
|
|
2329
|
+
if (baseQty === 0)
|
|
2330
|
+
return NaN;
|
|
2331
|
+
const baseAmount = line.quantity * line.unitPrice / baseQty;
|
|
2332
|
+
const chargeTotal = (line.charges ?? []).reduce((sum, c) => sum + c.amount, 0);
|
|
2333
|
+
const allowanceTotal = (line.allowances ?? []).reduce((sum, a) => sum + a.amount, 0);
|
|
2334
|
+
return baseAmount + chargeTotal - allowanceTotal;
|
|
2335
|
+
}
|
|
2336
|
+
var br02 = (input) => {
|
|
2337
|
+
if (!input.number?.trim()) {
|
|
2338
|
+
return [violation("BR-02", "error", "Invoice number is required.", "number")];
|
|
2339
|
+
}
|
|
2340
|
+
return [];
|
|
2341
|
+
};
|
|
2342
|
+
var br03 = (input) => {
|
|
2343
|
+
if (!input.date) {
|
|
2344
|
+
return [
|
|
2345
|
+
violation("BR-03", "warning", "Invoice issue date is not set. The SDK will default to today's date.", "date")
|
|
2346
|
+
];
|
|
2347
|
+
}
|
|
2348
|
+
return [];
|
|
2349
|
+
};
|
|
2350
|
+
var br06 = (input) => {
|
|
2351
|
+
if (input.from && !input.from.vatNumber) {
|
|
2352
|
+
return [
|
|
2353
|
+
violation("BR-06", "warning", "Seller party has no VAT number. The gateway will use the account's VAT registration.", "from.vatNumber")
|
|
2354
|
+
];
|
|
2355
|
+
}
|
|
2356
|
+
return [];
|
|
2357
|
+
};
|
|
2358
|
+
var br07 = (input) => {
|
|
2359
|
+
if (!input.to?.name?.trim()) {
|
|
2360
|
+
return [violation("BR-07", "error", "Buyer name is required.", "to.name")];
|
|
2361
|
+
}
|
|
2362
|
+
return [];
|
|
2363
|
+
};
|
|
2364
|
+
var br08 = (input) => {
|
|
2365
|
+
if (!input.lines || input.lines.length === 0) {
|
|
2366
|
+
return [violation("BR-08", "error", "Invoice must have at least one line item.", "lines")];
|
|
2367
|
+
}
|
|
2368
|
+
return [];
|
|
2369
|
+
};
|
|
2370
|
+
var br09 = (input) => {
|
|
2371
|
+
if (!input.dueDate && !input.paymentTerms) {
|
|
2372
|
+
return [
|
|
2373
|
+
violation("BR-09", "warning", "Neither due date nor payment terms specified. At least one is recommended.", "dueDate")
|
|
2374
|
+
];
|
|
2375
|
+
}
|
|
2376
|
+
return [];
|
|
2377
|
+
};
|
|
2378
|
+
var br10 = (input) => {
|
|
2379
|
+
if (!input.buyerReference && !input.orderReference) {
|
|
2380
|
+
return [
|
|
2381
|
+
violation("BR-10", "warning", "Neither buyerReference nor orderReference specified. Peppol BIS 3.0 requires at least one.", "buyerReference")
|
|
2382
|
+
];
|
|
2383
|
+
}
|
|
2384
|
+
return [];
|
|
2385
|
+
};
|
|
2386
|
+
var brCo10 = (input) => {
|
|
2387
|
+
const violations = [];
|
|
2388
|
+
if (!input.lines)
|
|
2389
|
+
return violations;
|
|
2390
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2391
|
+
const line = input.lines[i];
|
|
2392
|
+
const net = computeLineNet(line);
|
|
2393
|
+
if (!Number.isFinite(net)) {
|
|
2394
|
+
const baseQty = line.baseQuantity ?? 1;
|
|
2395
|
+
const detail = baseQty === 0 ? "baseQuantity is 0, causing division by zero." : "Computed line amount is not a finite number.";
|
|
2396
|
+
violations.push(violation("BR-CO-10", "error", `Line ${i}: invalid net amount. ${detail}`, `lines[${i}]`));
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
return violations;
|
|
2400
|
+
};
|
|
2401
|
+
var brCo13 = (input) => {
|
|
2402
|
+
if (!input.lines || input.lines.length === 0)
|
|
2403
|
+
return [];
|
|
2404
|
+
let totalVat = 0;
|
|
2405
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2406
|
+
const line = input.lines[i];
|
|
2407
|
+
const net = computeLineNet(line);
|
|
2408
|
+
if (!Number.isFinite(net))
|
|
2409
|
+
continue;
|
|
2410
|
+
totalVat += net * (line.vatRate / 100);
|
|
2411
|
+
}
|
|
2412
|
+
for (const allowance of input.allowances ?? []) {
|
|
2413
|
+
totalVat -= allowance.amount * (allowance.vatRate / 100);
|
|
2414
|
+
}
|
|
2415
|
+
for (const charge of input.charges ?? []) {
|
|
2416
|
+
totalVat += charge.amount * (charge.vatRate / 100);
|
|
2417
|
+
}
|
|
2418
|
+
if (!Number.isFinite(totalVat)) {
|
|
2419
|
+
return [
|
|
2420
|
+
violation("BR-CO-13", "error", "Computed total VAT amount is not a finite number. Check line amounts and VAT rates.")
|
|
2421
|
+
];
|
|
2422
|
+
}
|
|
2423
|
+
if (totalVat < -0.01) {
|
|
2424
|
+
return [
|
|
2425
|
+
violation("BR-CO-13", "warning", `Computed total VAT is negative (${totalVat.toFixed(2)}). This is unusual for an invoice.`)
|
|
2426
|
+
];
|
|
2427
|
+
}
|
|
2428
|
+
return [];
|
|
2429
|
+
};
|
|
2430
|
+
var brCo15 = (input) => {
|
|
2431
|
+
if (!input.lines || input.lines.length === 0)
|
|
2432
|
+
return [];
|
|
2433
|
+
let lineTotal = 0;
|
|
2434
|
+
let vatTotal = 0;
|
|
2435
|
+
for (const line of input.lines) {
|
|
2436
|
+
const net = computeLineNet(line);
|
|
2437
|
+
if (!Number.isFinite(net))
|
|
2438
|
+
continue;
|
|
2439
|
+
lineTotal += net;
|
|
2440
|
+
vatTotal += net * (line.vatRate / 100);
|
|
2441
|
+
}
|
|
2442
|
+
for (const allowance of input.allowances ?? []) {
|
|
2443
|
+
lineTotal -= allowance.amount;
|
|
2444
|
+
vatTotal -= allowance.amount * (allowance.vatRate / 100);
|
|
2445
|
+
}
|
|
2446
|
+
for (const charge of input.charges ?? []) {
|
|
2447
|
+
lineTotal += charge.amount;
|
|
2448
|
+
vatTotal += charge.amount * (charge.vatRate / 100);
|
|
2449
|
+
}
|
|
2450
|
+
const taxInclusive = lineTotal + vatTotal;
|
|
2451
|
+
if (!Number.isFinite(taxInclusive)) {
|
|
2452
|
+
return [
|
|
2453
|
+
violation("BR-CO-15", "error", "Computed tax-inclusive amount is not a finite number.")
|
|
2454
|
+
];
|
|
2455
|
+
}
|
|
2456
|
+
if (taxInclusive < -0.01) {
|
|
2457
|
+
return [
|
|
2458
|
+
violation("BR-CO-15", "warning", `Computed tax-inclusive amount is negative (${taxInclusive.toFixed(2)}). Consider using a credit note instead.`)
|
|
2459
|
+
];
|
|
2460
|
+
}
|
|
2461
|
+
return [];
|
|
2462
|
+
};
|
|
2463
|
+
var brCo16 = (input) => {
|
|
2464
|
+
if (!input.lines || input.lines.length === 0)
|
|
2465
|
+
return [];
|
|
2466
|
+
let lineTotal = 0;
|
|
2467
|
+
let vatTotal = 0;
|
|
2468
|
+
for (const line of input.lines) {
|
|
2469
|
+
const net = computeLineNet(line);
|
|
2470
|
+
if (!Number.isFinite(net))
|
|
2471
|
+
continue;
|
|
2472
|
+
lineTotal += net;
|
|
2473
|
+
vatTotal += net * (line.vatRate / 100);
|
|
2474
|
+
}
|
|
2475
|
+
for (const allowance of input.allowances ?? []) {
|
|
2476
|
+
lineTotal -= allowance.amount;
|
|
2477
|
+
vatTotal -= allowance.amount * (allowance.vatRate / 100);
|
|
2478
|
+
}
|
|
2479
|
+
for (const charge of input.charges ?? []) {
|
|
2480
|
+
lineTotal += charge.amount;
|
|
2481
|
+
vatTotal += charge.amount * (charge.vatRate / 100);
|
|
2482
|
+
}
|
|
2483
|
+
const taxInclusive = lineTotal + vatTotal;
|
|
2484
|
+
const prepaid = input.prepaidAmount ?? 0;
|
|
2485
|
+
const rounding = input.roundingAmount ?? 0;
|
|
2486
|
+
const payable = taxInclusive - prepaid + rounding;
|
|
2487
|
+
if (!Number.isFinite(payable)) {
|
|
2488
|
+
return [
|
|
2489
|
+
violation("BR-CO-16", "error", "Computed payable amount is not a finite number.")
|
|
2490
|
+
];
|
|
2491
|
+
}
|
|
2492
|
+
if (payable < -0.01) {
|
|
2493
|
+
return [
|
|
2494
|
+
violation("BR-CO-16", "warning", `Computed payable amount is negative (${payable.toFixed(2)}). Prepaid amount (${prepaid}) exceeds the invoice total.`)
|
|
2495
|
+
];
|
|
2496
|
+
}
|
|
2497
|
+
return [];
|
|
2498
|
+
};
|
|
2499
|
+
var brS01 = (input) => {
|
|
2500
|
+
const violations = [];
|
|
2501
|
+
if (!input.lines)
|
|
2502
|
+
return violations;
|
|
2503
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2504
|
+
const line = input.lines[i];
|
|
2505
|
+
const category = line.vatCategory ?? "S";
|
|
2506
|
+
if (category === "S" && (line.vatRate === void 0 || line.vatRate <= 0)) {
|
|
2507
|
+
violations.push(violation("BR-S-01", "error", `Line ${i}: standard rate (S) requires vatRate > 0, got ${line.vatRate ?? "undefined"}.`, `lines[${i}].vatRate`));
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return violations;
|
|
2511
|
+
};
|
|
2512
|
+
var brS05 = (input) => {
|
|
2513
|
+
const violations = [];
|
|
2514
|
+
if (!input.lines)
|
|
2515
|
+
return violations;
|
|
2516
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2517
|
+
const line = input.lines[i];
|
|
2518
|
+
if (line.vatCategory === "Z" && line.vatRate !== 0) {
|
|
2519
|
+
violations.push(violation("BR-S-05", "error", `Line ${i}: zero-rated (Z) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
return violations;
|
|
2523
|
+
};
|
|
2524
|
+
var brS06 = (input) => {
|
|
2525
|
+
const violations = [];
|
|
2526
|
+
if (!input.lines)
|
|
2527
|
+
return violations;
|
|
2528
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2529
|
+
const line = input.lines[i];
|
|
2530
|
+
if (line.vatCategory === "E" && line.vatRate !== 0) {
|
|
2531
|
+
violations.push(violation("BR-S-06", "error", `Line ${i}: exempt (E) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return violations;
|
|
2535
|
+
};
|
|
2536
|
+
var brS08 = (input) => {
|
|
2537
|
+
const violations = [];
|
|
2538
|
+
if (!input.lines)
|
|
2539
|
+
return violations;
|
|
2540
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2541
|
+
const line = input.lines[i];
|
|
2542
|
+
if (line.vatCategory === "AE" && line.vatRate !== 0) {
|
|
2543
|
+
violations.push(violation("BR-S-08", "error", `Line ${i}: reverse charge (AE) requires vatRate = 0, got ${line.vatRate}.`, `lines[${i}].vatRate`));
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return violations;
|
|
2547
|
+
};
|
|
2548
|
+
var peppolR004 = (input) => {
|
|
2549
|
+
if (!input.to?.peppolId) {
|
|
2550
|
+
return [
|
|
2551
|
+
violation("PEPPOL-EN16931-R004", "error", "Buyer electronic address (peppolId) is required for Peppol delivery.", "to.peppolId")
|
|
2552
|
+
];
|
|
26
2553
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
function formatWarning(item) {
|
|
41
|
-
const ruleId = "ruleId" in item && item.ruleId ? pc.dim(` (${item.ruleId})`) : "";
|
|
42
|
-
const field = "field" in item && item.field ? `${item.field} \u2014 ` : "";
|
|
43
|
-
return ` ${pc.yellow("\u26A0")} ${field}${item.message}${ruleId}`;
|
|
44
|
-
}
|
|
45
|
-
function formatSection(title, errors, warnings) {
|
|
46
|
-
const lines = [sectionHeader(title)];
|
|
47
|
-
if (errors.length === 0 && warnings.length === 0) {
|
|
48
|
-
lines.push(` ${pc.green("\u2713")} All rules passed`);
|
|
49
|
-
return lines.join("\n");
|
|
2554
|
+
return [];
|
|
2555
|
+
};
|
|
2556
|
+
var peppolR006 = (input) => {
|
|
2557
|
+
const violations = [];
|
|
2558
|
+
if (!input.lines)
|
|
2559
|
+
return violations;
|
|
2560
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2561
|
+
const line = input.lines[i];
|
|
2562
|
+
const cat = line.vatCategory;
|
|
2563
|
+
if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
|
|
2564
|
+
violations.push(violation("PEPPOL-EN16931-R006", "error", `Line ${i}: invalid VAT category "${cat}". Must be one of: S, Z, E, AE, K, G, O, L, M.`, `lines[${i}].vatCategory`));
|
|
2565
|
+
}
|
|
50
2566
|
}
|
|
51
|
-
|
|
52
|
-
|
|
2567
|
+
for (let i = 0; i < (input.allowances ?? []).length; i++) {
|
|
2568
|
+
const cat = input.allowances[i].vatCategory;
|
|
2569
|
+
if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
|
|
2570
|
+
violations.push(violation("PEPPOL-EN16931-R006", "error", `Allowance ${i}: invalid VAT category "${cat}".`, `allowances[${i}].vatCategory`));
|
|
2571
|
+
}
|
|
53
2572
|
}
|
|
54
|
-
for (
|
|
55
|
-
|
|
2573
|
+
for (let i = 0; i < (input.charges ?? []).length; i++) {
|
|
2574
|
+
const cat = input.charges[i].vatCategory;
|
|
2575
|
+
if (cat !== void 0 && !VALID_VAT_CATEGORIES.has(cat)) {
|
|
2576
|
+
violations.push(violation("PEPPOL-EN16931-R006", "error", `Charge ${i}: invalid VAT category "${cat}".`, `charges[${i}].vatCategory`));
|
|
2577
|
+
}
|
|
56
2578
|
}
|
|
57
|
-
|
|
58
|
-
|
|
2579
|
+
return violations;
|
|
2580
|
+
};
|
|
2581
|
+
var peppolR080 = (input) => {
|
|
2582
|
+
const violations = [];
|
|
2583
|
+
if (!input.lines)
|
|
2584
|
+
return violations;
|
|
2585
|
+
const knownCodes = getKnownUnitCodes();
|
|
2586
|
+
for (let i = 0; i < input.lines.length; i++) {
|
|
2587
|
+
const line = input.lines[i];
|
|
2588
|
+
if (line.unit) {
|
|
2589
|
+
const resolved = resolveUnit(line.unit);
|
|
2590
|
+
if (!knownCodes.has(resolved)) {
|
|
2591
|
+
violations.push(violation("PEPPOL-EN16931-R080", "warning", `Line ${i}: unit "${line.unit}" (resolved: "${resolved}") is not a known UN/ECE Rec20 code. Common codes: EA, HUR, DAY, KGM.`, `lines[${i}].unit`));
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
59
2594
|
}
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
` ${pc.green(pc.bold("\u2713 Invoice is valid"))} ${pc.dim(`(${totalWarnings} warning${totalWarnings === 1 ? "" : "s"})`)}`
|
|
98
|
-
);
|
|
99
|
-
} else {
|
|
100
|
-
const parts = [];
|
|
101
|
-
parts.push(`${totalErrors} error${totalErrors === 1 ? "" : "s"}`);
|
|
102
|
-
if (totalWarnings > 0) {
|
|
103
|
-
parts.push(`${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`);
|
|
2595
|
+
return violations;
|
|
2596
|
+
};
|
|
2597
|
+
var ALL_RULES = [
|
|
2598
|
+
// Required fields (BR)
|
|
2599
|
+
br02,
|
|
2600
|
+
br03,
|
|
2601
|
+
br06,
|
|
2602
|
+
br07,
|
|
2603
|
+
br08,
|
|
2604
|
+
br09,
|
|
2605
|
+
br10,
|
|
2606
|
+
// Calculations (BR-CO)
|
|
2607
|
+
brCo10,
|
|
2608
|
+
brCo13,
|
|
2609
|
+
brCo15,
|
|
2610
|
+
brCo16,
|
|
2611
|
+
// Tax categories (BR-S)
|
|
2612
|
+
brS01,
|
|
2613
|
+
brS05,
|
|
2614
|
+
brS06,
|
|
2615
|
+
brS08,
|
|
2616
|
+
// Peppol-specific
|
|
2617
|
+
peppolR004,
|
|
2618
|
+
peppolR006,
|
|
2619
|
+
peppolR080
|
|
2620
|
+
];
|
|
2621
|
+
function validateSchematron(input) {
|
|
2622
|
+
const errors = [];
|
|
2623
|
+
const warnings = [];
|
|
2624
|
+
for (const rule of ALL_RULES) {
|
|
2625
|
+
const violations = rule(input);
|
|
2626
|
+
for (const v of violations) {
|
|
2627
|
+
if (v.severity === "error") {
|
|
2628
|
+
errors.push(v);
|
|
2629
|
+
} else {
|
|
2630
|
+
warnings.push(v);
|
|
2631
|
+
}
|
|
104
2632
|
}
|
|
105
|
-
lines.push(
|
|
106
|
-
` ${pc.red(pc.bold(`\u2717 ${parts.join(", ")}`))} \u2014 invoice non-compliant`
|
|
107
|
-
);
|
|
108
2633
|
}
|
|
109
|
-
|
|
110
|
-
|
|
2634
|
+
return {
|
|
2635
|
+
valid: errors.length === 0,
|
|
2636
|
+
errors,
|
|
2637
|
+
warnings
|
|
2638
|
+
};
|
|
111
2639
|
}
|
|
112
2640
|
|
|
113
2641
|
// src/commands/validate.ts
|
|
114
|
-
import {
|
|
115
|
-
validateInvoice,
|
|
116
|
-
validateSchematron,
|
|
117
|
-
validateCountryRules
|
|
118
|
-
} from "@getpeppr/sdk";
|
|
119
2642
|
function runValidation(input) {
|
|
120
2643
|
const structure = validateInvoice(input);
|
|
121
2644
|
const schematron = validateSchematron(input);
|
|
@@ -136,18 +2659,7 @@ function runValidation(input) {
|
|
|
136
2659
|
}
|
|
137
2660
|
function registerValidateCommand(program2) {
|
|
138
2661
|
program2.command("validate").description("Validate a Peppol invoice JSON file").argument("<file>", "path to invoice JSON file").option("--json", "output results as JSON").option("--quiet", "exit code only, no output").action(async (file, options) => {
|
|
139
|
-
const
|
|
140
|
-
if (!parseResult.ok) {
|
|
141
|
-
process.stderr.write(parseResult.error + "\n");
|
|
142
|
-
process.exit(2);
|
|
143
|
-
}
|
|
144
|
-
if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
|
|
145
|
-
process.stderr.write(
|
|
146
|
-
"Error: JSON file must contain an object, not an array or primitive\n"
|
|
147
|
-
);
|
|
148
|
-
process.exit(2);
|
|
149
|
-
}
|
|
150
|
-
const input = parseResult.data;
|
|
2662
|
+
const input = readAndValidateInvoiceJson(file);
|
|
151
2663
|
const result = runValidation(input);
|
|
152
2664
|
if (options.quiet) {
|
|
153
2665
|
process.exit(result.valid ? 0 : 1);
|
|
@@ -249,10 +2761,9 @@ function registerInitCommand(program2) {
|
|
|
249
2761
|
(filename, options) => {
|
|
250
2762
|
const resolved = resolve2(filename);
|
|
251
2763
|
if (existsSync2(resolved) && !options.force) {
|
|
252
|
-
|
|
2764
|
+
exitWithError(
|
|
253
2765
|
`Error: ${filename} already exists. Use --force to overwrite.`
|
|
254
2766
|
);
|
|
255
|
-
process.exit(2);
|
|
256
2767
|
}
|
|
257
2768
|
const template = options.creditNote ? CREDIT_NOTE_TEMPLATE : INVOICE_TEMPLATE;
|
|
258
2769
|
try {
|
|
@@ -262,15 +2773,15 @@ function registerInitCommand(program2) {
|
|
|
262
2773
|
"utf-8"
|
|
263
2774
|
);
|
|
264
2775
|
} catch {
|
|
265
|
-
|
|
266
|
-
process.exit(2);
|
|
2776
|
+
exitWithError(`Error: could not write file \u2014 ${resolved}`);
|
|
267
2777
|
}
|
|
268
|
-
|
|
2778
|
+
process.stderr.write(`${pc2.green("\u2713")} Created ${filename}
|
|
269
2779
|
|
|
270
2780
|
Next steps:
|
|
271
2781
|
1. Edit the file with your invoice data
|
|
272
2782
|
2. Validate: getpeppr validate ${filename}
|
|
273
|
-
3. Convert to XML: getpeppr convert ${filename}
|
|
2783
|
+
3. Convert to XML: getpeppr convert ${filename}
|
|
2784
|
+
`);
|
|
274
2785
|
process.exit(0);
|
|
275
2786
|
}
|
|
276
2787
|
);
|
|
@@ -279,27 +2790,12 @@ function registerInitCommand(program2) {
|
|
|
279
2790
|
// src/commands/convert.ts
|
|
280
2791
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
281
2792
|
import pc3 from "picocolors";
|
|
282
|
-
import {
|
|
283
|
-
buildInvoiceXml,
|
|
284
|
-
buildCreditNoteXml
|
|
285
|
-
} from "@getpeppr/sdk";
|
|
286
2793
|
function registerConvertCommand(program2) {
|
|
287
2794
|
program2.command("convert").description(
|
|
288
2795
|
"Convert a getpeppr JSON invoice to Peppol BIS 3.0 UBL XML"
|
|
289
2796
|
).argument("<file>", "path to invoice JSON file").option("-o, --output <file>", "write XML to file instead of stdout").option("--validate", "validate the invoice before converting").action(
|
|
290
2797
|
async (file, options) => {
|
|
291
|
-
const
|
|
292
|
-
if (!parseResult.ok) {
|
|
293
|
-
process.stderr.write(parseResult.error + "\n");
|
|
294
|
-
process.exit(2);
|
|
295
|
-
}
|
|
296
|
-
if (typeof parseResult.data !== "object" || parseResult.data === null || Array.isArray(parseResult.data)) {
|
|
297
|
-
process.stderr.write(
|
|
298
|
-
"Error: JSON file must contain an object, not an array or primitive\n"
|
|
299
|
-
);
|
|
300
|
-
process.exit(2);
|
|
301
|
-
}
|
|
302
|
-
const input = parseResult.data;
|
|
2798
|
+
const input = readAndValidateInvoiceJson(file);
|
|
303
2799
|
if (options.validate) {
|
|
304
2800
|
const result = runValidation(input);
|
|
305
2801
|
const formatted = formatValidationResult(file, result);
|
|
@@ -321,9 +2817,7 @@ function registerConvertCommand(program2) {
|
|
|
321
2817
|
}
|
|
322
2818
|
} catch (err) {
|
|
323
2819
|
const message = err instanceof Error ? err.message : "Unknown error";
|
|
324
|
-
|
|
325
|
-
`);
|
|
326
|
-
process.exit(2);
|
|
2820
|
+
exitWithError(`Error: XML generation failed \u2014 ${message}`);
|
|
327
2821
|
}
|
|
328
2822
|
xml = xml.replace(/^[ \t]*\n/gm, "");
|
|
329
2823
|
const docType = isCreditNote ? "UBL 2.1 CreditNote" : "UBL 2.1 Invoice";
|
|
@@ -545,19 +3039,14 @@ function registerLookupCommand(program2) {
|
|
|
545
3039
|
const isSearch = Boolean(options.name);
|
|
546
3040
|
const isLookup = Boolean(peppolId);
|
|
547
3041
|
if (!isSearch && !isLookup) {
|
|
548
|
-
|
|
549
|
-
"Provide a Peppol ID or use --name to search.\n"
|
|
550
|
-
);
|
|
551
|
-
process.exit(2);
|
|
3042
|
+
exitWithError("Provide a Peppol ID or use --name to search.");
|
|
552
3043
|
}
|
|
553
3044
|
if (options.country) {
|
|
554
3045
|
const normalized = options.country.toUpperCase();
|
|
555
3046
|
if (!/^[A-Z]{2}$/.test(normalized)) {
|
|
556
|
-
|
|
557
|
-
`Invalid country code "${options.country}". Must be 2 letters (e.g. BE, DE, FR)
|
|
558
|
-
`
|
|
3047
|
+
exitWithError(
|
|
3048
|
+
`Invalid country code "${options.country}". Must be 2 letters (e.g. BE, DE, FR).`
|
|
559
3049
|
);
|
|
560
|
-
process.exit(2);
|
|
561
3050
|
}
|
|
562
3051
|
options.country = normalized;
|
|
563
3052
|
}
|
|
@@ -572,18 +3061,15 @@ function registerLookupCommand(program2) {
|
|
|
572
3061
|
async function handleLookup(peppolId, options) {
|
|
573
3062
|
const parsed = validatePeppolId(peppolId);
|
|
574
3063
|
if (!parsed.ok) {
|
|
575
|
-
|
|
576
|
-
process.exit(2);
|
|
3064
|
+
exitWithError(parsed.error);
|
|
577
3065
|
}
|
|
578
3066
|
let result;
|
|
579
3067
|
try {
|
|
580
3068
|
result = await lookupParticipant(parsed.scheme, parsed.id);
|
|
581
3069
|
} catch {
|
|
582
|
-
|
|
583
|
-
`${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection
|
|
584
|
-
`
|
|
3070
|
+
exitWithError(
|
|
3071
|
+
`${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
|
|
585
3072
|
);
|
|
586
|
-
process.exit(2);
|
|
587
3073
|
}
|
|
588
3074
|
if (!result) {
|
|
589
3075
|
if (options.json) {
|
|
@@ -605,11 +3091,9 @@ async function handleLookup(peppolId, options) {
|
|
|
605
3091
|
}
|
|
606
3092
|
async function handleSearch(options) {
|
|
607
3093
|
if (options.name && options.name.length < 3) {
|
|
608
|
-
|
|
609
|
-
`Search name must be at least 3 characters. Got: "${options.name}"
|
|
610
|
-
`
|
|
3094
|
+
exitWithError(
|
|
3095
|
+
`Search name must be at least 3 characters. Got: "${options.name}"`
|
|
611
3096
|
);
|
|
612
|
-
process.exit(2);
|
|
613
3097
|
}
|
|
614
3098
|
const limit = parseInt(options.limit ?? "10", 10);
|
|
615
3099
|
let result;
|
|
@@ -620,11 +3104,9 @@ async function handleSearch(options) {
|
|
|
620
3104
|
limit
|
|
621
3105
|
});
|
|
622
3106
|
} catch {
|
|
623
|
-
|
|
624
|
-
`${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection
|
|
625
|
-
`
|
|
3107
|
+
exitWithError(
|
|
3108
|
+
`${pc4.red("\u2717")} Could not reach Peppol Directory. Check your internet connection.`
|
|
626
3109
|
);
|
|
627
|
-
process.exit(2);
|
|
628
3110
|
}
|
|
629
3111
|
if (result.matches.length === 0) {
|
|
630
3112
|
if (options.json) {
|
|
@@ -642,6 +3124,522 @@ async function handleSearch(options) {
|
|
|
642
3124
|
process.exit(0);
|
|
643
3125
|
}
|
|
644
3126
|
|
|
3127
|
+
// src/commands/send.ts
|
|
3128
|
+
import pc6 from "picocolors";
|
|
3129
|
+
|
|
3130
|
+
// src/lib/credentials-store.ts
|
|
3131
|
+
import {
|
|
3132
|
+
chmodSync,
|
|
3133
|
+
existsSync as existsSync3,
|
|
3134
|
+
mkdirSync,
|
|
3135
|
+
readFileSync as readFileSync2,
|
|
3136
|
+
rmSync,
|
|
3137
|
+
statSync,
|
|
3138
|
+
writeFileSync as writeFileSync3,
|
|
3139
|
+
renameSync
|
|
3140
|
+
} from "fs";
|
|
3141
|
+
import { homedir, platform } from "os";
|
|
3142
|
+
import { dirname, join } from "path";
|
|
3143
|
+
function getCredentialsPath() {
|
|
3144
|
+
if (platform() === "win32") {
|
|
3145
|
+
const appdata = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
|
|
3146
|
+
return join(appdata, "getpeppr", "credentials.json");
|
|
3147
|
+
}
|
|
3148
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
3149
|
+
return join(xdg, "getpeppr", "credentials.json");
|
|
3150
|
+
}
|
|
3151
|
+
function readCredentials() {
|
|
3152
|
+
const path = getCredentialsPath();
|
|
3153
|
+
if (!existsSync3(path)) return null;
|
|
3154
|
+
if (platform() !== "win32") {
|
|
3155
|
+
const stats = statSync(path);
|
|
3156
|
+
const mode = stats.mode & 511;
|
|
3157
|
+
if (mode !== 384) {
|
|
3158
|
+
process.stderr.write(
|
|
3159
|
+
`\u26A0 Config file mode was ${mode.toString(8)}; restoring to 600.
|
|
3160
|
+
`
|
|
3161
|
+
);
|
|
3162
|
+
chmodSync(path, 384);
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
let raw;
|
|
3166
|
+
try {
|
|
3167
|
+
raw = readFileSync2(path, "utf-8");
|
|
3168
|
+
} catch {
|
|
3169
|
+
process.stderr.write(`\u26A0 Could not read config file: ${path}
|
|
3170
|
+
`);
|
|
3171
|
+
return null;
|
|
3172
|
+
}
|
|
3173
|
+
try {
|
|
3174
|
+
const data = JSON.parse(raw);
|
|
3175
|
+
return data;
|
|
3176
|
+
} catch {
|
|
3177
|
+
process.stderr.write(`\u26A0 Malformed JSON in config file: ${path}
|
|
3178
|
+
`);
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
function writeCredentials(creds) {
|
|
3183
|
+
const path = getCredentialsPath();
|
|
3184
|
+
const dir = dirname(path);
|
|
3185
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
3186
|
+
const tmpPath = `${path}.tmp`;
|
|
3187
|
+
const data = JSON.stringify(creds, null, 2) + "\n";
|
|
3188
|
+
try {
|
|
3189
|
+
rmSync(tmpPath, { force: true });
|
|
3190
|
+
} catch {
|
|
3191
|
+
}
|
|
3192
|
+
writeFileSync3(tmpPath, data, { mode: 384, flag: "wx" });
|
|
3193
|
+
chmodSync(tmpPath, 384);
|
|
3194
|
+
try {
|
|
3195
|
+
renameSync(tmpPath, path);
|
|
3196
|
+
} catch (e) {
|
|
3197
|
+
try {
|
|
3198
|
+
rmSync(tmpPath, { force: true });
|
|
3199
|
+
} catch {
|
|
3200
|
+
}
|
|
3201
|
+
throw e;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
function deleteCredentials() {
|
|
3205
|
+
const path = getCredentialsPath();
|
|
3206
|
+
if (!existsSync3(path)) return false;
|
|
3207
|
+
rmSync(path);
|
|
3208
|
+
return true;
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// src/lib/auth.ts
|
|
3212
|
+
var AuthError = class extends Error {
|
|
3213
|
+
constructor(message) {
|
|
3214
|
+
super(message);
|
|
3215
|
+
this.name = "AuthError";
|
|
3216
|
+
}
|
|
3217
|
+
};
|
|
3218
|
+
function resolveApiKey(opts) {
|
|
3219
|
+
const env = opts.forceProd ? "live" : "sandbox";
|
|
3220
|
+
if (opts.flagKey) {
|
|
3221
|
+
return { apiKey: opts.flagKey, source: "flag", environment: env };
|
|
3222
|
+
}
|
|
3223
|
+
const envKey = process.env.GETPEPPR_API_KEY;
|
|
3224
|
+
if (envKey) {
|
|
3225
|
+
return { apiKey: envKey, source: "env", environment: env };
|
|
3226
|
+
}
|
|
3227
|
+
const creds = readCredentials();
|
|
3228
|
+
if (creds) {
|
|
3229
|
+
const key = env === "live" ? creds.live : creds.sandbox;
|
|
3230
|
+
if (key) {
|
|
3231
|
+
return { apiKey: key, source: "config", environment: env };
|
|
3232
|
+
}
|
|
3233
|
+
if (env === "live") {
|
|
3234
|
+
throw new AuthError(
|
|
3235
|
+
`Config has no live API key. Run \`getpeppr login\` again with --live, or pass --key.`
|
|
3236
|
+
);
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
throw new AuthError(
|
|
3240
|
+
`No API key found. Run \`getpeppr login\` or set GETPEPPR_API_KEY.`
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
// src/lib/send-payload.ts
|
|
3245
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
3246
|
+
import { resolve as resolve3 } from "path";
|
|
3247
|
+
|
|
3248
|
+
// src/templates/send-default.ts
|
|
3249
|
+
var MINIMAL_PDF = `%PDF-1.0
|
|
3250
|
+
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
|
|
3251
|
+
xref
|
|
3252
|
+
0 4
|
|
3253
|
+
0000000000 65535 f
|
|
3254
|
+
0000000009 00000 n
|
|
3255
|
+
0000000058 00000 n
|
|
3256
|
+
0000000115 00000 n
|
|
3257
|
+
trailer<</Size 4/Root 1 0 R>>
|
|
3258
|
+
startxref
|
|
3259
|
+
190
|
|
3260
|
+
%%EOF`;
|
|
3261
|
+
var MINIMAL_TEST_PDF_BASE64 = Buffer.from(MINIMAL_PDF).toString("base64");
|
|
3262
|
+
function buildDefaultSendPayload(overrides = {}) {
|
|
3263
|
+
const today = /* @__PURE__ */ new Date();
|
|
3264
|
+
const due = new Date(today.getTime() + 30 * 864e5);
|
|
3265
|
+
const isoToday = today.toISOString().slice(0, 10);
|
|
3266
|
+
const isoDue = due.toISOString().slice(0, 10);
|
|
3267
|
+
const peppolId = overrides.to ?? "9925:BE0314595348";
|
|
3268
|
+
const amount = overrides.amount ?? 100;
|
|
3269
|
+
const currency = overrides.currency ?? "EUR";
|
|
3270
|
+
const description = overrides.description ?? "Test service from getpeppr";
|
|
3271
|
+
const randomSuffix = Math.floor(Math.random() * 65536).toString(16).toUpperCase().padStart(4, "0");
|
|
3272
|
+
const number = `TEST-${Date.now().toString(36).toUpperCase()}-${randomSuffix}`;
|
|
3273
|
+
const country = peppolId.split(":")[1]?.slice(0, 2) ?? "BE";
|
|
3274
|
+
const payload = {
|
|
3275
|
+
number,
|
|
3276
|
+
date: isoToday,
|
|
3277
|
+
dueDate: isoDue,
|
|
3278
|
+
currency,
|
|
3279
|
+
to: {
|
|
3280
|
+
name: peppolId === "9925:BE0314595348" ? "SPF Economie (TEST)" : "Test Recipient",
|
|
3281
|
+
peppolId,
|
|
3282
|
+
country,
|
|
3283
|
+
street: "Rue de la Loi 1",
|
|
3284
|
+
city: "Brussels",
|
|
3285
|
+
postalCode: "1000"
|
|
3286
|
+
},
|
|
3287
|
+
lines: [
|
|
3288
|
+
{
|
|
3289
|
+
description,
|
|
3290
|
+
quantity: 1,
|
|
3291
|
+
unitPrice: amount,
|
|
3292
|
+
vatRate: 0,
|
|
3293
|
+
// vatCategory "O" = "Services outside scope of tax" (UBL 2.1 / EN 16931).
|
|
3294
|
+
// Public entities (SPF Economie) are VAT-exempt. No `taxExemptReason` field
|
|
3295
|
+
// exists in InvoiceLine — Storecove derives exemption from the category code.
|
|
3296
|
+
// If sandbox returns 422 on this combination, fallback is vatRate: 21 + vatCategory: "S".
|
|
3297
|
+
vatCategory: "O"
|
|
3298
|
+
}
|
|
3299
|
+
]
|
|
3300
|
+
};
|
|
3301
|
+
if (overrides.attachment) {
|
|
3302
|
+
payload.attachments = [
|
|
3303
|
+
{
|
|
3304
|
+
id: "ATT-001",
|
|
3305
|
+
description: "Test document",
|
|
3306
|
+
filename: "test.pdf",
|
|
3307
|
+
mimeType: "application/pdf",
|
|
3308
|
+
content: MINIMAL_TEST_PDF_BASE64
|
|
3309
|
+
}
|
|
3310
|
+
];
|
|
3311
|
+
}
|
|
3312
|
+
return payload;
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
// src/lib/send-payload.ts
|
|
3316
|
+
var MutexError = class extends Error {
|
|
3317
|
+
constructor() {
|
|
3318
|
+
super("Cannot combine custom file with override flags. Use one or the other.");
|
|
3319
|
+
this.name = "MutexError";
|
|
3320
|
+
}
|
|
3321
|
+
};
|
|
3322
|
+
function hasOverrides(o) {
|
|
3323
|
+
if (!o) return false;
|
|
3324
|
+
return Boolean(
|
|
3325
|
+
o.to != null && o.to !== "" || o.amount != null || o.currency || o.description || o.attachment === true
|
|
3326
|
+
);
|
|
3327
|
+
}
|
|
3328
|
+
function buildPayload(opts) {
|
|
3329
|
+
if (opts.file && hasOverrides(opts.overrides)) {
|
|
3330
|
+
throw new MutexError();
|
|
3331
|
+
}
|
|
3332
|
+
if (opts.file) {
|
|
3333
|
+
const absPath = resolve3(opts.file);
|
|
3334
|
+
if (!existsSync4(absPath)) {
|
|
3335
|
+
throw new Error(`Error: file not found \u2014 ${absPath}`);
|
|
3336
|
+
}
|
|
3337
|
+
let raw;
|
|
3338
|
+
try {
|
|
3339
|
+
raw = readFileSync3(absPath, "utf-8");
|
|
3340
|
+
} catch {
|
|
3341
|
+
throw new Error(`Error: could not read file \u2014 ${absPath}`);
|
|
3342
|
+
}
|
|
3343
|
+
try {
|
|
3344
|
+
return JSON.parse(raw);
|
|
3345
|
+
} catch {
|
|
3346
|
+
throw new Error(`Error: invalid JSON in file \u2014 ${absPath}`);
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
return buildDefaultSendPayload(opts.overrides);
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
// src/lib/confirm.ts
|
|
3353
|
+
import { createInterface } from "readline";
|
|
3354
|
+
async function confirmInteractive(opts) {
|
|
3355
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
3356
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
3357
|
+
if (stdin.isTTY !== true) {
|
|
3358
|
+
return opts.defaultYes;
|
|
3359
|
+
}
|
|
3360
|
+
const suffix = opts.defaultYes ? "[Y/n]" : "[y/N]";
|
|
3361
|
+
return new Promise((resolve4) => {
|
|
3362
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
3363
|
+
let settled = false;
|
|
3364
|
+
rl.question(`${opts.prompt} ${suffix} `, (answer) => {
|
|
3365
|
+
settled = true;
|
|
3366
|
+
rl.close();
|
|
3367
|
+
const trimmed = answer.trim().toLowerCase();
|
|
3368
|
+
if (trimmed === "") return resolve4(opts.defaultYes);
|
|
3369
|
+
if (trimmed === "y" || trimmed === "yes") return resolve4(true);
|
|
3370
|
+
if (trimmed === "n" || trimmed === "no") return resolve4(false);
|
|
3371
|
+
return resolve4(opts.defaultYes);
|
|
3372
|
+
});
|
|
3373
|
+
rl.once("close", () => {
|
|
3374
|
+
if (!settled) resolve4(opts.defaultYes);
|
|
3375
|
+
});
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
// src/lib/watch.ts
|
|
3380
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([
|
|
3381
|
+
"delivered",
|
|
3382
|
+
"accepted",
|
|
3383
|
+
"rejected",
|
|
3384
|
+
"failed"
|
|
3385
|
+
]);
|
|
3386
|
+
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3387
|
+
async function pollUntilTerminal(client, documentId, options = {}) {
|
|
3388
|
+
const intervalMs = options.intervalMs ?? 2e3;
|
|
3389
|
+
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
3390
|
+
const start = Date.now();
|
|
3391
|
+
let lastStatus = "";
|
|
3392
|
+
while (Date.now() - start < timeoutMs) {
|
|
3393
|
+
const { status } = await client.invoices.getStatus(documentId);
|
|
3394
|
+
if (status !== lastStatus) {
|
|
3395
|
+
options.onTransition?.(status);
|
|
3396
|
+
lastStatus = status;
|
|
3397
|
+
}
|
|
3398
|
+
if (TERMINAL_STATES.has(status)) {
|
|
3399
|
+
return { finalStatus: status, timedOut: false };
|
|
3400
|
+
}
|
|
3401
|
+
await sleep2(intervalMs);
|
|
3402
|
+
}
|
|
3403
|
+
return { finalStatus: lastStatus, timedOut: true };
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
// src/formatters/send-result.ts
|
|
3407
|
+
import pc5 from "picocolors";
|
|
3408
|
+
function formatSendResult(result, mode) {
|
|
3409
|
+
if (mode === "quiet") return "";
|
|
3410
|
+
if (mode === "json") {
|
|
3411
|
+
return JSON.stringify(result, null, 2);
|
|
3412
|
+
}
|
|
3413
|
+
const lines = [];
|
|
3414
|
+
lines.push(`${pc5.green("\u2713")} Sent ${pc5.bold(result.number)}`);
|
|
3415
|
+
lines.push(` id: ${result.id}`);
|
|
3416
|
+
lines.push(` Status: ${pc5.cyan(result.status)}`);
|
|
3417
|
+
lines.push(` Track: ${pc5.dim(result.dashboardUrl)}`);
|
|
3418
|
+
const wCount = result.warnings?.length ?? 0;
|
|
3419
|
+
if (wCount > 0) {
|
|
3420
|
+
lines.push(` ${pc5.yellow(`${wCount} warning${wCount === 1 ? "" : "s"}`)}`);
|
|
3421
|
+
for (const w of result.warnings ?? []) {
|
|
3422
|
+
lines.push(` ${pc5.yellow("\u26A0")} ${w.message}`);
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
return lines.join("\n");
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// src/commands/send.ts
|
|
3429
|
+
var API_BASE = "https://api.getpeppr.dev/v1";
|
|
3430
|
+
var LOCAL_BASE = "http://localhost:3001/api/v1";
|
|
3431
|
+
var DASHBOARD_BASE = "https://console.getpeppr.dev/invoices";
|
|
3432
|
+
function registerSendCommand(program2) {
|
|
3433
|
+
program2.command("send").description("Send an invoice to the Peppol network via getpeppr API").argument("[file]", "optional path to invoice JSON (mutex with --to/--amount/...)").option("--prod", "target production (live keys + confirmation)").option("--local", "target localhost:3001 dev server").option("--key <key>", "override API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer GETPEPPR_API_KEY env var.").option("--to <peppol-id>", "recipient peppol id (e.g., 0208:BE0314595348)").option("--amount <number>", "line amount in major currency units (decimal allowed)").option("--currency <iso>", "ISO 4217 currency (default EUR)").option("--desc <text>", "line description").option("--attachment", "attach the test PDF").option("--watch", "poll status until delivered (60s timeout)").option("-y, --yes", "skip --prod confirmation prompt").option("--no-validate", "skip pre-validation locally").option("--json", "output JSON").option("--quiet", "exit code only, no output").action(async (file, flags) => {
|
|
3434
|
+
let auth;
|
|
3435
|
+
try {
|
|
3436
|
+
auth = resolveApiKey({
|
|
3437
|
+
flagKey: flags.key,
|
|
3438
|
+
forceProd: Boolean(flags.prod),
|
|
3439
|
+
forceLocal: Boolean(flags.local)
|
|
3440
|
+
});
|
|
3441
|
+
} catch (e) {
|
|
3442
|
+
if (e instanceof AuthError) {
|
|
3443
|
+
exitWithError(e.message);
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
throw e;
|
|
3447
|
+
}
|
|
3448
|
+
const overrides = {
|
|
3449
|
+
to: flags.to,
|
|
3450
|
+
amount: flags.amount != null ? Number(flags.amount) : void 0,
|
|
3451
|
+
currency: flags.currency,
|
|
3452
|
+
description: flags.desc,
|
|
3453
|
+
attachment: flags.attachment
|
|
3454
|
+
};
|
|
3455
|
+
let payload;
|
|
3456
|
+
try {
|
|
3457
|
+
payload = buildPayload({ file, overrides });
|
|
3458
|
+
} catch (e) {
|
|
3459
|
+
if (e instanceof MutexError) {
|
|
3460
|
+
exitWithError(e.message);
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
if (e instanceof Error) {
|
|
3464
|
+
exitWithError(e.message);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
throw e;
|
|
3468
|
+
}
|
|
3469
|
+
if (flags.validate !== false) {
|
|
3470
|
+
const result2 = runValidation(payload);
|
|
3471
|
+
if (!result2.valid) {
|
|
3472
|
+
process.stderr.write(
|
|
3473
|
+
`${pc6.red("\u2717")} Pre-validation failed (${result2.totalErrors} errors). Use --no-validate to skip.
|
|
3474
|
+
`
|
|
3475
|
+
);
|
|
3476
|
+
process.exit(2);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
if (flags.prod && !flags.yes) {
|
|
3480
|
+
const confirmed = await confirmInteractive({
|
|
3481
|
+
prompt: pc6.yellow("\u26A0 About to send a REAL invoice on the Peppol network. Continue?"),
|
|
3482
|
+
defaultYes: false
|
|
3483
|
+
});
|
|
3484
|
+
if (!confirmed) {
|
|
3485
|
+
if (!flags.quiet) process.stderr.write("Cancelled.\n");
|
|
3486
|
+
process.exit(0);
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
const baseUrl = flags.local ? LOCAL_BASE : API_BASE;
|
|
3490
|
+
const client = new Peppol({ apiKey: auth.apiKey, baseUrl });
|
|
3491
|
+
let result;
|
|
3492
|
+
try {
|
|
3493
|
+
result = await client.invoices.send(payload);
|
|
3494
|
+
} catch (e) {
|
|
3495
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3496
|
+
process.stderr.write(`${pc6.red("\u2717")} ${msg}
|
|
3497
|
+
`);
|
|
3498
|
+
process.exit(1);
|
|
3499
|
+
}
|
|
3500
|
+
const dashboardUrl = `${DASHBOARD_BASE}/${result.id}`;
|
|
3501
|
+
let finalStatus = result.status;
|
|
3502
|
+
let timedOut = false;
|
|
3503
|
+
if (flags.watch) {
|
|
3504
|
+
const onTransition = (s) => {
|
|
3505
|
+
if (!flags.quiet && !flags.json) {
|
|
3506
|
+
process.stderr.write(` ${pc6.cyan("\u2192")} ${s}
|
|
3507
|
+
`);
|
|
3508
|
+
}
|
|
3509
|
+
};
|
|
3510
|
+
try {
|
|
3511
|
+
const w = await pollUntilTerminal(client, result.id, {
|
|
3512
|
+
intervalMs: 2e3,
|
|
3513
|
+
timeoutMs: 6e4,
|
|
3514
|
+
onTransition
|
|
3515
|
+
});
|
|
3516
|
+
finalStatus = w.finalStatus;
|
|
3517
|
+
timedOut = w.timedOut;
|
|
3518
|
+
} catch (e) {
|
|
3519
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3520
|
+
process.stderr.write(`${pc6.yellow("\u26A0")} Watch error: ${msg}
|
|
3521
|
+
`);
|
|
3522
|
+
}
|
|
3523
|
+
if (timedOut) {
|
|
3524
|
+
process.stderr.write(
|
|
3525
|
+
`${pc6.yellow("\u26A0")} Timeout \u2014 invoice was sent but delivery not confirmed in 60s.
|
|
3526
|
+
`
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
const mode = flags.quiet ? "quiet" : flags.json ? "json" : "formatted";
|
|
3531
|
+
const output = formatSendResult(
|
|
3532
|
+
{
|
|
3533
|
+
id: result.id,
|
|
3534
|
+
number: payload.number,
|
|
3535
|
+
status: finalStatus,
|
|
3536
|
+
warnings: result.warnings,
|
|
3537
|
+
dashboardUrl
|
|
3538
|
+
},
|
|
3539
|
+
mode
|
|
3540
|
+
);
|
|
3541
|
+
if (output) process.stdout.write(output + "\n");
|
|
3542
|
+
if (finalStatus === "rejected" || finalStatus === "failed") {
|
|
3543
|
+
process.exit(1);
|
|
3544
|
+
}
|
|
3545
|
+
process.exit(0);
|
|
3546
|
+
});
|
|
3547
|
+
}
|
|
3548
|
+
|
|
3549
|
+
// src/commands/login.ts
|
|
3550
|
+
import { createInterface as createInterface2 } from "readline";
|
|
3551
|
+
import pc7 from "picocolors";
|
|
3552
|
+
async function promptMaskedKey(envLabel) {
|
|
3553
|
+
if (process.stdin.isTTY !== true) {
|
|
3554
|
+
exitWithError("Error: --key flag required when stdin is not a TTY (CI mode).");
|
|
3555
|
+
}
|
|
3556
|
+
process.stdout.write(`Paste your ${envLabel} API key (input hidden): `);
|
|
3557
|
+
return new Promise((resolve4) => {
|
|
3558
|
+
let buffer = "";
|
|
3559
|
+
const onData = (chunk) => {
|
|
3560
|
+
const c = chunk.toString("utf-8");
|
|
3561
|
+
if (c === "\n" || c === "\r" || c === "\r\n") {
|
|
3562
|
+
process.stdin.setRawMode(false);
|
|
3563
|
+
process.stdin.removeListener("data", onData);
|
|
3564
|
+
process.stdin.pause();
|
|
3565
|
+
process.stdout.write("\n");
|
|
3566
|
+
resolve4(buffer);
|
|
3567
|
+
return;
|
|
3568
|
+
}
|
|
3569
|
+
if (c === "") {
|
|
3570
|
+
process.stdin.setRawMode(false);
|
|
3571
|
+
process.stdin.removeListener("data", onData);
|
|
3572
|
+
process.stdin.pause();
|
|
3573
|
+
process.stdout.write("\n");
|
|
3574
|
+
process.exit(130);
|
|
3575
|
+
}
|
|
3576
|
+
if (c === "\x7F" || c === "\b") {
|
|
3577
|
+
buffer = buffer.slice(0, -1);
|
|
3578
|
+
return;
|
|
3579
|
+
}
|
|
3580
|
+
buffer += c;
|
|
3581
|
+
};
|
|
3582
|
+
process.stdin.setRawMode(true);
|
|
3583
|
+
process.stdin.resume();
|
|
3584
|
+
process.stdin.on("data", onData);
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
async function promptEnvironment() {
|
|
3588
|
+
if (process.stdin.isTTY !== true) return "sandbox";
|
|
3589
|
+
return new Promise((resolve4) => {
|
|
3590
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
3591
|
+
rl.question("Environment? (s)andbox / (l)ive [sandbox]: ", (answer) => {
|
|
3592
|
+
rl.close();
|
|
3593
|
+
const a = answer.trim().toLowerCase();
|
|
3594
|
+
if (a === "l" || a === "live") return resolve4("live");
|
|
3595
|
+
return resolve4("sandbox");
|
|
3596
|
+
});
|
|
3597
|
+
});
|
|
3598
|
+
}
|
|
3599
|
+
function registerLoginCommand(program2) {
|
|
3600
|
+
program2.command("login").description("Save a getpeppr API key to ~/.config/getpeppr/credentials.json").option("--key <key>", "API key \u2014 for CI/scripted use only; visible in `ps` and shell history. Prefer the interactive prompt or GETPEPPR_API_KEY env var.").option("--sandbox", "store as sandbox key (default)").option("--live", "store as live (production) key").action(async (flags) => {
|
|
3601
|
+
if (!flags.live && !flags.sandbox && process.stdin.isTTY !== true) {
|
|
3602
|
+
exitWithError("Error: --sandbox or --live required when stdin is not a TTY (CI mode).");
|
|
3603
|
+
}
|
|
3604
|
+
let env;
|
|
3605
|
+
if (flags.live) env = "live";
|
|
3606
|
+
else if (flags.sandbox) env = "sandbox";
|
|
3607
|
+
else env = await promptEnvironment();
|
|
3608
|
+
let key;
|
|
3609
|
+
if (flags.key) {
|
|
3610
|
+
key = flags.key;
|
|
3611
|
+
} else {
|
|
3612
|
+
key = (await promptMaskedKey(env)).trim();
|
|
3613
|
+
if (!key) exitWithError("Error: empty API key.");
|
|
3614
|
+
}
|
|
3615
|
+
const existing = readCredentials() ?? {};
|
|
3616
|
+
const next = { ...existing, [env]: key };
|
|
3617
|
+
writeCredentials(next);
|
|
3618
|
+
const path = getCredentialsPath();
|
|
3619
|
+
process.stderr.write(
|
|
3620
|
+
`${pc7.green("\u2713")} Saved ${env} key to ${path} (mode 600)
|
|
3621
|
+
`
|
|
3622
|
+
);
|
|
3623
|
+
});
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
// src/commands/logout.ts
|
|
3627
|
+
import pc8 from "picocolors";
|
|
3628
|
+
function registerLogoutCommand(program2) {
|
|
3629
|
+
program2.command("logout").description("Remove ~/.config/getpeppr/credentials.json").action(() => {
|
|
3630
|
+
const path = getCredentialsPath();
|
|
3631
|
+
const removed = deleteCredentials();
|
|
3632
|
+
if (removed) {
|
|
3633
|
+
process.stderr.write(`${pc8.green("\u2713")} Removed ${path}
|
|
3634
|
+
`);
|
|
3635
|
+
} else {
|
|
3636
|
+
process.stderr.write(`No credentials to remove (${path})
|
|
3637
|
+
`);
|
|
3638
|
+
}
|
|
3639
|
+
process.exit(0);
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
|
|
645
3643
|
// src/index.ts
|
|
646
3644
|
var require2 = createRequire(import.meta.url);
|
|
647
3645
|
var { version } = require2("../package.json");
|
|
@@ -651,5 +3649,8 @@ registerValidateCommand(program);
|
|
|
651
3649
|
registerInitCommand(program);
|
|
652
3650
|
registerConvertCommand(program);
|
|
653
3651
|
registerLookupCommand(program);
|
|
3652
|
+
registerSendCommand(program);
|
|
3653
|
+
registerLoginCommand(program);
|
|
3654
|
+
registerLogoutCommand(program);
|
|
654
3655
|
program.parse();
|
|
655
3656
|
//# sourceMappingURL=index.js.map
|