@voyantjs/plugin-netopia 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.
@@ -0,0 +1,254 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createNetopiaClient, resolveNetopiaRuntimeOptions } from "../../src/client.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
+ describe("resolveNetopiaRuntimeOptions", () => {
23
+ it("merges env bindings with defaults", () => {
24
+ const options = resolveNetopiaRuntimeOptions({
25
+ NETOPIA_URL: "https://secure.mobilpay.ro/pay",
26
+ NETOPIA_API_KEY: "api-key",
27
+ NETOPIA_POS_SIGNATURE: "pos-signature",
28
+ NETOPIA_NOTIFY_URL: "https://api.example.com/netopia/callback",
29
+ NETOPIA_REDIRECT_URL: "https://app.example.com/checkout/return",
30
+ });
31
+ expect(options.language).toBe("ro");
32
+ expect(options.emailTemplate).toBe("confirm");
33
+ expect(options.successStatuses).toEqual([3, 5]);
34
+ });
35
+ it("throws when required config is missing", () => {
36
+ expect(() => resolveNetopiaRuntimeOptions({})).toThrow(/NETOPIA_URL/);
37
+ });
38
+ });
39
+ describe("createNetopiaClient.startCardPayment", () => {
40
+ it("posts to /payment/card/start with Authorization header", async () => {
41
+ const fetchMock = vi.fn(async () => jsonResponse(200, {
42
+ payment: { paymentURL: "https://secure.example.com/pay", ntpID: "ntp_123", status: 1 },
43
+ }));
44
+ const client = createNetopiaClient({
45
+ apiUrl: "https://secure.mobilpay.ro/pay/",
46
+ apiKey: "api-key",
47
+ fetch: fetchMock,
48
+ });
49
+ const response = await client.startCardPayment({
50
+ config: {
51
+ emailTemplate: "confirm",
52
+ notifyUrl: "https://api.example.com/callback",
53
+ redirectUrl: "https://app.example.com/return",
54
+ language: "ro",
55
+ },
56
+ payment: {
57
+ options: { installments: 1 },
58
+ },
59
+ order: {
60
+ posSignature: "pos-signature",
61
+ dateTime: new Date().toISOString(),
62
+ description: "Tour deposit",
63
+ orderID: "pmss_123",
64
+ amount: 125,
65
+ currency: "RON",
66
+ billing: {
67
+ email: "traveler@example.com",
68
+ phone: "0712345678",
69
+ firstName: "Ana",
70
+ lastName: "Popescu",
71
+ city: "Bucharest",
72
+ country: 40,
73
+ state: "B",
74
+ postalCode: "010101",
75
+ details: "Str. Exemplu 1",
76
+ },
77
+ shipping: {
78
+ email: "traveler@example.com",
79
+ phone: "0712345678",
80
+ firstName: "Ana",
81
+ lastName: "Popescu",
82
+ city: "Bucharest",
83
+ country: 40,
84
+ state: "B",
85
+ postalCode: "010101",
86
+ details: "Str. Exemplu 1",
87
+ },
88
+ products: [{ name: "Tour deposit", price: 125, vat: 0 }],
89
+ installments: { selected: 1, available: [0] },
90
+ },
91
+ });
92
+ expect(response.payment?.ntpID).toBe("ntp_123");
93
+ const [url, init] = fetchMock.mock.calls[0];
94
+ expect(url).toBe("https://secure.mobilpay.ro/pay/payment/card/start");
95
+ expect(init.method).toBe("POST");
96
+ expect(init.headers.Authorization).toBe("api-key");
97
+ });
98
+ it("throws on provider error payload", async () => {
99
+ const fetchMock = vi.fn(async () => jsonResponse(200, { error: { code: "bad_request", message: "No merchant" } }));
100
+ const client = createNetopiaClient({
101
+ apiUrl: "https://secure.mobilpay.ro/pay",
102
+ apiKey: "api-key",
103
+ fetch: fetchMock,
104
+ });
105
+ await expect(client.startCardPayment({
106
+ config: {
107
+ emailTemplate: "confirm",
108
+ notifyUrl: "https://api.example.com/callback",
109
+ redirectUrl: "https://app.example.com/return",
110
+ language: "ro",
111
+ },
112
+ payment: {
113
+ options: { installments: 1 },
114
+ },
115
+ order: {
116
+ posSignature: "pos-signature",
117
+ dateTime: new Date().toISOString(),
118
+ description: "Tour deposit",
119
+ orderID: "pmss_123",
120
+ amount: 125,
121
+ currency: "RON",
122
+ billing: {
123
+ email: "traveler@example.com",
124
+ phone: "0712345678",
125
+ firstName: "Ana",
126
+ lastName: "Popescu",
127
+ city: "Bucharest",
128
+ country: 40,
129
+ state: "B",
130
+ postalCode: "010101",
131
+ details: "Str. Exemplu 1",
132
+ },
133
+ shipping: {
134
+ email: "traveler@example.com",
135
+ phone: "0712345678",
136
+ firstName: "Ana",
137
+ lastName: "Popescu",
138
+ city: "Bucharest",
139
+ country: 40,
140
+ state: "B",
141
+ postalCode: "010101",
142
+ details: "Str. Exemplu 1",
143
+ },
144
+ products: [{ name: "Tour deposit", price: 125, vat: 0 }],
145
+ installments: { selected: 1, available: [0] },
146
+ },
147
+ })).rejects.toThrow(/Netopia start payment failed/);
148
+ });
149
+ it("throws when fetch is unavailable", async () => {
150
+ const originalFetch = globalThis.fetch;
151
+ globalThis.fetch = undefined;
152
+ try {
153
+ // biome-ignore lint/suspicious/noExplicitAny: test stub
154
+ const client = createNetopiaClient({ apiUrl: "https://secure.mobilpay.ro/pay", apiKey: "api-key", fetch: undefined });
155
+ await expect(client.startCardPayment({
156
+ config: {
157
+ emailTemplate: "confirm",
158
+ notifyUrl: "https://api.example.com/callback",
159
+ redirectUrl: "https://app.example.com/return",
160
+ language: "ro",
161
+ },
162
+ payment: {
163
+ options: { installments: 1 },
164
+ },
165
+ order: {
166
+ posSignature: "pos-signature",
167
+ dateTime: new Date().toISOString(),
168
+ description: "Tour deposit",
169
+ orderID: "pmss_123",
170
+ amount: 125,
171
+ currency: "RON",
172
+ billing: {
173
+ email: "traveler@example.com",
174
+ phone: "0712345678",
175
+ firstName: "Ana",
176
+ lastName: "Popescu",
177
+ city: "Bucharest",
178
+ country: 40,
179
+ state: "B",
180
+ postalCode: "010101",
181
+ details: "Str. Exemplu 1",
182
+ },
183
+ shipping: {
184
+ email: "traveler@example.com",
185
+ phone: "0712345678",
186
+ firstName: "Ana",
187
+ lastName: "Popescu",
188
+ city: "Bucharest",
189
+ country: 40,
190
+ state: "B",
191
+ postalCode: "010101",
192
+ details: "Str. Exemplu 1",
193
+ },
194
+ products: [{ name: "Tour deposit", price: 125, vat: 0 }],
195
+ installments: { selected: 1, available: [0] },
196
+ },
197
+ })).rejects.toThrow(/requires a fetch implementation/);
198
+ }
199
+ finally {
200
+ globalThis.fetch = originalFetch;
201
+ }
202
+ });
203
+ it("surfaces non-2xx HTTP failures", async () => {
204
+ const fetchMock = vi.fn(async () => textResponse(500, "gateway error"));
205
+ const client = createNetopiaClient({
206
+ apiUrl: "https://secure.mobilpay.ro/pay",
207
+ apiKey: "api-key",
208
+ fetch: fetchMock,
209
+ });
210
+ await expect(client.startCardPayment({
211
+ config: {
212
+ emailTemplate: "confirm",
213
+ notifyUrl: "https://api.example.com/callback",
214
+ redirectUrl: "https://app.example.com/return",
215
+ language: "ro",
216
+ },
217
+ payment: {
218
+ options: { installments: 1 },
219
+ },
220
+ order: {
221
+ posSignature: "pos-signature",
222
+ dateTime: new Date().toISOString(),
223
+ description: "Tour deposit",
224
+ orderID: "pmss_123",
225
+ amount: 125,
226
+ currency: "RON",
227
+ billing: {
228
+ email: "traveler@example.com",
229
+ phone: "0712345678",
230
+ firstName: "Ana",
231
+ lastName: "Popescu",
232
+ city: "Bucharest",
233
+ country: 40,
234
+ state: "B",
235
+ postalCode: "010101",
236
+ details: "Str. Exemplu 1",
237
+ },
238
+ shipping: {
239
+ email: "traveler@example.com",
240
+ phone: "0712345678",
241
+ firstName: "Ana",
242
+ lastName: "Popescu",
243
+ city: "Bucharest",
244
+ country: 40,
245
+ state: "B",
246
+ postalCode: "010101",
247
+ details: "Str. Exemplu 1",
248
+ },
249
+ products: [{ name: "Tour deposit", price: 125, vat: 0 }],
250
+ installments: { selected: 1, available: [0] },
251
+ },
252
+ })).rejects.toThrow(/\(500\)/);
253
+ });
254
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=plugin.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/plugin.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,346 @@
1
+ import { financeService } from "@voyantjs/finance";
2
+ import { notificationsService } from "@voyantjs/notifications";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { deriveNetopiaOrderId, mapNetopiaPaymentStatus, netopiaService } from "../../src/service.js";
5
+ const baseSession = {
6
+ id: "pmss_123",
7
+ targetType: "booking",
8
+ targetId: "book_123",
9
+ bookingId: "book_123",
10
+ orderId: null,
11
+ invoiceId: null,
12
+ bookingPaymentScheduleId: null,
13
+ bookingGuaranteeId: null,
14
+ paymentInstrumentId: null,
15
+ paymentAuthorizationId: null,
16
+ paymentCaptureId: null,
17
+ paymentId: null,
18
+ status: "pending",
19
+ provider: null,
20
+ providerSessionId: null,
21
+ providerPaymentId: null,
22
+ externalReference: null,
23
+ idempotencyKey: null,
24
+ clientReference: "client_ref_123",
25
+ currency: "RON",
26
+ amountCents: 12500,
27
+ paymentMethod: null,
28
+ payerPersonId: null,
29
+ payerOrganizationId: null,
30
+ payerEmail: "traveler@example.com",
31
+ payerName: "Ana Popescu",
32
+ redirectUrl: null,
33
+ returnUrl: null,
34
+ cancelUrl: null,
35
+ callbackUrl: null,
36
+ expiresAt: null,
37
+ completedAt: null,
38
+ failedAt: null,
39
+ cancelledAt: null,
40
+ expiredAt: null,
41
+ failureCode: null,
42
+ failureMessage: null,
43
+ notes: "Tour deposit",
44
+ providerPayload: null,
45
+ metadata: null,
46
+ createdAt: new Date(),
47
+ updatedAt: new Date(),
48
+ };
49
+ const runtimeOptions = {
50
+ apiUrl: "https://secure.mobilpay.ro/pay",
51
+ apiKey: "api-key",
52
+ posSignature: "pos-signature",
53
+ notifyUrl: "https://api.example.com/netopia/callback",
54
+ redirectUrl: "https://app.example.com/checkout/return",
55
+ };
56
+ const billingInput = {
57
+ email: "traveler@example.com",
58
+ phone: "0712345678",
59
+ firstName: "Ana",
60
+ lastName: "Popescu",
61
+ city: "Bucharest",
62
+ country: 40,
63
+ state: "B",
64
+ postalCode: "010101",
65
+ details: "Str. Exemplu 1",
66
+ };
67
+ afterEach(() => {
68
+ vi.restoreAllMocks();
69
+ });
70
+ describe("deriveNetopiaOrderId", () => {
71
+ it("prefers externalReference, then clientReference, then session id", () => {
72
+ expect(deriveNetopiaOrderId({ ...baseSession, externalReference: "external_1" })).toBe("external_1");
73
+ expect(deriveNetopiaOrderId(baseSession)).toBe("client_ref_123");
74
+ expect(deriveNetopiaOrderId({ ...baseSession, clientReference: null })).toBe("pmss_123");
75
+ });
76
+ });
77
+ describe("mapNetopiaPaymentStatus", () => {
78
+ const config = { successStatuses: [3, 5], processingStatuses: [1, 15] };
79
+ it("maps success statuses", () => {
80
+ expect(mapNetopiaPaymentStatus(3, config)).toBe("completed");
81
+ expect(mapNetopiaPaymentStatus(5, config)).toBe("completed");
82
+ });
83
+ it("maps in-flight statuses", () => {
84
+ expect(mapNetopiaPaymentStatus(1, config)).toBe("processing");
85
+ expect(mapNetopiaPaymentStatus(15, config)).toBe("processing");
86
+ });
87
+ it("treats everything else as failed", () => {
88
+ expect(mapNetopiaPaymentStatus(12, config)).toBe("failed");
89
+ });
90
+ });
91
+ describe("netopiaService.startPaymentSession", () => {
92
+ it("starts a hosted payment and persists redirect state", async () => {
93
+ vi.spyOn(financeService, "getPaymentSessionById").mockResolvedValue(baseSession);
94
+ const updated = {
95
+ ...baseSession,
96
+ provider: "netopia",
97
+ status: "requires_redirect",
98
+ externalReference: "client_ref_123",
99
+ providerSessionId: "ntp_123",
100
+ redirectUrl: "https://secure.example.com/pay",
101
+ };
102
+ const markSpy = vi
103
+ .spyOn(financeService, "markPaymentSessionRequiresRedirect")
104
+ .mockResolvedValue(updated);
105
+ const client = {
106
+ startCardPayment: vi.fn(async () => ({
107
+ payment: {
108
+ paymentURL: "https://secure.example.com/pay",
109
+ ntpID: "ntp_123",
110
+ status: 1,
111
+ amount: 125,
112
+ currency: "RON",
113
+ },
114
+ })),
115
+ };
116
+ const result = await netopiaService.startPaymentSession({}, baseSession.id, {
117
+ billing: {
118
+ email: "traveler@example.com",
119
+ phone: "0712345678",
120
+ firstName: "Ana",
121
+ lastName: "Popescu",
122
+ city: "Bucharest",
123
+ country: 40,
124
+ state: "B",
125
+ postalCode: "010101",
126
+ details: "Str. Exemplu 1",
127
+ },
128
+ }, {
129
+ ...runtimeOptions,
130
+ }, client);
131
+ expect(result.orderId).toBe("client_ref_123");
132
+ expect(markSpy).toHaveBeenCalledWith({}, baseSession.id, expect.objectContaining({
133
+ provider: "netopia",
134
+ providerSessionId: "ntp_123",
135
+ redirectUrl: "https://secure.example.com/pay",
136
+ }));
137
+ });
138
+ });
139
+ describe("netopiaService.collect flows", () => {
140
+ it("creates, starts, and notifies for a booking schedule collection", async () => {
141
+ const createdSession = {
142
+ ...baseSession,
143
+ targetType: "booking_payment_schedule",
144
+ targetId: "bpms_123",
145
+ bookingPaymentScheduleId: "bpms_123",
146
+ provider: "netopia",
147
+ };
148
+ vi.spyOn(financeService, "createPaymentSessionFromBookingSchedule").mockResolvedValue(createdSession);
149
+ const started = {
150
+ session: {
151
+ ...createdSession,
152
+ status: "requires_redirect",
153
+ redirectUrl: "https://pay.example",
154
+ },
155
+ providerResponse: { payment: { paymentURL: "https://pay.example", ntpID: "ntp_123" } },
156
+ orderId: "order_123",
157
+ };
158
+ vi.spyOn(netopiaService, "startPaymentSession").mockResolvedValue(started);
159
+ const notifySpy = vi
160
+ .spyOn(notificationsService, "sendPaymentSessionNotification")
161
+ .mockResolvedValue({ id: "ntdl_123" });
162
+ const result = await netopiaService.collectBookingSchedule({}, "bpms_123", {
163
+ paymentSession: { payerEmail: "traveler@example.com" },
164
+ netopia: { billing: billingInput },
165
+ notification: {
166
+ channel: "email",
167
+ templateSlug: "payment-link",
168
+ subject: "Pay now",
169
+ },
170
+ }, runtimeOptions, undefined, {});
171
+ expect(financeService.createPaymentSessionFromBookingSchedule).toHaveBeenCalledWith({}, "bpms_123", expect.objectContaining({
172
+ provider: "netopia",
173
+ payerEmail: "traveler@example.com",
174
+ }));
175
+ expect(notifySpy).toHaveBeenCalledWith({}, {}, started.session.id, expect.objectContaining({
176
+ templateSlug: "payment-link",
177
+ }));
178
+ expect(result.paymentSessionNotification?.id).toBe("ntdl_123");
179
+ });
180
+ it("creates, starts, and can send both invoice and payment-session notifications", async () => {
181
+ const createdSession = {
182
+ ...baseSession,
183
+ targetType: "invoice",
184
+ targetId: "inv_123",
185
+ invoiceId: "inv_123",
186
+ provider: "netopia",
187
+ };
188
+ vi.spyOn(financeService, "createPaymentSessionFromInvoice").mockResolvedValue(createdSession);
189
+ const started = {
190
+ session: {
191
+ ...createdSession,
192
+ status: "requires_redirect",
193
+ redirectUrl: "https://pay.example",
194
+ },
195
+ providerResponse: { payment: { paymentURL: "https://pay.example", ntpID: "ntp_123" } },
196
+ orderId: "INV-123",
197
+ };
198
+ vi.spyOn(netopiaService, "startPaymentSession").mockResolvedValue(started);
199
+ const paymentNotifySpy = vi
200
+ .spyOn(notificationsService, "sendPaymentSessionNotification")
201
+ .mockResolvedValue({ id: "ntdl_pay_123" });
202
+ const invoiceNotifySpy = vi
203
+ .spyOn(notificationsService, "sendInvoiceNotification")
204
+ .mockResolvedValue({ id: "ntdl_inv_123" });
205
+ const result = await netopiaService.collectInvoice({}, "inv_123", {
206
+ paymentSession: { payerEmail: "traveler@example.com" },
207
+ netopia: { billing: billingInput },
208
+ paymentSessionNotification: {
209
+ channel: "email",
210
+ templateSlug: "payment-link",
211
+ subject: "Complete payment",
212
+ },
213
+ invoiceNotification: {
214
+ channel: "email",
215
+ templateSlug: "invoice-issued",
216
+ subject: "Invoice ready",
217
+ },
218
+ }, runtimeOptions, undefined, {});
219
+ expect(financeService.createPaymentSessionFromInvoice).toHaveBeenCalledWith({}, "inv_123", expect.objectContaining({
220
+ provider: "netopia",
221
+ }));
222
+ expect(paymentNotifySpy).toHaveBeenCalledTimes(1);
223
+ expect(invoiceNotifySpy).toHaveBeenCalledWith({}, {}, "inv_123", expect.objectContaining({
224
+ templateSlug: "invoice-issued",
225
+ }));
226
+ expect(result.paymentSessionNotification?.id).toBe("ntdl_pay_123");
227
+ expect(result.invoiceNotification?.id).toBe("ntdl_inv_123");
228
+ });
229
+ });
230
+ describe("netopiaService.handleCallback", () => {
231
+ it("completes a successful callback exactly once", async () => {
232
+ vi.spyOn(financeService, "listPaymentSessions").mockResolvedValue({
233
+ data: [{ ...baseSession, provider: "netopia", externalReference: "client_ref_123" }],
234
+ total: 1,
235
+ limit: 1,
236
+ offset: 0,
237
+ });
238
+ const completeSpy = vi.spyOn(financeService, "completePaymentSession").mockResolvedValue({
239
+ ...baseSession,
240
+ provider: "netopia",
241
+ status: "paid",
242
+ externalReference: "client_ref_123",
243
+ providerSessionId: "ntp_123",
244
+ providerPaymentId: "ntp_123",
245
+ });
246
+ const result = await netopiaService.handleCallback({}, {
247
+ order: { orderID: "client_ref_123" },
248
+ payment: {
249
+ amount: 125,
250
+ currency: "RON",
251
+ ntpID: "ntp_123",
252
+ status: 3,
253
+ data: { AuthCode: "AUTH1", RRN: "RRN1" },
254
+ },
255
+ }, runtimeOptions);
256
+ expect(result.action).toBe("completed");
257
+ expect(completeSpy).toHaveBeenCalledTimes(1);
258
+ });
259
+ it("marks in-flight statuses as processing", async () => {
260
+ vi.spyOn(financeService, "listPaymentSessions").mockResolvedValue({
261
+ data: [{ ...baseSession, provider: "netopia", externalReference: "client_ref_123" }],
262
+ total: 1,
263
+ limit: 1,
264
+ offset: 0,
265
+ });
266
+ const updateSpy = vi.spyOn(financeService, "updatePaymentSession").mockResolvedValue({
267
+ ...baseSession,
268
+ provider: "netopia",
269
+ status: "processing",
270
+ externalReference: "client_ref_123",
271
+ });
272
+ const result = await netopiaService.handleCallback({}, {
273
+ order: { orderID: "client_ref_123" },
274
+ payment: {
275
+ amount: 125,
276
+ currency: "RON",
277
+ ntpID: "ntp_123",
278
+ status: 1,
279
+ },
280
+ }, runtimeOptions);
281
+ expect(result.action).toBe("processing");
282
+ expect(updateSpy).toHaveBeenCalledTimes(1);
283
+ });
284
+ it("fails when callback amount does not match the stored session", async () => {
285
+ vi.spyOn(financeService, "listPaymentSessions").mockResolvedValue({
286
+ data: [{ ...baseSession, provider: "netopia", externalReference: "client_ref_123" }],
287
+ total: 1,
288
+ limit: 1,
289
+ offset: 0,
290
+ });
291
+ const failSpy = vi.spyOn(financeService, "failPaymentSession").mockResolvedValue({
292
+ ...baseSession,
293
+ provider: "netopia",
294
+ status: "failed",
295
+ externalReference: "client_ref_123",
296
+ });
297
+ const result = await netopiaService.handleCallback({}, {
298
+ order: { orderID: "client_ref_123" },
299
+ payment: {
300
+ amount: 999,
301
+ currency: "RON",
302
+ ntpID: "ntp_123",
303
+ status: 3,
304
+ },
305
+ }, runtimeOptions);
306
+ expect(result.action).toBe("failed");
307
+ expect(failSpy).toHaveBeenCalledWith({}, baseSession.id, expect.objectContaining({
308
+ failureCode: "amount_or_currency_mismatch",
309
+ }));
310
+ });
311
+ it("treats duplicate success callbacks as idempotent", async () => {
312
+ vi.spyOn(financeService, "listPaymentSessions").mockResolvedValue({
313
+ data: [
314
+ {
315
+ ...baseSession,
316
+ provider: "netopia",
317
+ status: "paid",
318
+ externalReference: "client_ref_123",
319
+ },
320
+ ],
321
+ total: 1,
322
+ limit: 1,
323
+ offset: 0,
324
+ });
325
+ const updateSpy = vi.spyOn(financeService, "updatePaymentSession").mockResolvedValue({
326
+ ...baseSession,
327
+ provider: "netopia",
328
+ status: "paid",
329
+ externalReference: "client_ref_123",
330
+ });
331
+ const completeSpy = vi.spyOn(financeService, "completePaymentSession");
332
+ const result = await netopiaService.handleCallback({}, {
333
+ order: { orderID: "client_ref_123" },
334
+ payment: {
335
+ amount: 125,
336
+ currency: "RON",
337
+ ntpID: "ntp_123",
338
+ status: 5,
339
+ },
340
+ }, runtimeOptions);
341
+ expect(result.action).toBe("ignored");
342
+ expect(result.reason).toBe("already_completed");
343
+ expect(updateSpy).toHaveBeenCalledTimes(1);
344
+ expect(completeSpy).not.toHaveBeenCalled();
345
+ });
346
+ });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@voyantjs/plugin-netopia",
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
+ "./service": {
20
+ "types": "./dist/service.d.ts",
21
+ "import": "./dist/service.js"
22
+ },
23
+ "./types": {
24
+ "types": "./dist/types.d.ts",
25
+ "import": "./dist/types.js"
26
+ },
27
+ "./validation": {
28
+ "types": "./dist/validation.d.ts",
29
+ "import": "./dist/validation.js"
30
+ }
31
+ },
32
+ "dependencies": {
33
+ "drizzle-orm": "^0.45.2",
34
+ "hono": "^4.12.10",
35
+ "zod": "^4.3.6",
36
+ "@voyantjs/hono": "0.1.0",
37
+ "@voyantjs/core": "0.1.0",
38
+ "@voyantjs/notifications": "0.1.0",
39
+ "@voyantjs/finance": "0.1.0"
40
+ },
41
+ "devDependencies": {
42
+ "typescript": "^6.0.2",
43
+ "vitest": "^4.1.2",
44
+ "@voyantjs/voyant-typescript-config": "0.1.0"
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "scripts": {
53
+ "typecheck": "tsc --noEmit",
54
+ "lint": "biome check src/",
55
+ "test": "vitest run",
56
+ "build": "tsc -p tsconfig.json",
57
+ "clean": "rm -rf dist"
58
+ },
59
+ "main": "./dist/index.js",
60
+ "types": "./dist/index.d.ts"
61
+ }