update-cultural-pricing.js
1 #!/usr/bin/env node 2 /** 3 * Cultural Pricing Optimization Script 4 * 5 * Updates country prices based on cultural numerology and psychological pricing research. 6 * Optimizes prices for: 7 * - Cultural number preferences (lucky/unlucky numbers) 8 * - Regional pricing psychology (charm pricing vs round numbers) 9 * - Consumer behavior patterns 10 * 11 * Usage: 12 * node scripts/update-cultural-pricing.js [--dry-run] [--priority critical|high|medium|all] 13 * 14 * Flags: 15 * --dry-run: Preview changes without applying them 16 * --priority: Only update countries at specified priority level (default: all) 17 * 18 * Examples: 19 * node scripts/update-cultural-pricing.js --dry-run 20 * node scripts/update-cultural-pricing.js --priority critical 21 * node scripts/update-cultural-pricing.js --priority high --dry-run 22 */ 23 24 import { createDatabaseConnection } from '../src/utils/db.js'; 25 import Logger from '../src/utils/logger.js'; 26 27 const logger = new Logger('CulturalPricing'); 28 29 // Price optimization config with cultural rationale 30 const PRICE_UPDATES = { 31 // CRITICAL: Cultural issues that could be offensive 32 KR: { 33 priority: 'critical', 34 oldPrice: 325777, 35 newPrice: 328000, 36 reason: 'Cultural optimization: Avoid unlucky triple-7, use round number with lucky 8', 37 notes: 38 'Korean numerology: 7 is unlucky (ghost associations), 8 is lucky (prosperity). Round thousands preferred in high-value currencies.', 39 }, 40 CN: { 41 priority: 'critical', 42 oldPrice: 637, 43 newPrice: 688, 44 reason: 'Cultural optimization: Use lucky double-8 for prosperity', 45 notes: 46 'Chinese numerology: 8 is extremely lucky (prosperity/wealth), 7 is unlucky (ghost month). Double-8 ending is highly auspicious.', 47 }, 48 IN: { 49 priority: 'high', 50 oldPrice: 3097, 51 newPrice: 3001, 52 reason: 'Cultural optimization: Use auspicious 1 ending for new beginnings', 53 notes: 54 'Indian numerology: Prices ending in 1 are auspicious (new beginnings). Common in Indian business pricing. Alternative: 3111 (triple-1).', 55 }, 56 57 // HIGH: Significant conversion impact 58 US: { 59 priority: 'high', 60 oldPrice: 307, 61 newPrice: 297, 62 reason: 'Psychological optimization: Charm pricing + lucky 7', 63 notes: 64 'Western charm pricing: .97 endings increase conversions 24%+ (MIT research). Number 7 is universally lucky in Western culture.', 65 }, 66 CA: { 67 priority: 'high', 68 oldPrice: 307, 69 newPrice: 297, 70 reason: 'Psychological optimization: Charm pricing + lucky 7', 71 notes: 72 'Canadian market follows US charm pricing patterns. Strong consumer response to .97 endings.', 73 }, 74 DE: { 75 priority: 'high', 76 oldPrice: 207, 77 newPrice: 210, 78 reason: 'Cultural optimization: Round numbers for German efficiency preference', 79 notes: 80 'Germans prioritize efficiency and quick decisions. Round numbers facilitate faster purchasing. All German prices should be rounded.', 81 }, 82 NO: { 83 priority: 'high', 84 oldPrice: 2847, 85 newPrice: 2850, 86 reason: 'Cultural optimization: Nordic simplicity and clarity preference', 87 notes: 'Nordic countries prefer round, simple pricing over complex charm pricing structures.', 88 }, 89 SE: { 90 priority: 'high', 91 oldPrice: 2277, 92 newPrice: 2300, 93 reason: 'Cultural optimization: Nordic simplicity and clarity preference', 94 notes: 'Swedish consumers value transparency and round numbers for ease of decision-making.', 95 }, 96 DK: { 97 priority: 'high', 98 oldPrice: 1687, 99 newPrice: 1700, 100 reason: 'Cultural optimization: Nordic simplicity and clarity preference', 101 notes: 'Danish market shows strong preference for round, clear pricing.', 102 }, 103 104 // MEDIUM: Optimization improvements 105 JP: { 106 priority: 'medium', 107 oldPrice: 30487, 108 newPrice: 31000, 109 reason: 'Cultural optimization: Round "honest" pricing preferred in Japan', 110 notes: 111 'Japanese consumers perceive rounded prices as more honest and straightforward. Charm pricing less effective. Alternative: 30800 (lucky 8).', 112 }, 113 AU: { 114 priority: 'medium', 115 oldPrice: 347, 116 newPrice: 337, 117 reason: 'Psychological optimization: Enhanced charm pricing', 118 notes: 119 'Australian market responds well to charm pricing. 337 maintains lucky 7 while improving perceived value. Alternative: 339.', 120 }, 121 NZ: { 122 priority: 'medium', 123 oldPrice: 347, 124 newPrice: 337, 125 reason: 'Psychological optimization: Enhanced charm pricing', 126 notes: 127 'New Zealand follows Australian charm pricing patterns. Strong response to .97/.99 endings.', 128 }, 129 CH: { 130 priority: 'medium', 131 oldPrice: 227, 132 newPrice: 230, 133 reason: 'Cultural optimization: Swiss clarity and transparency requirement', 134 notes: 135 'Swiss law requires price clarity. Consumers highly attentive to transparency. Round numbers signal honesty.', 136 }, 137 MX: { 138 priority: 'medium', 139 oldPrice: 1537, 140 newPrice: 1500, 141 reason: 'Cultural optimization: Latin American round pricing preference', 142 notes: 143 'High-context cultures (Mexico) perceive odd endings as manipulative. Round numbers signal stability during economic uncertainty.', 144 }, 145 AT: { 146 priority: 'medium', 147 oldPrice: 227, 148 newPrice: 230, 149 reason: 'Cultural optimization: Austrian efficiency preference (follows German pattern)', 150 notes: 'Austria shares German preference for round numbers and efficient decision-making.', 151 }, 152 }; 153 154 /** 155 * Get database connection 156 */ 157 function getDb() { 158 const dbPath = process.env.DATABASE_PATH || './db/sites.db'; 159 return createDatabaseConnection(dbPath); 160 } 161 162 /** 163 * Parse command line arguments 164 */ 165 function parseArgs() { 166 const args = process.argv.slice(2); 167 const dryRun = args.includes('--dry-run'); 168 const priorityIndex = args.indexOf('--priority'); 169 const priority = priorityIndex !== -1 ? args[priorityIndex + 1] : 'all'; 170 171 return { dryRun, priority }; 172 } 173 174 /** 175 * Filter updates by priority 176 */ 177 function filterByPriority(updates, priority) { 178 if (priority === 'all') return updates; 179 180 return Object.entries(updates) 181 .filter(([, config]) => config.priority === priority) 182 .reduce((acc, [code, config]) => { 183 acc[code] = config; 184 return acc; 185 }, {}); 186 } 187 188 /** 189 * Get current price for a country 190 */ 191 function getCurrentPrice(db, countryCode) { 192 return db 193 .prepare( 194 ` 195 SELECT 196 country_code, 197 country_name, 198 price_local, 199 price_local_formatted, 200 currency_symbol, 201 price_overridden, 202 override_reason, 203 market_notes 204 FROM countries 205 WHERE country_code = ? 206 ` 207 ) 208 .get(countryCode); 209 } 210 211 /** 212 * Update country price 213 */ 214 function updatePrice(db, countryCode, config, dryRun = false) { 215 const current = getCurrentPrice(db, countryCode); 216 217 if (!current) { 218 logger.warn(`Country not found: ${countryCode}`); 219 return { success: false, reason: 'not_found' }; 220 } 221 222 // Convert to cents (price_local stores in smallest currency unit) 223 const newPriceLocal = config.newPrice * 100; 224 const newPriceFormatted = config.newPrice.toString(); 225 226 // Check if already at target price 227 if (current.price_local === newPriceLocal) { 228 logger.info( 229 `${countryCode}: Already at target price ${current.currency_symbol}${config.newPrice}` 230 ); 231 return { success: true, reason: 'already_updated', skipped: true }; 232 } 233 234 logger.info(`\n${countryCode} (${current.country_name}):`); 235 logger.info( 236 ` Current: ${current.currency_symbol}${current.price_local_formatted} (${current.price_local} cents)` 237 ); 238 logger.info( 239 ` New: ${current.currency_symbol}${newPriceFormatted} (${newPriceLocal} cents)` 240 ); 241 logger.info(` Reason: ${config.reason}`); 242 logger.info(` Notes: ${config.notes}`); 243 244 if (dryRun) { 245 logger.warn(` [DRY RUN] Would update but --dry-run flag is set`); 246 return { success: true, reason: 'dry_run', skipped: true }; 247 } 248 249 try { 250 const result = db 251 .prepare( 252 ` 253 UPDATE countries 254 SET 255 price_local = ?, 256 price_local_formatted = ?, 257 price_overridden = 1, 258 override_reason = ?, 259 market_notes = ?, 260 override_date = CURRENT_TIMESTAMP, 261 updated_at = CURRENT_TIMESTAMP 262 WHERE country_code = ? 263 ` 264 ) 265 .run(newPriceLocal, newPriceFormatted, config.reason, config.notes, countryCode); 266 267 if (result.changes > 0) { 268 logger.success(` ✓ Updated successfully`); 269 return { success: true, reason: 'updated' }; 270 } else { 271 logger.error(` ✗ Update failed (no changes)`); 272 return { success: false, reason: 'no_changes' }; 273 } 274 } catch (error) { 275 logger.error(` ✗ Update failed: ${error.message}`); 276 return { success: false, reason: 'error', error }; 277 } 278 } 279 280 /** 281 * Generate summary report 282 */ 283 function generateSummary(results) { 284 const total = results.length; 285 const updated = results.filter(r => r.success && r.reason === 'updated').length; 286 const alreadyUpdated = results.filter(r => r.reason === 'already_updated').length; 287 const dryRun = results.filter(r => r.reason === 'dry_run').length; 288 const failed = results.filter(r => !r.success).length; 289 290 return { 291 total, 292 updated, 293 alreadyUpdated, 294 dryRun, 295 failed, 296 }; 297 } 298 299 /** 300 * Main execution 301 */ 302 async function main() { 303 const { dryRun, priority } = parseArgs(); 304 305 logger.info('═══════════════════════════════════════════════════════════'); 306 logger.info('Cultural Pricing Optimization Script'); 307 logger.info('═══════════════════════════════════════════════════════════\n'); 308 309 if (dryRun) { 310 logger.warn('⚠️ DRY RUN MODE - No changes will be applied\n'); 311 } 312 313 logger.info(`Priority filter: ${priority}`); 314 logger.info(''); 315 316 const filteredUpdates = filterByPriority(PRICE_UPDATES, priority); 317 const updateCount = Object.keys(filteredUpdates).length; 318 319 if (updateCount === 0) { 320 logger.warn(`No updates found for priority: ${priority}`); 321 logger.info('Valid priorities: critical, high, medium, all'); 322 process.exit(1); 323 } 324 325 logger.info(`Found ${updateCount} countries to update\n`); 326 327 const db = getDb(); 328 const results = []; 329 330 try { 331 // Process each country update 332 for (const [countryCode, config] of Object.entries(filteredUpdates)) { 333 const result = updatePrice(db, countryCode, config, dryRun); 334 results.push({ countryCode, ...result }); 335 } 336 337 // Generate summary 338 logger.info('\n═══════════════════════════════════════════════════════════'); 339 logger.info('Summary'); 340 logger.info('═══════════════════════════════════════════════════════════\n'); 341 342 const summary = generateSummary(results); 343 344 logger.info(`Total countries processed: ${summary.total}`); 345 if (dryRun) { 346 logger.warn(`Would update: ${summary.dryRun}`); 347 } else { 348 logger.success(`Updated: ${summary.updated}`); 349 } 350 logger.info(`Already at target: ${summary.alreadyUpdated}`); 351 if (summary.failed > 0) { 352 logger.error(`Failed: ${summary.failed}`); 353 } 354 355 if (dryRun) { 356 logger.info('\n💡 Run without --dry-run to apply changes'); 357 } else { 358 logger.success('\n✓ Cultural pricing optimization complete!'); 359 logger.info('\nNext steps:'); 360 logger.info('1. Test proposal generation with new prices'); 361 logger.info('2. Verify PayPal integration with updated amounts'); 362 logger.info('3. Monitor conversion rates over 2-4 weeks'); 363 logger.info('4. Update weekly repricing logic to respect cultural rules'); 364 } 365 } catch (error) { 366 logger.error(`Fatal error: ${error.message}`); 367 console.error(error); 368 process.exit(1); 369 } finally { 370 db.close(); 371 } 372 } 373 374 // Run if called directly 375 if (import.meta.url === `file://${process.argv[1]}`) { 376 main().catch(error => { 377 console.error('Fatal error:', error); 378 process.exit(1); 379 }); 380 } 381 382 export { PRICE_UPDATES, updatePrice, filterByPriority, getCurrentPrice };