@86d-app/payments 0.0.3 → 0.0.6

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 (63) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/AGENTS.md +52 -45
  3. package/README.md +27 -7
  4. package/dist/__tests__/controllers.test.d.ts +2 -0
  5. package/dist/__tests__/controllers.test.d.ts.map +1 -0
  6. package/dist/__tests__/edge-cases.test.d.ts +2 -0
  7. package/dist/__tests__/edge-cases.test.d.ts.map +1 -0
  8. package/dist/__tests__/endpoint-security.test.d.ts +2 -0
  9. package/dist/__tests__/endpoint-security.test.d.ts.map +1 -0
  10. package/dist/__tests__/financial-safety.test.d.ts +2 -0
  11. package/dist/__tests__/financial-safety.test.d.ts.map +1 -0
  12. package/dist/__tests__/service-impl.test.d.ts +2 -0
  13. package/dist/__tests__/service-impl.test.d.ts.map +1 -0
  14. package/dist/admin/components/index.d.ts +2 -0
  15. package/dist/admin/components/index.d.ts.map +1 -0
  16. package/dist/admin/components/payments-admin.d.ts +2 -0
  17. package/dist/admin/components/payments-admin.d.ts.map +1 -0
  18. package/dist/admin/endpoints/create-refund.d.ts +20 -0
  19. package/dist/admin/endpoints/create-refund.d.ts.map +1 -0
  20. package/dist/admin/endpoints/get-intent.d.ts +16 -0
  21. package/dist/admin/endpoints/get-intent.d.ts.map +1 -0
  22. package/dist/admin/endpoints/index.d.ts +63 -0
  23. package/dist/admin/endpoints/index.d.ts.map +1 -0
  24. package/dist/admin/endpoints/list-intents.d.ts +22 -0
  25. package/dist/admin/endpoints/list-intents.d.ts.map +1 -0
  26. package/dist/admin/endpoints/list-refunds.d.ts +10 -0
  27. package/dist/admin/endpoints/list-refunds.d.ts.map +1 -0
  28. package/dist/index.d.ts +11 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/schema.d.ts +169 -0
  31. package/dist/schema.d.ts.map +1 -0
  32. package/dist/service-impl.d.ts +4 -0
  33. package/dist/service-impl.d.ts.map +1 -0
  34. package/dist/service.d.ts +125 -0
  35. package/dist/service.d.ts.map +1 -0
  36. package/dist/store/endpoints/cancel-intent.d.ts +16 -0
  37. package/dist/store/endpoints/cancel-intent.d.ts.map +1 -0
  38. package/dist/store/endpoints/confirm-intent.d.ts +16 -0
  39. package/dist/store/endpoints/confirm-intent.d.ts.map +1 -0
  40. package/dist/store/endpoints/create-intent.d.ts +15 -0
  41. package/dist/store/endpoints/create-intent.d.ts.map +1 -0
  42. package/dist/store/endpoints/delete-method.d.ts +16 -0
  43. package/dist/store/endpoints/delete-method.d.ts.map +1 -0
  44. package/dist/store/endpoints/get-intent.d.ts +16 -0
  45. package/dist/store/endpoints/get-intent.d.ts.map +1 -0
  46. package/dist/store/endpoints/index.d.ts +83 -0
  47. package/dist/store/endpoints/index.d.ts.map +1 -0
  48. package/dist/store/endpoints/list-methods.d.ts +12 -0
  49. package/dist/store/endpoints/list-methods.d.ts.map +1 -0
  50. package/package.json +3 -3
  51. package/src/__tests__/controllers.test.ts +1043 -0
  52. package/src/__tests__/edge-cases.test.ts +547 -0
  53. package/src/__tests__/endpoint-security.test.ts +360 -0
  54. package/src/__tests__/financial-safety.test.ts +576 -0
  55. package/src/__tests__/service-impl.test.ts +30 -5
  56. package/src/admin/endpoints/create-refund.ts +11 -6
  57. package/src/service-impl.ts +60 -0
  58. package/src/store/endpoints/cancel-intent.ts +20 -4
  59. package/src/store/endpoints/confirm-intent.ts +20 -4
  60. package/src/store/endpoints/create-intent.ts +18 -5
  61. package/src/store/endpoints/delete-method.ts +11 -1
  62. package/src/store/endpoints/get-intent.ts +1 -1
  63. package/src/store/endpoints/list-methods.ts +7 -5
@@ -0,0 +1,547 @@
1
+ import { createMockDataService } from "@86d-app/core/test-utils";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import type { PaymentProvider } from "../service";
4
+ import { createPaymentController } from "../service-impl";
5
+
6
+ describe("payments edge cases", () => {
7
+ let mockData: ReturnType<typeof createMockDataService>;
8
+ let controller: ReturnType<typeof createPaymentController>;
9
+
10
+ beforeEach(() => {
11
+ mockData = createMockDataService();
12
+ controller = createPaymentController(mockData);
13
+ });
14
+
15
+ // ── State transition edge cases ─────────────────────────────────────
16
+
17
+ describe("confirmIntent state transitions", () => {
18
+ it("confirms a pending intent", async () => {
19
+ const intent = await controller.createIntent({ amount: 1000 });
20
+ expect(intent.status).toBe("pending");
21
+ const confirmed = await controller.confirmIntent(intent.id);
22
+ expect(confirmed?.status).toBe("succeeded");
23
+ });
24
+
25
+ it("confirms a processing intent", async () => {
26
+ const mockProvider: PaymentProvider = {
27
+ createIntent: vi.fn().mockResolvedValue({
28
+ providerIntentId: "pi_proc",
29
+ status: "processing",
30
+ providerMetadata: {},
31
+ }),
32
+ confirmIntent: vi.fn().mockResolvedValue({
33
+ status: "succeeded",
34
+ providerMetadata: { confirmed: true },
35
+ }),
36
+ cancelIntent: vi.fn(),
37
+ createRefund: vi.fn(),
38
+ };
39
+ const ctrl = createPaymentController(mockData, mockProvider);
40
+ const intent = await ctrl.createIntent({ amount: 1000 });
41
+ expect(intent.status).toBe("processing");
42
+
43
+ const confirmed = await ctrl.confirmIntent(intent.id);
44
+ expect(confirmed?.status).toBe("succeeded");
45
+ });
46
+
47
+ it("sets updatedAt when confirming", async () => {
48
+ const intent = await controller.createIntent({ amount: 1000 });
49
+ const before = intent.updatedAt;
50
+ // Small delay to ensure different timestamp
51
+ await new Promise((r) => setTimeout(r, 5));
52
+ const confirmed = await controller.confirmIntent(intent.id);
53
+ expect(confirmed?.updatedAt.getTime()).toBeGreaterThanOrEqual(
54
+ before.getTime(),
55
+ );
56
+ });
57
+ });
58
+
59
+ describe("cancelIntent state transitions", () => {
60
+ it("sets updatedAt when cancelling", async () => {
61
+ const intent = await controller.createIntent({ amount: 1000 });
62
+ const before = intent.updatedAt;
63
+ await new Promise((r) => setTimeout(r, 5));
64
+ const cancelled = await controller.cancelIntent(intent.id);
65
+ expect(cancelled?.updatedAt.getTime()).toBeGreaterThanOrEqual(
66
+ before.getTime(),
67
+ );
68
+ });
69
+ });
70
+
71
+ // ── Provider error handling ──────────────────────────────────────────
72
+
73
+ describe("provider error handling", () => {
74
+ it("propagates provider.createIntent errors", async () => {
75
+ const mockProvider: PaymentProvider = {
76
+ createIntent: vi.fn().mockRejectedValue(new Error("Gateway timeout")),
77
+ confirmIntent: vi.fn(),
78
+ cancelIntent: vi.fn(),
79
+ createRefund: vi.fn(),
80
+ };
81
+ const ctrl = createPaymentController(mockData, mockProvider);
82
+ await expect(ctrl.createIntent({ amount: 1000 })).rejects.toThrow(
83
+ "Gateway timeout",
84
+ );
85
+ });
86
+
87
+ it("propagates provider.confirmIntent errors", async () => {
88
+ const mockProvider: PaymentProvider = {
89
+ createIntent: vi.fn().mockResolvedValue({
90
+ providerIntentId: "pi_err",
91
+ status: "pending",
92
+ providerMetadata: {},
93
+ }),
94
+ confirmIntent: vi.fn().mockRejectedValue(new Error("Card declined")),
95
+ cancelIntent: vi.fn(),
96
+ createRefund: vi.fn(),
97
+ };
98
+ const ctrl = createPaymentController(mockData, mockProvider);
99
+ const intent = await ctrl.createIntent({ amount: 1000 });
100
+ await expect(ctrl.confirmIntent(intent.id)).rejects.toThrow(
101
+ "Card declined",
102
+ );
103
+ });
104
+
105
+ it("propagates provider.cancelIntent errors", async () => {
106
+ const mockProvider: PaymentProvider = {
107
+ createIntent: vi.fn().mockResolvedValue({
108
+ providerIntentId: "pi_cancel_err",
109
+ status: "pending",
110
+ providerMetadata: {},
111
+ }),
112
+ confirmIntent: vi.fn(),
113
+ cancelIntent: vi.fn().mockRejectedValue(new Error("Cannot cancel")),
114
+ createRefund: vi.fn(),
115
+ };
116
+ const ctrl = createPaymentController(mockData, mockProvider);
117
+ const intent = await ctrl.createIntent({ amount: 1000 });
118
+ await expect(ctrl.cancelIntent(intent.id)).rejects.toThrow(
119
+ "Cannot cancel",
120
+ );
121
+ });
122
+
123
+ it("propagates provider.createRefund errors", async () => {
124
+ const mockProvider: PaymentProvider = {
125
+ createIntent: vi.fn().mockResolvedValue({
126
+ providerIntentId: "pi_ref_err",
127
+ status: "succeeded",
128
+ providerMetadata: {},
129
+ }),
130
+ confirmIntent: vi.fn(),
131
+ cancelIntent: vi.fn(),
132
+ createRefund: vi
133
+ .fn()
134
+ .mockRejectedValue(new Error("Refund limit exceeded")),
135
+ };
136
+ const ctrl = createPaymentController(mockData, mockProvider);
137
+ const intent = await ctrl.createIntent({ amount: 5000 });
138
+ // Provider-created intent already has 'succeeded' status
139
+ await expect(ctrl.createRefund({ intentId: intent.id })).rejects.toThrow(
140
+ "Refund limit exceeded",
141
+ );
142
+ });
143
+ });
144
+
145
+ // ── Provider returning non-success statuses ─────────────────────────
146
+
147
+ describe("provider status variants", () => {
148
+ it("handles provider returning failed status on createIntent", async () => {
149
+ const mockProvider: PaymentProvider = {
150
+ createIntent: vi.fn().mockResolvedValue({
151
+ providerIntentId: "pi_fail",
152
+ status: "failed",
153
+ providerMetadata: { reason: "insufficient_funds" },
154
+ }),
155
+ confirmIntent: vi.fn(),
156
+ cancelIntent: vi.fn(),
157
+ createRefund: vi.fn(),
158
+ };
159
+ const ctrl = createPaymentController(mockData, mockProvider);
160
+ const intent = await ctrl.createIntent({ amount: 1000 });
161
+ expect(intent.status).toBe("failed");
162
+ expect(intent.providerMetadata).toEqual({
163
+ reason: "insufficient_funds",
164
+ });
165
+ });
166
+
167
+ it("handles provider returning processing on confirmIntent", async () => {
168
+ const mockProvider: PaymentProvider = {
169
+ createIntent: vi.fn().mockResolvedValue({
170
+ providerIntentId: "pi_slow",
171
+ status: "pending",
172
+ providerMetadata: {},
173
+ }),
174
+ confirmIntent: vi.fn().mockResolvedValue({
175
+ status: "processing",
176
+ providerMetadata: { note: "3ds_required" },
177
+ }),
178
+ cancelIntent: vi.fn(),
179
+ createRefund: vi.fn(),
180
+ };
181
+ const ctrl = createPaymentController(mockData, mockProvider);
182
+ const intent = await ctrl.createIntent({ amount: 2000 });
183
+ const confirmed = await ctrl.confirmIntent(intent.id);
184
+ expect(confirmed?.status).toBe("processing");
185
+ });
186
+
187
+ it("handles provider returning pending refund status", async () => {
188
+ const mockProvider: PaymentProvider = {
189
+ createIntent: vi.fn().mockResolvedValue({
190
+ providerIntentId: "pi_pend_ref",
191
+ status: "succeeded",
192
+ providerMetadata: {},
193
+ }),
194
+ confirmIntent: vi.fn(),
195
+ cancelIntent: vi.fn(),
196
+ createRefund: vi.fn().mockResolvedValue({
197
+ providerRefundId: "re_pend",
198
+ status: "pending",
199
+ }),
200
+ };
201
+ const ctrl = createPaymentController(mockData, mockProvider);
202
+ const intent = await ctrl.createIntent({ amount: 5000 });
203
+ // Provider-created intent already has 'succeeded' status
204
+ const refund = await ctrl.createRefund({ intentId: intent.id });
205
+ expect(refund.status).toBe("pending");
206
+ });
207
+ });
208
+
209
+ // ── Multiple refunds ────────────────────────────────────────────────
210
+
211
+ describe("multiple refunds", () => {
212
+ it("supports multiple partial refunds on the same intent", async () => {
213
+ const intent = await controller.createIntent({ amount: 10000 });
214
+ await controller.confirmIntent(intent.id);
215
+ const r1 = await controller.createRefund({
216
+ intentId: intent.id,
217
+ amount: 3000,
218
+ reason: "Item 1 returned",
219
+ });
220
+ const r2 = await controller.createRefund({
221
+ intentId: intent.id,
222
+ amount: 2000,
223
+ reason: "Item 2 returned",
224
+ });
225
+ expect(r1.amount).toBe(3000);
226
+ expect(r2.amount).toBe(2000);
227
+
228
+ const refunds = await controller.listRefunds(intent.id);
229
+ expect(refunds).toHaveLength(2);
230
+ const totalRefunded = refunds.reduce((sum, r) => sum + r.amount, 0);
231
+ expect(totalRefunded).toBe(5000);
232
+ });
233
+
234
+ it("each refund gets a unique ID", async () => {
235
+ const intent = await controller.createIntent({ amount: 10000 });
236
+ await controller.confirmIntent(intent.id);
237
+ const r1 = await controller.createRefund({
238
+ intentId: intent.id,
239
+ amount: 1000,
240
+ });
241
+ const r2 = await controller.createRefund({
242
+ intentId: intent.id,
243
+ amount: 2000,
244
+ });
245
+ expect(r1.id).not.toBe(r2.id);
246
+ });
247
+ });
248
+
249
+ // ── Combined filters ────────────────────────────────────────────────
250
+
251
+ describe("listIntents combined filters", () => {
252
+ it("combines customerId and status filters", async () => {
253
+ await controller.createIntent({
254
+ amount: 100,
255
+ customerId: "cust_a",
256
+ });
257
+ const i2 = await controller.createIntent({
258
+ amount: 200,
259
+ customerId: "cust_a",
260
+ });
261
+ await controller.confirmIntent(i2.id);
262
+ await controller.createIntent({
263
+ amount: 300,
264
+ customerId: "cust_b",
265
+ });
266
+
267
+ const results = await controller.listIntents({
268
+ customerId: "cust_a",
269
+ status: "succeeded",
270
+ });
271
+ expect(results).toHaveLength(1);
272
+ expect(results[0]?.amount).toBe(200);
273
+ });
274
+
275
+ it("combines orderId and status filters", async () => {
276
+ await controller.createIntent({
277
+ amount: 100,
278
+ orderId: "ord_1",
279
+ });
280
+ const i2 = await controller.createIntent({
281
+ amount: 200,
282
+ orderId: "ord_1",
283
+ });
284
+ await controller.confirmIntent(i2.id);
285
+
286
+ const results = await controller.listIntents({
287
+ orderId: "ord_1",
288
+ status: "pending",
289
+ });
290
+ expect(results).toHaveLength(1);
291
+ expect(results[0].amount).toBe(100);
292
+ });
293
+
294
+ it("returns empty when no intents match filters", async () => {
295
+ await controller.createIntent({ amount: 100 });
296
+ const results = await controller.listIntents({
297
+ customerId: "nonexistent",
298
+ });
299
+ expect(results).toHaveLength(0);
300
+ });
301
+
302
+ it("returns empty when status filter matches nothing", async () => {
303
+ await controller.createIntent({ amount: 100 });
304
+ const results = await controller.listIntents({ status: "failed" });
305
+ expect(results).toHaveLength(0);
306
+ });
307
+ });
308
+
309
+ // ── Payment methods edge cases ──────────────────────────────────────
310
+
311
+ describe("payment methods edge cases", () => {
312
+ it("returns empty array for customer with no methods", async () => {
313
+ const methods = await controller.listPaymentMethods("no_methods_cust");
314
+ expect(methods).toHaveLength(0);
315
+ });
316
+
317
+ it("does not clear defaults for different customers", async () => {
318
+ const m1 = await controller.savePaymentMethod({
319
+ customerId: "cust_1",
320
+ providerMethodId: "pm_1",
321
+ isDefault: true,
322
+ });
323
+ const m2 = await controller.savePaymentMethod({
324
+ customerId: "cust_2",
325
+ providerMethodId: "pm_2",
326
+ isDefault: true,
327
+ });
328
+
329
+ // Both should still be defaults
330
+ const check1 = await controller.getPaymentMethod(m1.id);
331
+ const check2 = await controller.getPaymentMethod(m2.id);
332
+ expect(check1?.isDefault).toBe(true);
333
+ expect(check2?.isDefault).toBe(true);
334
+ });
335
+
336
+ it("saves method with all optional fields", async () => {
337
+ const method = await controller.savePaymentMethod({
338
+ customerId: "cust_1",
339
+ providerMethodId: "pm_full",
340
+ type: "card",
341
+ last4: "1234",
342
+ brand: "mastercard",
343
+ expiryMonth: 6,
344
+ expiryYear: 2028,
345
+ isDefault: false,
346
+ });
347
+ expect(method.type).toBe("card");
348
+ expect(method.last4).toBe("1234");
349
+ expect(method.brand).toBe("mastercard");
350
+ expect(method.expiryMonth).toBe(6);
351
+ expect(method.expiryYear).toBe(2028);
352
+ });
353
+
354
+ it("saves method with non-card type", async () => {
355
+ const method = await controller.savePaymentMethod({
356
+ customerId: "cust_1",
357
+ providerMethodId: "pm_bank",
358
+ type: "bank_account",
359
+ });
360
+ expect(method.type).toBe("bank_account");
361
+ });
362
+ });
363
+
364
+ // ── Webhook edge cases ──────────────────────────────────────────────
365
+
366
+ describe("webhook edge cases", () => {
367
+ it("handleWebhookEvent merges providerMetadata", async () => {
368
+ const mockProvider: PaymentProvider = {
369
+ createIntent: vi.fn().mockResolvedValue({
370
+ providerIntentId: "pi_merge",
371
+ status: "pending",
372
+ providerMetadata: { initial: true },
373
+ }),
374
+ confirmIntent: vi.fn(),
375
+ cancelIntent: vi.fn(),
376
+ createRefund: vi.fn(),
377
+ };
378
+ const ctrl = createPaymentController(mockData, mockProvider);
379
+ await ctrl.createIntent({ amount: 1000 });
380
+
381
+ const result = await ctrl.handleWebhookEvent({
382
+ providerIntentId: "pi_merge",
383
+ status: "succeeded",
384
+ providerMetadata: { webhook: true },
385
+ });
386
+ expect(result?.providerMetadata).toEqual({
387
+ initial: true,
388
+ webhook: true,
389
+ });
390
+ });
391
+
392
+ it("handleWebhookEvent without providerMetadata preserves existing", async () => {
393
+ const mockProvider: PaymentProvider = {
394
+ createIntent: vi.fn().mockResolvedValue({
395
+ providerIntentId: "pi_no_meta",
396
+ status: "pending",
397
+ providerMetadata: { original: "data" },
398
+ }),
399
+ confirmIntent: vi.fn(),
400
+ cancelIntent: vi.fn(),
401
+ createRefund: vi.fn(),
402
+ };
403
+ const ctrl = createPaymentController(mockData, mockProvider);
404
+ await ctrl.createIntent({ amount: 2000 });
405
+
406
+ const result = await ctrl.handleWebhookEvent({
407
+ providerIntentId: "pi_no_meta",
408
+ status: "succeeded",
409
+ });
410
+ expect(result?.providerMetadata).toEqual({ original: "data" });
411
+ });
412
+
413
+ it("handleWebhookEvent transitions to failed status", async () => {
414
+ const mockProvider: PaymentProvider = {
415
+ createIntent: vi.fn().mockResolvedValue({
416
+ providerIntentId: "pi_will_fail",
417
+ status: "pending",
418
+ providerMetadata: {},
419
+ }),
420
+ confirmIntent: vi.fn(),
421
+ cancelIntent: vi.fn(),
422
+ createRefund: vi.fn(),
423
+ };
424
+ const ctrl = createPaymentController(mockData, mockProvider);
425
+ await ctrl.createIntent({ amount: 3000 });
426
+
427
+ const result = await ctrl.handleWebhookEvent({
428
+ providerIntentId: "pi_will_fail",
429
+ status: "failed",
430
+ providerMetadata: { error: "card_declined" },
431
+ });
432
+ expect(result?.status).toBe("failed");
433
+ });
434
+
435
+ it("handleWebhookRefund with reason", async () => {
436
+ const mockProvider: PaymentProvider = {
437
+ createIntent: vi.fn().mockResolvedValue({
438
+ providerIntentId: "pi_ref_reason",
439
+ status: "succeeded",
440
+ providerMetadata: {},
441
+ }),
442
+ confirmIntent: vi.fn(),
443
+ cancelIntent: vi.fn(),
444
+ createRefund: vi.fn(),
445
+ };
446
+ const ctrl = createPaymentController(mockData, mockProvider);
447
+ await ctrl.createIntent({ amount: 5000 });
448
+
449
+ const result = await ctrl.handleWebhookRefund({
450
+ providerIntentId: "pi_ref_reason",
451
+ providerRefundId: "re_reason",
452
+ amount: 2500,
453
+ reason: "Duplicate charge",
454
+ });
455
+ expect(result?.refund.reason).toBe("Duplicate charge");
456
+ expect(result?.refund.amount).toBe(2500);
457
+ });
458
+
459
+ it("handleWebhookRefund marks intent as refunded", async () => {
460
+ const mockProvider: PaymentProvider = {
461
+ createIntent: vi.fn().mockResolvedValue({
462
+ providerIntentId: "pi_ref_status",
463
+ status: "succeeded",
464
+ providerMetadata: {},
465
+ }),
466
+ confirmIntent: vi.fn(),
467
+ cancelIntent: vi.fn(),
468
+ createRefund: vi.fn(),
469
+ };
470
+ const ctrl = createPaymentController(mockData, mockProvider);
471
+ await ctrl.createIntent({ amount: 3000 });
472
+
473
+ const result = await ctrl.handleWebhookRefund({
474
+ providerIntentId: "pi_ref_status",
475
+ providerRefundId: "re_status",
476
+ amount: 3000,
477
+ });
478
+ expect(result?.intent.status).toBe("refunded");
479
+
480
+ // Verify persisted
481
+ const all = await ctrl.listIntents({ status: "refunded" });
482
+ expect(all).toHaveLength(1);
483
+ });
484
+ });
485
+
486
+ // ── Refund edge cases ───────────────────────────────────────────────
487
+
488
+ describe("refund edge cases", () => {
489
+ it("refund reason is optional", async () => {
490
+ const intent = await controller.createIntent({ amount: 5000 });
491
+ await controller.confirmIntent(intent.id);
492
+ const refund = await controller.createRefund({
493
+ intentId: intent.id,
494
+ });
495
+ expect(refund.reason).toBeUndefined();
496
+ });
497
+
498
+ it("refund preserves paymentIntentId", async () => {
499
+ const intent = await controller.createIntent({ amount: 2000 });
500
+ await controller.confirmIntent(intent.id);
501
+ const refund = await controller.createRefund({
502
+ intentId: intent.id,
503
+ amount: 500,
504
+ });
505
+ expect(refund.paymentIntentId).toBe(intent.id);
506
+ });
507
+
508
+ it("refund sets createdAt and updatedAt", async () => {
509
+ const intent = await controller.createIntent({ amount: 2000 });
510
+ await controller.confirmIntent(intent.id);
511
+ const refund = await controller.createRefund({
512
+ intentId: intent.id,
513
+ });
514
+ expect(refund.createdAt).toBeInstanceOf(Date);
515
+ expect(refund.updatedAt).toBeInstanceOf(Date);
516
+ });
517
+ });
518
+
519
+ // ── No-op confirmIntent on local controller ─────────────────────────
520
+
521
+ describe("local mode (no provider)", () => {
522
+ it("confirmIntent skips provider delegation", async () => {
523
+ const intent = await controller.createIntent({ amount: 1000 });
524
+ const confirmed = await controller.confirmIntent(intent.id);
525
+ expect(confirmed?.status).toBe("succeeded");
526
+ expect(confirmed?.providerMetadata).toEqual({});
527
+ });
528
+
529
+ it("cancelIntent skips provider delegation", async () => {
530
+ const intent = await controller.createIntent({ amount: 1000 });
531
+ const cancelled = await controller.cancelIntent(intent.id);
532
+ expect(cancelled?.status).toBe("cancelled");
533
+ expect(cancelled?.providerMetadata).toEqual({});
534
+ });
535
+
536
+ it("createRefund works without provider for local intents", async () => {
537
+ const intent = await controller.createIntent({ amount: 3000 });
538
+ await controller.confirmIntent(intent.id);
539
+ const refund = await controller.createRefund({
540
+ intentId: intent.id,
541
+ amount: 1000,
542
+ });
543
+ expect(refund.providerRefundId).toMatch(/^local_re_/);
544
+ expect(refund.status).toBe("succeeded");
545
+ });
546
+ });
547
+ });