@voyant-travel/plugin-smartbill 0.119.2

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.
Files changed (96) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +324 -0
  3. package/dist/artifacts.d.ts +80 -0
  4. package/dist/artifacts.d.ts.map +1 -0
  5. package/dist/artifacts.js +295 -0
  6. package/dist/client/errors.d.ts +28 -0
  7. package/dist/client/errors.d.ts.map +1 -0
  8. package/dist/client/errors.js +32 -0
  9. package/dist/client/fetch.d.ts +4 -0
  10. package/dist/client/fetch.d.ts.map +1 -0
  11. package/dist/client/fetch.js +32 -0
  12. package/dist/client/rate-limit.d.ts +8 -0
  13. package/dist/client/rate-limit.d.ts.map +1 -0
  14. package/dist/client/rate-limit.js +54 -0
  15. package/dist/client/resilience.d.ts +36 -0
  16. package/dist/client/resilience.d.ts.map +1 -0
  17. package/dist/client/resilience.js +23 -0
  18. package/dist/client.d.ts +67 -0
  19. package/dist/client.d.ts.map +1 -0
  20. package/dist/client.js +234 -0
  21. package/dist/hono.d.ts +199 -0
  22. package/dist/hono.d.ts.map +1 -0
  23. package/dist/hono.js +77 -0
  24. package/dist/index.d.ts +22 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +10 -0
  27. package/dist/invoice-ui-data.d.ts +46 -0
  28. package/dist/invoice-ui-data.d.ts.map +1 -0
  29. package/dist/invoice-ui-data.js +62 -0
  30. package/dist/invoice-ui.d.ts +438 -0
  31. package/dist/invoice-ui.d.ts.map +1 -0
  32. package/dist/invoice-ui.js +97 -0
  33. package/dist/mapping.d.ts +50 -0
  34. package/dist/mapping.d.ts.map +1 -0
  35. package/dist/mapping.js +219 -0
  36. package/dist/mock/node.d.ts +4 -0
  37. package/dist/mock/node.d.ts.map +1 -0
  38. package/dist/mock/node.js +14 -0
  39. package/dist/mock/pdf.d.ts +7 -0
  40. package/dist/mock/pdf.d.ts.map +1 -0
  41. package/dist/mock/pdf.js +44 -0
  42. package/dist/mock/types.d.ts +94 -0
  43. package/dist/mock/types.d.ts.map +1 -0
  44. package/dist/mock/types.js +1 -0
  45. package/dist/mock.d.ts +4 -0
  46. package/dist/mock.d.ts.map +1 -0
  47. package/dist/mock.js +431 -0
  48. package/dist/plugin.d.ts +55 -0
  49. package/dist/plugin.d.ts.map +1 -0
  50. package/dist/plugin.js +192 -0
  51. package/dist/runtime.d.ts +25 -0
  52. package/dist/runtime.d.ts.map +1 -0
  53. package/dist/runtime.js +42 -0
  54. package/dist/settlement.d.ts +33 -0
  55. package/dist/settlement.d.ts.map +1 -0
  56. package/dist/settlement.js +65 -0
  57. package/dist/sync/events.d.ts +5 -0
  58. package/dist/sync/events.d.ts.map +1 -0
  59. package/dist/sync/events.js +96 -0
  60. package/dist/sync/helpers.d.ts +66 -0
  61. package/dist/sync/helpers.d.ts.map +1 -0
  62. package/dist/sync/helpers.js +353 -0
  63. package/dist/sync/invoice.d.ts +3 -0
  64. package/dist/sync/invoice.d.ts.map +1 -0
  65. package/dist/sync/invoice.js +25 -0
  66. package/dist/sync/types.d.ts +71 -0
  67. package/dist/sync/types.d.ts.map +1 -0
  68. package/dist/sync/types.js +1 -0
  69. package/dist/sync.d.ts +4 -0
  70. package/dist/sync.d.ts.map +1 -0
  71. package/dist/sync.js +2 -0
  72. package/dist/types.d.ts +189 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +1 -0
  75. package/dist/validation.d.ts +39 -0
  76. package/dist/validation.d.ts.map +1 -0
  77. package/dist/validation.js +120 -0
  78. package/dist/workflow-candidates.d.ts +36 -0
  79. package/dist/workflow-candidates.d.ts.map +1 -0
  80. package/dist/workflow-candidates.js +182 -0
  81. package/dist/workflow-remote-discovery.d.ts +6 -0
  82. package/dist/workflow-remote-discovery.d.ts.map +1 -0
  83. package/dist/workflow-remote-discovery.js +83 -0
  84. package/dist/workflows/refs.d.ts +13 -0
  85. package/dist/workflows/refs.d.ts.map +1 -0
  86. package/dist/workflows/refs.js +51 -0
  87. package/dist/workflows/spaced-client.d.ts +5 -0
  88. package/dist/workflows/spaced-client.d.ts.map +1 -0
  89. package/dist/workflows/spaced-client.js +44 -0
  90. package/dist/workflows/types.d.ts +142 -0
  91. package/dist/workflows/types.d.ts.map +1 -0
  92. package/dist/workflows/types.js +1 -0
  93. package/dist/workflows.d.ts +5 -0
  94. package/dist/workflows.d.ts.map +1 -0
  95. package/dist/workflows.js +229 -0
  96. package/package.json +129 -0
package/dist/mock.js ADDED
@@ -0,0 +1,431 @@
1
+ import { concat, importNodeHttp } from "./mock/node.js";
2
+ import { createPlaceholderPdf } from "./mock/pdf.js";
3
+ const defaultTaxes = [
4
+ { name: "Normala", percentage: 19, default: true },
5
+ { name: "Redusa", percentage: 9 },
6
+ { name: "Redusa", percentage: 5 },
7
+ { name: "Scutit", percentage: 0 },
8
+ ];
9
+ const defaultSeries = [
10
+ { name: "SB-TEST", type: "invoice", nextNumber: 1 },
11
+ { name: "PF-TEST", type: "estimate", nextNumber: 1 },
12
+ ];
13
+ export function createSmartbillMockServer(options = {}) {
14
+ const documents = new Map();
15
+ const taxes = options.taxes ?? defaultTaxes;
16
+ const initialSeries = options.series ?? defaultSeries;
17
+ const seriesCounters = new Map();
18
+ const now = options.now ?? (() => new Date());
19
+ function reset() {
20
+ documents.clear();
21
+ seriesCounters.clear();
22
+ for (const item of initialSeries) {
23
+ seriesCounters.set(seriesKey(item.type, item.name), { ...item });
24
+ }
25
+ }
26
+ function listDocuments() {
27
+ return [...documents.values()].map(cloneDocument);
28
+ }
29
+ function getDocument(kind, companyVatCode, seriesName, number) {
30
+ const document = documents.get(documentKey(kind, companyVatCode, seriesName, number));
31
+ return document ? cloneDocument(document) : null;
32
+ }
33
+ function convertEstimateToInvoice(args) {
34
+ const estimate = findRequiredDocument("estimate", args.companyVatCode, args.seriesName, args.number);
35
+ const invoice = createDocument("invoice", {
36
+ ...estimate.body,
37
+ seriesName: args.invoiceSeriesName ?? estimate.body.seriesName,
38
+ mentions: appendTestMention(estimate.body.mentions, `Converted from proforma ${args.seriesName}-${args.number}`),
39
+ });
40
+ const response = toCreateResponse(invoice);
41
+ estimate.convertedInvoices.push(response);
42
+ documents.set(documentKey(estimate.kind, estimate.companyVatCode, estimate.seriesName, estimate.number), estimate);
43
+ return response;
44
+ }
45
+ async function handleRequest(request) {
46
+ const method = request.method.toUpperCase();
47
+ const url = new URL(request.url, "http://smartbill-mock.local");
48
+ const path = normalizeSmartbillPath(url.pathname);
49
+ try {
50
+ if (method === "GET" && path === "/tax")
51
+ return json(200, taxesEnvelope(taxes));
52
+ if (method === "GET" && path === "/series")
53
+ return json(200, seriesEnvelope(listSeries()));
54
+ if (method === "POST" && path === "/invoice") {
55
+ const body = parseBody(request.body);
56
+ if (body.useEstimateDetails === true && body.estimate) {
57
+ return json(200, convertEstimateToInvoice({
58
+ companyVatCode: body.companyVatCode,
59
+ seriesName: body.estimate.seriesName,
60
+ number: body.estimate.number,
61
+ invoiceSeriesName: body.seriesName,
62
+ }));
63
+ }
64
+ return json(200, toCreateResponse(createDocument("invoice", body)));
65
+ }
66
+ if (method === "POST" && path === "/estimate") {
67
+ return json(200, toCreateResponse(createDocument("estimate", parseBody(request.body))));
68
+ }
69
+ if (method === "GET" && path === "/invoice/pdf") {
70
+ return pdf(findByQuery("invoice", url));
71
+ }
72
+ if (method === "GET" && path === "/estimate/pdf") {
73
+ return pdf(findByQuery("estimate", url));
74
+ }
75
+ if (method === "GET" && path === "/estimate/invoices") {
76
+ return json(200, estimateInvoicesEnvelope(findByQuery("estimate", url)));
77
+ }
78
+ if (method === "GET" && path === "/invoice/paymentstatus") {
79
+ return json(200, paymentStatusEnvelope(findByQuery("invoice", url)));
80
+ }
81
+ if (method === "PUT" && path === "/invoice/cancel") {
82
+ const invoice = updateInvoiceStatus(parseBody(request.body), "cancelled");
83
+ return json(200, ackEnvelope(`Invoice ${invoice.seriesName}-${invoice.number} cancelled`));
84
+ }
85
+ if (method === "PUT" && path === "/invoice/reverse") {
86
+ const invoice = updateInvoiceStatus(parseBody(request.body), "reversed");
87
+ const reversal = createDocument("invoice", {
88
+ ...invoice.body,
89
+ products: invoice.body.products.map((product) => ({
90
+ ...product,
91
+ quantity: -Math.abs(product.quantity),
92
+ })),
93
+ mentions: appendTestMention(invoice.body.mentions, `Reversal for ${invoice.seriesName}-${invoice.number}`),
94
+ });
95
+ return json(200, toCreateResponse(reversal));
96
+ }
97
+ if (method === "PUT" && path === "/invoice/restore") {
98
+ const invoice = updateInvoiceStatus(parseBody(request.body), "restored");
99
+ return json(200, ackEnvelope(`Invoice ${invoice.seriesName}-${invoice.number} restored`));
100
+ }
101
+ if (method === "DELETE" && path === "/invoice") {
102
+ const invoice = updateByQuery(url, "deleted");
103
+ return json(200, ackEnvelope(`Invoice ${invoice.seriesName}-${invoice.number} deleted`));
104
+ }
105
+ return json(404, errorEnvelope(`SmartBill mock endpoint not found: ${method} ${path}`));
106
+ }
107
+ catch (error) {
108
+ return json(error instanceof SmartbillMockError ? error.status : 500, errorEnvelope(error instanceof Error ? error.message : "SmartBill mock failed"));
109
+ }
110
+ }
111
+ const fetch = async (input, init) => {
112
+ const response = await handleRequest({
113
+ method: init.method,
114
+ url: input,
115
+ body: init.body,
116
+ });
117
+ const isBinary = response.body instanceof Uint8Array;
118
+ const bytes = isBinary
119
+ ? response.body
120
+ : new TextEncoder().encode(response.body);
121
+ const text = isBinary
122
+ ? new TextDecoder().decode(response.body)
123
+ : response.body;
124
+ return {
125
+ ok: response.status >= 200 && response.status < 300,
126
+ status: response.status,
127
+ json: async () => {
128
+ if (isBinary)
129
+ throw new Error("SmartBill mock response is not JSON");
130
+ return JSON.parse(text);
131
+ },
132
+ text: async () => text,
133
+ arrayBuffer: async () => {
134
+ const copy = new ArrayBuffer(bytes.byteLength);
135
+ new Uint8Array(copy).set(bytes);
136
+ return copy;
137
+ },
138
+ headers: {
139
+ get: (name) => response.headers[name.toLowerCase()] ?? null,
140
+ },
141
+ };
142
+ };
143
+ async function listen(listenOptions = {}) {
144
+ const { createServer } = await importNodeHttp();
145
+ const hostname = listenOptions.hostname ?? "127.0.0.1";
146
+ const server = createServer(async (req, res) => {
147
+ const chunks = [];
148
+ for await (const chunk of req) {
149
+ chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
150
+ }
151
+ const host = Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host;
152
+ const requestUrl = new URL(req.url ?? "/", `http://${host ?? hostname}`);
153
+ const response = await handleRequest({
154
+ method: req.method ?? "GET",
155
+ url: requestUrl.toString(),
156
+ body: new TextDecoder().decode(concat(chunks)),
157
+ });
158
+ for (const [key, value] of Object.entries(response.headers)) {
159
+ res.setHeader(key, value);
160
+ }
161
+ res.statusCode = response.status;
162
+ res.end(response.body);
163
+ });
164
+ await new Promise((resolve, reject) => {
165
+ server.once("error", reject);
166
+ server.listen(listenOptions.port ?? 0, hostname, () => {
167
+ server.off("error", reject);
168
+ resolve();
169
+ });
170
+ });
171
+ const address = server.address();
172
+ if (!address || typeof address === "string") {
173
+ server.close();
174
+ throw new Error("SmartBill mock server failed to resolve its listening address");
175
+ }
176
+ return {
177
+ apiUrl: `http://${hostname}:${address.port}`,
178
+ close: () => new Promise((resolve, reject) => {
179
+ server.close((error) => (error ? reject(error) : resolve()));
180
+ }),
181
+ };
182
+ }
183
+ function createDocument(kind, body) {
184
+ const seriesName = body.seriesName;
185
+ const number = nextNumber(kind, seriesName);
186
+ const companyVatCode = body.companyVatCode;
187
+ const total = totalAmount(body, taxes);
188
+ const paid = paidAmount(body, total);
189
+ const payments = body.payment
190
+ ? [
191
+ {
192
+ type: body.payment.type,
193
+ value: paid,
194
+ paidDate: now().toISOString().slice(0, 10),
195
+ isCash: body.payment.isCash,
196
+ },
197
+ ]
198
+ : [];
199
+ const document = {
200
+ kind,
201
+ companyVatCode,
202
+ seriesName,
203
+ number,
204
+ status: "issued",
205
+ body: {
206
+ ...body,
207
+ mentions: appendTestMention(body.mentions, "TEST DOCUMENT - SmartBill local mock"),
208
+ },
209
+ url: `smartbill-mock://test-document/${kind}/${encodeURIComponent(companyVatCode)}/${encodeURIComponent(seriesName)}/${number}.pdf`,
210
+ total,
211
+ paidAmount: paid,
212
+ payments,
213
+ createdAt: now().toISOString(),
214
+ convertedInvoices: [],
215
+ };
216
+ documents.set(documentKey(kind, companyVatCode, seriesName, number), document);
217
+ return document;
218
+ }
219
+ function nextNumber(kind, seriesName) {
220
+ const key = seriesKey(kind, seriesName);
221
+ const series = seriesCounters.get(key) ?? { name: seriesName, type: kind, nextNumber: 1 };
222
+ const number = String(series.nextNumber);
223
+ seriesCounters.set(key, { ...series, nextNumber: series.nextNumber + 1 });
224
+ return number;
225
+ }
226
+ function listSeries() {
227
+ return [...seriesCounters.values()].map((item) => ({ ...item }));
228
+ }
229
+ function findByQuery(kind, url) {
230
+ const companyVatCode = url.searchParams.get("cif") ?? url.searchParams.get("companyVatCode");
231
+ const seriesName = url.searchParams.get("seriesname") ?? url.searchParams.get("seriesName");
232
+ const number = url.searchParams.get("number");
233
+ if (!companyVatCode || !seriesName || !number) {
234
+ throw new SmartbillMockError(400, "SmartBill mock request is missing cif, seriesname, or number");
235
+ }
236
+ return findRequiredDocument(kind, companyVatCode, seriesName, number);
237
+ }
238
+ function findRequiredDocument(kind, companyVatCode, seriesName, number) {
239
+ const document = documents.get(documentKey(kind, companyVatCode, seriesName, number));
240
+ if (!document) {
241
+ throw new SmartbillMockError(404, `SmartBill mock ${kind} not found: ${seriesName}-${number}`);
242
+ }
243
+ return document;
244
+ }
245
+ function updateInvoiceStatus(body, status) {
246
+ const reference = parseInvoiceReference(body);
247
+ const invoice = findRequiredDocument("invoice", reference.companyVatCode, reference.seriesName, reference.number);
248
+ invoice.status = status;
249
+ documents.set(documentKey("invoice", reference.companyVatCode, reference.seriesName, reference.number), invoice);
250
+ return invoice;
251
+ }
252
+ function updateByQuery(url, status) {
253
+ const invoice = findByQuery("invoice", url);
254
+ invoice.status = status;
255
+ documents.set(documentKey("invoice", invoice.companyVatCode, invoice.seriesName, invoice.number), invoice);
256
+ return invoice;
257
+ }
258
+ function pdf(document) {
259
+ const label = `${document.kind === "estimate" ? "Proforma" : "Invoice"} ${document.seriesName}-${document.number} (TEST)`;
260
+ return {
261
+ status: 200,
262
+ headers: {
263
+ "access-control-allow-origin": "*",
264
+ "content-type": "application/pdf",
265
+ "content-disposition": `inline; filename="${document.seriesName}-${document.number}.pdf"`,
266
+ "x-mock-pdf-url": document.url,
267
+ },
268
+ body: createPlaceholderPdf(label),
269
+ };
270
+ }
271
+ reset();
272
+ return {
273
+ fetch,
274
+ handleRequest,
275
+ listen,
276
+ reset,
277
+ listDocuments,
278
+ getDocument,
279
+ convertEstimateToInvoice,
280
+ };
281
+ }
282
+ class SmartbillMockError extends Error {
283
+ status;
284
+ constructor(status, message) {
285
+ super(message);
286
+ this.status = status;
287
+ }
288
+ }
289
+ function normalizeSmartbillPath(pathname) {
290
+ return pathname.replace(/^\/SBORO\/api/, "") || "/";
291
+ }
292
+ function json(status, payload) {
293
+ return {
294
+ status,
295
+ headers: {
296
+ "access-control-allow-origin": "*",
297
+ "content-type": "application/json; charset=utf-8",
298
+ },
299
+ body: JSON.stringify(payload),
300
+ };
301
+ }
302
+ function parseBody(body) {
303
+ if (!body)
304
+ throw new SmartbillMockError(400, "SmartBill mock request requires a JSON body");
305
+ try {
306
+ return JSON.parse(body);
307
+ }
308
+ catch {
309
+ throw new SmartbillMockError(400, "SmartBill mock request body is not valid JSON");
310
+ }
311
+ }
312
+ function parseInvoiceReference(body) {
313
+ const value = body;
314
+ if (typeof value.companyVatCode !== "string" ||
315
+ typeof value.seriesName !== "string" ||
316
+ typeof value.number !== "string") {
317
+ throw new SmartbillMockError(400, "SmartBill mock invoice reference requires companyVatCode, seriesName, and number");
318
+ }
319
+ return {
320
+ companyVatCode: value.companyVatCode,
321
+ seriesName: value.seriesName,
322
+ number: value.number,
323
+ };
324
+ }
325
+ function totalAmount(body, taxes) {
326
+ return roundMoney(body.products.reduce((total, product) => {
327
+ const lineNet = product.price * product.quantity;
328
+ if (product.isTaxIncluded)
329
+ return total + lineNet;
330
+ return total + lineNet * (1 + taxPercentage(product.taxPercentage, taxes) / 100);
331
+ }, 0));
332
+ }
333
+ function taxPercentage(lineTaxPercentage, taxes) {
334
+ if (typeof lineTaxPercentage === "number" && Number.isFinite(lineTaxPercentage)) {
335
+ return lineTaxPercentage;
336
+ }
337
+ return taxes.find((tax) => tax.default)?.percentage ?? defaultTaxes[0]?.percentage ?? 0;
338
+ }
339
+ function paidAmount(body, total) {
340
+ return roundMoney(Math.min(total, body.payment?.value ?? 0));
341
+ }
342
+ function toCreateResponse(document) {
343
+ return {
344
+ status: "Ok",
345
+ message: "",
346
+ errorText: "",
347
+ series: document.seriesName,
348
+ number: document.number,
349
+ url: document.url,
350
+ };
351
+ }
352
+ function ackEnvelope(message) {
353
+ return { status: "Ok", message, errorText: "" };
354
+ }
355
+ function errorEnvelope(message) {
356
+ return { status: "Error", message: "", errorText: message };
357
+ }
358
+ function taxesEnvelope(taxes) {
359
+ return {
360
+ status: "Ok",
361
+ message: "",
362
+ errorText: "",
363
+ taxes: taxes.map(({ name, percentage }) => ({ name, percentage })),
364
+ };
365
+ }
366
+ function seriesEnvelope(series) {
367
+ return {
368
+ status: "Ok",
369
+ message: "",
370
+ errorText: "",
371
+ list: series.map((item) => ({
372
+ name: item.name,
373
+ nextNumber: item.nextNumber,
374
+ type: item.type === "invoice" ? "f" : "p",
375
+ })),
376
+ };
377
+ }
378
+ function paymentStatusEnvelope(document) {
379
+ const unpaid = Math.max(0, roundMoney(document.total - document.paidAmount));
380
+ return {
381
+ status: "Ok",
382
+ message: paymentStatusMessage(document),
383
+ errorText: "",
384
+ paid: document.total > 0 && document.paidAmount >= document.total,
385
+ invoiceTotalAmount: document.total,
386
+ paidAmount: document.paidAmount,
387
+ unpaidAmount: unpaid,
388
+ payments: document.payments.map((entry) => ({ ...entry })),
389
+ };
390
+ }
391
+ function paymentStatusMessage(document) {
392
+ if (document.status === "cancelled")
393
+ return "Invoice cancelled";
394
+ if (document.status === "deleted")
395
+ return "Invoice deleted";
396
+ if (document.status === "reversed")
397
+ return "Invoice reversed";
398
+ return "";
399
+ }
400
+ function estimateInvoicesEnvelope(estimate) {
401
+ const last = estimate.convertedInvoices[estimate.convertedInvoices.length - 1];
402
+ return {
403
+ status: "Ok",
404
+ message: "",
405
+ errorText: "",
406
+ series: last?.series ?? "",
407
+ number: last?.number ?? "",
408
+ areInvoicesCreated: estimate.convertedInvoices.length > 0,
409
+ invoices: estimate.convertedInvoices.map((invoice) => ({ ...invoice })),
410
+ };
411
+ }
412
+ function appendTestMention(existing, mention) {
413
+ return existing ? `${existing}\n${mention}` : mention;
414
+ }
415
+ function seriesKey(kind, seriesName) {
416
+ return `${kind}:${seriesName}`;
417
+ }
418
+ function documentKey(kind, companyVatCode, seriesName, number) {
419
+ return `${kind}:${companyVatCode}:${seriesName}:${number}`;
420
+ }
421
+ function cloneDocument(document) {
422
+ return {
423
+ ...document,
424
+ body: structuredClone(document.body),
425
+ payments: document.payments.map((entry) => ({ ...entry })),
426
+ convertedInvoices: document.convertedInvoices.map((invoice) => ({ ...invoice })),
427
+ };
428
+ }
429
+ function roundMoney(value) {
430
+ return Math.round(value * 100) / 100;
431
+ }
@@ -0,0 +1,55 @@
1
+ import type { Plugin } from "@voyant-travel/core";
2
+ import type { SmartbillArtifactPersistenceOptions } from "./artifacts.js";
3
+ import type { SmartbillClientOptions } from "./client.js";
4
+ import type { SmartbillMappingOptions } from "./mapping.js";
5
+ import type { SmartbillInvoiceBody, SmartbillInvoiceResponse, VoyantInvoiceEvent } from "./types.js";
6
+ export interface SmartbillSyncEventNames {
7
+ issued?: string;
8
+ proformaIssued?: string;
9
+ proformaConverted?: string;
10
+ voided?: string;
11
+ syncRequested?: string;
12
+ }
13
+ export interface SmartbillLogger {
14
+ error: (message: string, meta?: unknown) => void;
15
+ info?: (message: string, meta?: unknown) => void;
16
+ }
17
+ export type SmartbillMapFn = (event: VoyantInvoiceEvent) => SmartbillInvoiceBody | Promise<SmartbillInvoiceBody>;
18
+ export interface SmartbillIdempotencyOptions {
19
+ /**
20
+ * When artifact DB access is configured, skip SmartBill create calls for
21
+ * duplicate invoice events that already have a non-error SmartBill ref.
22
+ * Defaults to true.
23
+ */
24
+ skipExistingExternalRef?: boolean;
25
+ }
26
+ export type SmartbillErrorHandler = (event: VoyantInvoiceEvent, error: unknown) => void | Promise<void>;
27
+ export type SmartbillInvoiceNumberWriteBackFormatter = (event: VoyantInvoiceEvent, result: SmartbillInvoiceResponse) => string | Promise<string>;
28
+ export interface SmartbillPluginOptions extends SmartbillClientOptions, SmartbillMappingOptions {
29
+ events?: SmartbillSyncEventNames;
30
+ mapEvent?: SmartbillMapFn;
31
+ logger?: SmartbillLogger;
32
+ idempotency?: SmartbillIdempotencyOptions;
33
+ onError?: SmartbillErrorHandler;
34
+ /**
35
+ * When enabled, mirrors SmartBill's issued series-number back onto
36
+ * `invoices.invoice_number` after a successful create. `true` uses
37
+ * `${series}-${number}`; a formatter can return a custom value.
38
+ */
39
+ writeBackInvoiceNumber?: boolean | SmartbillInvoiceNumberWriteBackFormatter;
40
+ /**
41
+ * Optional finance artifact persistence. When `db` is supplied, the plugin
42
+ * registers the SmartBill external ref after creation. When
43
+ * `documentStorage` is also supplied, it downloads and stores the generated
44
+ * SmartBill PDF as both an invoice rendition and attachment.
45
+ */
46
+ artifacts?: SmartbillArtifactPersistenceOptions;
47
+ /** @deprecated Use `artifacts.db`. */
48
+ db?: SmartbillArtifactPersistenceOptions["db"];
49
+ /** @deprecated Use `artifacts.documentStorage`. */
50
+ documentStorage?: SmartbillArtifactPersistenceOptions["documentStorage"];
51
+ /** @deprecated Use `artifacts.documentStorageKeyPrefix`. */
52
+ documentStorageKeyPrefix?: SmartbillArtifactPersistenceOptions["documentStorageKeyPrefix"];
53
+ }
54
+ export declare function smartbillPlugin(options: SmartbillPluginOptions): Plugin;
55
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,qBAAqB,CAAA;AAI7D,OAAO,KAAK,EAAE,mCAAmC,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AACzD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAO3D,OAAO,KAAK,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAGpG,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IAChD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CACjD;AAED,MAAM,MAAM,cAAc,GAAG,CAC3B,KAAK,EAAE,kBAAkB,KACtB,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;AAEzD,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,MAAM,qBAAqB,GAAG,CAClC,KAAK,EAAE,kBAAkB,EACzB,KAAK,EAAE,OAAO,KACX,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEzB,MAAM,MAAM,wCAAwC,GAAG,CACrD,KAAK,EAAE,kBAAkB,EACzB,MAAM,EAAE,wBAAwB,KAC7B,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;AAE7B,MAAM,WAAW,sBAAuB,SAAQ,sBAAsB,EAAE,uBAAuB;IAC7F,MAAM,CAAC,EAAE,uBAAuB,CAAA;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,WAAW,CAAC,EAAE,2BAA2B,CAAA;IACzC,OAAO,CAAC,EAAE,qBAAqB,CAAA;IAC/B;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,OAAO,GAAG,wCAAwC,CAAA;IAC3E;;;;;OAKG;IACH,SAAS,CAAC,EAAE,mCAAmC,CAAA;IAC/C,sCAAsC;IACtC,EAAE,CAAC,EAAE,mCAAmC,CAAC,IAAI,CAAC,CAAA;IAC9C,mDAAmD;IACnD,eAAe,CAAC,EAAE,mCAAmC,CAAC,iBAAiB,CAAC,CAAA;IACxE,4DAA4D;IAC5D,wBAAwB,CAAC,EAAE,mCAAmC,CAAC,0BAA0B,CAAC,CAAA;CAC3F;AAmBD,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAmLvE"}
package/dist/plugin.js ADDED
@@ -0,0 +1,192 @@
1
+ import { financeService } from "@voyant-travel/finance";
2
+ import { createSmartbillSyncRuntime } from "./runtime.js";
3
+ import { syncSmartbillInvoiceEvent, syncSmartbillInvoiceVoidEvent, syncSmartbillProformaConversion, } from "./sync.js";
4
+ import { parseSmartbillPluginOptions } from "./validation.js";
5
+ function resolveStaticArtifactDb(db) {
6
+ if (!db || typeof db === "function")
7
+ return null;
8
+ return db;
9
+ }
10
+ function coerceEvent(data) {
11
+ if (data == null || typeof data !== "object")
12
+ return null;
13
+ const maybe = data;
14
+ if (typeof maybe.id === "string")
15
+ return maybe;
16
+ if (typeof maybe.invoiceId === "string") {
17
+ return { ...maybe, id: maybe.invoiceId };
18
+ }
19
+ return null;
20
+ }
21
+ export function smartbillPlugin(options) {
22
+ const validatedOptions = parseSmartbillPluginOptions(options);
23
+ const runtime = createSmartbillSyncRuntime(validatedOptions);
24
+ const { client, logger, eventNames } = runtime;
25
+ async function ensureNumberSeries() {
26
+ const db = resolveStaticArtifactDb(runtime.artifacts.db);
27
+ if (!db || typeof validatedOptions.seriesName !== "string")
28
+ return;
29
+ try {
30
+ await financeService.ensureExternalInvoiceNumberSeries(db, [
31
+ {
32
+ provider: "smartbill",
33
+ scope: "invoice",
34
+ code: "smartbill-invoice",
35
+ name: "SmartBill invoices",
36
+ externalConfigKey: validatedOptions.seriesName,
37
+ isDefault: true,
38
+ },
39
+ {
40
+ provider: "smartbill",
41
+ scope: "proforma",
42
+ code: "smartbill-proforma",
43
+ name: "SmartBill proformas",
44
+ externalConfigKey: validatedOptions.seriesName,
45
+ isDefault: true,
46
+ },
47
+ ]);
48
+ }
49
+ catch (error) {
50
+ logger.error("[smartbill] invoice number series bootstrap failed", error);
51
+ }
52
+ }
53
+ async function resolveConfiguredSeriesName(event) {
54
+ const value = validatedOptions.seriesName;
55
+ return typeof value === "function" ? await value(event) : value;
56
+ }
57
+ async function resolveExternalSeriesName(event) {
58
+ return typeof event.externalSeriesName === "string"
59
+ ? event.externalSeriesName
60
+ : await resolveConfiguredSeriesName(event);
61
+ }
62
+ const subscribers = [
63
+ {
64
+ event: eventNames.issued,
65
+ handler: async (envelope) => {
66
+ const event = coerceEvent(envelope.data);
67
+ if (!event)
68
+ return;
69
+ if (event.skipExternalSync === true) {
70
+ logger.info?.(`[smartbill] skipping invoice ${event.id} (skipExternalSync=true)`);
71
+ return;
72
+ }
73
+ if (typeof event.convertedFromInvoiceId === "string" && event.convertedFromInvoiceId) {
74
+ logger.info?.(`[smartbill] skipping invoice create for converted proforma ${event.id}; waiting for "${eventNames.proformaConverted}"`);
75
+ return;
76
+ }
77
+ try {
78
+ await syncSmartbillInvoiceEvent({
79
+ event,
80
+ documentType: "invoice",
81
+ runtime,
82
+ pluginOptions: validatedOptions,
83
+ operationLabel: `createInvoice on "${eventNames.issued}"`,
84
+ });
85
+ }
86
+ catch {
87
+ // `syncSmartbillInvoiceEvent` logs and records retryable state.
88
+ }
89
+ },
90
+ },
91
+ {
92
+ event: eventNames.proformaIssued,
93
+ handler: async (envelope) => {
94
+ const event = coerceEvent(envelope.data);
95
+ if (!event)
96
+ return;
97
+ if (event.skipExternalSync === true) {
98
+ logger.info?.(`[smartbill] skipping proforma ${event.id} (skipExternalSync=true)`);
99
+ return;
100
+ }
101
+ try {
102
+ await syncSmartbillInvoiceEvent({
103
+ event,
104
+ documentType: "proforma",
105
+ runtime,
106
+ pluginOptions: validatedOptions,
107
+ operationLabel: `createProforma on "${eventNames.proformaIssued}"`,
108
+ });
109
+ }
110
+ catch {
111
+ // `syncSmartbillInvoiceEvent` logs and records retryable state.
112
+ }
113
+ },
114
+ },
115
+ {
116
+ event: eventNames.proformaConverted,
117
+ handler: async (envelope) => {
118
+ const event = coerceEvent(envelope.data);
119
+ if (!event)
120
+ return;
121
+ if (event.skipExternalSync === true) {
122
+ logger.info?.(`[smartbill] skipping proforma conversion ${event.id} (skipExternalSync=true)`);
123
+ return;
124
+ }
125
+ try {
126
+ await syncSmartbillProformaConversion({
127
+ event,
128
+ runtime,
129
+ pluginOptions: validatedOptions,
130
+ });
131
+ }
132
+ catch {
133
+ // `syncSmartbillProformaConversion` logs and records retryable state.
134
+ }
135
+ },
136
+ },
137
+ {
138
+ event: eventNames.voided,
139
+ handler: async (envelope) => {
140
+ const event = coerceEvent(envelope.data);
141
+ if (!event)
142
+ return;
143
+ try {
144
+ await syncSmartbillInvoiceVoidEvent({
145
+ event,
146
+ runtime,
147
+ pluginOptions: validatedOptions,
148
+ });
149
+ }
150
+ catch {
151
+ // `syncSmartbillInvoiceVoidEvent` logs and records retryable state.
152
+ }
153
+ },
154
+ },
155
+ {
156
+ event: eventNames.syncRequested,
157
+ handler: async (envelope) => {
158
+ const event = coerceEvent(envelope.data);
159
+ if (!event)
160
+ return;
161
+ try {
162
+ const seriesName = await resolveExternalSeriesName(event);
163
+ const number = typeof event.externalNumber === "string"
164
+ ? event.externalNumber
165
+ : typeof event.invoiceNumber === "string"
166
+ ? event.invoiceNumber
167
+ : undefined;
168
+ if (!number) {
169
+ logger.error(`[smartbill] cannot sync invoice ${event.id}: missing external number`);
170
+ return;
171
+ }
172
+ const status = await client.getPaymentStatus(validatedOptions.companyVatCode, seriesName, number);
173
+ const paymentLabel = status.paid
174
+ ? "paid"
175
+ : (status.paidAmount ?? 0) > 0
176
+ ? "partially_paid"
177
+ : "unpaid";
178
+ logger.info?.(`[smartbill] payment status for ${seriesName}-${number}: ${paymentLabel}`, status);
179
+ }
180
+ catch (err) {
181
+ logger.error(`[smartbill] getPaymentStatus on "${eventNames.syncRequested}" failed for ${event.id}`, err);
182
+ }
183
+ },
184
+ },
185
+ ];
186
+ return {
187
+ name: "smartbill",
188
+ version: "0.1.0",
189
+ bootstrap: ensureNumberSeries,
190
+ subscribers,
191
+ };
192
+ }