paypal-generate-message.test.js
1 /** 2 * Tests for generatePaymentMessage in src/payment/paypal.js 3 * 4 * generatePaymentMessage calls getPrice() which queries the countries table. 5 * We use a dedicated temp DB (overriding DATABASE_PATH) to avoid polluting 6 * the shared test DB used by other tests. 7 * 8 * country-pricing.js reads DATABASE_PATH dynamically on each getDb() call, 9 * so setting process.env.DATABASE_PATH here takes effect immediately. 10 */ 11 12 import { test, describe, mock, after } from 'node:test'; 13 import assert from 'node:assert/strict'; 14 import { existsSync, unlinkSync } from 'fs'; 15 import { join } from 'path'; 16 import { tmpdir } from 'os'; 17 import Database from 'better-sqlite3'; 18 import { createPgMock } from '../helpers/pg-mock.js'; 19 20 // Use a dedicated temp DB to avoid polluting the shared test DB 21 const TEST_DB = join(tmpdir(), `test-paypal-msg-${Date.now()}.db`); 22 23 // Seed BEFORE mocking db.js 24 const _pricingDb = new Database(TEST_DB); 25 _pricingDb.pragma('journal_mode = WAL'); 26 _pricingDb.exec(` 27 CREATE TABLE IF NOT EXISTS countries ( 28 country_code TEXT PRIMARY KEY, 29 country_name TEXT NOT NULL, 30 currency_code TEXT NOT NULL DEFAULT 'USD', 31 currency_symbol TEXT NOT NULL DEFAULT '$', 32 price_usd_base INTEGER DEFAULT 29700, 33 price_usd_ppp INTEGER DEFAULT 29700, 34 price_local INTEGER DEFAULT 29700, 35 price_local_formatted TEXT DEFAULT '297', 36 pricing_tier TEXT DEFAULT 'Standard', 37 pricing_variant TEXT DEFAULT 'charm', 38 is_active INTEGER DEFAULT 1, 39 is_price_sensitive INTEGER DEFAULT 0, 40 price_overridden INTEGER DEFAULT 0, 41 override_reason TEXT, 42 market_notes TEXT, 43 exchange_rate REAL DEFAULT 1.0, 44 price_last_updated TEXT, 45 twilio_phone_number TEXT 46 ) 47 `); 48 _pricingDb.prepare( 49 `INSERT OR IGNORE INTO countries 50 (country_code, country_name, currency_code, currency_symbol, price_usd_ppp, price_local, price_local_formatted) 51 VALUES 52 ('AU', 'Australia', 'AUD', 'A$', 33700, 49700, '497'), 53 ('US', 'United States', 'USD', '$', 29700, 29700, '297')` 54 ).run(); 55 56 // Mock db.js before importing paypal.js 57 mock.module('../../src/utils/db.js', { 58 namedExports: createPgMock(_pricingDb), 59 }); 60 61 const { generatePaymentMessage } = await import('../../src/payment/paypal.js'); 62 63 after(() => { 64 _pricingDb.close(); 65 if (existsSync(TEST_DB)) { 66 try { unlinkSync(TEST_DB); } catch { /* ignore */ } 67 } 68 }); 69 70 describe('generatePaymentMessage', () => { 71 test('returns SMS-formatted message for sms channel', async () => { 72 const msg = await generatePaymentMessage('https://pay.example.com/abc', 'sms', 'example.com', 'AU'); 73 assert.ok(typeof msg === 'string', 'should return a string'); 74 // SMS messages should be concise 75 assert.ok(msg.length < 500, 'SMS message should be short'); 76 assert.ok(msg.includes('example.com'), 'should include domain'); 77 assert.ok(msg.includes('https://pay.example.com/abc'), 'should include payment link'); 78 }); 79 80 test('returns email-formatted message for email channel', async () => { 81 const msg = await generatePaymentMessage('https://pay.example.com/abc', 'email', 'mysite.com', 'US'); 82 assert.ok(typeof msg === 'string', 'should return a string'); 83 // Email messages should be longer and more detailed 84 assert.ok(msg.length > 200, 'email message should be longer'); 85 assert.ok(msg.includes('mysite.com'), 'should include domain'); 86 assert.ok(msg.includes('https://pay.example.com/abc'), 'should include payment link'); 87 assert.ok( 88 msg.includes('Conversion Audit') || msg.includes('Homepage'), 89 'should mention conversion audit or homepage' 90 ); 91 }); 92 93 test('includes country-specific pricing for AU', async () => { 94 const msg = await generatePaymentMessage( 95 'https://pay.example.com/abc', 96 'sms', 97 'example.com.au', 98 'AU' 99 ); 100 assert.ok( 101 msg.includes('A$') || msg.includes('AUD') || msg.includes('$'), 102 'should include price' 103 ); 104 }); 105 106 test('includes country-specific pricing for US', async () => { 107 const msg = await generatePaymentMessage('https://pay.example.com/xyz', 'email', 'example.com', 'US'); 108 assert.ok(msg.includes('$'), 'should include dollar sign'); 109 }); 110 111 test('handles null country code by defaulting to US', async () => { 112 await assert.doesNotReject(async () => { 113 await generatePaymentMessage('https://pay.example.com/abc', 'sms', 'example.com', null); 114 }); 115 }); 116 117 test('handles undefined country code by defaulting to US', async () => { 118 await assert.doesNotReject(async () => { 119 await generatePaymentMessage('https://pay.example.com/abc', 'email', 'example.com', undefined); 120 }); 121 }); 122 123 test('non-sms channel returns email format', async () => { 124 const emailMsg = await generatePaymentMessage('https://pay.example.com', 'email', 'site.com', 'AU'); 125 const smsMsg = await generatePaymentMessage('https://pay.example.com', 'sms', 'site.com', 'AU'); 126 127 // Email should have newlines and be longer 128 assert.ok(emailMsg.length > smsMsg.length, 'email should be longer than SMS'); 129 assert.ok(emailMsg.includes('\n'), 'email should have newlines'); 130 assert.ok(!smsMsg.includes('\n\n'), 'SMS should not have double newlines'); 131 }); 132 133 test('payment link appears in both SMS and email formats', async () => { 134 const link = 'https://paypal.com/order/TESTORDER123'; 135 const smsMsg = await generatePaymentMessage(link, 'sms', 'test.com', 'AU'); 136 const emailMsg = await generatePaymentMessage(link, 'email', 'test.com', 'AU'); 137 138 assert.ok(smsMsg.includes(link), 'link should appear in SMS'); 139 assert.ok(emailMsg.includes(link), 'link should appear in email'); 140 }); 141 });