/ __quarantined_tests__ / payments / poll-purchases.test.js
poll-purchases.test.js
  1  /**
  2   * Poll Purchases Unit Tests
  3   * Tests pollPurchases() with mocked CF Worker and database
  4   */
  5  
  6  import { describe, test, mock, beforeEach } from 'node:test';
  7  import assert from 'node:assert/strict';
  8  import { initTestDb } from '../../src/utils/test-db.js';
  9  
 10  // Mock axios
 11  const axiosGetMock = mock.fn();
 12  const axiosDeleteMock = mock.fn();
 13  mock.module('axios', {
 14    defaultExport: {
 15      get: axiosGetMock,
 16      delete: axiosDeleteMock,
 17    },
 18  });
 19  
 20  // Mock confirmation email
 21  const sendConfirmationMock = mock.fn();
 22  mock.module('../../src/reports/purchase-confirmation.js', {
 23    namedExports: {
 24      sendConfirmationEmail: sendConfirmationMock,
 25    },
 26  });
 27  
 28  let testDb;
 29  
 30  class DatabaseMock {
 31    constructor() {
 32      // Return a proxy that intercepts close() to prevent closing the shared test db
 33      const handler = {
 34        get(target, prop) {
 35          if (prop === 'close') {
 36            return () => {};
 37          }
 38          if (prop === 'pragma') {
 39            return () => {};
 40          }
 41          return target[prop];
 42        },
 43      };
 44      return new Proxy(testDb, handler);
 45    }
 46  }
 47  mock.module('better-sqlite3', {
 48    defaultExport: DatabaseMock,
 49  });
 50  
 51  // Mock dotenv
 52  mock.module('dotenv', {
 53    defaultExport: { config: () => {} },
 54    namedExports: { config: () => {} },
 55  });
 56  
 57  process.env.AUDITANDFIX_WORKER_URL = 'https://test-worker.dev';
 58  process.env.AUDITANDFIX_WORKER_SECRET = 'test-secret';
 59  
 60  const { pollPurchases } = await import('../../src/cron/poll-purchases.js');
 61  
 62  describe('pollPurchases', () => {
 63    beforeEach(() => {
 64      testDb = initTestDb();
 65  
 66      // poll-purchases.js still uses conversation_id (pre-migration name).
 67      // Add the column so source code inserts work against the test DB.
 68      try {
 69        testDb.exec('ALTER TABLE purchases ADD COLUMN conversation_id INTEGER');
 70      } catch {
 71        // Column may already exist
 72      }
 73      axiosGetMock.mock.resetCalls();
 74      axiosDeleteMock.mock.resetCalls();
 75      sendConfirmationMock.mock.resetCalls();
 76      sendConfirmationMock.mock.mockImplementation(async () => ({ success: true }));
 77      axiosDeleteMock.mock.mockImplementation(async () => ({ status: 200 }));
 78    });
 79  
 80    test('returns zero counts when no purchases found', async () => {
 81      axiosGetMock.mock.mockImplementation(async () => ({
 82        data: { purchases: [] },
 83      }));
 84  
 85      const result = await pollPurchases();
 86      assert.equal(result.processed, 0);
 87      assert.equal(result.successful, 0);
 88      assert.equal(result.failed, 0);
 89    });
 90  
 91    test('skips when worker not configured', async () => {
 92      const origUrl = process.env.AUDITANDFIX_WORKER_URL;
 93      delete process.env.AUDITANDFIX_WORKER_URL;
 94  
 95      const result = await pollPurchases();
 96      assert.equal(result.processed, 0);
 97  
 98      process.env.AUDITANDFIX_WORKER_URL = origUrl;
 99    });
100  
101    test('inserts new purchase into database', async () => {
102      axiosGetMock.mock.mockImplementation(async () => ({
103        data: {
104          purchases: [
105            {
106              id: 'kv_1',
107              email: 'customer@example.com',
108              landing_page_url: 'https://test-site.com',
109              phone: '+61412345678',
110              paypal_order_id: 'ORDER_NEW_1',
111              paypal_payer_id: 'PAYER_1',
112              paypal_capture_id: 'CAPTURE_1',
113              amount: 29700,
114              currency: 'USD',
115              amount_usd: 29700,
116              country_code: 'US',
117              ip_address: '1.2.3.4',
118              user_agent: 'TestAgent/1.0',
119            },
120          ],
121        },
122      }));
123  
124      const result = await pollPurchases();
125  
126      assert.equal(result.processed, 1);
127      assert.equal(result.successful, 1);
128      assert.equal(result.failed, 0);
129  
130      // Verify DB insert
131      const purchase = testDb
132        .prepare('SELECT * FROM purchases WHERE paypal_order_id = ?')
133        .get('ORDER_NEW_1');
134      assert.ok(purchase);
135      assert.equal(purchase.email, 'customer@example.com');
136      assert.equal(purchase.amount, 29700);
137      assert.equal(purchase.status, 'paid');
138  
139      // Verify confirmation email sent
140      assert.equal(sendConfirmationMock.mock.calls.length, 1);
141  
142      // Verify CF Worker delete called
143      assert.equal(axiosDeleteMock.mock.calls.length, 1);
144      assert.ok(axiosDeleteMock.mock.calls[0].arguments[0].includes('kv_1'));
145    });
146  
147    test('skips duplicate purchases', async () => {
148      // Pre-insert a purchase
149      testDb
150        .prepare(
151          `INSERT INTO purchases (email, landing_page_url, paypal_order_id, amount, currency, amount_usd, status)
152           VALUES (?, ?, ?, ?, ?, ?, ?)`
153        )
154        .run(
155          'existing@test.com',
156          'https://existing.com',
157          'ORDER_EXISTING',
158          29700,
159          'USD',
160          29700,
161          'paid'
162        );
163  
164      axiosGetMock.mock.mockImplementation(async () => ({
165        data: {
166          purchases: [
167            {
168              id: 'kv_2',
169              email: 'existing@test.com',
170              landing_page_url: 'https://existing.com',
171              paypal_order_id: 'ORDER_EXISTING',
172              amount: 29700,
173              currency: 'USD',
174              amount_usd: 29700,
175            },
176          ],
177        },
178      }));
179  
180      const result = await pollPurchases();
181  
182      assert.equal(result.successful, 1); // Still counted as successful processing
183      // Confirmation email should NOT be sent for existing purchase
184      assert.equal(sendConfirmationMock.mock.calls.length, 0);
185    });
186  
187    test('handles CF Worker fetch error', async () => {
188      axiosGetMock.mock.mockImplementation(async () => {
189        throw new Error('Connection refused');
190      });
191  
192      const result = await pollPurchases();
193      assert.equal(result.processed, 0);
194      assert.equal(result.failed, 0);
195    });
196  
197    test('processes multiple purchases', async () => {
198      axiosGetMock.mock.mockImplementation(async () => ({
199        data: {
200          purchases: [
201            {
202              id: 'kv_a',
203              email: 'a@test.com',
204              landing_page_url: 'https://site-a.com',
205              paypal_order_id: 'ORDER_A',
206              amount: 29700,
207              currency: 'USD',
208              amount_usd: 29700,
209            },
210            {
211              id: 'kv_b',
212              email: 'b@test.com',
213              landing_page_url: 'https://site-b.com',
214              paypal_order_id: 'ORDER_B',
215              amount: 33700,
216              currency: 'AUD',
217              amount_usd: 24100,
218            },
219          ],
220        },
221      }));
222  
223      const result = await pollPurchases();
224  
225      assert.equal(result.processed, 2);
226      assert.equal(result.successful, 2);
227  
228      // Both should be in DB
229      const count = testDb.prepare('SELECT COUNT(*) as c FROM purchases').get().c;
230      assert.equal(count, 2);
231    });
232  });