@voyantjs/plugin-smartbill 0.1.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/LICENSE +109 -0
- package/README.md +43 -0
- package/dist/src/client.d.ts +38 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +105 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/mapping.d.ts +32 -0
- package/dist/src/mapping.d.ts.map +1 -0
- package/dist/src/mapping.js +90 -0
- package/dist/src/plugin.d.ts +55 -0
- package/dist/src/plugin.d.ts.map +1 -0
- package/dist/src/plugin.js +118 -0
- package/dist/src/types.d.ts +109 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/dist/tests/unit/client.test.d.ts +2 -0
- package/dist/tests/unit/client.test.d.ts.map +1 -0
- package/dist/tests/unit/client.test.js +182 -0
- package/dist/tests/unit/mapping.test.d.ts +2 -0
- package/dist/tests/unit/mapping.test.d.ts.map +1 -0
- package/dist/tests/unit/mapping.test.js +149 -0
- package/dist/tests/unit/plugin.test.d.ts +2 -0
- package/dist/tests/unit/plugin.test.d.ts.map +1 -0
- package/dist/tests/unit/plugin.test.js +216 -0
- package/package.json +47 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { smartbillPlugin } from "../../src/plugin.js";
|
|
3
|
+
function jsonResponse(status, body) {
|
|
4
|
+
const text = JSON.stringify(body);
|
|
5
|
+
return {
|
|
6
|
+
ok: status >= 200 && status < 300,
|
|
7
|
+
status,
|
|
8
|
+
json: async () => JSON.parse(text),
|
|
9
|
+
text: async () => text,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function textResponse(status, text) {
|
|
13
|
+
return {
|
|
14
|
+
ok: status >= 200 && status < 300,
|
|
15
|
+
status,
|
|
16
|
+
json: async () => {
|
|
17
|
+
throw new Error("not json");
|
|
18
|
+
},
|
|
19
|
+
text: async () => text,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const baseOptions = {
|
|
23
|
+
username: "user@test.com",
|
|
24
|
+
apiToken: "tok",
|
|
25
|
+
companyVatCode: "RO12345678",
|
|
26
|
+
seriesName: "A",
|
|
27
|
+
};
|
|
28
|
+
function makeLogger() {
|
|
29
|
+
return {
|
|
30
|
+
error: vi.fn(),
|
|
31
|
+
info: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
describe("smartbillPlugin structure", () => {
|
|
35
|
+
it("returns a Plugin with name and version", () => {
|
|
36
|
+
const fetchMock = vi.fn();
|
|
37
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock });
|
|
38
|
+
expect(plugin.name).toBe("smartbill");
|
|
39
|
+
expect(plugin.version).toBe("0.1.0");
|
|
40
|
+
expect(plugin.subscribers).toHaveLength(3);
|
|
41
|
+
});
|
|
42
|
+
it("subscribes to default event names", () => {
|
|
43
|
+
const fetchMock = vi.fn();
|
|
44
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock });
|
|
45
|
+
const events = plugin.subscribers.map((s) => s.event);
|
|
46
|
+
expect(events).toEqual(["invoice.issued", "invoice.voided", "invoice.external.sync.requested"]);
|
|
47
|
+
});
|
|
48
|
+
it("subscribes to custom event names", () => {
|
|
49
|
+
const fetchMock = vi.fn();
|
|
50
|
+
const plugin = smartbillPlugin({
|
|
51
|
+
...baseOptions,
|
|
52
|
+
fetch: fetchMock,
|
|
53
|
+
events: {
|
|
54
|
+
issued: "custom.issued",
|
|
55
|
+
voided: "custom.voided",
|
|
56
|
+
syncRequested: "custom.sync",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const events = plugin.subscribers.map((s) => s.event);
|
|
60
|
+
expect(events).toEqual(["custom.issued", "custom.voided", "custom.sync"]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe("smartbillPlugin — invoice.issued subscriber", () => {
|
|
64
|
+
it("calls createInvoice with mapped body", async () => {
|
|
65
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, { number: "1", series: "A" }));
|
|
66
|
+
const logger = makeLogger();
|
|
67
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
68
|
+
const handler = plugin.subscribers[0].handler;
|
|
69
|
+
await handler({
|
|
70
|
+
id: "inv_123",
|
|
71
|
+
clientName: "Test SRL",
|
|
72
|
+
currency: "RON",
|
|
73
|
+
lineItems: [{ name: "Tour", quantity: 1, unitPrice: 50000 }],
|
|
74
|
+
});
|
|
75
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
76
|
+
const [url, init] = fetchMock.mock.calls[0];
|
|
77
|
+
expect(url).toContain("/invoice");
|
|
78
|
+
expect(init.method).toBe("POST");
|
|
79
|
+
const body = JSON.parse(init.body ?? "{}");
|
|
80
|
+
expect(body.companyVatCode).toBe("RO12345678");
|
|
81
|
+
expect(body.seriesName).toBe("A");
|
|
82
|
+
expect(body.client.name).toBe("Test SRL");
|
|
83
|
+
expect(logger.info).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
it("logs error and does not throw on failure", async () => {
|
|
86
|
+
const fetchMock = vi.fn(async () => textResponse(500, "boom"));
|
|
87
|
+
const logger = makeLogger();
|
|
88
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
89
|
+
const handler = plugin.subscribers[0].handler;
|
|
90
|
+
// Should not throw
|
|
91
|
+
await handler({ id: "inv_fail", lineItems: [] });
|
|
92
|
+
expect(logger.error).toHaveBeenCalledOnce();
|
|
93
|
+
expect(logger.error.mock.calls[0][0]).toContain("createInvoice");
|
|
94
|
+
expect(logger.error.mock.calls[0][0]).toContain("inv_fail");
|
|
95
|
+
});
|
|
96
|
+
it("ignores null data", async () => {
|
|
97
|
+
const fetchMock = vi.fn();
|
|
98
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock });
|
|
99
|
+
await plugin.subscribers[0].handler(null);
|
|
100
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
it("ignores data without id", async () => {
|
|
103
|
+
const fetchMock = vi.fn();
|
|
104
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock });
|
|
105
|
+
await plugin.subscribers[0].handler({ noId: true });
|
|
106
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe("smartbillPlugin — invoice.voided subscriber", () => {
|
|
110
|
+
it("calls cancelInvoice with external number", async () => {
|
|
111
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, {}));
|
|
112
|
+
const logger = makeLogger();
|
|
113
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
114
|
+
const handler = plugin.subscribers[1].handler;
|
|
115
|
+
await handler({
|
|
116
|
+
id: "inv_void",
|
|
117
|
+
externalSeriesName: "B",
|
|
118
|
+
externalNumber: "42",
|
|
119
|
+
});
|
|
120
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
121
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body ?? "{}");
|
|
122
|
+
expect(body).toEqual({
|
|
123
|
+
companyVatCode: "RO12345678",
|
|
124
|
+
seriesName: "B",
|
|
125
|
+
number: "42",
|
|
126
|
+
});
|
|
127
|
+
expect(logger.info).toHaveBeenCalledOnce();
|
|
128
|
+
});
|
|
129
|
+
it("falls back to invoiceNumber when externalNumber missing", async () => {
|
|
130
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, {}));
|
|
131
|
+
const logger = makeLogger();
|
|
132
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
133
|
+
const handler = plugin.subscribers[1].handler;
|
|
134
|
+
await handler({ id: "inv_void2", invoiceNumber: "99" });
|
|
135
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body ?? "{}");
|
|
136
|
+
expect(body.seriesName).toBe("A"); // falls back to options.seriesName
|
|
137
|
+
expect(body.number).toBe("99");
|
|
138
|
+
});
|
|
139
|
+
it("logs error when no number is available", async () => {
|
|
140
|
+
const fetchMock = vi.fn();
|
|
141
|
+
const logger = makeLogger();
|
|
142
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
143
|
+
const handler = plugin.subscribers[1].handler;
|
|
144
|
+
await handler({ id: "inv_no_num" });
|
|
145
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
146
|
+
expect(logger.error).toHaveBeenCalledOnce();
|
|
147
|
+
expect(logger.error.mock.calls[0][0]).toContain("missing external number");
|
|
148
|
+
});
|
|
149
|
+
it("logs error on cancel failure (fire-and-forget)", async () => {
|
|
150
|
+
const fetchMock = vi.fn(async () => textResponse(500, "error"));
|
|
151
|
+
const logger = makeLogger();
|
|
152
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
153
|
+
const handler = plugin.subscribers[1].handler;
|
|
154
|
+
await handler({ id: "inv_err", externalNumber: "1" });
|
|
155
|
+
expect(logger.error).toHaveBeenCalledOnce();
|
|
156
|
+
expect(logger.error.mock.calls[0][0]).toContain("cancelInvoice");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe("smartbillPlugin — invoice.external.sync.requested subscriber", () => {
|
|
160
|
+
it("calls getPaymentStatus and logs result", async () => {
|
|
161
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, { status: "paid", paidAmount: 100 }));
|
|
162
|
+
const logger = makeLogger();
|
|
163
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
164
|
+
const handler = plugin.subscribers[2].handler;
|
|
165
|
+
await handler({ id: "inv_sync", externalNumber: "55" });
|
|
166
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
167
|
+
const [url] = fetchMock.mock.calls[0];
|
|
168
|
+
expect(url).toContain("/invoice/paymentstatus");
|
|
169
|
+
expect(logger.info).toHaveBeenCalledOnce();
|
|
170
|
+
expect(logger.info.mock.calls[0][0]).toContain("paid");
|
|
171
|
+
});
|
|
172
|
+
it("logs error when no number is available", async () => {
|
|
173
|
+
const fetchMock = vi.fn();
|
|
174
|
+
const logger = makeLogger();
|
|
175
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
176
|
+
const handler = plugin.subscribers[2].handler;
|
|
177
|
+
await handler({ id: "inv_no_num" });
|
|
178
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
179
|
+
expect(logger.error).toHaveBeenCalledOnce();
|
|
180
|
+
expect(logger.error.mock.calls[0][0]).toContain("missing external number");
|
|
181
|
+
});
|
|
182
|
+
it("logs error on failure (fire-and-forget)", async () => {
|
|
183
|
+
const fetchMock = vi.fn(async () => textResponse(500, "timeout"));
|
|
184
|
+
const logger = makeLogger();
|
|
185
|
+
const plugin = smartbillPlugin({ ...baseOptions, fetch: fetchMock, logger });
|
|
186
|
+
const handler = plugin.subscribers[2].handler;
|
|
187
|
+
await handler({ id: "inv_err", externalNumber: "1" });
|
|
188
|
+
expect(logger.error).toHaveBeenCalledOnce();
|
|
189
|
+
expect(logger.error.mock.calls[0][0]).toContain("getPaymentStatus");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe("smartbillPlugin — custom mapEvent", () => {
|
|
193
|
+
it("uses custom mapper when provided", async () => {
|
|
194
|
+
const fetchMock = vi.fn(async () => jsonResponse(200, { number: "1", series: "A" }));
|
|
195
|
+
const logger = makeLogger();
|
|
196
|
+
const customMapper = vi.fn().mockReturnValue({
|
|
197
|
+
companyVatCode: "CUSTOM",
|
|
198
|
+
client: { name: "Custom" },
|
|
199
|
+
seriesName: "Z",
|
|
200
|
+
currency: "EUR",
|
|
201
|
+
products: [],
|
|
202
|
+
});
|
|
203
|
+
const plugin = smartbillPlugin({
|
|
204
|
+
...baseOptions,
|
|
205
|
+
fetch: fetchMock,
|
|
206
|
+
logger,
|
|
207
|
+
mapEvent: customMapper,
|
|
208
|
+
});
|
|
209
|
+
const handler = plugin.subscribers[0].handler;
|
|
210
|
+
await handler({ id: "inv_custom" });
|
|
211
|
+
expect(customMapper).toHaveBeenCalledOnce();
|
|
212
|
+
expect(customMapper.mock.calls[0][0].id).toBe("inv_custom");
|
|
213
|
+
const body = JSON.parse(fetchMock.mock.calls[0][1].body ?? "{}");
|
|
214
|
+
expect(body.companyVatCode).toBe("CUSTOM");
|
|
215
|
+
});
|
|
216
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/plugin-smartbill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"types": "./dist/client.d.ts",
|
|
13
|
+
"import": "./dist/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./plugin": {
|
|
16
|
+
"types": "./dist/plugin.d.ts",
|
|
17
|
+
"import": "./dist/plugin.js"
|
|
18
|
+
},
|
|
19
|
+
"./types": {
|
|
20
|
+
"types": "./dist/types.d.ts",
|
|
21
|
+
"import": "./dist/types.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@voyantjs/core": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"typescript": "^6.0.2",
|
|
29
|
+
"vitest": "^4.1.2",
|
|
30
|
+
"@voyantjs/voyant-typescript-config": "0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"typecheck": "tsc --noEmit",
|
|
40
|
+
"lint": "biome check src/",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"build": "tsc -p tsconfig.json",
|
|
43
|
+
"clean": "rm -rf dist"
|
|
44
|
+
},
|
|
45
|
+
"main": "./dist/index.js",
|
|
46
|
+
"types": "./dist/index.d.ts"
|
|
47
|
+
}
|