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.
This commit is contained in:
parent
f848773887
commit
aa6a176fb9
1 changed files with 112 additions and 0 deletions
112
scripts/migrate-annual-contribution-to-cadence-unit.cjs
Normal file
112
scripts/migrate-annual-contribution-to-cadence-unit.cjs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* 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)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue