/** * One-time migration: convert Member.contributionAmount from monthly-equivalent * to cadence-unit for billingCadence='annual' members. After this migration, * an annual member with contributionAmount=180 means "$180/year" (vs the old * interpretation "$15/month × 12 = $180/year"). * * Companion to commit 5023fb1 which dropped × 12 from the server's recurringAmount * computation. Run AFTER deploying the new server code, BEFORE annual members * try to renew or update. * * IDEMPOTENT via a transient marker field (`contributionAmountConverted: true`). * The script only acts on rows where this flag is not set, so re-running is * safe — the second run will find 0 unconverted rows. * * Trade-off: this pollutes the schema with a transient field. A follow-up * migration can $unset `contributionAmountConverted` from all docs once every * environment is confirmed migrated. The field is harmless if left in place. * (Option A from Task 9's plan — chosen because no migration-tracking * collection exists in this codebase.) * * Usage: * node scripts/migrate-annual-contribution-to-cadence-unit.cjs # dry-run * node scripts/migrate-annual-contribution-to-cadence-unit.cjs --apply # writes */ require('dotenv').config() const mongoose = require('mongoose') async function run() { const apply = process.argv.includes('--apply') if (!process.env.MONGODB_URI) { console.error('MONGODB_URI not set in environment') process.exit(1) } await mongoose.connect(process.env.MONGODB_URI) const db = mongoose.connection.db const col = db.collection('members') // Find annual members that haven't been converted yet. const filter = { billingCadence: 'annual', contributionAmountConverted: { $ne: true }, } const legacyCount = await col.countDocuments(filter) console.log(`Found ${legacyCount} annual members not yet converted`) if (legacyCount === 0) { console.log('Nothing to migrate.') await mongoose.disconnect() return } const cursor = col.find(filter) let updated = 0 let skipped = 0 while (await cursor.hasNext()) { const doc = await cursor.next() const current = doc.contributionAmount if (current === null || current === undefined) { console.warn(` SKIP ${doc._id}: contributionAmount is ${current}`) skipped++ continue } if (!Number.isInteger(current) || current < 0) { console.warn( ` SKIP ${doc._id}: contributionAmount=${JSON.stringify(current)} is not a non-negative integer`, ) skipped++ continue } const next = current * 12 if (current === 0) { console.log(` ${doc._id}: contributionAmount=0, no change (still 0 after ×12)`) } else { console.log( ` ${apply ? 'Update' : 'Would update'} ${doc._id}: contributionAmount ${current} → ${next}`, ) } if (apply) { await col.updateOne( { _id: doc._id }, { $set: { contributionAmount: next, contributionAmountConverted: true, }, }, ) } updated++ } console.log( `\n${apply ? 'Updated' : 'Would update'} ${updated} documents, skipped ${skipped}`, ) if (!apply) console.log('Dry-run complete. Re-run with --apply to write changes.') await mongoose.disconnect() } run().catch(async (err) => { console.error(err) try { await mongoose.disconnect() } catch {} process.exit(1) })