pricing.js
1 /** 2 * Country-Specific Pricing Configuration 3 * 4 * Uses Purchasing Power Parity (PPP) adjusted pricing to ensure fair 5 * pricing across different economic markets. Prices are calculated based 6 * on PPP GDP per capita relative to the United States baseline. 7 * 8 * Data source: World Bank PPP GDP per capita 2023 (international dollars) 9 * Base price: $300 USD (United States) 10 * 11 * Formula: Local Price = Base Price × (Local PPP GDP per capita / US PPP GDP per capita) 12 */ 13 14 /** 15 * PPP GDP per capita data (2023, international dollars) 16 * Source: World Bank World Development Indicators 17 */ 18 const PPP_GDP_PER_CAPITA = { 19 US: 80412, // Baseline 20 CN: 24558, 21 JP: 52120, 22 DE: 63835, 23 IN: 9183, 24 UK: 56836, 25 FR: 58765, 26 IT: 53203, 27 CA: 60177, 28 MX: 23451, 29 KR: 59330, 30 AU: 64674, 31 ES: 51693, 32 ID: 15016, 33 NL: 71888, 34 CH: 76493, 35 PL: 48283, 36 SE: 67168, 37 BE: 65355, 38 NO: 77808, 39 IE: 109205, // Note: Unusually high due to corporate tax structures 40 AT: 70277, 41 SG: 111247, // Note: High-income city-state 42 DK: 71403, 43 NZ: 54196, 44 }; 45 46 /** 47 * Base price in USD (United States market) 48 */ 49 const BASE_PRICE_USD = 300; 50 51 /** 52 * Minimum price floor (prevents extremely low prices) 53 * Set at 20% of base price 54 */ 55 const MIN_PRICE_FLOOR = BASE_PRICE_USD * 0.2; // $60 56 57 /** 58 * Maximum price ceiling (prevents extreme outliers) 59 * Set at 150% of base price 60 */ 61 const MAX_PRICE_CEILING = BASE_PRICE_USD * 1.5; // $450 62 63 /** 64 * Calculate PPP-adjusted price for a country 65 * @param {string} countryCode - ISO country code 66 * @returns {number} Price in local currency equivalent value 67 */ 68 function calculatePPPPrice(countryCode) { 69 const localPPP = PPP_GDP_PER_CAPITA[countryCode.toUpperCase()]; 70 const usPPP = PPP_GDP_PER_CAPITA.US; 71 72 if (!localPPP) { 73 console.warn(`No PPP data for ${countryCode}, using base price`); 74 return BASE_PRICE_USD; 75 } 76 77 // Calculate PPP-adjusted price 78 const adjustedPrice = BASE_PRICE_USD * (localPPP / usPPP); 79 80 // Apply floor and ceiling constraints 81 const constrainedPrice = Math.max(MIN_PRICE_FLOOR, Math.min(MAX_PRICE_CEILING, adjustedPrice)); 82 83 // Apply psychological pricing (97 or 47 endings) 84 return applyPsychologicalPricing(constrainedPrice); 85 } 86 87 /** 88 * Apply psychological pricing strategy (97 or 47 endings) 89 * @param {number} price - Raw calculated price 90 * @returns {number} Price with psychological pricing applied 91 */ 92 function applyPsychologicalPricing(price) { 93 // Get the base hundred (e.g., 243 -> 200) 94 const baseHundred = Math.floor(price / 100) * 100; 95 96 // Calculate both 97 and 47 options 97 const option97 = baseHundred + 97; 98 const option47 = baseHundred + 47; 99 const nextOption97 = baseHundred + 197; 100 101 // Choose the closest option 102 const diffTo47 = Math.abs(price - option47); 103 const diffTo97 = Math.abs(price - option97); 104 const diffToNext97 = Math.abs(price - nextOption97); 105 106 // Pick the closest psychological price point 107 if (diffTo47 < diffTo97 && diffTo47 < diffToNext97) { 108 return option47; 109 } 110 if (diffTo97 < diffToNext97) { 111 return option97; 112 } 113 return nextOption97; 114 } 115 116 /** 117 * Pre-calculated PPP-adjusted prices for all countries 118 * Prices use psychological pricing (97 or 47 endings) 119 * Prices are in USD-equivalent value (will be converted to local currency for display) 120 */ 121 export const COUNTRY_PRICES = { 122 US: { usdPrice: 297, tier: 'Premium', notes: 'Baseline market' }, 123 SG: { 124 usdPrice: 397, 125 tier: 'Premium+', 126 notes: 'High-income city-state, capped at ceiling', 127 cappedAtCeiling: true, 128 }, 129 IE: { 130 usdPrice: 397, 131 tier: 'Premium+', 132 notes: 'High GDP per capita (corporate tax effect), capped at ceiling', 133 cappedAtCeiling: true, 134 }, 135 NO: { usdPrice: 297, tier: 'Premium', notes: 'High purchasing power' }, 136 CH: { usdPrice: 297, tier: 'Premium', notes: 'High cost of living' }, 137 NL: { usdPrice: 297, tier: 'Premium', notes: 'Strong economy' }, 138 DK: { usdPrice: 297, tier: 'Premium', notes: 'High standard of living' }, 139 AT: { usdPrice: 247, tier: 'Premium', notes: 'Strong European economy' }, 140 SE: { usdPrice: 247, tier: 'Standard', notes: 'Nordic country' }, 141 BE: { usdPrice: 247, tier: 'Standard', notes: 'Western Europe' }, 142 AU: { usdPrice: 247, tier: 'Standard', notes: 'Developed market' }, 143 DE: { usdPrice: 247, tier: 'Standard', notes: 'Largest European economy' }, 144 CA: { usdPrice: 197, tier: 'Standard', notes: 'North American market' }, 145 KR: { usdPrice: 197, tier: 'Standard', notes: 'Advanced economy' }, 146 FR: { usdPrice: 197, tier: 'Standard', notes: 'Major European market' }, 147 UK: { usdPrice: 197, tier: 'Standard', notes: 'Post-Brexit economy' }, 148 NZ: { usdPrice: 197, tier: 'Standard', notes: 'Pacific developed market' }, 149 IT: { usdPrice: 197, tier: 'Standard', notes: 'Southern Europe' }, 150 JP: { usdPrice: 197, tier: 'Standard', notes: 'Mature economy, high costs' }, 151 ES: { usdPrice: 197, tier: 'Moderate', notes: 'Southern Europe' }, 152 PL: { usdPrice: 197, tier: 'Moderate', notes: 'Central Europe, growing' }, 153 CN: { usdPrice: 97, tier: 'Emerging', notes: 'Large emerging market' }, 154 MX: { usdPrice: 97, tier: 'Emerging', notes: 'Latin American market' }, 155 ID: { usdPrice: 47, tier: 'Developing', notes: 'Southeast Asia, capped at floor' }, 156 IN: { usdPrice: 47, tier: 'Developing', notes: 'Price-sensitive market, capped at floor' }, 157 }; 158 159 /** 160 * Get pricing information for a country 161 * @param {string} countryCode - ISO country code 162 * @returns {Object} Pricing information 163 */ 164 export function getCountryPrice(countryCode) { 165 const code = countryCode.toUpperCase(); 166 const priceInfo = COUNTRY_PRICES[code]; 167 168 if (!priceInfo) { 169 console.warn(`No pricing data for ${countryCode}, using base price`); 170 return { 171 usdPrice: BASE_PRICE_USD, 172 tier: 'Standard', 173 notes: 'Default pricing', 174 }; 175 } 176 177 return priceInfo; 178 } 179 180 /** 181 * Get price in local currency (formatted) 182 * @param {string} countryCode - ISO country code 183 * @param {Object} countryConfig - Country configuration from countries.js 184 * @returns {Object} Price with currency formatting 185 */ 186 export function getLocalPrice(countryCode, countryConfig) { 187 const { usdPrice } = getCountryPrice(countryCode); 188 const { currency, currencySymbol } = countryConfig; 189 190 // Note: Exchange rates should be fetched from a live API in production 191 // For now, returning USD equivalent. You'll want to integrate with 192 // a forex API like exchangerate-api.com or openexchangerates.org 193 return { 194 amount: usdPrice, 195 currency, 196 currencySymbol, 197 formatted: `${currencySymbol}${usdPrice}`, 198 usdEquivalent: usdPrice, 199 }; 200 } 201 202 /** 203 * Get pricing tier distribution (for analytics) 204 * @returns {Object} Count of countries in each tier 205 */ 206 export function getPricingTierDistribution() { 207 const distribution = { 208 'Premium+': [], 209 Premium: [], 210 Standard: [], 211 Moderate: [], 212 Emerging: [], 213 Developing: [], 214 }; 215 216 Object.entries(COUNTRY_PRICES).forEach(([code, info]) => { 217 distribution[info.tier].push({ code, price: info.usdPrice }); 218 }); 219 220 return distribution; 221 } 222 223 /** 224 * Get price range statistics 225 * @returns {Object} Min, max, average, median prices 226 */ 227 export function getPriceStatistics() { 228 const prices = Object.values(COUNTRY_PRICES).map(p => p.usdPrice); 229 230 prices.sort((a, b) => a - b); 231 232 return { 233 min: prices[0], 234 max: prices[prices.length - 1], 235 average: Math.round(prices.reduce((sum, p) => sum + p, 0) / prices.length), 236 median: prices[Math.floor(prices.length / 2)], 237 basePrice: BASE_PRICE_USD, 238 floor: MIN_PRICE_FLOOR, 239 ceiling: MAX_PRICE_CEILING, 240 }; 241 } 242 243 /** 244 * Override price for a specific country (for manual adjustments) 245 * Use this when you have market research that suggests a different price 246 * @param {string} countryCode - ISO country code 247 * @param {number} usdPrice - New price in USD equivalent 248 * @param {string} reason - Reason for override 249 */ 250 export function overrideCountryPrice(countryCode, usdPrice, reason) { 251 const code = countryCode.toUpperCase(); 252 253 if (!COUNTRY_PRICES[code]) { 254 throw new Error(`Unknown country code: ${countryCode}`); 255 } 256 257 console.warn(`Overriding price for ${code}: $${usdPrice} (${reason})`); 258 259 COUNTRY_PRICES[code].usdPrice = usdPrice; 260 COUNTRY_PRICES[code].notes = `${COUNTRY_PRICES[code].notes} | OVERRIDE: ${reason}`; 261 COUNTRY_PRICES[code].overridden = true; 262 } 263 264 /** 265 * Export configuration constants 266 */ 267 export const PRICING_CONFIG = { 268 BASE_PRICE_USD, 269 MIN_PRICE_FLOOR, 270 MAX_PRICE_CEILING, 271 PPP_SOURCE: 'World Bank PPP GDP per capita 2023', 272 LAST_UPDATED: '2024-01-15', 273 }; 274 275 export default { 276 COUNTRY_PRICES, 277 getCountryPrice, 278 getLocalPrice, 279 getPricingTierDistribution, 280 getPriceStatistics, 281 overrideCountryPrice, 282 PRICING_CONFIG, 283 };