pricing-export.js
1 /** 2 * Pricing Export to Cloudflare Worker 3 * 4 * Exports country pricing data to the auditandfix-api CF Worker KV 5 * so the PHP sales page can serve localized prices. 6 * Also writes a local backup to data/pricing-export.json. 7 */ 8 9 import axios from 'axios'; 10 import { writeFileSync, mkdirSync } from 'fs'; 11 import { join, dirname } from 'path'; 12 import { fileURLToPath } from 'url'; 13 import { getOne } from '../utils/db.js'; 14 import { getAllCountries, getPrice } from '../utils/country-pricing.js'; 15 import Logger from '../utils/logger.js'; 16 import '../utils/load-env.js'; 17 18 const __filename = fileURLToPath(import.meta.url); 19 const __dirname = dirname(__filename); 20 const projectRoot = join(__dirname, '../..'); 21 22 const logger = new Logger('PricingExport'); 23 24 /** 25 * Export pricing data to CF Worker and local backup 26 * @returns {Object} { success, countries } 27 */ 28 export async function exportPricing() { 29 logger.info('Exporting pricing data...'); 30 31 // Get all active country pricing 32 const countries = getAllCountries({ activeOnly: true }); 33 34 // Transform to simple format for PHP/JS consumption 35 const pricingData = {}; 36 for (const country of countries) { 37 const p = getPrice(country.country_code); 38 if (!p) continue; 39 // Fall back to USD when local price hasn't been set by repricing cron yet. 40 // Don't use local currency symbol with a USD amount — show USD instead. 41 const hasLocal = p.priceLocalCents !== null && p.priceLocalCents > 0; 42 const priceCents = hasLocal ? p.priceLocalCents : country.price_usd; 43 const currency = hasLocal ? p.currency : 'USD'; 44 const symbol = hasLocal ? p.currencySymbol : '$'; 45 const formatted = hasLocal ? p.formattedPrice : `$${Math.round(country.price_usd / 100)}`; 46 pricingData[country.country_code] = { 47 price: priceCents, 48 currency, 49 symbol, 50 formatted, 51 country_name: country.country_name || country.country_code, 52 }; 53 } 54 55 // GB is an alias for UK (ISO uses GB; our DB uses UK) 56 if (pricingData['UK'] && !pricingData['GB']) { 57 pricingData['GB'] = pricingData['UK']; 58 } 59 60 // Count real countries — exclude _meta and the GB alias (added above) 61 const gbAliasAdded = pricingData['UK'] && pricingData['GB'] === pricingData['UK']; 62 const countryCount = Object.keys(pricingData).length - (gbAliasAdded ? 1 : 0); 63 64 // Add _meta: sites_scored count from DB (live social proof for sales page) 65 try { 66 const row = await getOne("SELECT COUNT(*) as cnt FROM sites WHERE status NOT IN ('found')"); 67 pricingData['_meta'] = { sites_scored: Number(row?.cnt ?? 10000) }; 68 logger.info(`Sites scored count: ${pricingData['_meta'].sites_scored}`); 69 } catch (error) { 70 logger.warn(`Failed to get sites_scored count: ${error.message}`); 71 pricingData['_meta'] = { sites_scored: 10000 }; 72 } 73 logger.info(`Prepared pricing for ${countryCount} countries`); 74 75 // Write local backup 76 try { 77 const backupDir = join(projectRoot, 'data'); 78 mkdirSync(backupDir, { recursive: true }); 79 writeFileSync(join(backupDir, 'pricing-export.json'), JSON.stringify(pricingData, null, 2)); 80 logger.info('Local backup written to data/pricing-export.json'); 81 } catch (error) { 82 logger.warn(`Failed to write local backup: ${error.message}`); 83 } 84 85 // Push to CF Worker 86 const workerUrl = process.env.API_WORKER_URL; 87 const workerSecret = process.env.API_WORKER_SECRET; 88 89 if (!workerUrl || !workerSecret) { 90 logger.warn( 91 'API_WORKER_URL or API_WORKER_SECRET not configured, skipping push' 92 ); 93 return { success: false, countries: countryCount, reason: 'not_configured' }; 94 } 95 96 try { 97 await axios.post(`${workerUrl}/pricing`, pricingData, { 98 headers: { 99 'Content-Type': 'application/json', 100 'X-Auth-Secret': workerSecret, 101 }, 102 timeout: 15000, 103 }); 104 105 logger.success(`Pricing exported to CF Worker: ${countryCount} countries`); 106 return { success: true, countries: countryCount }; 107 } catch (error) { 108 logger.error('Failed to export pricing to CF Worker', error); 109 return { success: false, countries: countryCount, error: error.message }; 110 } 111 } 112 113 // CLI 114 if (import.meta.url === `file://${process.argv[1]}`) { 115 exportPricing() 116 .then(result => { 117 console.log('Export result:', result); 118 process.exit(result.success ? 0 : 1); 119 }) 120 .catch(error => { 121 logger.error('Export failed:', error); 122 process.exit(1); 123 }); 124 }