wip: black
This commit is contained in:
@@ -183,7 +183,12 @@ export async function POST(input: APIEvent) {
|
|||||||
.set({
|
.set({
|
||||||
customerID,
|
customerID,
|
||||||
subscriptionID,
|
subscriptionID,
|
||||||
subscriptionCouponID: couponID,
|
subscription: {
|
||||||
|
status: "subscribed",
|
||||||
|
coupon: couponID,
|
||||||
|
seats: 1,
|
||||||
|
plan: "200",
|
||||||
|
},
|
||||||
paymentMethodID: paymentMethod.id,
|
paymentMethodID: paymentMethod.id,
|
||||||
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
paymentMethodLast4: paymentMethod.card?.last4 ?? null,
|
||||||
paymentMethodType: paymentMethod.type,
|
paymentMethodType: paymentMethod.type,
|
||||||
@@ -408,7 +413,7 @@ export async function POST(input: APIEvent) {
|
|||||||
await Database.transaction(async (tx) => {
|
await Database.transaction(async (tx) => {
|
||||||
await tx
|
await tx
|
||||||
.update(BillingTable)
|
.update(BillingTable)
|
||||||
.set({ subscriptionID: null, subscriptionCouponID: null })
|
.set({ subscriptionID: null, subscription: null })
|
||||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `billing` ADD `subscription` json;
|
||||||
1316
packages/console/core/migrations/meta/0053_snapshot.json
Normal file
1316
packages/console/core/migrations/meta/0053_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -372,6 +372,13 @@
|
|||||||
"when": 1768343920467,
|
"when": 1768343920467,
|
||||||
"tag": "0052_aromatic_agent_zero",
|
"tag": "0052_aromatic_agent_zero",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 53,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768599366758,
|
||||||
|
"tag": "0053_gigantic_hardball",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
112
packages/console/core/script/black-gift.ts
Normal file
112
packages/console/core/script/black-gift.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { Billing } from "../src/billing.js"
|
||||||
|
import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
|
||||||
|
import { UserTable } from "../src/schema/user.sql.js"
|
||||||
|
import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
|
||||||
|
import { Identifier } from "../src/identifier.js"
|
||||||
|
import { centsToMicroCents } from "../src/util/price.js"
|
||||||
|
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||||
|
|
||||||
|
const plan = "200"
|
||||||
|
const workspaceID = process.argv[2]
|
||||||
|
const seats = parseInt(process.argv[3])
|
||||||
|
|
||||||
|
console.log(`Gifting ${seats} seats of Black to workspace ${workspaceID}`)
|
||||||
|
|
||||||
|
if (!workspaceID || !seats) throw new Error("Usage: bun foo.ts <workspaceID> <seats>")
|
||||||
|
|
||||||
|
// Get workspace user
|
||||||
|
const users = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
id: UserTable.id,
|
||||||
|
role: UserTable.role,
|
||||||
|
email: AuthTable.subject,
|
||||||
|
})
|
||||||
|
.from(UserTable)
|
||||||
|
.leftJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
||||||
|
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||||
|
)
|
||||||
|
if (users.length === 0) throw new Error(`Error: No users found in workspace ${workspaceID}`)
|
||||||
|
if (users.length !== seats)
|
||||||
|
throw new Error(`Error: Workspace ${workspaceID} has ${users.length} users, expected ${seats}`)
|
||||||
|
const adminUser = users.find((user) => user.role === "admin")
|
||||||
|
if (!adminUser) throw new Error(`Error: No admin user found in workspace ${workspaceID}`)
|
||||||
|
if (!adminUser.email) throw new Error(`Error: Admin user ${adminUser.id} has no email`)
|
||||||
|
|
||||||
|
// Get Billing
|
||||||
|
const billing = await Database.use((tx) =>
|
||||||
|
tx
|
||||||
|
.select({
|
||||||
|
customerID: BillingTable.customerID,
|
||||||
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
|
})
|
||||||
|
.from(BillingTable)
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
.then((rows) => rows[0]),
|
||||||
|
)
|
||||||
|
if (!billing) throw new Error(`Error: Workspace ${workspaceID} has no billing record`)
|
||||||
|
if (billing.subscriptionID) throw new Error(`Error: Workspace ${workspaceID} already has a subscription`)
|
||||||
|
|
||||||
|
// Look up the Stripe customer by email
|
||||||
|
const customerID =
|
||||||
|
billing.customerID ??
|
||||||
|
(await (() =>
|
||||||
|
Billing.stripe()
|
||||||
|
.customers.create({
|
||||||
|
email: adminUser.email,
|
||||||
|
metadata: {
|
||||||
|
workspaceID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((customer) => customer.id))())
|
||||||
|
console.log(`Customer ID: ${customerID}`)
|
||||||
|
|
||||||
|
const couponID = "JAIr0Pe1"
|
||||||
|
const subscription = await Billing.stripe().subscriptions.create({
|
||||||
|
customer: customerID!,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
price: `price_1SmfyI2StuRr0lbXovxJNeZn`,
|
||||||
|
discounts: [{ coupon: couponID }],
|
||||||
|
quantity: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
console.log(`Subscription ID: ${subscription.id}`)
|
||||||
|
|
||||||
|
await Database.transaction(async (tx) => {
|
||||||
|
// Set customer id, subscription id, and payment method on workspace billing
|
||||||
|
await tx
|
||||||
|
.update(BillingTable)
|
||||||
|
.set({
|
||||||
|
customerID,
|
||||||
|
subscriptionID: subscription.id,
|
||||||
|
subscription: { status: "subscribed", coupon: couponID, seats, plan },
|
||||||
|
})
|
||||||
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
|
// Create a row in subscription table
|
||||||
|
for (const user of users) {
|
||||||
|
await tx.insert(SubscriptionTable).values({
|
||||||
|
workspaceID,
|
||||||
|
id: Identifier.create("subscription"),
|
||||||
|
userID: user.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// // Create a row in payments table
|
||||||
|
// await tx.insert(PaymentTable).values({
|
||||||
|
// workspaceID,
|
||||||
|
// id: Identifier.create("payment"),
|
||||||
|
// amount: centsToMicroCents(amountInCents),
|
||||||
|
// customerID,
|
||||||
|
// invoiceID,
|
||||||
|
// paymentID,
|
||||||
|
// enrichment: {
|
||||||
|
// type: "subscription",
|
||||||
|
// couponID,
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`done`)
|
||||||
@@ -12,7 +12,7 @@ const email = process.argv[3]
|
|||||||
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
||||||
|
|
||||||
if (!workspaceID || !email) {
|
if (!workspaceID || !email) {
|
||||||
console.error("Usage: bun onboard-zen-black.ts <workspaceID> <email>")
|
console.error("Usage: bun foo.ts <workspaceID> <email>")
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const existingSubscription = await Database.use((tx) =>
|
|||||||
tx
|
tx
|
||||||
.select({ workspaceID: BillingTable.workspaceID })
|
.select({ workspaceID: BillingTable.workspaceID })
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
.where(eq(BillingTable.subscriptionID, subscriptionID))
|
.where(sql`JSON_EXTRACT(${BillingTable.subscription}, '$.id') = ${subscriptionID}`)
|
||||||
.then((rows) => rows[0]),
|
.then((rows) => rows[0]),
|
||||||
)
|
)
|
||||||
if (existingSubscription) {
|
if (existingSubscription) {
|
||||||
@@ -128,10 +128,15 @@ await Database.transaction(async (tx) => {
|
|||||||
.set({
|
.set({
|
||||||
customerID,
|
customerID,
|
||||||
subscriptionID,
|
subscriptionID,
|
||||||
subscriptionCouponID: couponID,
|
|
||||||
paymentMethodID,
|
paymentMethodID,
|
||||||
paymentMethodLast4,
|
paymentMethodLast4,
|
||||||
paymentMethodType,
|
paymentMethodType,
|
||||||
|
subscription: {
|
||||||
|
status: "subscribed",
|
||||||
|
coupon: couponID,
|
||||||
|
seats: 1,
|
||||||
|
plan: "200",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ const fromBilling = await Database.use((tx) =>
|
|||||||
.select({
|
.select({
|
||||||
customerID: BillingTable.customerID,
|
customerID: BillingTable.customerID,
|
||||||
subscriptionID: BillingTable.subscriptionID,
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
subscriptionCouponID: BillingTable.subscriptionCouponID,
|
subscription: BillingTable.subscription,
|
||||||
paymentMethodID: BillingTable.paymentMethodID,
|
paymentMethodID: BillingTable.paymentMethodID,
|
||||||
paymentMethodType: BillingTable.paymentMethodType,
|
paymentMethodType: BillingTable.paymentMethodType,
|
||||||
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
paymentMethodLast4: BillingTable.paymentMethodLast4,
|
||||||
@@ -119,7 +119,7 @@ await Database.transaction(async (tx) => {
|
|||||||
.set({
|
.set({
|
||||||
customerID: fromPrevPayment.customerID,
|
customerID: fromPrevPayment.customerID,
|
||||||
subscriptionID: null,
|
subscriptionID: null,
|
||||||
subscriptionCouponID: null,
|
subscription: null,
|
||||||
paymentMethodID: fromPrevPaymentMethods.data[0].id,
|
paymentMethodID: fromPrevPaymentMethods.data[0].id,
|
||||||
paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null,
|
paymentMethodLast4: fromPrevPaymentMethods.data[0].card?.last4 ?? null,
|
||||||
paymentMethodType: fromPrevPaymentMethods.data[0].type,
|
paymentMethodType: fromPrevPaymentMethods.data[0].type,
|
||||||
@@ -131,7 +131,7 @@ await Database.transaction(async (tx) => {
|
|||||||
.set({
|
.set({
|
||||||
customerID: fromBilling.customerID,
|
customerID: fromBilling.customerID,
|
||||||
subscriptionID: fromBilling.subscriptionID,
|
subscriptionID: fromBilling.subscriptionID,
|
||||||
subscriptionCouponID: fromBilling.subscriptionCouponID,
|
subscription: fromBilling.subscription,
|
||||||
paymentMethodID: fromBilling.paymentMethodID,
|
paymentMethodID: fromBilling.paymentMethodID,
|
||||||
paymentMethodLast4: fromBilling.paymentMethodLast4,
|
paymentMethodLast4: fromBilling.paymentMethodLast4,
|
||||||
paymentMethodType: fromBilling.paymentMethodType,
|
paymentMethodType: fromBilling.paymentMethodType,
|
||||||
|
|||||||
@@ -55,8 +55,9 @@ if (identifier.startsWith("wrk_")) {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Get all payments for these workspaces
|
for (const user of users) {
|
||||||
await Promise.all(users.map((u: { workspaceID: string }) => printWorkspace(u.workspaceID)))
|
await printWorkspace(user.workspaceID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function printWorkspace(workspaceID: string) {
|
async function printWorkspace(workspaceID: string) {
|
||||||
@@ -114,11 +115,11 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
balance: BillingTable.balance,
|
balance: BillingTable.balance,
|
||||||
customerID: BillingTable.customerID,
|
customerID: BillingTable.customerID,
|
||||||
reload: BillingTable.reload,
|
reload: BillingTable.reload,
|
||||||
|
subscriptionID: BillingTable.subscriptionID,
|
||||||
subscription: {
|
subscription: {
|
||||||
id: BillingTable.subscriptionID,
|
|
||||||
couponID: BillingTable.subscriptionCouponID,
|
|
||||||
plan: BillingTable.subscriptionPlan,
|
plan: BillingTable.subscriptionPlan,
|
||||||
booked: BillingTable.timeSubscriptionBooked,
|
booked: BillingTable.timeSubscriptionBooked,
|
||||||
|
enrichment: BillingTable.subscription,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.from(BillingTable)
|
.from(BillingTable)
|
||||||
@@ -128,8 +129,13 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
rows.map((row) => ({
|
rows.map((row) => ({
|
||||||
...row,
|
...row,
|
||||||
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
balance: `$${(row.balance / 100000000).toFixed(2)}`,
|
||||||
subscription: row.subscription.id
|
subscription: row.subscriptionID
|
||||||
? `Subscribed ${row.subscription.couponID ? `(coupon: ${row.subscription.couponID}) ` : ""}`
|
? [
|
||||||
|
`Black ${row.subscription.enrichment!.plan}`,
|
||||||
|
row.subscription.enrichment!.seats > 1 ? `X ${row.subscription.enrichment!.seats} seats` : "",
|
||||||
|
row.subscription.enrichment!.coupon ? `(coupon: ${row.subscription.enrichment!.coupon})` : "",
|
||||||
|
`(ref: ${row.subscriptionID})`,
|
||||||
|
].join(" ")
|
||||||
: row.subscription.booked
|
: row.subscription.booked
|
||||||
? `Waitlist ${row.subscription.plan} plan`
|
? `Waitlist ${row.subscription.plan} plan`
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ export const BillingTable = mysqlTable(
|
|||||||
reloadError: varchar("reload_error", { length: 255 }),
|
reloadError: varchar("reload_error", { length: 255 }),
|
||||||
timeReloadError: utc("time_reload_error"),
|
timeReloadError: utc("time_reload_error"),
|
||||||
timeReloadLockedTill: utc("time_reload_locked_till"),
|
timeReloadLockedTill: utc("time_reload_locked_till"),
|
||||||
|
subscription: json("subscription").$type<{
|
||||||
|
status: "subscribed"
|
||||||
|
coupon?: string
|
||||||
|
seats: number
|
||||||
|
plan: "20" | "100" | "200"
|
||||||
|
}>(),
|
||||||
subscriptionID: varchar("subscription_id", { length: 28 }),
|
subscriptionID: varchar("subscription_id", { length: 28 }),
|
||||||
subscriptionCouponID: varchar("subscription_coupon_id", { length: 28 }),
|
|
||||||
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
||||||
timeSubscriptionBooked: utc("time_subscription_booked"),
|
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [...workspaceIndexes(table), uniqueIndex("global_customer_id").on(table.customerID)],
|
||||||
...workspaceIndexes(table),
|
|
||||||
uniqueIndex("global_customer_id").on(table.customerID),
|
|
||||||
uniqueIndex("global_subscription_id").on(table.subscriptionID),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export const SubscriptionTable = mysqlTable(
|
export const SubscriptionTable = mysqlTable(
|
||||||
|
|||||||
Reference in New Issue
Block a user