forked from CodebuffAI/codebuff
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathupdate-stripe-subscriptions.ts
More file actions
111 lines (92 loc) · 3.07 KB
/
Copy pathupdate-stripe-subscriptions.ts
File metadata and controls
111 lines (92 loc) · 3.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import fs from 'fs'
import { db } from '@codebuff/common/db'
import { eq } from 'drizzle-orm'
import * as schema from '../common/src/db/schema'
import { stripeServer } from '../common/src/util/stripe'
import type Stripe from 'stripe'
const USAGE_PRICE_ID = process.env.STRIPE_USAGE_PRICE_ID
if (!USAGE_PRICE_ID) {
console.error('Missing STRIPE_USAGE_PRICE_ID in env')
process.exit(1)
}
interface MigrationEntry {
userId: string
stripeCustomerId: string | null
}
const migrationData: MigrationEntry[] = JSON.parse(
fs.readFileSync('credit-migration-data.json', 'utf-8'),
)
const progressPath = 'update-stripe-progress.json'
let processedSubs = new Set<string>()
if (fs.existsSync(progressPath)) {
processedSubs = new Set(JSON.parse(fs.readFileSync(progressPath, 'utf-8')))
}
const processedPathKey = (customerId: string) => `${customerId}` // helper
async function processCustomer(entry: MigrationEntry) {
if (!entry.stripeCustomerId) {
console.warn(`User ${entry.userId} missing stripeCustomerId`)
return
}
if (processedSubs.has(processedPathKey(entry.stripeCustomerId))) {
return // already handled customer
}
// Fetch active subscriptions
const subs = await stripeServer.subscriptions.list({
customer: entry.stripeCustomerId,
status: 'active',
limit: 100,
expand: ['data.items.data.price'],
})
// Try to find legacy licensed subscription
const legacySub = subs.data.find((sub) =>
sub.items.data.some(
(item: Stripe.SubscriptionItem) =>
item.price.recurring?.usage_type === 'licensed',
),
)
// Cancel legacy immediately (no refund) if it exists
if (legacySub && legacySub.status !== 'canceled') {
await stripeServer.subscriptions.cancel(legacySub.id, {
invoice_now: false,
prorate: false,
})
console.log(`Canceled legacy sub ${legacySub.id} (no prorate).`)
}
// Does customer already have usage‑based sub?
const hasUsageBasedSub = subs.data.some((sub) =>
sub.items.data.every(
(item: Stripe.SubscriptionItem) => item.price.id === USAGE_PRICE_ID,
),
)
if (!hasUsageBasedSub) {
// Create new usage‑based subscription
const newSub = await stripeServer.subscriptions.create({
customer: entry.stripeCustomerId,
items: [{ price: USAGE_PRICE_ID }],
payment_behavior: 'default_incomplete',
expand: ['items.data.price'],
})
console.log(
`Created usage sub ${newSub.id} for customer ${entry.stripeCustomerId}`,
)
}
// Persist price ID to DB
await db
.update(schema.user)
.set({ stripe_price_id: USAGE_PRICE_ID })
.where(eq(schema.user.id, entry.userId))
// Mark customer processed
processedSubs.add(processedPathKey(entry.stripeCustomerId))
fs.writeFileSync(
progressPath,
JSON.stringify(Array.from(processedSubs), null, 2),
)
console.log(`Processed customer ${entry.stripeCustomerId}`)
}
;(async () => {
console.log(`Processing ${migrationData.length} migrated users...`)
for (const entry of migrationData) {
await processCustomer(entry)
}
console.log('Stripe subscription updates complete!')
})()