/ tests / config / pricing.test.js
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  });