/ scripts / update-cultural-pricing.js
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 };