ghostguild-org/scripts/migrate-annual-contribution-to-cadence-unit.cjs
Jennie Robinson Faber aa6a176fb9 feat(migration): convert annual contributionAmount to cadence-unit
One-time script to convert existing annual Member records from the legacy
monthly-equivalent interpretation to cadence-unit. Multiplies
contributionAmount by 12 for billingCadence='annual' members.

Companion to commit 5023fb1 which dropped the server's ×12 on annual
recurringAmount. Must run after deploy, before annual members renew.

Idempotent via a transient `contributionAmountConverted: true` marker
field on each migrated doc — re-runs are safe. Dry-run by default;
`--apply` to write. Skips null/undefined contributionAmount, logs $0
amounts as no-ops.
2026-05-23 15:54:51 +01:00

112 lines
3.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)
})