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 });