/ tests / refund-processor.test.js
refund-processor.test.js
  1  import { test, describe, mock } from 'node:test';
  2  import assert from 'node:assert/strict';
  3  
  4  // ─── Mocks (must be before import) ───────────────────────────────────────────
  5  
  6  process.env.LOGS_DIR = '/tmp/test-logs';
  7  process.env.NODE_ENV = 'test';
  8  
  9  mock.module('../src/utils/load-env.js', {
 10    defaultExport: {},
 11  });
 12  
 13  mock.module('../src/utils/logger.js', {
 14    defaultExport: class {
 15      info() {}
 16      warn() {}
 17      error() {}
 18      success() {}
 19      debug() {}
 20    },
 21  });
 22  
 23  mock.module('../src/payment/paypal.js', {
 24    namedExports: {
 25      refundPayment: async () => ({ refund_id: 'REFUND_TEST', status: 'COMPLETED' }),
 26    },
 27  });
 28  
 29  mock.module('resend', {
 30    namedExports: {
 31      Resend: class {
 32        constructor() {
 33          this.emails = { send: async () => ({ id: 'email-ok' }) };
 34        }
 35      },
 36    },
 37  });
 38  
 39  // ─── db.js mock ───────────────────────────────────────────────────────────────
 40  
 41  let mockPurchaseRow = null;
 42  const runCalls = [];
 43  
 44  mock.module('../src/utils/db.js', {
 45    namedExports: {
 46      getOne: async (sql, params) => {
 47        if (sql.includes('purchases')) return mockPurchaseRow;
 48        return null;
 49      },
 50      run: async (sql, params) => {
 51        runCalls.push({ sql, params });
 52        return { changes: 1 };
 53      },
 54      getAll: async () => [],
 55      query: async () => ({ rows: [], rowCount: 0 }),
 56      withTransaction: async (fn) => fn({ query: async () => ({ rows: [], rowCount: 0 }) }),
 57    },
 58  });
 59  
 60  // ─── Import module under test ─────────────────────────────────────────────────
 61  
 62  const { isRefundRequest, findEligiblePurchase } =
 63    await import('../src/payment/refund-processor.js');
 64  
 65  // ─── isRefundRequest ──────────────────────────────────────────────────────────
 66  
 67  describe('isRefundRequest', () => {
 68    test('detects "refund" keyword', () => {
 69      assert.equal(isRefundRequest('I want a refund please'), true);
 70    });
 71  
 72    test('detects "money back" keyword', () => {
 73      assert.equal(isRefundRequest('I want my money back'), true);
 74    });
 75  
 76    test('detects "chargeback" keyword', () => {
 77      assert.equal(isRefundRequest('I will do a chargeback'), true);
 78    });
 79  
 80    test('detects "cancel my order"', () => {
 81      assert.equal(isRefundRequest('Please cancel my order'), true);
 82    });
 83  
 84    test('is case-insensitive', () => {
 85      assert.equal(isRefundRequest('PLEASE REFUND ME'), true);
 86    });
 87  
 88    test('returns false for normal reply', () => {
 89      assert.equal(isRefundRequest('Thanks for the report, very helpful'), false);
 90    });
 91  
 92    test('returns false for empty body', () => {
 93      assert.equal(isRefundRequest(''), false);
 94    });
 95  
 96    test('returns false for null', () => {
 97      assert.equal(isRefundRequest(null), false);
 98    });
 99  });
100  
101  // ─── findEligiblePurchase ─────────────────────────────────────────────────────
102  
103  describe('findEligiblePurchase', () => {
104    test('returns no_purchase when no record found', async () => {
105      mockPurchaseRow = null;
106      const result = await findEligiblePurchase('nobody@example.com');
107      assert.equal(result.eligible, false);
108      assert.equal(result.reason, 'no_purchase');
109    });
110  
111    test('returns eligible for fresh purchase within 7 days', async () => {
112      const yesterday = new Date();
113      yesterday.setDate(yesterday.getDate() - 1);
114      mockPurchaseRow = {
115        id: 1,
116        email: 'customer@example.com',
117        paypal_capture_id: 'CAP123',
118        amount: 29700,
119        currency: 'USD',
120        status: 'paid',
121        created_at: yesterday.toISOString(),
122      };
123      const result = await findEligiblePurchase('customer@example.com');
124      assert.equal(result.eligible, true);
125      assert.equal(result.reason, null);
126    });
127  
128    test('returns outside_window for purchase older than 7 days', async () => {
129      const eightDaysAgo = new Date();
130      eightDaysAgo.setDate(eightDaysAgo.getDate() - 8);
131      mockPurchaseRow = {
132        id: 2,
133        email: 'old@example.com',
134        paypal_capture_id: 'CAP456',
135        amount: 29700,
136        currency: 'USD',
137        status: 'paid',
138        created_at: eightDaysAgo.toISOString(),
139      };
140      const result = await findEligiblePurchase('old@example.com');
141      assert.equal(result.eligible, false);
142      assert.equal(result.reason, 'outside_window');
143    });
144  
145    test('returns already_refunded for refunded purchase', async () => {
146      mockPurchaseRow = {
147        id: 3,
148        email: 'refunded@example.com',
149        paypal_capture_id: 'CAP789',
150        amount: 29700,
151        currency: 'USD',
152        status: 'refunded',
153        created_at: new Date().toISOString(),
154      };
155      const result = await findEligiblePurchase('refunded@example.com');
156      assert.equal(result.eligible, false);
157      assert.equal(result.reason, 'already_refunded');
158    });
159  
160    test('returns no_capture_id when paypal_capture_id is null', async () => {
161      mockPurchaseRow = {
162        id: 4,
163        email: 'nocap@example.com',
164        paypal_capture_id: null,
165        amount: 29700,
166        currency: 'USD',
167        status: 'paid',
168        created_at: new Date().toISOString(),
169      };
170      const result = await findEligiblePurchase('nocap@example.com');
171      assert.equal(result.eligible, false);
172      assert.equal(result.reason, 'no_capture_id');
173    });
174  
175    test('is case-insensitive on email lookup', async () => {
176      const yesterday = new Date();
177      yesterday.setDate(yesterday.getDate() - 1);
178      mockPurchaseRow = {
179        id: 5,
180        email: 'Customer@Example.COM',
181        paypal_capture_id: 'CAP999',
182        amount: 29700,
183        currency: 'USD',
184        status: 'paid',
185        created_at: yesterday.toISOString(),
186      };
187      const result = await findEligiblePurchase('customer@example.com');
188      assert.equal(result.eligible, true);
189    });
190  
191    test('picks most recent purchase when customer has multiple (mock returns most recent)', async () => {
192      // The mock always returns one row; this tests that the result is correctly
193      // classified. In production the SQL ORDER BY created_at DESC LIMIT 1 handles ordering.
194      const twoDaysAgo = new Date();
195      twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
196      mockPurchaseRow = {
197        id: 6,
198        email: 'multi@example.com',
199        paypal_capture_id: 'CAP_NEW',
200        amount: 29700,
201        currency: 'USD',
202        status: 'paid',
203        created_at: twoDaysAgo.toISOString(),
204      };
205      const result = await findEligiblePurchase('multi@example.com');
206      assert.equal(result.eligible, true);
207      assert.equal(result.purchase.paypal_capture_id, 'CAP_NEW');
208    });
209  });