/** * Simplified Promo Priority Selection Test * * Tests priority selection with existing Stripe coupons * Focuses on match level and priority logic */ const path = require('path'); // Load environment const args = process.argv.slice(2); let envFile = './environment.env'; for (let i = 0; i < args.length; i++) { if (args[i] === '--env' && args[i + 1]) { envFile = args[i + 1]; i++; } } const envPath = path.resolve(process.cwd(), envFile); require('dotenv').config({ path: envPath }); const axios = require('axios'); const assert = require('assert'); const { httpsAgent, sleep, requestWithRetry } = require('./test-helpers'); const BASE_URL = process.env.APP_URL || 'https://localhost:4200'; const API_URL = BASE_URL.replace('4200', '4100'); const ADMIN_USER = 'admin@agnav.com'; const ADMIN_PASSWORD = 'admin'; // Test customer account for invoice preview const CUSTOMER_EMAIL = 'trungduyhoang@gmail.com'; const CUSTOMER_PASSWORD = 'secret'; // Use existing coupon from your setup const EXISTING_COUPON = '50OFF'; // Use timestamp for unique promo names to avoid conflicts const TEST_RUN_ID = Date.now(); const createdPromoIds = []; // Track promos we create for cleanup let authToken = null; let customerToken = null; let customerId = null; // Stripe customer ID async function login() { console.log('\n--- Login Admin ---'); const response = await axios.post(`${API_URL}/api/users/login`, { username: ADMIN_USER, password: ADMIN_PASSWORD }, { httpsAgent }); authToken = response.data.token; console.log('✅ Admin logged in'); } async function loginCustomer() { console.log('\n--- Login Customer ---'); const response = await axios.post(`${API_URL}/api/users/login`, { username: CUSTOMER_EMAIL, password: CUSTOMER_PASSWORD }, { httpsAgent }); customerToken = response.data.token; customerId = response.data.membership?.custId; console.log('✅ Customer logged in'); console.log(` Stripe Customer ID: ${customerId}`); if (!customerId) { throw new Error('Customer does not have a Stripe customer ID. Please create subscription first.'); } } /** * Cleanup only the promos we created in this test run */ async function cleanupCreatedPromos() { if (createdPromoIds.length === 0) { console.log(' No promos to clean up'); return; } console.log(` Cleaning up ${createdPromoIds.length} promos created in this run`); const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); for (const promoId of createdPromoIds) { try { await requestWithRetry('put', `${API_URL}/api/admin/subscriptionPromos/${promoId}`, { validUntil: yesterday, enabled: false }, { headers: { 'Authorization': `Bearer ${authToken}` }, httpsAgent } ); console.log(` ✓ Disabled promo ${promoId}`); } catch (error) { console.log(` ⚠ Could not disable ${promoId}: ${error.message}`); } await sleep(100); } } /** * Create a new promo with unique name * @param {object} data - Promo data * @returns {Promise} Created promo */ async function createPromo(data) { // Add unique run ID to name to avoid conflicts const uniqueData = { ...data, name: `${data.name}_${TEST_RUN_ID}` }; const response = await requestWithRetry('post', `${API_URL}/api/admin/subscriptionPromos/add`, uniqueData, { headers: { 'Authorization': `Bearer ${authToken}` }, httpsAgent } ); // Track created promo ID for cleanup const promos = response.data.promos || []; const createdPromo = promos.find(p => p.name === uniqueData.name); if (createdPromo) { createdPromoIds.push(createdPromo._id); } await sleep(100); return response.data; } async function testInvoicePreview(targetPackage = 'ess_2') { try { const response = await axios.post(`${API_URL}/api/subscription/retrieveNextInvoices`, { custId: customerId, package: targetPackage // Use ess_2 to simulate upgrade (customer has ess_1) }, { headers: { 'Authorization': `Bearer ${customerToken}` }, httpsAgent } ); console.log(` DEBUG: Response length: ${response.data?.length}`); const invoice = response.data?.[0]; console.log(` DEBUG: Invoice discount:`, invoice?.discount); const discount = invoice?.discount; return discount ? { applied: true, coupon: discount.coupon?.id } : { applied: false }; } catch (error) { console.error(' ❌ Invoice preview error:', error.response?.status, error.response?.statusText); console.error(' ', error.response?.data); throw error; } } /** * Test 1: Exact match beats catchall (both same priority) */ async function test1_ExactMatchWins() { console.log('\n' + '='.repeat(60)); console.log('TEST 1: Exact Match Wins Over Catchall'); console.log('='.repeat(60)); // Catchall (priority 0) await createPromo({ name: 'TEST_Catchall_P0', couponId: EXISTING_COUPON, priority: 0, eligibility: 'all', validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); console.log(' Created catchall (priority 0)'); // Exact match (priority 0) await createPromo({ name: 'TEST_Exact_P0', type: 'package', priceKey: 'ess_2', couponId: EXISTING_COUPON, priority: 0, eligibility: 'all', validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); console.log(' Created exact match (priority 0)'); const result = await testInvoicePreview(); assert(result.applied, 'Should apply promo'); console.log(` ✅ PASS: Promo applied (exact match selected)`); } /** * Test 2: Higher priority wins */ async function test2_HigherPriorityWins() { console.log('\n' + '='.repeat(60)); console.log('TEST 2: Higher Priority Wins'); console.log('='.repeat(60)); // Low priority await createPromo({ name: 'TEST_Low_P1', type: 'package', priceKey: 'ess_2', couponId: EXISTING_COUPON, priority: 1, eligibility: 'all', validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); console.log(' Created low priority (priority 1)'); // High priority await createPromo({ name: 'TEST_High_P10', type: 'package', priceKey: 'ess_2', couponId: EXISTING_COUPON, priority: 10, eligibility: 'all', validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); console.log(' Created high priority (priority 10)'); const result = await testInvoicePreview(); assert(result.applied, 'Should apply promo'); console.log(` ✅ PASS: Higher priority promo selected`); } /** * Test 3: Duration-based promo (no validUntil) is active */ async function test3_DurationBasedPromo() { console.log('\n' + '='.repeat(60)); console.log('TEST 3: Duration-Based Promo (durationInMonths)'); console.log('='.repeat(60)); // Duration-based promo await createPromo({ name: 'TEST_Duration_12M', type: 'package', priceKey: 'ess_2', couponId: EXISTING_COUPON, priority: 5, eligibility: 'all', durationInMonths: 12 // Note: NO validUntil }); console.log(' Created duration-based promo (12 months, no validUntil)'); const result = await testInvoicePreview(); assert(result.applied, 'Should apply promo'); console.log(` ✅ PASS: Duration-based promo active without validUntil`); } /** * Test 4: Exact match high priority beats catchall low priority */ async function test4_ExactHighBeatsCatchallLow() { console.log('\n' + '='.repeat(60)); console.log('TEST 4: Exact Match (High) Beats Catchall (Low)'); console.log('='.repeat(60)); // Catchall low priority await createPromo({ name: 'TEST_Catchall_P0_v2', couponId: EXISTING_COUPON, priority: 0, eligibility: 'all', validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() }); console.log(' Created catchall (priority 0)'); // Exact match high priority await createPromo({ name: 'TEST_Exact_P5', type: 'package', priceKey: 'ess_2', couponId: EXISTING_COUPON, priority: 5, eligibility: 'all', durationInMonths: 12 }); console.log(' Created exact match (priority 5, durationInMonths)'); const result = await testInvoicePreview(); assert(result.applied, 'Should apply promo'); console.log(` ✅ PASS: Exact match with higher priority selected`); } /** * Main */ async function runTests() { console.log('='.repeat(60)); console.log('PROMO PRIORITY SELECTION TEST'); console.log('='.repeat(60)); console.log(`API: ${API_URL}`); console.log(`Coupon: ${EXISTING_COUPON}`); console.log(`Test Run ID: ${TEST_RUN_ID}`); try { await login(); await loginCustomer(); // Run all tests (unique names prevent conflicts) await test1_ExactMatchWins(); await test2_HigherPriorityWins(); await test3_DurationBasedPromo(); await test4_ExactHighBeatsCatchallLow(); // Cleanup only what we created console.log('\n' + '='.repeat(60)); console.log('CLEANUP'); console.log('='.repeat(60)); await cleanupCreatedPromos(); console.log('\n' + '='.repeat(60)); console.log('✅ ALL TESTS PASSED (4/4)'); console.log('='.repeat(60)); } catch (error) { console.error('\n' + '='.repeat(60)); console.error('❌ TEST FAILED'); console.error('='.repeat(60)); console.error(error.response?.data || error.message); // Try to clean up even on failure try { await cleanupCreatedPromos(); } catch (cleanupError) { console.error('⚠️ Failed to clean up:', cleanupError.message); } process.exit(1); } } runTests();