255 lines
9.0 KiB
JavaScript
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);
|
|
}
|
|
})(); |