agmission/Development/server/scripts/resume_addon_subs.js

255 lines
9.0 KiB
JavaScript

// /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> Path to environment file (default: ./environment.env)
* --reason <string> Filter by pause reason (only resume subs paused with this reason)
* --dry-run Preview changes without applying
* --limit <number> 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> Path to environment file (default: ./environment.env)
--reason <string> Filter by pause reason (only resume subs paused with this reason)
--dry-run Preview changes without applying
--limit <number> 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);
}
})();