Skip to content

Commit e762b65

Browse files
feat(billing): migration — backfill 500 signupGrant for existing Free orgs
1 parent 3a3ca09 commit e762b65

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Migration: Backfill 500 signup-grant credits to existing Free-plan orgs.
3+
*
4+
* Credits every organization currently on the Free plan (plan === 'free')
5+
* that has not been canceled (status !== 'canceled') and does not already hold
6+
* a 'signup_grant' ledger entry, mirroring what new Free signups receive via
7+
* BillingSignupGrantService (N2 — one-shot 500-compute grant).
8+
*
9+
* Idempotent: the synthetic idempotency key `signup_grant-<orgId>` stored as
10+
* `ledger[].refId` is the same key `creditGrant` uses at signup time.
11+
* Running this migration a second time skips every org (all are already credited).
12+
*
13+
* Safe to run while the app is live: each updateOne is atomic (single-document
14+
* write) and the migration runner serialises execution via a DB-level claim so
15+
* only one pod runs this at startup even in multi-replica deploys.
16+
*
17+
* down() removes all signup_grant ledger entries. Note: cachedBalance is NOT
18+
* adjusted on rollback — run a reconcile sweep if down() is invoked in prod.
19+
*/
20+
import mongoose from 'mongoose';
21+
22+
const SIGNUP_GRANT_AMOUNT = 500;
23+
const GRANT_SOURCE = 'signup_grant';
24+
25+
/**
26+
* @returns {Promise<void>}
27+
*/
28+
export async function up() {
29+
const db = mongoose.connection.db;
30+
const subscriptions = db.collection('subscriptions');
31+
const extraBalances = db.collection('billingextrabalances');
32+
33+
const cursor = subscriptions.find(
34+
{ plan: 'free', status: { $ne: 'canceled' } },
35+
{ projection: { organization: 1 } },
36+
);
37+
38+
let granted = 0;
39+
let skipped = 0;
40+
41+
for await (const sub of cursor) {
42+
const orgId = sub.organization;
43+
if (!orgId) { skipped += 1; continue; }
44+
45+
const idempotencyKey = `${GRANT_SOURCE}-${orgId.toString()}`;
46+
47+
// Skip if this org already has a signup_grant entry (idempotent re-run).
48+
const existing = await extraBalances.findOne(
49+
{ organization: orgId, 'ledger.refId': idempotencyKey },
50+
{ projection: { _id: 1 } },
51+
);
52+
if (existing) { skipped += 1; continue; }
53+
54+
// Step 1: ensure the ExtraBalance document exists (no-op if already present).
55+
await extraBalances.updateOne(
56+
{ organization: orgId },
57+
{
58+
$setOnInsert: {
59+
organization: orgId,
60+
ledger: [],
61+
cachedBalance: 0,
62+
cachedBalanceAt: new Date(),
63+
createdAt: new Date(),
64+
updatedAt: new Date(),
65+
},
66+
},
67+
{ upsert: true },
68+
);
69+
70+
// Step 2: push the grant entry (idempotency-guarded, no upsert).
71+
const result = await extraBalances.updateOne(
72+
{ organization: orgId, 'ledger.refId': { $ne: idempotencyKey } },
73+
{
74+
$push: {
75+
ledger: {
76+
kind: 'topup',
77+
amount: SIGNUP_GRANT_AMOUNT,
78+
source: GRANT_SOURCE,
79+
refId: idempotencyKey,
80+
at: new Date(),
81+
},
82+
},
83+
$inc: { cachedBalance: SIGNUP_GRANT_AMOUNT },
84+
$set: { cachedBalanceAt: new Date(), updatedAt: new Date() },
85+
},
86+
);
87+
88+
if (result.modifiedCount > 0) {
89+
granted += 1;
90+
} else {
91+
// Another concurrent writer beat us to this org — harmless race, already credited.
92+
skipped += 1;
93+
}
94+
}
95+
96+
console.info(`[migration] grant-backfill: complete — granted=${granted} skipped=${skipped}`);
97+
}
98+
99+
/**
100+
* Reverse: remove all signup_grant ledger entries.
101+
*
102+
* WARNING: cachedBalance is NOT adjusted. If down() is applied to a live
103+
* database, run a cachedBalance reconcile sweep afterwards.
104+
*
105+
* @returns {Promise<void>}
106+
*/
107+
export async function down() {
108+
const db = mongoose.connection.db;
109+
110+
const result = await db.collection('billingextrabalances').updateMany(
111+
{ 'ledger.source': GRANT_SOURCE },
112+
{ $pull: { ledger: { source: GRANT_SOURCE } } },
113+
);
114+
115+
console.warn(
116+
`[migration] grant-backfill DOWN: removed signup_grant entries from ${result.modifiedCount} docs. cachedBalance NOT adjusted — run reconcile if applied in prod.`,
117+
);
118+
}

0 commit comments

Comments
 (0)