agmission/Development/server/tests/test_promo_priority_selection.js

513 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Test Promo Priority Selection Logic
*
* Tests that findMatchingPromo correctly selects promos based on:
* 1. Match level (exact > type-only > catchall)
* 2. Priority (higher number = higher priority)
* 3. Eligibility filtering (new_only, renew_only, all)
* 4. Duration types (validUntil, durationInMonths)
*
* Prerequisites:
* - MongoDB running
* - Server running on port 4100
* - Admin user credentials
*/
const path = require('path');
// Parse --env argument (default: ./environment.env)
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++;
}
}
// Load environment before requiring any modules
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');
// Test configuration
const BASE_URL = process.env.APP_URL || 'https://localhost:4200';
const API_URL = BASE_URL.replace('4200', '4100');
// Admin credentials for promo management
const ADMIN_USER = 'admin@agnav.com';
const ADMIN_PASSWORD = 'admin'; // Update with actual password
// Use existing Stripe coupon (must exist in Stripe account)
const EXISTING_COUPON = '50OFF'; // Update with a valid coupon ID from your Stripe account
// 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;
/**
* Login as admin
*/
async function loginAsAdmin() {
try {
console.log('\n--- Login as 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 login successful');
return true;
} catch (error) {
console.error('❌ Admin login failed:', error.response?.data || error.message);
console.error('\n⚠ Update ADMIN_PASSWORD in script with valid admin credentials');
return false;
}
}
/**
* 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 test promo
* @param {object} promoData - Promo data
* @returns {Promise<object>} Created promo
*/
async function createPromo(promoData) {
try {
// Add unique run ID to name to avoid conflicts
const uniqueData = {
...promoData,
name: `${promoData.name}_${TEST_RUN_ID}`
};
const response = await requestWithRetry('post', `${API_URL}/api/admin/subscriptionPromos/add`,
uniqueData,
{ headers: { 'Authorization': `Bearer ${authToken}` }, httpsAgent }
);
// Small pause between creations (100ms for safer margin)
await sleep(100 + Math.random() * 50);
// Response is { promos: [...], currentMode: {...} }
// Find the newly created promo and track its ID
const createdPromo = response.data.promos.find(p => p.name === uniqueData.name);
if (createdPromo) {
createdPromoIds.push(createdPromo._id);
}
return createdPromo;
} catch (error) {
console.error(`❌ Failed to create promo "${promoData.name}":`, error.response?.data || error.message);
throw error;
}
}
/**
* Test invoice preview to see which promo is selected
*/
async function testInvoicePreview(targetPackage = 'ess_2') {
try {
// Get customer's Stripe customer ID
const custResponse = await axios.post(`${API_URL}/api/users/login`,
{
username: 'trungduyhoang@gmail.com',
password: 'secret'
},
{ httpsAgent }
);
const customerId = custResponse.data.membership?.custId;
if (!customerId) {
throw new Error('Customer does not have Stripe customer ID');
}
console.log(` DEBUG: Calling invoice preview with custId=${customerId}, package=${targetPackage}`);
const response = await axios.post(`${API_URL}/api/subscription/retrieveNextInvoices`,
{
custId: customerId,
package: targetPackage
},
{
headers: { 'Authorization': `Bearer ${custResponse.data.token}` },
httpsAgent
}
);
// Extract applied discount from invoice (response is array of invoices)
const invoice = response.data?.[0];
const discount = invoice?.discount;
if (discount) {
return {
applied: true,
couponId: discount.coupon?.id,
amount: discount.coupon?.amount_off || discount.coupon?.percent_off,
type: discount.coupon?.amount_off ? 'amount' : 'percent'
};
}
return { applied: false };
} catch (error) {
console.error(`❌ Invoice preview failed:`, error.response?.data || error.message);
if (error.response?.data) {
console.error(' Error details:', JSON.stringify(error.response.data, null, 2));
}
return { applied: false, error: error.message };
}
}
/**
* Test Case 1: Exact match wins over catchall (same priority)
*/
async function testCase1_ExactMatchWins() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 1: Exact Match Wins Over Catchall');
console.log('='.repeat(70));
// Create catchall promo (priority 0)
const catchall = await createPromo({
name: 'TEST Catchall',
couponId: EXISTING_COUPON,
priority: 0,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
console.log(` Created: ${catchall.name}`);
// Create exact match promo (same priority, different name to avoid duplicate, use ess_2 for upgrade)
const exact = await createPromo({
name: 'TEST Exact Match ESS2',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 0,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
console.log(` Created: ${exact.name}`);
const result = await testInvoicePreview('ess_2');
console.log(`Expected: ${EXISTING_COUPON} (exact match over catchall)`);
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should apply a promo');
console.log('✅ PASSED: Exact match selected correctly');
}
/**
* Test Case 2: Higher priority wins within same match level
*/
async function testCase2_HigherPriorityWins() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 2: Higher Priority Wins');
console.log('='.repeat(70));
// Clean up
// Create low priority exact match
await createPromo({
name: 'Low Priority 15% Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 1,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
// Create high priority exact match
await createPromo({
name: 'High Priority 25% Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 10,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: Promo applied (priority 10 wins)');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should apply higher priority promo');
console.log('✅ PASSED: Higher priority selected correctly');
}
/**
* Test Case 3: Repeating coupon (durationInMonths) is active
*/
async function testCase3_RepeatingCouponActive() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 3: Repeating Coupon (durationInMonths) Active');
console.log('='.repeat(70));
// Clean up
// Create repeating coupon promo (no validUntil, only durationInMonths)
await createPromo({
name: 'First Year Discount',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 5,
eligibility: 'all',
durationInMonths: 12
// Note: NO validUntil
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: Promo applied');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should select promo without validUntil');
console.log('✅ PASSED: Repeating coupon active without validUntil');
}
/**
* Test Case 4: Exact match high priority beats catchall low priority
*/
async function testCase4_ExactMatchHighPriorityWins() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 4: Exact Match (High Priority) Beats Catchall (Low Priority)');
console.log('='.repeat(70));
// Clean up
// Create catchall with low priority
await createPromo({
name: 'Catchall 50% Off',
couponId: '50OFF',
priority: 0,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
// Create exact match with high priority and durationInMonths
await createPromo({
name: 'Package First Year',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 5,
eligibility: 'all',
durationInMonths: 12
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: FIRSTYEAR (exact match, priority 5)');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should select exact match with higher priority over catchall');
console.log('✅ PASSED: Exact match with high priority wins');
}
/**
* Test Case 5: Type-only match beats catchall
*/
async function testCase5_TypeOnlyMatchWins() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 5: Type-Only Match Beats Catchall');
console.log('='.repeat(70));
// Clean up
// Create catchall
await createPromo({
name: 'Catchall 5% Off',
couponId: EXISTING_COUPON,
priority: 0,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
// Create type-only match (package, no priceKey)
await createPromo({
name: 'All Packages 15% Off',
type: 'package',
couponId: EXISTING_COUPON,
priority: 0,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: Promo applied');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should select type-only match over catchall');
console.log('✅ PASSED: Type-only match wins over catchall');
}
/**
* Test Case 6: Expired promo (past validUntil) is not selected
*/
async function testCase6_ExpiredPromoIgnored() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 6: Expired Promo Not Selected');
console.log('='.repeat(70));
// Clean up
// Create expired promo
await createPromo({
name: 'Expired 90% Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 100,
eligibility: 'all',
validUntil: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() // Yesterday
});
// Create valid promo
await createPromo({
name: 'Valid 10% Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 1,
eligibility: 'all',
validUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: Promo applied');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should ignore expired promo');
console.log('✅ PASSED: Expired promo correctly ignored');
}
/**
* Test Case 7: Mix of validUntil and durationInMonths promos
*/
async function testCase7_MixedDurationTypes() {
console.log('\n' + '='.repeat(70));
console.log('TEST CASE 7: Mix of validUntil and durationInMonths');
console.log('='.repeat(70));
// Clean up
// Create time-limited promo (validUntil, lower priority)
await createPromo({
name: 'Limited Time 20% Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 3,
eligibility: 'all',
validUntil: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days
});
// Create duration-based promo (durationInMonths, higher priority)
await createPromo({
name: 'First 6 Months Off',
type: 'package',
priceKey: 'ess_2',
couponId: EXISTING_COUPON,
priority: 8,
eligibility: 'all',
durationInMonths: 6
});
const result = await testInvoicePreview('ess_2');
console.log('Expected: Promo applied');
console.log(`Actual: ${result.couponId}`);
assert.strictEqual(result.applied, true, 'Should select higher priority repeating coupon');
console.log('✅ PASSED: Higher priority durationInMonths promo selected');
}
/**
* Clean up test promos only (don't restore 100+ backups to avoid rate limits)
*/
/**
* Clean up test promos created during test execution
* Optimized with batching to respect Stripe's 25 ops/sec limit
*/
/**
* Main test execution
*/
async function runTests() {
console.log('='.repeat(70));
console.log('PROMO PRIORITY SELECTION TEST SUITE');
console.log('='.repeat(70));
console.log(`API URL: ${API_URL}`);
console.log(`Admin: ${ADMIN_USER}`);
try {
// Login
const loginSuccess = await loginAsAdmin();
if (!loginSuccess) {
process.exit(1);
}
// Note: We don't backup/restore to avoid rate limits (100+ promos = 200+ API calls)
// Instead, we just clean up test promos at the end
// Run test cases
await testCase1_ExactMatchWins();
await testCase2_HigherPriorityWins();
await testCase3_RepeatingCouponActive();
await testCase4_ExactMatchHighPriorityWins();
await testCase5_TypeOnlyMatchWins();
await testCase6_ExpiredPromoIgnored();
await testCase7_MixedDurationTypes();
// Clean up test promos
await cleanupCreatedPromos();
console.log('\n' + '='.repeat(70));
console.log('✅ ALL TESTS PASSED (7/7)');
console.log('='.repeat(70));
} catch (error) {
console.error('\n' + '='.repeat(70));
console.error('❌ TEST FAILED');
console.error('='.repeat(70));
console.error(error);
// Try to clean up test promos even on failure
try {
await cleanupCreatedPromos();
} catch (cleanupError) {
console.error('⚠️ Failed to clean up test promos:', cleanupError.message);
}
process.exit(1);
}
}
// Run tests
runTests();