@voyantjs/plugin-netopia 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/client.d.ts.map +1 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/{src/plugin.d.ts → plugin.d.ts} +32 -32
  4. package/dist/{src/plugin.d.ts.map → plugin.d.ts.map} +1 -1
  5. package/dist/service-callback.d.ts +5 -0
  6. package/dist/service-callback.d.ts.map +1 -0
  7. package/dist/service-callback.js +124 -0
  8. package/dist/service-collect.d.ts +9 -0
  9. package/dist/service-collect.d.ts.map +1 -0
  10. package/dist/service-collect.js +70 -0
  11. package/dist/service-shared.d.ts +33 -0
  12. package/dist/service-shared.d.ts.map +1 -0
  13. package/dist/service-shared.js +44 -0
  14. package/dist/service-start.d.ts +6 -0
  15. package/dist/service-start.d.ts.map +1 -0
  16. package/dist/service-start.js +81 -0
  17. package/dist/service.d.ts +13 -0
  18. package/dist/service.d.ts.map +1 -0
  19. package/dist/service.js +12 -0
  20. package/dist/types.d.ts.map +1 -0
  21. package/dist/validation.d.ts.map +1 -0
  22. package/package.json +6 -6
  23. package/dist/src/client.d.ts.map +0 -1
  24. package/dist/src/index.d.ts.map +0 -1
  25. package/dist/src/service.d.ts +0 -36
  26. package/dist/src/service.d.ts.map +0 -1
  27. package/dist/src/service.js +0 -314
  28. package/dist/src/types.d.ts.map +0 -1
  29. package/dist/src/validation.d.ts.map +0 -1
  30. package/dist/tests/unit/client.test.d.ts +0 -2
  31. package/dist/tests/unit/client.test.d.ts.map +0 -1
  32. package/dist/tests/unit/client.test.js +0 -254
  33. package/dist/tests/unit/plugin.test.d.ts +0 -2
  34. package/dist/tests/unit/plugin.test.d.ts.map +0 -1
  35. package/dist/tests/unit/plugin.test.js +0 -346
  36. /package/dist/{src/client.d.ts → client.d.ts} +0 -0
  37. /package/dist/{src/client.js → client.js} +0 -0
  38. /package/dist/{src/index.d.ts → index.d.ts} +0 -0
  39. /package/dist/{src/index.js → index.js} +0 -0
  40. /package/dist/{src/plugin.js → plugin.js} +0 -0
  41. /package/dist/{src/types.d.ts → types.d.ts} +0 -0
  42. /package/dist/{src/types.js → types.js} +0 -0
  43. /package/dist/{src/validation.d.ts → validation.d.ts} +0 -0
  44. /package/dist/{src/validation.js → validation.js} +0 -0
@@ -1,254 +0,0 @@
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
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=plugin.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"plugin.test.d.ts","sourceRoot":"","sources":["../../../tests/unit/plugin.test.ts"],"names":[],"mappings":""}
@@ -1,346 +0,0 @@
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
- });
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes