@voyantjs/plugin-smartbill 0.25.0 → 0.26.1
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 +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/mock.d.ts +65 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +341 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -50,8 +50,69 @@ contract.
|
|
|
50
50
|
| `.` | Barrel re-exports |
|
|
51
51
|
| `./plugin` | `smartbillPlugin(options)` — packaged adapter/subscriber bundle |
|
|
52
52
|
| `./client` | `createSmartbillClient` — `createInvoice`, `cancelInvoice`, `viewPdf`, `getPaymentStatus`, etc. |
|
|
53
|
+
| `./mock` | `createSmartbillMockServer` — stateful local SmartBill-compatible mock for tests |
|
|
53
54
|
| `./types` | SmartBill adapter and bundle types |
|
|
54
55
|
|
|
56
|
+
## Local SmartBill Mock
|
|
57
|
+
|
|
58
|
+
SmartBill does not provide a practical sandbox, and invoice/proforma calls can
|
|
59
|
+
create real accounting documents. Use the packaged mock for local workflows and
|
|
60
|
+
end-to-end tests instead of pointing development credentials at the live API.
|
|
61
|
+
|
|
62
|
+
For in-process tests, pass the mock `fetch` implementation and any local
|
|
63
|
+
`apiUrl`:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { createSmartbillClient } from "@voyantjs/plugin-smartbill/client"
|
|
67
|
+
import { createSmartbillMockServer } from "@voyantjs/plugin-smartbill/mock"
|
|
68
|
+
|
|
69
|
+
const smartbill = createSmartbillMockServer()
|
|
70
|
+
const client = createSmartbillClient({
|
|
71
|
+
username: "local",
|
|
72
|
+
apiToken: "local",
|
|
73
|
+
apiUrl: "http://smartbill.local/SBORO/api",
|
|
74
|
+
fetch: smartbill.fetch,
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For full app tests, start the local HTTP listener and wire the plugin/client to
|
|
79
|
+
the returned base URL:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const smartbill = createSmartbillMockServer()
|
|
83
|
+
const server = await smartbill.listen({ port: 4555 })
|
|
84
|
+
|
|
85
|
+
const plugin = smartbillPlugin({
|
|
86
|
+
username: "local",
|
|
87
|
+
apiToken: "local",
|
|
88
|
+
apiUrl: server.apiUrl,
|
|
89
|
+
companyVatCode: "RO12345678",
|
|
90
|
+
seriesName: "SB-TEST",
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
await server.close()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The mock supports the SmartBill endpoints used by the plugin and common local
|
|
97
|
+
billing flows:
|
|
98
|
+
|
|
99
|
+
- `GET /tax`
|
|
100
|
+
- `GET /series`
|
|
101
|
+
- `POST /invoice`
|
|
102
|
+
- `GET /invoice/pdf`
|
|
103
|
+
- `GET /invoice/paymentstatus`
|
|
104
|
+
- `PUT /invoice/cancel`
|
|
105
|
+
- `PUT /invoice/reverse`
|
|
106
|
+
- `PUT /invoice/restore`
|
|
107
|
+
- `DELETE /invoice`
|
|
108
|
+
- `POST /estimate`
|
|
109
|
+
- `GET /estimate/pdf`
|
|
110
|
+
- `GET /estimate/invoices`
|
|
111
|
+
|
|
112
|
+
Documents are stateful and deterministic per series. Generated PDF URLs use the
|
|
113
|
+
`smartbill-mock://test-document/...` scheme, and stored document mentions are
|
|
114
|
+
marked with `TEST DOCUMENT - SmartBill local mock`.
|
|
115
|
+
|
|
55
116
|
## License
|
|
56
117
|
|
|
57
118
|
Apache-2.0
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ export type { SmartbillClientApi, SmartbillClientOptions } from "./client.js";
|
|
|
2
2
|
export { createSmartbillClient } from "./client.js";
|
|
3
3
|
export type { SmartbillMappingOptions } from "./mapping.js";
|
|
4
4
|
export { mapClient, mapLineItems, mapVoyantInvoiceToSmartbill } from "./mapping.js";
|
|
5
|
+
export type { SmartbillMockDocument, SmartbillMockDocumentKind, SmartbillMockDocumentStatus, SmartbillMockListenOptions, SmartbillMockRequest, SmartbillMockResponse, SmartbillMockSeries, SmartbillMockServer, SmartbillMockServerHandle, SmartbillMockServerOptions, SmartbillMockTax, } from "./mock.js";
|
|
6
|
+
export { createSmartbillMockServer } from "./mock.js";
|
|
5
7
|
export type { SmartbillLogger, SmartbillMapFn, SmartbillPluginOptions, SmartbillSyncEventNames, } from "./plugin.js";
|
|
6
8
|
export { smartbillPlugin } from "./plugin.js";
|
|
7
9
|
export type { ResolvedSmartbillSyncEventNames, SmartbillSyncRuntime } from "./runtime.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA;AACnF,YAAY,EACV,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,YAAY,EAAE,+BAA+B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EACV,gCAAgC,EAChC,uCAAuC,EACvC,8BAA8B,EAC9B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,sCAAsC,EAAE,MAAM,iBAAiB,CAAA;AACxE,YAAY,EACV,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAA;AAC7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnD,YAAY,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAC3D,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,2BAA2B,EAAE,MAAM,cAAc,CAAA;AACnF,YAAY,EACV,qBAAqB,EACrB,yBAAyB,EACzB,2BAA2B,EAC3B,0BAA0B,EAC1B,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,mBAAmB,EACnB,yBAAyB,EACzB,0BAA0B,EAC1B,gBAAgB,GACjB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAA;AACrD,YAAY,EACV,eAAe,EACf,cAAc,EACd,sBAAsB,EACtB,uBAAuB,GACxB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,YAAY,EAAE,+BAA+B,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AACzD,YAAY,EACV,gCAAgC,EAChC,uCAAuC,EACvC,8BAA8B,EAC9B,0BAA0B,EAC1B,gCAAgC,EAChC,+BAA+B,GAChC,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,sCAAsC,EAAE,MAAM,iBAAiB,CAAA;AACxE,YAAY,EACV,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,wBAAwB,EACxB,oBAAoB,EACpB,gBAAgB,EAChB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createSmartbillClient } from "./client.js";
|
|
2
2
|
export { mapClient, mapLineItems, mapVoyantInvoiceToSmartbill } from "./mapping.js";
|
|
3
|
+
export { createSmartbillMockServer } from "./mock.js";
|
|
3
4
|
export { smartbillPlugin } from "./plugin.js";
|
|
4
5
|
export { createSmartbillSyncRuntime } from "./runtime.js";
|
|
5
6
|
export { createSmartbillInvoiceSettlementPoller } from "./settlement.js";
|
package/dist/mock.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SmartbillFetch, SmartbillInvoiceBody, SmartbillInvoiceResponse } from "./types.js";
|
|
2
|
+
export type SmartbillMockDocumentKind = "invoice" | "estimate";
|
|
3
|
+
export type SmartbillMockDocumentStatus = "issued" | "cancelled" | "deleted" | "reversed" | "restored";
|
|
4
|
+
export interface SmartbillMockTax {
|
|
5
|
+
name: string;
|
|
6
|
+
percentage: number;
|
|
7
|
+
default?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface SmartbillMockSeries {
|
|
10
|
+
name: string;
|
|
11
|
+
type: SmartbillMockDocumentKind;
|
|
12
|
+
nextNumber: number;
|
|
13
|
+
}
|
|
14
|
+
export interface SmartbillMockDocument {
|
|
15
|
+
kind: SmartbillMockDocumentKind;
|
|
16
|
+
companyVatCode: string;
|
|
17
|
+
seriesName: string;
|
|
18
|
+
number: string;
|
|
19
|
+
status: SmartbillMockDocumentStatus;
|
|
20
|
+
body: SmartbillInvoiceBody;
|
|
21
|
+
url: string;
|
|
22
|
+
total: number;
|
|
23
|
+
paidAmount: number;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
convertedInvoices: SmartbillInvoiceResponse[];
|
|
26
|
+
}
|
|
27
|
+
export interface SmartbillMockServerOptions {
|
|
28
|
+
taxes?: SmartbillMockTax[];
|
|
29
|
+
series?: SmartbillMockSeries[];
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
}
|
|
32
|
+
export interface SmartbillMockListenOptions {
|
|
33
|
+
port?: number;
|
|
34
|
+
hostname?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface SmartbillMockServerHandle {
|
|
37
|
+
apiUrl: string;
|
|
38
|
+
close: () => Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export interface SmartbillMockRequest {
|
|
41
|
+
method: string;
|
|
42
|
+
url: string;
|
|
43
|
+
body?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SmartbillMockResponse {
|
|
46
|
+
status: number;
|
|
47
|
+
headers: Record<string, string>;
|
|
48
|
+
body: string;
|
|
49
|
+
}
|
|
50
|
+
export interface SmartbillMockServer {
|
|
51
|
+
fetch: SmartbillFetch;
|
|
52
|
+
handleRequest: (request: SmartbillMockRequest) => Promise<SmartbillMockResponse>;
|
|
53
|
+
listen: (options?: SmartbillMockListenOptions) => Promise<SmartbillMockServerHandle>;
|
|
54
|
+
reset: () => void;
|
|
55
|
+
listDocuments: () => SmartbillMockDocument[];
|
|
56
|
+
getDocument: (kind: SmartbillMockDocumentKind, companyVatCode: string, seriesName: string, number: string) => SmartbillMockDocument | null;
|
|
57
|
+
convertEstimateToInvoice: (args: {
|
|
58
|
+
companyVatCode: string;
|
|
59
|
+
seriesName: string;
|
|
60
|
+
number: string;
|
|
61
|
+
invoiceSeriesName?: string;
|
|
62
|
+
}) => SmartbillInvoiceResponse;
|
|
63
|
+
}
|
|
64
|
+
export declare function createSmartbillMockServer(options?: SmartbillMockServerOptions): SmartbillMockServer;
|
|
65
|
+
//# sourceMappingURL=mock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock.d.ts","sourceRoot":"","sources":["../src/mock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAEhG,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,UAAU,CAAA;AAE9D,MAAM,MAAM,2BAA2B,GACnC,QAAQ,GACR,WAAW,GACX,SAAS,GACT,UAAU,GACV,UAAU,CAAA;AAEd,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,yBAAyB,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,yBAAyB,CAAA;IAC/B,cAAc,EAAE,MAAM,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,2BAA2B,CAAA;IACnC,IAAI,EAAE,oBAAoB,CAAA;IAC1B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;IACjB,iBAAiB,EAAE,wBAAwB,EAAE,CAAA;CAC9C;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAA;IAC1B,MAAM,CAAC,EAAE,mBAAmB,EAAE,CAAA;IAC9B,GAAG,CAAC,EAAE,MAAM,IAAI,CAAA;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,MAAM,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,cAAc,CAAA;IACrB,aAAa,EAAE,CAAC,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAA;IAChF,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,0BAA0B,KAAK,OAAO,CAAC,yBAAyB,CAAC,CAAA;IACpF,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,aAAa,EAAE,MAAM,qBAAqB,EAAE,CAAA;IAC5C,WAAW,EAAE,CACX,IAAI,EAAE,yBAAyB,EAC/B,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,KACX,qBAAqB,GAAG,IAAI,CAAA;IACjC,wBAAwB,EAAE,CAAC,IAAI,EAAE;QAC/B,cAAc,EAAE,MAAM,CAAA;QACtB,UAAU,EAAE,MAAM,CAAA;QAClB,MAAM,EAAE,MAAM,CAAA;QACd,iBAAiB,CAAC,EAAE,MAAM,CAAA;KAC3B,KAAK,wBAAwB,CAAA;CAC/B;AA0CD,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,0BAA+B,GACvC,mBAAmB,CAwSrB"}
|
package/dist/mock.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
const defaultTaxes = [
|
|
2
|
+
{ name: "Normala", percentage: 19, default: true },
|
|
3
|
+
{ name: "Redusa", percentage: 9 },
|
|
4
|
+
{ name: "Redusa", percentage: 5 },
|
|
5
|
+
{ name: "Scutit", percentage: 0 },
|
|
6
|
+
];
|
|
7
|
+
const defaultSeries = [
|
|
8
|
+
{ name: "SB-TEST", type: "invoice", nextNumber: 1 },
|
|
9
|
+
{ name: "PF-TEST", type: "estimate", nextNumber: 1 },
|
|
10
|
+
];
|
|
11
|
+
export function createSmartbillMockServer(options = {}) {
|
|
12
|
+
const documents = new Map();
|
|
13
|
+
const taxes = options.taxes ?? defaultTaxes;
|
|
14
|
+
const initialSeries = options.series ?? defaultSeries;
|
|
15
|
+
const seriesCounters = new Map();
|
|
16
|
+
const now = options.now ?? (() => new Date());
|
|
17
|
+
function reset() {
|
|
18
|
+
documents.clear();
|
|
19
|
+
seriesCounters.clear();
|
|
20
|
+
for (const item of initialSeries) {
|
|
21
|
+
seriesCounters.set(seriesKey(item.type, item.name), { ...item });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function listDocuments() {
|
|
25
|
+
return [...documents.values()].map(cloneDocument);
|
|
26
|
+
}
|
|
27
|
+
function getDocument(kind, companyVatCode, seriesName, number) {
|
|
28
|
+
const document = documents.get(documentKey(kind, companyVatCode, seriesName, number));
|
|
29
|
+
return document ? cloneDocument(document) : null;
|
|
30
|
+
}
|
|
31
|
+
function convertEstimateToInvoice(args) {
|
|
32
|
+
const estimate = findRequiredDocument("estimate", args.companyVatCode, args.seriesName, args.number);
|
|
33
|
+
const invoice = createDocument("invoice", {
|
|
34
|
+
...estimate.body,
|
|
35
|
+
seriesName: args.invoiceSeriesName ?? estimate.body.seriesName,
|
|
36
|
+
mentions: appendTestMention(estimate.body.mentions, `Converted from proforma ${args.seriesName}-${args.number}`),
|
|
37
|
+
});
|
|
38
|
+
const response = toDocumentResponse(invoice);
|
|
39
|
+
estimate.convertedInvoices.push(response);
|
|
40
|
+
documents.set(documentKey(estimate.kind, estimate.companyVatCode, estimate.seriesName, estimate.number), estimate);
|
|
41
|
+
return response;
|
|
42
|
+
}
|
|
43
|
+
async function handleRequest(request) {
|
|
44
|
+
const method = request.method.toUpperCase();
|
|
45
|
+
const url = new URL(request.url, "http://smartbill-mock.local");
|
|
46
|
+
const path = normalizeSmartbillPath(url.pathname);
|
|
47
|
+
try {
|
|
48
|
+
if (method === "GET" && path === "/tax")
|
|
49
|
+
return json(200, taxes);
|
|
50
|
+
if (method === "GET" && path === "/series")
|
|
51
|
+
return json(200, listSeries());
|
|
52
|
+
if (method === "POST" && path === "/invoice") {
|
|
53
|
+
return json(200, toDocumentResponse(createDocument("invoice", parseBody(request.body))));
|
|
54
|
+
}
|
|
55
|
+
if (method === "POST" && path === "/estimate") {
|
|
56
|
+
return json(200, toDocumentResponse(createDocument("estimate", parseBody(request.body))));
|
|
57
|
+
}
|
|
58
|
+
if (method === "GET" && path === "/invoice/pdf") {
|
|
59
|
+
return json(200, { url: findByQuery("invoice", url).url });
|
|
60
|
+
}
|
|
61
|
+
if (method === "GET" && path === "/estimate/pdf") {
|
|
62
|
+
return json(200, { url: findByQuery("estimate", url).url });
|
|
63
|
+
}
|
|
64
|
+
if (method === "GET" && path === "/estimate/invoices") {
|
|
65
|
+
return json(200, { invoices: findByQuery("estimate", url).convertedInvoices });
|
|
66
|
+
}
|
|
67
|
+
if (method === "GET" && path === "/invoice/paymentstatus") {
|
|
68
|
+
const invoice = findByQuery("invoice", url);
|
|
69
|
+
return json(200, {
|
|
70
|
+
status: invoice.status === "cancelled" || invoice.status === "deleted"
|
|
71
|
+
? invoice.status
|
|
72
|
+
: paymentStatus(invoice),
|
|
73
|
+
paidAmount: invoice.paidAmount,
|
|
74
|
+
unpaidAmount: Math.max(0, invoice.total - invoice.paidAmount),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (method === "PUT" && path === "/invoice/cancel") {
|
|
78
|
+
updateInvoiceStatus(parseBody(request.body), "cancelled");
|
|
79
|
+
return json(200, {});
|
|
80
|
+
}
|
|
81
|
+
if (method === "PUT" && path === "/invoice/reverse") {
|
|
82
|
+
const invoice = updateInvoiceStatus(parseBody(request.body), "reversed");
|
|
83
|
+
const reversal = createDocument("invoice", {
|
|
84
|
+
...invoice.body,
|
|
85
|
+
products: invoice.body.products.map((product) => ({
|
|
86
|
+
...product,
|
|
87
|
+
quantity: -Math.abs(product.quantity),
|
|
88
|
+
})),
|
|
89
|
+
mentions: appendTestMention(invoice.body.mentions, `Reversal for ${invoice.seriesName}-${invoice.number}`),
|
|
90
|
+
});
|
|
91
|
+
return json(200, toDocumentResponse(reversal));
|
|
92
|
+
}
|
|
93
|
+
if (method === "PUT" && path === "/invoice/restore") {
|
|
94
|
+
updateInvoiceStatus(parseBody(request.body), "restored");
|
|
95
|
+
return json(200, {});
|
|
96
|
+
}
|
|
97
|
+
if (method === "DELETE" && path === "/invoice") {
|
|
98
|
+
updateByQuery(url, "deleted");
|
|
99
|
+
return json(200, {});
|
|
100
|
+
}
|
|
101
|
+
return json(404, { errorText: `SmartBill mock endpoint not found: ${method} ${path}` });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return json(error instanceof SmartbillMockError ? error.status : 500, {
|
|
105
|
+
errorText: error instanceof Error ? error.message : "SmartBill mock failed",
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const fetch = async (input, init) => {
|
|
110
|
+
const response = await handleRequest({
|
|
111
|
+
method: init.method,
|
|
112
|
+
url: input,
|
|
113
|
+
body: init.body,
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
ok: response.status >= 200 && response.status < 300,
|
|
117
|
+
status: response.status,
|
|
118
|
+
json: async () => JSON.parse(response.body),
|
|
119
|
+
text: async () => response.body,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
async function listen(listenOptions = {}) {
|
|
123
|
+
const { createServer } = await importNodeHttp();
|
|
124
|
+
const hostname = listenOptions.hostname ?? "127.0.0.1";
|
|
125
|
+
const server = createServer(async (req, res) => {
|
|
126
|
+
const chunks = [];
|
|
127
|
+
for await (const chunk of req) {
|
|
128
|
+
chunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
129
|
+
}
|
|
130
|
+
const host = Array.isArray(req.headers.host) ? req.headers.host[0] : req.headers.host;
|
|
131
|
+
const requestUrl = new URL(req.url ?? "/", `http://${host ?? hostname}`);
|
|
132
|
+
const response = await handleRequest({
|
|
133
|
+
method: req.method ?? "GET",
|
|
134
|
+
url: requestUrl.toString(),
|
|
135
|
+
body: new TextDecoder().decode(concat(chunks)),
|
|
136
|
+
});
|
|
137
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
138
|
+
res.setHeader(key, value);
|
|
139
|
+
}
|
|
140
|
+
res.statusCode = response.status;
|
|
141
|
+
res.end(response.body);
|
|
142
|
+
});
|
|
143
|
+
await new Promise((resolve, reject) => {
|
|
144
|
+
server.once("error", reject);
|
|
145
|
+
server.listen(listenOptions.port ?? 0, hostname, () => {
|
|
146
|
+
server.off("error", reject);
|
|
147
|
+
resolve();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
const address = server.address();
|
|
151
|
+
if (!address || typeof address === "string") {
|
|
152
|
+
server.close();
|
|
153
|
+
throw new Error("SmartBill mock server failed to resolve its listening address");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
apiUrl: `http://${hostname}:${address.port}`,
|
|
157
|
+
close: () => new Promise((resolve, reject) => {
|
|
158
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function createDocument(kind, body) {
|
|
163
|
+
const seriesName = body.seriesName;
|
|
164
|
+
const number = nextNumber(kind, seriesName);
|
|
165
|
+
const companyVatCode = body.companyVatCode;
|
|
166
|
+
const total = totalAmount(body, taxes);
|
|
167
|
+
const document = {
|
|
168
|
+
kind,
|
|
169
|
+
companyVatCode,
|
|
170
|
+
seriesName,
|
|
171
|
+
number,
|
|
172
|
+
status: "issued",
|
|
173
|
+
body: {
|
|
174
|
+
...body,
|
|
175
|
+
mentions: appendTestMention(body.mentions, "TEST DOCUMENT - SmartBill local mock"),
|
|
176
|
+
},
|
|
177
|
+
url: `smartbill-mock://test-document/${kind}/${encodeURIComponent(companyVatCode)}/${encodeURIComponent(seriesName)}/${number}.pdf`,
|
|
178
|
+
total,
|
|
179
|
+
paidAmount: paidAmount(body, total),
|
|
180
|
+
createdAt: now().toISOString(),
|
|
181
|
+
convertedInvoices: [],
|
|
182
|
+
};
|
|
183
|
+
documents.set(documentKey(kind, companyVatCode, seriesName, number), document);
|
|
184
|
+
return document;
|
|
185
|
+
}
|
|
186
|
+
function nextNumber(kind, seriesName) {
|
|
187
|
+
const key = seriesKey(kind, seriesName);
|
|
188
|
+
const series = seriesCounters.get(key) ?? { name: seriesName, type: kind, nextNumber: 1 };
|
|
189
|
+
const number = String(series.nextNumber);
|
|
190
|
+
seriesCounters.set(key, { ...series, nextNumber: series.nextNumber + 1 });
|
|
191
|
+
return number;
|
|
192
|
+
}
|
|
193
|
+
function listSeries() {
|
|
194
|
+
return [...seriesCounters.values()].map((item) => ({ ...item }));
|
|
195
|
+
}
|
|
196
|
+
function findByQuery(kind, url) {
|
|
197
|
+
const companyVatCode = url.searchParams.get("cif") ?? url.searchParams.get("companyVatCode");
|
|
198
|
+
const seriesName = url.searchParams.get("seriesname") ?? url.searchParams.get("seriesName");
|
|
199
|
+
const number = url.searchParams.get("number");
|
|
200
|
+
if (!companyVatCode || !seriesName || !number) {
|
|
201
|
+
throw new SmartbillMockError(400, "SmartBill mock request is missing cif, seriesname, or number");
|
|
202
|
+
}
|
|
203
|
+
return findRequiredDocument(kind, companyVatCode, seriesName, number);
|
|
204
|
+
}
|
|
205
|
+
function findRequiredDocument(kind, companyVatCode, seriesName, number) {
|
|
206
|
+
const document = documents.get(documentKey(kind, companyVatCode, seriesName, number));
|
|
207
|
+
if (!document) {
|
|
208
|
+
throw new SmartbillMockError(404, `SmartBill mock ${kind} not found: ${seriesName}-${number}`);
|
|
209
|
+
}
|
|
210
|
+
return document;
|
|
211
|
+
}
|
|
212
|
+
function updateInvoiceStatus(body, status) {
|
|
213
|
+
const reference = parseInvoiceReference(body);
|
|
214
|
+
const invoice = findRequiredDocument("invoice", reference.companyVatCode, reference.seriesName, reference.number);
|
|
215
|
+
invoice.status = status;
|
|
216
|
+
documents.set(documentKey("invoice", reference.companyVatCode, reference.seriesName, reference.number), invoice);
|
|
217
|
+
return invoice;
|
|
218
|
+
}
|
|
219
|
+
function updateByQuery(url, status) {
|
|
220
|
+
const invoice = findByQuery("invoice", url);
|
|
221
|
+
invoice.status = status;
|
|
222
|
+
documents.set(documentKey("invoice", invoice.companyVatCode, invoice.seriesName, invoice.number), invoice);
|
|
223
|
+
}
|
|
224
|
+
reset();
|
|
225
|
+
return {
|
|
226
|
+
fetch,
|
|
227
|
+
handleRequest,
|
|
228
|
+
listen,
|
|
229
|
+
reset,
|
|
230
|
+
listDocuments,
|
|
231
|
+
getDocument,
|
|
232
|
+
convertEstimateToInvoice,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
class SmartbillMockError extends Error {
|
|
236
|
+
status;
|
|
237
|
+
constructor(status, message) {
|
|
238
|
+
super(message);
|
|
239
|
+
this.status = status;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function normalizeSmartbillPath(pathname) {
|
|
243
|
+
return pathname.replace(/^\/SBORO\/api/, "") || "/";
|
|
244
|
+
}
|
|
245
|
+
function json(status, payload) {
|
|
246
|
+
return {
|
|
247
|
+
status,
|
|
248
|
+
headers: {
|
|
249
|
+
"access-control-allow-origin": "*",
|
|
250
|
+
"content-type": "application/json; charset=utf-8",
|
|
251
|
+
},
|
|
252
|
+
body: JSON.stringify(payload),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
function parseBody(body) {
|
|
256
|
+
if (!body)
|
|
257
|
+
throw new SmartbillMockError(400, "SmartBill mock request requires a JSON body");
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(body);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
throw new SmartbillMockError(400, "SmartBill mock request body is not valid JSON");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function parseInvoiceReference(body) {
|
|
266
|
+
const value = body;
|
|
267
|
+
if (typeof value.companyVatCode !== "string" ||
|
|
268
|
+
typeof value.seriesName !== "string" ||
|
|
269
|
+
typeof value.number !== "string") {
|
|
270
|
+
throw new SmartbillMockError(400, "SmartBill mock invoice reference requires companyVatCode, seriesName, and number");
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
companyVatCode: value.companyVatCode,
|
|
274
|
+
seriesName: value.seriesName,
|
|
275
|
+
number: value.number,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function totalAmount(body, taxes) {
|
|
279
|
+
return roundMoney(body.products.reduce((total, product) => {
|
|
280
|
+
const lineNet = product.price * product.quantity;
|
|
281
|
+
if (product.isTaxIncluded)
|
|
282
|
+
return total + lineNet;
|
|
283
|
+
return total + lineNet * (1 + taxPercentage(product.taxPercentage, taxes) / 100);
|
|
284
|
+
}, 0));
|
|
285
|
+
}
|
|
286
|
+
function taxPercentage(lineTaxPercentage, taxes) {
|
|
287
|
+
if (typeof lineTaxPercentage === "number" && Number.isFinite(lineTaxPercentage)) {
|
|
288
|
+
return lineTaxPercentage;
|
|
289
|
+
}
|
|
290
|
+
return taxes.find((tax) => tax.default)?.percentage ?? defaultTaxes[0]?.percentage ?? 0;
|
|
291
|
+
}
|
|
292
|
+
function paidAmount(body, total) {
|
|
293
|
+
return roundMoney(Math.min(total, body.payment?.value ?? 0));
|
|
294
|
+
}
|
|
295
|
+
function paymentStatus(document) {
|
|
296
|
+
if (document.paidAmount >= document.total && document.total > 0)
|
|
297
|
+
return "paid";
|
|
298
|
+
if (document.paidAmount > 0)
|
|
299
|
+
return "partially_paid";
|
|
300
|
+
return "unpaid";
|
|
301
|
+
}
|
|
302
|
+
function toDocumentResponse(document) {
|
|
303
|
+
return {
|
|
304
|
+
series: document.seriesName,
|
|
305
|
+
number: document.number,
|
|
306
|
+
url: document.url,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
function appendTestMention(existing, mention) {
|
|
310
|
+
return existing ? `${existing}\n${mention}` : mention;
|
|
311
|
+
}
|
|
312
|
+
function seriesKey(kind, seriesName) {
|
|
313
|
+
return `${kind}:${seriesName}`;
|
|
314
|
+
}
|
|
315
|
+
function documentKey(kind, companyVatCode, seriesName, number) {
|
|
316
|
+
return `${kind}:${companyVatCode}:${seriesName}:${number}`;
|
|
317
|
+
}
|
|
318
|
+
function cloneDocument(document) {
|
|
319
|
+
return {
|
|
320
|
+
...document,
|
|
321
|
+
body: structuredClone(document.body),
|
|
322
|
+
convertedInvoices: document.convertedInvoices.map((invoice) => ({ ...invoice })),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function roundMoney(value) {
|
|
326
|
+
return Math.round(value * 100) / 100;
|
|
327
|
+
}
|
|
328
|
+
function concat(chunks) {
|
|
329
|
+
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
330
|
+
const combined = new Uint8Array(length);
|
|
331
|
+
let offset = 0;
|
|
332
|
+
for (const chunk of chunks) {
|
|
333
|
+
combined.set(chunk, offset);
|
|
334
|
+
offset += chunk.byteLength;
|
|
335
|
+
}
|
|
336
|
+
return combined;
|
|
337
|
+
}
|
|
338
|
+
async function importNodeHttp() {
|
|
339
|
+
const specifier = "node:http";
|
|
340
|
+
return import(specifier);
|
|
341
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/plugin-smartbill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
"import": "./dist/client.js",
|
|
15
15
|
"default": "./dist/client.js"
|
|
16
16
|
},
|
|
17
|
+
"./mock": {
|
|
18
|
+
"types": "./dist/mock.d.ts",
|
|
19
|
+
"import": "./dist/mock.js",
|
|
20
|
+
"default": "./dist/mock.js"
|
|
21
|
+
},
|
|
17
22
|
"./plugin": {
|
|
18
23
|
"types": "./dist/plugin.d.ts",
|
|
19
24
|
"import": "./dist/plugin.js",
|
|
@@ -32,7 +37,7 @@
|
|
|
32
37
|
},
|
|
33
38
|
"dependencies": {
|
|
34
39
|
"zod": "^4.3.6",
|
|
35
|
-
"@voyantjs/core": "0.
|
|
40
|
+
"@voyantjs/core": "0.26.1"
|
|
36
41
|
},
|
|
37
42
|
"devDependencies": {
|
|
38
43
|
"typescript": "^6.0.2",
|