/ tests / payments / purchases-e2e.test.js
purchases-e2e.test.js
  1  /**
  2   * Purchases E2E Test
  3   *
  4   * Tests the complete purchase-to-delivery pipeline with a real database
  5   * and mocked external services (PayPal, CF Worker, Resend, OpenRouter, Playwright).
  6   *
  7   * Flow: Poll Purchase → Insert DB → Confirm Email → Process → Capture →
  8   *       Score → Crop → Generate PDF → Deliver Email → Update Status
  9   */
 10  
 11  import { describe, test, before, after } from 'node:test';
 12  import assert from 'node:assert/strict';
 13  import Database from 'better-sqlite3';
 14  import { join, dirname } from 'path';
 15  import { fileURLToPath } from 'url';
 16  import { existsSync, readFileSync, unlinkSync, mkdirSync, rmSync } from 'fs';
 17  import sharp from 'sharp';
 18  
 19  const __filename = fileURLToPath(import.meta.url);
 20  const __dirname = dirname(__filename);
 21  const projectRoot = join(__dirname, '../..');
 22  
 23  const TEST_DB_PATH = join(projectRoot, 'db/test-purchases-e2e.db');
 24  const TEST_REPORTS_DIR = join(projectRoot, 'reports/test-purchases-e2e');
 25  
 26  describe('Purchases E2E Pipeline', () => {
 27    let db;
 28    let purchaseId;
 29  
 30    before(() => {
 31      // Set up test database with full schema
 32      try {
 33        unlinkSync(TEST_DB_PATH);
 34      } catch {
 35        /* ok */
 36      }
 37  
 38      db = new Database(TEST_DB_PATH);
 39      const schema = readFileSync(join(projectRoot, 'db/schema.sql'), 'utf-8');
 40      db.exec(schema);
 41  
 42      // Apply purchases migration if not in schema
 43      const purchasesTable = db
 44        .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='purchases'")
 45        .get();
 46      if (!purchasesTable) {
 47        const migration = readFileSync(
 48          join(projectRoot, 'db/migrations/060-create-purchases-table.sql'),
 49          'utf-8'
 50        );
 51        db.exec(migration);
 52      }
 53  
 54      mkdirSync(TEST_REPORTS_DIR, { recursive: true });
 55    });
 56  
 57    after(() => {
 58      if (db) db.close();
 59      try {
 60        unlinkSync(TEST_DB_PATH);
 61      } catch {
 62        /* ok */
 63      }
 64      try {
 65        rmSync(TEST_REPORTS_DIR, { recursive: true });
 66      } catch {
 67        /* ok */
 68      }
 69    });
 70  
 71    test('Step 1: Ingest purchase from CF Worker queue', () => {
 72      // Simulate what poll-purchases.js does
 73      const purchaseData = {
 74        email: 'e2e-test@example.com',
 75        landing_page_url: 'https://e2e-test-business.com',
 76        phone: '+61412345678',
 77        paypal_order_id: 'E2E_ORDER_001',
 78        paypal_payer_id: 'E2E_PAYER_001',
 79        paypal_capture_id: 'E2E_CAPTURE_001',
 80        amount: 29700,
 81        currency: 'USD',
 82        amount_usd: 29700,
 83        country_code: 'US',
 84        ip_address: '203.0.113.42',
 85        user_agent: 'E2E-Test/1.0',
 86      };
 87  
 88      const result = db
 89        .prepare(
 90          `INSERT INTO purchases
 91            (email, landing_page_url, phone, paypal_order_id, paypal_payer_id,
 92             paypal_capture_id, amount, currency, amount_usd, country_code,
 93             ip_address, user_agent, status)
 94           VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'paid')`
 95        )
 96        .run(
 97          purchaseData.email,
 98          purchaseData.landing_page_url,
 99          purchaseData.phone,
100          purchaseData.paypal_order_id,
101          purchaseData.paypal_payer_id,
102          purchaseData.paypal_capture_id,
103          purchaseData.amount,
104          purchaseData.currency,
105          purchaseData.amount_usd,
106          purchaseData.country_code,
107          purchaseData.ip_address,
108          purchaseData.user_agent
109        );
110  
111      purchaseId = result.lastInsertRowid;
112      assert.ok(purchaseId, 'Purchase should be inserted');
113  
114      const purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(purchaseId);
115      assert.equal(purchase.status, 'paid');
116      assert.equal(purchase.email, 'e2e-test@example.com');
117      assert.equal(purchase.retry_count, 0);
118    });
119  
120    test('Step 2: Duplicate purchase is ignored', () => {
121      const result = db
122        .prepare(
123          `INSERT OR IGNORE INTO purchases
124            (email, landing_page_url, paypal_order_id, amount, currency, amount_usd, status)
125           VALUES (?, ?, ?, ?, ?, ?, 'paid')`
126        )
127        .run(
128          'e2e-test@example.com',
129          'https://e2e-test-business.com',
130          'E2E_ORDER_001',
131          29700,
132          'USD',
133          29700
134        );
135  
136      assert.equal(result.changes, 0, 'Duplicate should be ignored');
137    });
138  
139    test('Step 3: Find or create site record', () => {
140      const domain = 'e2e-test-business.com';
141      const url = 'https://e2e-test-business.com';
142  
143      // Check if site exists
144      let site = db.prepare('SELECT id FROM sites WHERE domain = ?').get(domain);
145  
146      if (!site) {
147        const result = db
148          .prepare(
149            "INSERT INTO sites (domain, landing_page_url, keyword, status, country_code) VALUES (?, ?, ?, 'found', ?)"
150          )
151          .run(domain, url, 'audit-report-purchase', 'US');
152        site = { id: result.lastInsertRowid };
153      }
154  
155      // Link site to purchase
156      db.prepare('UPDATE purchases SET site_id = ? WHERE id = ?').run(site.id, purchaseId);
157  
158      const purchase = db.prepare('SELECT site_id FROM purchases WHERE id = ?').get(purchaseId);
159      assert.equal(purchase.site_id, site.id);
160    });
161  
162    test('Step 4: Transition to processing', () => {
163      db.prepare(
164        "UPDATE purchases SET status = 'processing', updated_at = CURRENT_TIMESTAMP WHERE id = ?"
165      ).run(purchaseId);
166  
167      const purchase = db.prepare('SELECT status FROM purchases WHERE id = ?').get(purchaseId);
168      assert.equal(purchase.status, 'processing');
169    });
170  
171    test('Step 5: Generate audit report PDF', async () => {
172      // Create test screenshot buffers
173      const aboveFoldBuffer = await sharp({
174        create: { width: 1440, height: 900, channels: 3, background: { r: 100, g: 150, b: 200 } },
175      })
176        .png()
177        .toBuffer();
178  
179      // Import audit report generator directly (no mocking needed - it just generates PDFs)
180      const { generateAuditReport } = await import('../../src/reports/audit-report-generator.js');
181  
182      const scoreJson = {
183        website_url: 'https://e2e-test-business.com',
184        overall_calculation: {
185          conversion_score: 55,
186          letter_grade: 'F',
187          grade_interpretation: 'Average conversion potential',
188        },
189        factor_scores: {
190          headline_quality: { score: 5, weight: 15, reasoning: 'Generic headline' },
191          value_proposition: { score: 6, weight: 14, reasoning: 'Unclear value proposition' },
192          call_to_action: { score: 4, weight: 13, reasoning: 'CTA below fold' },
193          urgency_messaging: { score: 3, weight: 10, reasoning: 'No urgency' },
194          hook_engagement: { score: 5, weight: 9, reasoning: 'Minimal engagement' },
195          trust_signals: { score: 7, weight: 11, reasoning: 'Some trust present' },
196          imagery_design: { score: 6, weight: 8, reasoning: 'Stock imagery' },
197          offer_clarity: { score: 7, weight: 4, reasoning: 'Clear services' },
198          unique_selling_proposition: { score: 4, weight: 13, reasoning: 'Not differentiated' },
199          contextual_appropriateness: { score: 7, weight: 3, reasoning: 'Appropriate' },
200        },
201        key_strengths: ['Clear contact info', 'Service listing'],
202        critical_weaknesses: ['Weak headline', 'No above-fold CTA'],
203        quick_improvement_opportunities: ['Add CTA above fold'],
204        problem_areas: [
205          {
206            factor: 'call_to_action',
207            description: 'No CTA visible above fold',
208            approximate_y_position_percent: 65,
209            severity: 'high',
210            recommendation: 'Add CTA button above fold',
211          },
212        ],
213        technical_assessment: {
214          ssl_enabled: true,
215          security_headers_missing: ['Content-Security-Policy'],
216          mobile_responsive: true,
217        },
218        strategic_recommendations: [
219          {
220            priority: 1,
221            category: 'quick_win',
222            description: 'Add above-fold CTA',
223            expected_impact: 'high',
224            estimated_effort: 'low',
225          },
226        ],
227      };
228  
229      const reportPath = join(TEST_REPORTS_DIR, `${purchaseId}`, 'audit-report.pdf');
230  
231      await generateAuditReport({
232        domain: 'e2e-test-business.com',
233        url: 'https://e2e-test-business.com',
234        scoreJson,
235        aboveFoldBuffer,
236        problemCrops: [],
237        outputPath: reportPath,
238      });
239  
240      assert.ok(existsSync(reportPath), 'PDF report should exist');
241  
242      // Update purchase record
243      db.prepare(
244        `UPDATE purchases
245         SET status = 'report_generated', report_path = ?, report_score = ?, report_grade = ?, updated_at = CURRENT_TIMESTAMP
246         WHERE id = ?`
247      ).run(reportPath, 55, 'C', purchaseId);
248  
249      const purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(purchaseId);
250      assert.equal(purchase.status, 'report_generated');
251      assert.equal(purchase.report_score, 55);
252      assert.equal(purchase.report_grade, 'C');
253      assert.ok(purchase.report_path);
254    });
255  
256    test('Step 6: Mark as delivered', () => {
257      db.prepare(
258        "UPDATE purchases SET status = 'delivered', delivered_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP WHERE id = ?"
259      ).run(purchaseId);
260  
261      const purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(purchaseId);
262      assert.equal(purchase.status, 'delivered');
263      assert.ok(purchase.delivered_at);
264    });
265  
266    test('Step 7: Verify complete purchase record', () => {
267      const purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(purchaseId);
268  
269      assert.equal(purchase.email, 'e2e-test@example.com');
270      assert.equal(purchase.landing_page_url, 'https://e2e-test-business.com');
271      assert.equal(purchase.paypal_order_id, 'E2E_ORDER_001');
272      assert.equal(purchase.amount, 29700);
273      assert.equal(purchase.currency, 'USD');
274      assert.equal(purchase.amount_usd, 29700);
275      assert.equal(purchase.country_code, 'US');
276      assert.equal(purchase.status, 'delivered');
277      assert.equal(purchase.report_score, 55);
278      assert.equal(purchase.report_grade, 'C');
279      assert.ok(purchase.report_path);
280      assert.ok(purchase.delivered_at);
281      assert.ok(purchase.created_at);
282      assert.ok(purchase.site_id);
283    });
284  
285    test('Step 8: Refund flow', () => {
286      // Create a new purchase for refund testing
287      db.prepare(
288        `INSERT INTO purchases
289          (email, landing_page_url, paypal_order_id, paypal_capture_id, amount, currency, amount_usd, status)
290         VALUES (?, ?, ?, ?, ?, ?, ?, 'delivered')`
291      ).run(
292        'refund-e2e@test.com',
293        'https://refund-e2e.com',
294        'E2E_REFUND_001',
295        'E2E_CAP_REF_001',
296        29700,
297        'USD',
298        29700
299      );
300  
301      const refundId = db
302        .prepare("SELECT id FROM purchases WHERE paypal_order_id = 'E2E_REFUND_001'")
303        .get().id;
304  
305      db.prepare(
306        `UPDATE purchases
307         SET status = 'refunded', refunded_at = CURRENT_TIMESTAMP, refund_reason = ?
308         WHERE id = ?`
309      ).run('E2E test refund', refundId);
310  
311      const refunded = db.prepare('SELECT * FROM purchases WHERE id = ?').get(refundId);
312      assert.equal(refunded.status, 'refunded');
313      assert.ok(refunded.refunded_at);
314      assert.equal(refunded.refund_reason, 'E2E test refund');
315    });
316  
317    test('Step 9: Failed purchase retry flow', () => {
318      db.prepare(
319        `INSERT INTO purchases
320          (email, landing_page_url, paypal_order_id, amount, currency, amount_usd, status)
321         VALUES (?, ?, ?, ?, ?, ?, 'paid')`
322      ).run('retry-e2e@test.com', 'https://retry-e2e.com', 'E2E_RETRY_001', 29700, 'USD', 29700);
323  
324      const retryId = db
325        .prepare("SELECT id FROM purchases WHERE paypal_order_id = 'E2E_RETRY_001'")
326        .get().id;
327  
328      // Simulate processing failure
329      db.prepare(
330        "UPDATE purchases SET status = 'processing', retry_count = retry_count + 1, error_message = ? WHERE id = ?"
331      ).run('Browser timeout', retryId);
332  
333      // Simulate second failure
334      db.prepare(
335        'UPDATE purchases SET retry_count = retry_count + 1, error_message = ? WHERE id = ?'
336      ).run('LLM timeout', retryId);
337  
338      // Simulate third failure → mark as failed
339      db.prepare(
340        "UPDATE purchases SET status = 'failed', retry_count = retry_count + 1, error_message = ? WHERE id = ?"
341      ).run('Max retries exceeded', retryId);
342  
343      let purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(retryId);
344      assert.equal(purchase.status, 'failed');
345      assert.equal(purchase.retry_count, 3);
346  
347      // Retry: reset status
348      db.prepare(
349        "UPDATE purchases SET status = 'paid', error_message = NULL, retry_count = 0 WHERE id = ?"
350      ).run(retryId);
351  
352      purchase = db.prepare('SELECT * FROM purchases WHERE id = ?').get(retryId);
353      assert.equal(purchase.status, 'paid');
354      assert.equal(purchase.retry_count, 0);
355      assert.equal(purchase.error_message, null);
356    });
357  
358    test('Step 10: Revenue statistics', () => {
359      const stats = db
360        .prepare(
361          `SELECT
362            COUNT(*) as total_orders,
363            COALESCE(SUM(amount_usd), 0) as total_usd,
364            COALESCE(AVG(amount_usd), 0) as avg_usd
365          FROM purchases WHERE status NOT IN ('refunded', 'failed')`
366        )
367        .get();
368  
369      assert.ok(
370        stats.total_orders >= 2,
371        `Should have at least 2 active orders, got ${stats.total_orders}`
372      );
373      assert.ok(stats.total_usd > 0, 'Total revenue should be positive');
374  
375      const statusCounts = db
376        .prepare('SELECT status, COUNT(*) as count FROM purchases GROUP BY status')
377        .all();
378  
379      assert.ok(statusCounts.length >= 3, 'Should have multiple statuses');
380  
381      const deliveredCount = statusCounts.find(s => s.status === 'delivered');
382      assert.ok(deliveredCount, 'Should have delivered purchases');
383    });
384  
385    test('Step 11: Problem area cropping works with real images', async () => {
386      const { cropProblemAreas } = await import('../../src/reports/problem-area-cropper.js');
387  
388      const fullPageBuffer = await sharp({
389        create: { width: 1440, height: 8000, channels: 3, background: { r: 150, g: 150, b: 150 } },
390      })
391        .png()
392        .toBuffer();
393  
394      const problemAreas = [
395        {
396          factor: 'call_to_action',
397          description: 'No CTA above fold',
398          approximate_y_position_percent: 65,
399          severity: 'high',
400          recommendation: 'Add CTA',
401        },
402        {
403          factor: 'headline_quality',
404          description: 'Generic headline',
405          approximate_y_position_percent: 5,
406          severity: 'medium',
407          recommendation: 'Rewrite headline',
408        },
409      ];
410  
411      const crops = await cropProblemAreas(fullPageBuffer, problemAreas);
412  
413      assert.equal(crops.length, 2);
414      assert.ok(Buffer.isBuffer(crops[0].imageBuffer));
415      assert.ok(Buffer.isBuffer(crops[1].imageBuffer));
416  
417      // Verify dimensions
418      const meta0 = await sharp(crops[0].imageBuffer).metadata();
419      assert.ok(meta0.width <= 768);
420      assert.ok(meta0.height <= 400);
421    });
422  });