agmission/Development/server/scripts/pause_addon_subs.js

399 lines
15 KiB
JavaScript

// /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> Path to environment file (default: ./environment.env)
* --resume-date <date> Date to auto-resume (ISO format, e.g., 2026-03-01)
* --reason <string> Reason for pausing (stored in metadata)
* --dry-run Preview changes without applying
* --limit <number> 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> Path to environment file (default: ./environment.env)
--resume-date <date> Date to auto-resume (ISO format, e.g., 2026-03-01)
--reason <string> Reason for pausing (stored in metadata, default: addon_promo)
--dry-run Preview changes without applying
--limit <number> 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);
}
})();