pricing.test.js
1 /** 2 * Tests for src/config/pricing.js 3 * 4 * Covers: 5 * - COUNTRY_PRICES (exported constant) 6 * - getCountryPrice 7 * - getLocalPrice 8 * - getPricingTierDistribution 9 * - getPriceStatistics 10 * - overrideCountryPrice 11 * - PRICING_CONFIG 12 */ 13 14 import { test, describe } from 'node:test'; 15 import assert from 'node:assert/strict'; 16 17 import { 18 COUNTRY_PRICES, 19 getCountryPrice, 20 getLocalPrice, 21 getPricingTierDistribution, 22 getPriceStatistics, 23 overrideCountryPrice, 24 PRICING_CONFIG, 25 } from '../../src/config/pricing.js'; 26 27 // ─── COUNTRY_PRICES constant ───────────────────────────────────────────────── 28 29 describe('COUNTRY_PRICES', () => { 30 test('is an object with country codes as keys', () => { 31 assert.ok(typeof COUNTRY_PRICES === 'object'); 32 assert.ok(Object.keys(COUNTRY_PRICES).length > 0); 33 }); 34 35 test('contains known countries', () => { 36 assert.ok('US' in COUNTRY_PRICES); 37 assert.ok('AU' in COUNTRY_PRICES); 38 assert.ok('GB' in COUNTRY_PRICES || 'UK' in COUNTRY_PRICES); 39 }); 40 41 test('each country has usdPrice, tier, notes', () => { 42 for (const [code, info] of Object.entries(COUNTRY_PRICES)) { 43 assert.ok(typeof info.usdPrice === 'number', `${code}: usdPrice should be number`); 44 assert.ok(typeof info.tier === 'string', `${code}: tier should be string`); 45 assert.ok(typeof info.notes === 'string', `${code}: notes should be string`); 46 assert.ok(info.usdPrice > 0, `${code}: usdPrice should be positive`); 47 } 48 }); 49 50 test('US baseline is around $297', () => { 51 assert.equal(COUNTRY_PRICES.US.usdPrice, 297); 52 }); 53 }); 54 55 // ─── getCountryPrice ───────────────────────────────────────────────────────── 56 57 describe('getCountryPrice', () => { 58 test('returns pricing info for known country', () => { 59 const result = getCountryPrice('US'); 60 assert.ok(result, 'should return a result'); 61 assert.equal(typeof result.usdPrice, 'number'); 62 assert.equal(typeof result.tier, 'string'); 63 }); 64 65 test('is case-insensitive', () => { 66 const upper = getCountryPrice('AU'); 67 const lower = getCountryPrice('au'); 68 assert.equal(upper.usdPrice, lower.usdPrice); 69 }); 70 71 test('returns base price for unknown country code', () => { 72 const result = getCountryPrice('ZZ'); 73 assert.ok(result, 'should return default result'); 74 assert.equal(typeof result.usdPrice, 'number'); 75 assert.ok(result.usdPrice > 0); 76 }); 77 78 test('AU has Standard tier pricing', () => { 79 const result = getCountryPrice('AU'); 80 assert.equal(result.tier, 'Standard'); 81 }); 82 83 test('SG has Premium+ tier (capped at ceiling)', () => { 84 const result = getCountryPrice('SG'); 85 assert.equal(result.tier, 'Premium+'); 86 }); 87 88 test('IN has Developing tier (capped at floor)', () => { 89 const result = getCountryPrice('IN'); 90 assert.equal(result.tier, 'Developing'); 91 }); 92 }); 93 94 // ─── getLocalPrice ─────────────────────────────────────────────────────────── 95 96 describe('getLocalPrice', () => { 97 const mockAUCountry = { 98 currency: 'AUD', 99 currencySymbol: 'A$', 100 }; 101 102 test('returns amount, currency, currencySymbol, formatted, usdEquivalent', () => { 103 const result = getLocalPrice('AU', mockAUCountry); 104 assert.equal(typeof result.amount, 'number'); 105 assert.equal(result.currency, 'AUD'); 106 assert.equal(result.currencySymbol, 'A$'); 107 assert.ok(typeof result.formatted === 'string'); 108 assert.ok(result.formatted.includes('$')); 109 assert.equal(typeof result.usdEquivalent, 'number'); 110 }); 111 112 test('formatted string starts with currency symbol', () => { 113 const result = getLocalPrice('AU', mockAUCountry); 114 assert.ok(result.formatted.startsWith('A$'), 'should start with currency symbol'); 115 }); 116 117 test('works for US pricing', () => { 118 const mockUS = { currency: 'USD', currencySymbol: '$' }; 119 const result = getLocalPrice('US', mockUS); 120 assert.equal(result.amount, 297); 121 assert.equal(result.currency, 'USD'); 122 }); 123 }); 124 125 // ─── getPricingTierDistribution ────────────────────────────────────────────── 126 127 describe('getPricingTierDistribution', () => { 128 test('returns an object with tier names as keys', () => { 129 const result = getPricingTierDistribution(); 130 assert.ok(typeof result === 'object'); 131 assert.ok('Premium' in result || 'Standard' in result); 132 }); 133 134 test('each tier is an array of { code, price } objects', () => { 135 const result = getPricingTierDistribution(); 136 for (const [tier, countries] of Object.entries(result)) { 137 assert.ok(Array.isArray(countries), `${tier} should be array`); 138 for (const c of countries) { 139 assert.ok(typeof c.code === 'string', `${tier}: code should be string`); 140 assert.ok(typeof c.price === 'number', `${tier}: price should be number`); 141 } 142 } 143 }); 144 145 test('all countries appear in some tier', () => { 146 const result = getPricingTierDistribution(); 147 const allCodes = Object.values(result) 148 .flat() 149 .map(c => c.code); 150 const allExpected = Object.keys(COUNTRY_PRICES); 151 for (const code of allExpected) { 152 assert.ok(allCodes.includes(code), `${code} should appear in distribution`); 153 } 154 }); 155 156 test('Standard tier includes AU', () => { 157 const result = getPricingTierDistribution(); 158 const standard = result['Standard'] || []; 159 assert.ok( 160 standard.some(c => c.code === 'AU'), 161 'AU should be in Standard tier' 162 ); 163 }); 164 }); 165 166 // ─── getPriceStatistics ────────────────────────────────────────────────────── 167 168 describe('getPriceStatistics', () => { 169 test('returns min, max, average, median, basePrice, floor, ceiling', () => { 170 const stats = getPriceStatistics(); 171 assert.ok(typeof stats.min === 'number'); 172 assert.ok(typeof stats.max === 'number'); 173 assert.ok(typeof stats.average === 'number'); 174 assert.ok(typeof stats.median === 'number'); 175 assert.ok(typeof stats.basePrice === 'number'); 176 assert.ok(typeof stats.floor === 'number'); 177 assert.ok(typeof stats.ceiling === 'number'); 178 }); 179 180 test('min is less than or equal to max', () => { 181 const stats = getPriceStatistics(); 182 assert.ok(stats.min <= stats.max); 183 }); 184 185 test('average is between min and max', () => { 186 const stats = getPriceStatistics(); 187 assert.ok(stats.average >= stats.min); 188 assert.ok(stats.average <= stats.max); 189 }); 190 191 test('floor is less than base price', () => { 192 const stats = getPriceStatistics(); 193 assert.ok(stats.floor < stats.basePrice); 194 }); 195 196 test('ceiling is greater than base price', () => { 197 const stats = getPriceStatistics(); 198 assert.ok(stats.ceiling > stats.basePrice); 199 }); 200 }); 201 202 // ─── overrideCountryPrice ──────────────────────────────────────────────────── 203 204 describe('overrideCountryPrice', () => { 205 test('overrides price for known country', () => { 206 const originalPrice = getCountryPrice('NZ').usdPrice; 207 208 overrideCountryPrice('NZ', 150, 'test override'); 209 210 const updated = getCountryPrice('NZ'); 211 assert.equal(updated.usdPrice, 150); 212 assert.ok(updated.overridden === true); 213 214 // Restore original price to avoid polluting other tests 215 overrideCountryPrice('NZ', originalPrice, 'restore after test'); 216 }); 217 218 test('throws for unknown country code', () => { 219 assert.throws(() => overrideCountryPrice('ZZ', 100, 'test'), /Unknown country code/); 220 }); 221 222 test('is case-insensitive for country code', () => { 223 const originalPrice = getCountryPrice('MX').usdPrice; 224 overrideCountryPrice('mx', 75, 'test lowercase'); 225 const updated = getCountryPrice('MX'); 226 assert.equal(updated.usdPrice, 75); 227 overrideCountryPrice('MX', originalPrice, 'restore'); 228 }); 229 }); 230 231 // ─── PRICING_CONFIG ────────────────────────────────────────────────────────── 232 233 describe('PRICING_CONFIG', () => { 234 test('is an object with expected fields', () => { 235 assert.ok(typeof PRICING_CONFIG === 'object'); 236 assert.ok(typeof PRICING_CONFIG.BASE_PRICE_USD === 'number'); 237 assert.ok(typeof PRICING_CONFIG.MIN_PRICE_FLOOR === 'number'); 238 assert.ok(typeof PRICING_CONFIG.MAX_PRICE_CEILING === 'number'); 239 assert.ok(typeof PRICING_CONFIG.PPP_SOURCE === 'string'); 240 assert.ok(typeof PRICING_CONFIG.LAST_UPDATED === 'string'); 241 }); 242 243 test('floor is less than base price', () => { 244 assert.ok(PRICING_CONFIG.MIN_PRICE_FLOOR < PRICING_CONFIG.BASE_PRICE_USD); 245 }); 246 247 test('ceiling is greater than base price', () => { 248 assert.ok(PRICING_CONFIG.MAX_PRICE_CEILING > PRICING_CONFIG.BASE_PRICE_USD); 249 }); 250 });