/ tests / payments / refund-processor-supplement.test.js
refund-processor-supplement.test.js
  1  /**
  2   * Supplement tests for refund-processor.js processRefundRequest()
  3   *
  4   * Tests the full refund flow: isRefundRequest check → findEligiblePurchase →
  5   * refundPayment (PayPal) → UPDATE purchases → sendRefundConfirmation (Resend).
  6   *
  7   * Mocks: db.js, paypal.js, resend, logger, load-env.
  8   */
  9  
 10  import { test, describe, mock } from 'node:test';
 11  import assert from 'node:assert/strict';
 12  
 13  // ─── Mocks (must be before import) ───────────────────────────────────────────
 14  
 15  process.env.LOGS_DIR = '/tmp/test-logs';
 16  
 17  mock.module('../../src/utils/load-env.js', {
 18    defaultExport: {},
 19  });
 20  
 21  mock.module('../../src/utils/logger.js', {
 22    defaultExport: class {
 23      info() {}
 24      warn() {}
 25      error() {}
 26      success() {}
 27      debug() {}
 28    },
 29  });
 30  
 31  // PayPal refund mock
 32  const refundPaymentMock = mock.fn(async () => ({
 33    refund_id: 'REFUND_ABC123',
 34    status: 'COMPLETED',
 35  }));
 36  
 37  mock.module('../../src/payment/paypal.js', {
 38    namedExports: {
 39      refundPayment: refundPaymentMock,
 40    },
 41  });
 42  
 43  // Resend mock
 44  const emailsSendMock = mock.fn(async () => ({ id: 'email-123' }));
 45  
 46  mock.module('resend', {
 47    namedExports: {
 48      Resend: class {
 49        constructor() {
 50          this.emails = { send: emailsSendMock };
 51        }
 52      },
 53    },
 54  });
 55  
 56  // ─── db.js mock ───────────────────────────────────────────────────────────────
 57  
 58  let mockPurchase = null;
 59  let runCalls = [];
 60  
 61  mock.module('../../src/utils/db.js', {
 62    namedExports: {
 63      getOne: async (sql, params) => {
 64        if (sql.includes('purchases')) return mockPurchase;
 65        return null;
 66      },
 67      run: async (sql, params) => {
 68        runCalls.push({ sql, params });
 69        return { changes: 1 };
 70      },
 71      getAll: async () => [],
 72      query: async () => ({ rows: [], rowCount: 0 }),
 73      withTransaction: async (fn) => fn({ query: async () => ({ rows: [], rowCount: 0 }) }),
 74    },
 75  });
 76  
 77  // ─── Import module under test ─────────────────────────────────────────────────
 78  
 79  const { processRefundRequest } = await import('../../src/payment/refund-processor.js');
 80  
 81  // ─── Tests ───────────────────────────────────────────────────────────────────
 82  
 83  describe('processRefundRequest', () => {
 84    function resetMocks() {
 85      mockPurchase = null;
 86      runCalls = [];
 87      refundPaymentMock.mock.resetCalls();
 88      emailsSendMock.mock.resetCalls();
 89    }
 90  
 91    test('returns not_a_refund_request when body has no refund keywords', async () => {
 92      resetMocks();
 93      const result = await processRefundRequest('test@example.com', 'Thanks for the report!');
 94      assert.deepStrictEqual(result, { processed: false, reason: 'not_a_refund_request' });
 95      assert.strictEqual(refundPaymentMock.mock.calls.length, 0);
 96    });
 97  
 98    test('returns not_eligible when no purchase found for email', async () => {
 99      resetMocks();
100      mockPurchase = null; // No purchase record
101  
102      const result = await processRefundRequest('unknown@example.com', 'I want a refund');
103      assert.strictEqual(result.processed, false);
104      assert.ok(result.reason !== 'not_a_refund_request');
105      assert.strictEqual(refundPaymentMock.mock.calls.length, 0);
106    });
107  
108    test('processes refund successfully when eligible purchase exists', async () => {
109      resetMocks();
110      process.env.RESEND_API_KEY = 'test-resend-key';
111  
112      // Eligible purchase: paid, within 7-day window
113      mockPurchase = {
114        id: 42,
115        email: 'buyer@example.com',
116        paypal_capture_id: 'CAPTURE_XYZ',
117        amount: 29700,
118        currency: 'AUD',
119        status: 'paid',
120        created_at: new Date().toISOString(), // just now — within window
121      };
122  
123      const result = await processRefundRequest('buyer@example.com', 'I want a refund please');
124  
125      assert.strictEqual(result.processed, true);
126      assert.strictEqual(result.reason, 'refund_issued');
127      assert.strictEqual(result.purchaseId, 42);
128      assert.strictEqual(result.refundId, 'REFUND_ABC123');
129  
130      // Verify PayPal refund was called
131      assert.strictEqual(refundPaymentMock.mock.calls.length, 1);
132      assert.strictEqual(refundPaymentMock.mock.calls[0].arguments[0], 'CAPTURE_XYZ');
133  
134      // Verify purchases.status was updated
135      const updateCall = runCalls.find(c => c.sql.includes('UPDATE purchases'));
136      assert.ok(updateCall, 'Should have updated the purchase record');
137  
138      // Verify confirmation email was sent
139      assert.strictEqual(emailsSendMock.mock.calls.length, 1);
140  
141      delete process.env.RESEND_API_KEY;
142    });
143  
144    test('skips confirmation email when RESEND_API_KEY not set', async () => {
145      resetMocks();
146      delete process.env.RESEND_API_KEY;
147  
148      mockPurchase = {
149        id: 43,
150        email: 'nokey@example.com',
151        paypal_capture_id: 'CAPTURE_NOKEY',
152        amount: 15900,
153        currency: 'USD',
154        status: 'paid',
155        created_at: new Date().toISOString(),
156      };
157  
158      const result = await processRefundRequest('nokey@example.com', 'refund please');
159  
160      assert.strictEqual(result.processed, true);
161      assert.strictEqual(
162        emailsSendMock.mock.calls.length,
163        0,
164        'Should not send email without API key'
165      );
166    });
167  
168    test('returns error when refundPayment throws', async () => {
169      resetMocks();
170      refundPaymentMock.mock.mockImplementationOnce(async () => {
171        throw new Error('PayPal API timeout');
172      });
173  
174      mockPurchase = {
175        id: 44,
176        email: 'error@example.com',
177        paypal_capture_id: 'CAPTURE_ERR',
178        amount: 9700,
179        currency: 'USD',
180        status: 'paid',
181        created_at: new Date().toISOString(),
182      };
183  
184      const result = await processRefundRequest('error@example.com', 'I want my money back');
185  
186      assert.strictEqual(result.processed, false);
187      assert.ok(result.reason.includes('PayPal API timeout'), `Reason was: ${result.reason}`);
188    });
189  });