|
1 | | -import { describe, it, expect, jest, beforeEach } from '@jest/globals'; |
| 1 | +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; |
2 | 2 | import BraintreeImportService from '../../services/braintree-import'; |
3 | 3 | import { BraintreeConstructorArgs } from '../braintree-base'; |
4 | 4 |
|
@@ -49,10 +49,11 @@ describe('BraintreeImportService', () => { |
49 | 49 | context: { idempotency_key: 'idem' }, |
50 | 50 | } as any); |
51 | 51 | expect(init.id).toBeDefined(); |
| 52 | + expect(gateway.transaction.find).toHaveBeenCalledTimes(1); |
| 53 | + expect(gateway.transaction.find).toHaveBeenCalledWith('t1'); |
52 | 54 |
|
53 | 55 | const auth = await service.authorizePayment({ data: init.data } as any); |
54 | 56 | expect(auth.status).toBe('authorized'); |
55 | | - expect(gateway.transaction.find).not.toHaveBeenCalled(); |
56 | 57 |
|
57 | 58 | const cap = await service.capturePayment({ data: auth.data } as any); |
58 | 59 | expect((cap.data as any).status).toBe('captured'); |
@@ -83,10 +84,88 @@ describe('BraintreeImportService', () => { |
83 | 84 | const { service, gateway } = buildService(); |
84 | 85 | const session = { transactionId: 't3', importedAsRefunded: false, refundedTotal: 1.25, status: 'captured' } as any; |
85 | 86 | gateway.transaction.find.mockResolvedValueOnce({ id: 't3', status: 'settled' }); |
86 | | - gateway.transaction.refund.mockResolvedValueOnce({ transaction: { id: 'r3' } }); |
| 87 | + gateway.transaction.refund.mockResolvedValueOnce({ success: true, transaction: { id: 'r3' } }); |
87 | 88 |
|
88 | 89 | const res = await service.refundPayment({ amount: 2.75, data: session } as any); |
89 | 90 | expect(gateway.transaction.refund).toHaveBeenCalledWith('t3', '2.75'); |
90 | 91 | expect((res.data as any).refundedTotal).toBe(4.0); |
91 | 92 | }); |
| 93 | + |
| 94 | + it('gracefully handles already-refunded transactions when allowRefundOnRefunded is enabled', async () => { |
| 95 | + const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() } as any; |
| 96 | + const cache = { get: jest.fn(), set: jest.fn() } as any; |
| 97 | + |
| 98 | + const container: BraintreeConstructorArgs = { logger, cache }; |
| 99 | + |
| 100 | + const options = { |
| 101 | + environment: 'sandbox' as const, |
| 102 | + merchantId: 'merchant', |
| 103 | + publicKey: 'public', |
| 104 | + privateKey: 'private', |
| 105 | + enable3DSecure: false, |
| 106 | + savePaymentMethod: false, |
| 107 | + webhookSecret: 'whsec', |
| 108 | + autoCapture: true, |
| 109 | + allowRefundOnRefunded: true, // Enable graceful handling |
| 110 | + } as any; |
| 111 | + |
| 112 | + const service = new BraintreeImportService(container, options); |
| 113 | + |
| 114 | + const gateway = { |
| 115 | + transaction: { |
| 116 | + find: jest.fn(), |
| 117 | + void: jest.fn(), |
| 118 | + refund: jest.fn(), |
| 119 | + }, |
| 120 | + } as any; |
| 121 | + |
| 122 | + (service as any).gateway = gateway; |
| 123 | + |
| 124 | + const session = { transactionId: 't4', importedAsRefunded: false, refundedTotal: 0, status: 'captured' } as any; |
| 125 | + gateway.transaction.find.mockResolvedValueOnce({ id: 't4', status: 'settled' }); |
| 126 | + gateway.transaction.refund.mockRejectedValueOnce(new Error('Transaction has already been refunded')); |
| 127 | + |
| 128 | + const res = await service.refundPayment({ amount: 10, data: session } as any); |
| 129 | + |
| 130 | + // Should not throw error, but log warning and update locally |
| 131 | + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('already refunded in Braintree')); |
| 132 | + expect((res.data as any).refundedTotal).toBe(10); |
| 133 | + }); |
| 134 | + |
| 135 | + it('throws error on non-refund-related errors even with allowRefundOnRefunded enabled', async () => { |
| 136 | + const logger = { info: jest.fn(), warn: jest.fn(), error: jest.fn() } as any; |
| 137 | + const cache = { get: jest.fn(), set: jest.fn() } as any; |
| 138 | + |
| 139 | + const container: BraintreeConstructorArgs = { logger, cache }; |
| 140 | + |
| 141 | + const options = { |
| 142 | + environment: 'sandbox' as const, |
| 143 | + merchantId: 'merchant', |
| 144 | + publicKey: 'public', |
| 145 | + privateKey: 'private', |
| 146 | + enable3DSecure: false, |
| 147 | + savePaymentMethod: false, |
| 148 | + webhookSecret: 'whsec', |
| 149 | + autoCapture: true, |
| 150 | + allowRefundOnRefunded: true, |
| 151 | + } as any; |
| 152 | + |
| 153 | + const service = new BraintreeImportService(container, options); |
| 154 | + |
| 155 | + const gateway = { |
| 156 | + transaction: { |
| 157 | + find: jest.fn(), |
| 158 | + void: jest.fn(), |
| 159 | + refund: jest.fn(), |
| 160 | + }, |
| 161 | + } as any; |
| 162 | + |
| 163 | + (service as any).gateway = gateway; |
| 164 | + |
| 165 | + const session = { transactionId: 't5', importedAsRefunded: false, refundedTotal: 0, status: 'captured' } as any; |
| 166 | + gateway.transaction.find.mockResolvedValueOnce({ id: 't5', status: 'settled' }); |
| 167 | + gateway.transaction.refund.mockRejectedValueOnce(new Error('Network timeout')); |
| 168 | + |
| 169 | + await expect(service.refundPayment({ amount: 10, data: session } as any)).rejects.toThrow('Network timeout'); |
| 170 | + }); |
92 | 171 | }); |
0 commit comments