/* sync_stripe_customer_info.js — Admin/maintenance script (not a scheduled job) What it does: Pushes customer name, business_name, and individual_name from MongoDB → Stripe customer records. Stripe customer resolution (priority order): 1. membership.custId — retrieved directly via stripe.customers.retrieve(custId). This is the authoritative Stripe customer ID stored during subscription creation and is immune to email/username changes. 2. Email search fallback — used only when membership.custId is absent (legacy/unsubscribed records). Searches stripe.customers.search({ query: 'email:"..."' }). Less reliable: if the customer's username was changed after Stripe registration the lookup will fail. Why it's not a regular cron need: The subscription controller already syncs the customer name to Stripe whenever the billing address is updated (~subscription.js line 1076–1084). Customers going through the normal billing flow will have their names kept in sync automatically. When to run manually: 1. One-time backfill — customers created before name-sync logic was added. 2. Bulk name corrections — names changed directly in MongoDB (e.g. a migration) without going through the billing address update path. 3. Audit/reconciliation — verify Stripe records match the DB after a data migration. Usage: node scripts/sync_stripe_customer_info.js [--env ] [--dry-run] --env Path to environment file (default: ./environment.env) --dry-run Preview changes without writing to Stripe */ 'use strict'; const path = require('path'); // Parse --env argument (default: ./environment.env) const args = process.argv.slice(2); let envFile = './environment.env'; let dryRun = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--env' && args[i + 1]) { envFile = args[i + 1]; i++; } else if (args[i] === '--dry-run' || args[i] === '--preview') { dryRun = true; } } // Load environment before requiring any modules const envPath = path.resolve(process.cwd(), envFile); require('dotenv').config({ path: envPath }); const debug = require('debug')('agm:sync-stripe-customer'); const env = require('../helpers/env.js'); const { DBConnection } = require('../helpers/db/connect.js'); const { Customer } = require('../model/index.js'); const { UserTypes } = require('../helpers/constants'); const stripe = require('stripe')(env.STRIPE_SEC_KEY, { apiVersion: env.STRIPE_API_VERSION }); // Initialize database connection const workerDB = new DBConnection('Stripe Customer Sync Script'); const stats = { total: 0, matched: 0, updated: 0, unchanged: 0, noStripeMatch: 0, errors: [] }; process .on('uncaughtException', function (err) { console.error('Uncaught Exception:', err); process.exit(1); }) .on('unhandledRejection', (reason, p) => { console.error('Unhandled Rejection at Promise', p, 'reason:', reason); process.exit(1); }); /** * Check if Stripe customer needs updating */ function needsUpdate(stripeCustomer, businessName, individualName) { const changes = {}; // Check customer name and business name (both should match DB customer's name) if (businessName && businessName.trim()) { const trimmedBusinessName = businessName.trim(); // Update 'name' field (customer's full name or business name) if (stripeCustomer.name !== trimmedBusinessName) { changes.name = trimmedBusinessName; } // Update 'business_name' field (dedicated business name field) if (stripeCustomer.business_name !== trimmedBusinessName) { changes.business_name = trimmedBusinessName; } } // Check individual name (using Stripe's dedicated individual_name field) if (individualName && individualName.trim()) { const trimmedIndividualName = individualName.trim(); if (stripeCustomer.individual_name !== trimmedIndividualName) { changes.individual_name = trimmedIndividualName; } } return Object.keys(changes).length > 0 ? changes : null; } /** * Process a single customer */ async function processCustomer(customer) { try { const custId = customer.membership?.custId; const email = customer.username?.toLowerCase(); if (!custId && !email) { console.log(`⚠ Customer ${customer._id} has no membership.custId or email/username`); stats.errors.push({ customerId: customer._id, name: customer.name, reason: 'No membership.custId or email/username' }); return; } let stripeCustomer; if (custId) { // Primary: resolve directly by membership.custId (authoritative, immune to email changes) try { const retrieved = await stripe.customers.retrieve(custId); if (retrieved && !retrieved.deleted) { stripeCustomer = retrieved; } else { console.log(`✗ Stripe customer ${custId} is deleted or not found for DB customer ${customer._id}`); stats.noStripeMatch++; stats.errors.push({ customerId: customer._id, custId, name: customer.name, reason: 'Stripe customer deleted or not found' }); return; } } catch (err) { console.log(`✗ Failed to retrieve Stripe customer ${custId}: ${err.message}`); stats.noStripeMatch++; stats.errors.push({ customerId: customer._id, custId, name: customer.name, reason: err.message }); return; } } else { // Fallback: email search (for legacy records without membership.custId) if (!email) { console.log(`⚠ Customer ${customer._id} has no membership.custId and no email`); stats.errors.push({ customerId: customer._id, name: customer.name, reason: 'No custId or email' }); return; } const searchResult = await stripe.customers.search({ query: `email:"${email}"`, limit: 1 }); if (!searchResult.data || searchResult.data.length === 0) { console.log(`✗ No Stripe match for: ${email} (${customer.name || 'N/A'}) — no custId, email fallback failed`); stats.noStripeMatch++; stats.errors.push({ customerId: customer._id, username: email, name: customer.name, contact: customer.contact, reason: 'No Stripe customer found (email fallback)' }); return; } stripeCustomer = searchResult.data[0]; } stats.matched++; const businessName = customer.name || ''; const individualName = customer.contact || ''; const label = custId || email || String(customer._id); // Check if update is needed const changes = needsUpdate(stripeCustomer, businessName, individualName); if (!changes) { console.log(`✓ Already synced: ${label} (${businessName})`); stats.unchanged++; return; } // Show what would be updated console.log(`\n→ Update needed for: ${label}`); console.log(` Customer ID: ${customer._id}`); console.log(` Stripe ID: ${stripeCustomer.id}`); if (custId) console.log(` Resolved via: membership.custId`); else console.log(` Resolved via: email fallback (${email})`); if (changes.name) { console.log(` Name: "${stripeCustomer.name || '(empty)'}" → "${changes.name}"`); } if (changes.business_name) { console.log(` Business Name: "${stripeCustomer.business_name || '(empty)'}" → "${changes.business_name}"`); } if (changes.individual_name) { console.log(` Individual Name: "${stripeCustomer.individual_name || '(empty)'}" → "${changes.individual_name}"`); } // Update Stripe customer if not in dry-run mode if (!dryRun) { await stripe.customers.update(stripeCustomer.id, changes); console.log(` ✓ Updated in Stripe`); stats.updated++; } else { console.log(` [DRY RUN] Would update in Stripe`); stats.updated++; } } catch (error) { console.error(`✗ Error processing customer ${customer._id}:`, error.message); stats.errors.push({ customerId: customer._id, username: customer.username, name: customer.name, contact: customer.contact, reason: error.message }); } } /** * Main migration logic */ async function syncStripeCustomers() { console.log('\n=== Stripe Customer Info Sync ===\n'); console.log(`Environment: ${envFile}`); console.log(`Mode: ${dryRun ? 'DRY RUN (preview only)' : 'LIVE (will update Stripe)'}\n`); try { // Find all active master accounts (kind="1" means Customer/Applicator) const customers = await Customer.find({ kind: UserTypes.APP, active: true, markedDelete: { $ne: true } }) .select('_id username name contact email membership') .lean(); stats.total = customers.length; console.log(`Found ${stats.total} active master accounts\n`); console.log('---\n'); // Process each customer for (const customer of customers) { await processCustomer(customer); } // Print summary console.log('\n=== Summary ===\n'); console.log(`Total customers processed: ${stats.total}`); console.log(`Matched with Stripe: ${stats.matched}`); console.log(`Updated: ${stats.updated}`); console.log(`Already synced: ${stats.unchanged}`); console.log(`No Stripe match: ${stats.noStripeMatch}`); console.log(`Errors: ${stats.errors.length}`); // Show only non-Stripe matching customers in detail const noStripeMatches = stats.errors.filter(e => e.reason === 'No Stripe customer found'); if (noStripeMatches.length > 0) { console.log('\n=== Customers Without Stripe Match ===\n'); noStripeMatches.forEach((error, index) => { console.log(`${index + 1}. Customer ID: ${error.customerId}`); console.log(` Username: ${error.username || 'N/A'}`); console.log(` Name: ${error.name || 'N/A'}`); console.log(` Contact: ${error.contact || 'N/A'}\n`); }); } if (dryRun) { console.log('\n⚠ This was a DRY RUN - no changes were made to Stripe'); console.log('Run without --dry-run flag to apply changes\n'); } } catch (error) { console.error('Fatal error during sync:', error); throw error; } } // Initialize the database connection and start sync workerDB.initialize({ setupExitHandlers: false, onReady: async () => { try { await syncStripeCustomers(); process.exit(0); } catch (error) { console.error('Sync failed:', error); process.exit(1); } } });