// /home/trung/work/AgMission/branches/satloc-resume/server/scripts/pause_addon_subs.js /** * Pause Addon Subscriptions Script * * Pauses all active addon_1 subscriptions with optional auto-resume date. * * Usage: * node scripts/pause_addon_subs.js [options] * * Options: * --env Path to environment file (default: ./environment.env) * --resume-date Date to auto-resume (ISO format, e.g., 2026-03-01) * --reason Reason for pausing (stored in metadata) * --dry-run Preview changes without applying * --limit Max subscriptions to process (default: 500) * * Examples: * node scripts/pause_addon_subs.js --env ./environment_prod.env --resume-date 2026-03-01 * node scripts/pause_addon_subs.js --dry-run * node scripts/pause_addon_subs.js --reason "addon_launch_promo" --resume-date 2026-06-01 */ const path = require('path'); const moment = require('moment'); // Parse command line arguments const args = process.argv.slice(2); const options = { envFile: './environment.env', resumeDate: null, reason: 'addon_promo', dryRun: false, limit: 500 }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '-env': case '--env': options.envFile = args[++i]; break; case '-resume-date': case '--resume-date': options.resumeDate = args[++i]; break; case '-reason': case '--reason': options.reason = args[++i]; break; case '-dry-run': case '--dry-run': options.dryRun = true; break; case '-limit': case '--limit': options.limit = parseInt(args[++i], 10); break; case '--help': console.log(` Pause Addon Subscriptions Script Usage: node scripts/pause_addon_subs.js [options] Options: --env Path to environment file (default: ./environment.env) --resume-date Date to auto-resume (ISO format, e.g., 2026-03-01) --reason Reason for pausing (stored in metadata, default: addon_promo) --dry-run Preview changes without applying --limit Max subscriptions to process (default: 100) --help Show this help message Examples: node scripts/pause_addon_subs.js --env ./environment_prod.env --resume-date 2026-03-01 node scripts/pause_addon_subs.js --dry-run node scripts/pause_addon_subs.js --reason "addon_launch_promo" --resume-date 2026-06-01 `); process.exit(0); } } // Resolve and load environment file const envPath = path.resolve(process.cwd(), options.envFile); console.log(`Loading environment from: ${envPath}`); require('dotenv').config({ path: envPath }); const Stripe = require('stripe'); if (!process.env.STRIPE_SEC_KEY) { console.error('Error: STRIPE_SEC_KEY not found in environment file'); process.exit(1); } if (!process.env.ADDON_1) { console.error('Error: ADDON_1 price ID not found in environment file'); process.exit(1); } const stripe = Stripe(process.env.STRIPE_SEC_KEY, { apiVersion: process.env.STRIPE_API_VERSION || '2025-01-27.acacia' }); const ADDON_PRICE_ID = process.env.ADDON_1; (async () => { console.log('\n=== Pause Addon Subscriptions ===\n'); console.log(`Price ID: ${ADDON_PRICE_ID}`); console.log(`Resume Date: ${options.resumeDate || 'Not set (manual resume required)'}`); console.log(`Reason: ${options.reason}`); console.log(`Dry Run: ${options.dryRun}`); console.log(`Limit: ${options.limit}`); console.log(''); const resumeDateTs = options.resumeDate ? moment.utc(options.resumeDate).unix() : null; if (resumeDateTs && (isNaN(resumeDateTs) || resumeDateTs <= moment.utc().unix())) { console.error('Error: Invalid or past resume date'); process.exit(1); } try { // Find all active subscriptions with addon price // Use auto-pagination to get all subscriptions, filtering by status const addonSubs = []; const allSubsByCustomer = new Map(); // customerId -> { addon: sub, package: sub } // Fetch all active subscriptions to find both addon and package subs for await (const sub of stripe.subscriptions.list({ status: 'active', limit: 500, expand: ['data.customer', 'data.items.data.price'] })) { const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer?.id; if (!allSubsByCustomer.has(customerId)) { allSubsByCustomer.set(customerId, { addon: null, package: null }); } if (sub.metadata?.type === 'addon') { allSubsByCustomer.get(customerId).addon = sub; addonSubs.push(sub); } else if (sub.metadata?.type === 'package') { allSubsByCustomer.get(customerId).package = sub; } if (addonSubs.length >= options.limit) break; } // Also fetch trialing subscriptions if we haven't hit the limit if (addonSubs.length < options.limit) { for await (const sub of stripe.subscriptions.list({ status: 'trialing', limit: 100, expand: ['data.customer', 'data.items.data.price'] })) { const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer?.id; if (!allSubsByCustomer.has(customerId)) { allSubsByCustomer.set(customerId, { addon: null, package: null }); } if (sub.metadata?.type === 'addon') { allSubsByCustomer.get(customerId).addon = sub; addonSubs.push(sub); } else if (sub.metadata?.type === 'package') { allSubsByCustomer.get(customerId).package = sub; } if (addonSubs.length >= options.limit) break; } } console.log(`Found ${addonSubs.length} active/trialing addon subscriptions`); console.log(`Found ${allSubsByCustomer.size} unique customers with subscriptions\n`); // Separate paid vs trialing addon subscriptions const paidAddonSubs = addonSubs.filter(sub => sub.status === 'active'); const trialingAddonSubs = addonSubs.filter(sub => sub.status === 'trialing'); // Calculate quantities const paidQuantity = paidAddonSubs.reduce((sum, sub) => sum + (sub.items?.data?.reduce((s, item) => s + (item.quantity || 1), 0) || 1), 0); const trialingQuantity = trialingAddonSubs.reduce((sum, sub) => sum + (sub.items?.data?.reduce((s, item) => s + (item.quantity || 1), 0) || 1), 0); console.log('=== Addon Subscription Breakdown ==='); console.log(`Paid (active): ${paidAddonSubs.length} subscriptions, ${paidQuantity} total quantity`); console.log(`Trialing: ${trialingAddonSubs.length} subscriptions, ${trialingQuantity} total quantity`); console.log(''); // Show paid addon subscriptions with customer info if (paidAddonSubs.length > 0) { console.log('--- Paid Addon Subscriptions ---'); for (const sub of paidAddonSubs) { const customerEmail = sub.customer?.email || sub.customer; const customerName = sub.customer?.name || 'N/A'; const applicatorUsername = sub.metadata?.applicatorUsername || sub.metadata?.username || 'N/A'; const subQuantity = sub.items?.data?.reduce((s, item) => s + (item.quantity || 1), 0) || 1; const periodEnd = moment.utc(sub.current_period_end * 1000).format('YYYY-MM-DD'); console.log(` ${sub.id}: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}, Qty: ${subQuantity}, Period end: ${periodEnd}`); } console.log(''); } // Show trialing addon subscriptions with customer info if (trialingAddonSubs.length > 0) { console.log('--- Trialing Addon Subscriptions ---'); for (const sub of trialingAddonSubs) { const customerEmail = sub.customer?.email || sub.customer; const customerName = sub.customer?.name || 'N/A'; const applicatorUsername = sub.metadata?.applicatorUsername || sub.metadata?.username || 'N/A'; const subQuantity = sub.items?.data?.reduce((s, item) => s + (item.quantity || 1), 0) || 1; const trialEnd = sub.trial_end ? moment.utc(sub.trial_end * 1000).format('YYYY-MM-DD') : 'N/A'; console.log(` ${sub.id}: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}, Qty: ${subQuantity}, Trial end: ${trialEnd}`); } console.log(''); } if (addonSubs.length === 0) { console.log('No active addon subscriptions to pause.'); process.exit(0); } // Filter eligible subscriptions based on resume date validation const eligibleSubs = []; const skippedSubs = []; for (const addonSub of addonSubs) { const customerId = typeof addonSub.customer === 'string' ? addonSub.customer : addonSub.customer?.id; const customerData = allSubsByCustomer.get(customerId); const packageSub = customerData?.package; // Determine the reference end date: package sub end date, or addon end date if no package let referenceEndDate; let referenceType; if (packageSub) { referenceEndDate = packageSub.current_period_end; referenceType = 'package'; } else { referenceEndDate = addonSub.current_period_end; referenceType = 'addon-only'; } // Check if package or addon has a trial end date later than resume date const packageTrialEnd = packageSub?.trial_end || 0; const addonTrialEnd = addonSub.trial_end || 0; const maxTrialEnd = Math.max(packageTrialEnd, addonTrialEnd); // If resume date is specified, check if it's not later than the reference end date // Also skip if trial end date is later than resume date if (resumeDateTs && resumeDateTs > referenceEndDate) { skippedSubs.push({ sub: addonSub, reason: `resume-date (${options.resumeDate}) is later than ${referenceType} end date (${moment.utc(referenceEndDate * 1000).format('YYYY-MM-DD')})`, referenceType, referenceEndDate }); } else if (resumeDateTs && maxTrialEnd > 0 && resumeDateTs < maxTrialEnd) { const trialSource = addonTrialEnd >= packageTrialEnd ? 'addon' : 'package'; skippedSubs.push({ sub: addonSub, reason: `resume-date (${options.resumeDate}) is earlier than ${trialSource} trial end date (${moment.utc(maxTrialEnd * 1000).format('YYYY-MM-DD')})`, referenceType, referenceEndDate }); } else { eligibleSubs.push({ sub: addonSub, packageSub, referenceType, referenceEndDate }); } } console.log(`Eligible to pause: ${eligibleSubs.length}`); console.log(`Skipped (resume date too late): ${skippedSubs.length}\n`); // Show skipped subscriptions if (skippedSubs.length > 0) { console.log('--- Skipped Subscriptions ---'); for (const { sub, reason } of skippedSubs) { const customerEmail = sub.customer?.email || sub.customer; const customerName = sub.customer?.name || 'N/A'; const applicatorUsername = sub.metadata?.applicatorUsername || sub.metadata?.username || 'N/A'; console.log(` SKIP: ${sub.id}`); console.log(` Customer: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}`); console.log(` Reason: ${reason}`); } console.log(''); } if (eligibleSubs.length === 0) { console.log('No addon subscriptions eligible to pause.'); process.exit(0); } // Build pause configuration const pauseConfig = { pause_collection: { behavior: 'void' }, metadata: { pauseReason: options.reason, pausedAt: moment.utc().toISOString(), pausedBy: 'pause_addon_subs_script' } }; if (resumeDateTs) { pauseConfig.pause_collection.resumes_at = resumeDateTs; pauseConfig.metadata.scheduledResumeAt = options.resumeDate; } let successCount = 0; let failCount = 0; let totalQuantity = 0; const customerStats = new Map(); // Track by customer console.log('--- Processing Eligible Subscriptions ---'); for (const { sub, referenceType, referenceEndDate } of eligibleSubs) { const customerEmail = sub.customer?.email || sub.customer; const customerName = sub.customer?.name || 'N/A'; const applicatorUsername = sub.metadata?.applicatorUsername || sub.metadata?.username || 'N/A'; const customerId = typeof sub.customer === 'string' ? sub.customer : sub.customer?.id; // Get total quantity from subscription items const subQuantity = sub.items?.data?.reduce((sum, item) => sum + (item.quantity || 1), 0) || 1; // Track customer stats if (!customerStats.has(customerId)) { customerStats.set(customerId, { name: customerName, email: customerEmail, applicatorUsername, subscriptionCount: 0, totalQuantity: 0 }); } customerStats.get(customerId).subscriptionCount++; customerStats.get(customerId).totalQuantity += subQuantity; totalQuantity += subQuantity; if (options.dryRun) { console.log(`[DRY RUN] Would pause: ${sub.id}`); console.log(` Customer: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}`); console.log(` Quantity: ${subQuantity}`); console.log(` Reference: ${referenceType} ends ${moment.utc(referenceEndDate * 1000).format('YYYY-MM-DD')}`); console.log(` Addon period end: ${moment.utc(sub.current_period_end * 1000).format('YYYY-MM-DD')}`); successCount++; } else { try { await stripe.subscriptions.update(sub.id, pauseConfig); console.log(`✓ Paused: ${sub.id}`); console.log(` Customer: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}`); successCount++; } catch (err) { console.error(`✗ Failed: ${sub.id} - ${err.message}`); failCount++; } } } console.log('\n=== Summary ==='); console.log(`Total addon subscriptions found: ${addonSubs.length}`); console.log(`Eligible to pause: ${eligibleSubs.length}`); console.log(`Skipped (resume date too late): ${skippedSubs.length}`); console.log(`Successful: ${successCount}`); console.log(`Failed: ${failCount}`); console.log(`Total quantity (items): ${totalQuantity}`); // Customer/Applicator statistics console.log(`\n=== Customer/Applicator Stats ===`); console.log(`Unique customers affected: ${customerStats.size}`); console.log(''); for (const [customerId, stats] of customerStats) { console.log(` ${stats.name} (${stats.email})`); console.log(` Applicator: ${stats.applicatorUsername}`); console.log(` Subscriptions: ${stats.subscriptionCount}, Quantity: ${stats.totalQuantity}`); } if (options.dryRun) { console.log('\n[DRY RUN] No changes were made. Remove --dry-run to apply changes.'); } else if (options.resumeDate) { console.log(`\nSubscriptions will auto-resume on: ${options.resumeDate}`); } else { console.log('\nSubscriptions paused indefinitely. Run resume_addon_subs.js to resume.'); } } catch (err) { console.error('Error:', err.message); process.exit(1); } })();