// /home/trung/work/AgMission/branches/satloc-resume/server/scripts/resume_addon_subs.js /** * Resume Addon Subscriptions Script * * Resumes all paused addon subscriptions. * * Usage: * node scripts/resume_addon_subs.js [options] * * Options: * --env Path to environment file (default: ./environment.env) * --reason Filter by pause reason (only resume subs paused with this reason) * --dry-run Preview changes without applying * --limit Max subscriptions to process (default: 500) * --include-scheduled Also resume subscriptions that have a scheduled resume date * * Examples: * node scripts/resume_addon_subs.js --env ./environment_prod.env * node scripts/resume_addon_subs.js --dry-run * node scripts/resume_addon_subs.js --reason "addon_promo" */ const path = require('path'); const moment = require('moment'); // Parse command line arguments const args = process.argv.slice(2); const options = { envFile: './environment.env', reason: null, dryRun: false, limit: 500, includeScheduled: false }; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '-env': case '--env': options.envFile = 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 '-include-scheduled': case '--include-scheduled': options.includeScheduled = true; break; case '--help': console.log(` Resume Addon Subscriptions Script Usage: node scripts/resume_addon_subs.js [options] Options: --env Path to environment file (default: ./environment.env) --reason Filter by pause reason (only resume subs paused with this reason) --dry-run Preview changes without applying --limit Max subscriptions to process (default: 100) --include-scheduled Also resume subscriptions that have a scheduled resume date --help Show this help message Examples: node scripts/resume_addon_subs.js --env ./environment_prod.env node scripts/resume_addon_subs.js --dry-run node scripts/resume_addon_subs.js --reason "addon_promo" `); 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); } const stripe = Stripe(process.env.STRIPE_SEC_KEY, { apiVersion: process.env.STRIPE_API_VERSION || '2025-01-27.acacia' }); (async () => { console.log('\n=== Resume Addon Subscriptions ===\n'); console.log(`Filter by reason: ${options.reason || 'None (all paused addon subs)'}`); console.log(`Include scheduled: ${options.includeScheduled}`); console.log(`Dry Run: ${options.dryRun}`); console.log(`Limit: ${options.limit}`); console.log(''); try { // Find all subscriptions using metadata-based filtering (like pause script) const pausedAddonSubs = []; // Fetch active subscriptions and filter for paused addons for await (const sub of stripe.subscriptions.list({ status: 'active', limit: 100, expand: ['data.customer', 'data.items.data.price'] })) { // Check if it's an addon subscription with pause_collection if (sub.metadata?.type === 'addon' && sub.pause_collection) { // Filter by reason if specified if (options.reason && sub.metadata?.pauseReason !== options.reason) { continue; } // Skip if has scheduled resume and --include-scheduled not set if (!options.includeScheduled && sub.pause_collection.resumes_at) { continue; } pausedAddonSubs.push(sub); } if (pausedAddonSubs.length >= options.limit) break; } console.log(`Found ${pausedAddonSubs.length} paused addon subscriptions\n`); if (pausedAddonSubs.length === 0) { console.log('No paused addon subscriptions to resume.'); if (!options.includeScheduled) { console.log('(Use --include-scheduled to include subs with scheduled resume dates)'); } process.exit(0); } // Show paused subscriptions console.log('--- Paused Addon Subscriptions ---'); for (const sub of pausedAddonSubs) { 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 pauseReason = sub.metadata?.pauseReason || 'unknown'; const pausedAt = sub.metadata?.pausedAt || 'unknown'; const scheduledResume = sub.pause_collection?.resumes_at ? moment.utc(sub.pause_collection.resumes_at * 1000).format('YYYY-MM-DD') : 'none'; const subQuantity = sub.items?.data?.reduce((s, item) => s + (item.quantity || 1), 0) || 1; console.log(` ${sub.id}: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}, Qty: ${subQuantity}`); console.log(` Paused: ${pauseReason} at ${pausedAt}`); console.log(` Scheduled resume: ${scheduledResume}`); } console.log(''); let successCount = 0; let failCount = 0; let totalQuantity = 0; const customerStats = new Map(); console.log('--- Processing Subscriptions ---'); for (const sub of pausedAddonSubs) { 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; const pauseReason = sub.metadata?.pauseReason || 'unknown'; const pausedAt = sub.metadata?.pausedAt || 'unknown'; const scheduledResume = sub.pause_collection?.resumes_at ? moment.utc(sub.pause_collection.resumes_at * 1000).format('YYYY-MM-DD') : 'none'; // 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 resume: ${sub.id}`); console.log(` Customer: ${customerName} (${customerEmail})`); console.log(` Applicator: ${applicatorUsername}`); console.log(` Quantity: ${subQuantity}`); console.log(` Paused reason: ${pauseReason}`); console.log(` Paused at: ${pausedAt}`); console.log(` Scheduled resume: ${scheduledResume}`); successCount++; } else { try { await stripe.subscriptions.update(sub.id, { pause_collection: '', // Empty string removes pause metadata: { ...sub.metadata, resumedAt: moment.utc().toISOString(), resumedBy: 'resume_addon_subs_script' } }); console.log(`✓ Resumed: ${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 paused addon subscriptions found: ${pausedAddonSubs.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 { console.log('\nSubscriptions resumed. Billing will continue normally.'); } } catch (err) { console.error('Error:', err.message); process.exit(1); } })();