wip: zen
This commit is contained in:
Frank
2026-01-09 01:32:00 -05:00
parent cf97633d7d
commit 52fbd16e08
16 changed files with 4131 additions and 106 deletions
@@ -1,8 +1,7 @@
import { Billing } from "@opencode-ai/console-core/billing.js" import { Billing } from "@opencode-ai/console-core/billing.js"
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js" import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js" import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { Actor } from "@opencode-ai/console-core/actor.js" import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -380,7 +379,7 @@ export async function POST(input: APIEvent) {
await Database.transaction(async (tx) => { await Database.transaction(async (tx) => {
await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID)) await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID))
await tx.update(UserTable).set({ timeSubscribed: null }).where(eq(UserTable.workspaceID, workspaceID)) await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
}) })
} }
})() })()
@@ -1,8 +1,9 @@
import type { APIEvent } from "@solidjs/start/server" import type { APIEvent } from "@solidjs/start/server"
import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js" import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js" import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js" import { BillingTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js" import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
import { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
import { Identifier } from "@opencode-ai/console-core/identifier.js" import { Identifier } from "@opencode-ai/console-core/identifier.js"
import { Billing } from "@opencode-ai/console-core/billing.js" import { Billing } from "@opencode-ai/console-core/billing.js"
import { Actor } from "@opencode-ai/console-core/actor.js" import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -415,11 +416,11 @@ export async function handler(
timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated, timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
}, },
subscription: { subscription: {
timeSubscribed: UserTable.timeSubscribed, id: SubscriptionTable.id,
subIntervalUsage: UserTable.subIntervalUsage, rollingUsage: SubscriptionTable.rollingUsage,
subMonthlyUsage: UserTable.subMonthlyUsage, fixedUsage: SubscriptionTable.fixedUsage,
timeSubIntervalUsageUpdated: UserTable.timeSubIntervalUsageUpdated, timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
timeSubMonthlyUsageUpdated: UserTable.timeSubMonthlyUsageUpdated, timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
}, },
provider: { provider: {
credentials: ProviderTable.credentials, credentials: ProviderTable.credentials,
@@ -440,6 +441,14 @@ export async function handler(
) )
: sql`false`, : sql`false`,
) )
.leftJoin(
SubscriptionTable,
and(
eq(SubscriptionTable.workspaceID, KeyTable.workspaceID),
eq(SubscriptionTable.userID, KeyTable.userID),
isNull(SubscriptionTable.timeDeleted),
),
)
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted))) .where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
.then((rows) => rows[0]), .then((rows) => rows[0]),
) )
@@ -448,7 +457,7 @@ export async function handler(
logger.metric({ logger.metric({
api_key: data.apiKey, api_key: data.apiKey,
workspace: data.workspaceID, workspace: data.workspaceID,
isSubscription: data.subscription.timeSubscribed ? true : false, isSubscription: data.subscription ? true : false,
}) })
return { return {
@@ -456,7 +465,7 @@ export async function handler(
workspaceID: data.workspaceID, workspaceID: data.workspaceID,
billing: data.billing, billing: data.billing,
user: data.user, user: data.user,
subscription: data.subscription.timeSubscribed ? data.subscription : undefined, subscription: data.subscription,
provider: data.provider, provider: data.provider,
isFree: FREE_WORKSPACES.includes(data.workspaceID), isFree: FREE_WORKSPACES.includes(data.workspaceID),
isDisabled: !!data.timeDisabled, isDisabled: !!data.timeDisabled,
@@ -484,23 +493,11 @@ export async function handler(
return `${minutes}min` return `${minutes}min`
} }
// Check monthly limit (based on subscription billing cycle) // Check weekly limit
if ( if (sub.fixedUsage && sub.timeFixedUpdated) {
sub.subMonthlyUsage && const week = getWeekBounds(now)
sub.timeSubMonthlyUsageUpdated && if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
sub.subMonthlyUsage >= centsToMicroCents(black.monthlyLimit * 100) const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
) {
const subscribeDay = sub.timeSubscribed!.getUTCDate()
const cycleStart = new Date(
Date.UTC(
now.getUTCFullYear(),
now.getUTCDate() >= subscribeDay ? now.getUTCMonth() : now.getUTCMonth() - 1,
subscribeDay,
),
)
const cycleEnd = new Date(Date.UTC(cycleStart.getUTCFullYear(), cycleStart.getUTCMonth() + 1, subscribeDay))
if (sub.timeSubMonthlyUsageUpdated >= cycleStart && sub.timeSubMonthlyUsageUpdated < cycleEnd) {
const retryAfter = Math.ceil((cycleEnd.getTime() - now.getTime()) / 1000)
throw new SubscriptionError( throw new SubscriptionError(
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`, `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
retryAfter, retryAfter,
@@ -508,14 +505,12 @@ export async function handler(
} }
} }
// Check interval limit // Check rolling limit
const intervalMs = black.intervalLength * 3600 * 1000 if (sub.rollingUsage && sub.timeRollingUpdated) {
if (sub.subIntervalUsage && sub.timeSubIntervalUsageUpdated) { const rollingWindowMs = black.rollingWindow * 3600 * 1000
const currentInterval = Math.floor(now.getTime() / intervalMs) const windowStart = new Date(now.getTime() - rollingWindowMs)
const usageInterval = Math.floor(sub.timeSubIntervalUsageUpdated.getTime() / intervalMs) if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
if (currentInterval === usageInterval && sub.subIntervalUsage >= centsToMicroCents(black.intervalLimit * 100)) { const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
const nextInterval = (currentInterval + 1) * intervalMs
const retryAfter = Math.ceil((nextInterval - now.getTime()) / 1000)
throw new SubscriptionError( throw new SubscriptionError(
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`, `Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
retryAfter, retryAfter,
@@ -661,38 +656,34 @@ export async function handler(
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))), .where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
...(authInfo.subscription ...(authInfo.subscription
? (() => { ? (() => {
const now = new Date() const black = BlackData.get()
const subscribeDay = authInfo.subscription.timeSubscribed!.getUTCDate() const week = getWeekBounds(new Date())
const cycleStart = new Date( const rollingWindowSeconds = black.rollingWindow * 3600
Date.UTC(
now.getUTCFullYear(),
now.getUTCDate() >= subscribeDay ? now.getUTCMonth() : now.getUTCMonth() - 1,
subscribeDay,
),
)
const cycleEnd = new Date(
Date.UTC(cycleStart.getUTCFullYear(), cycleStart.getUTCMonth() + 1, subscribeDay),
)
return [ return [
db db
.update(UserTable) .update(SubscriptionTable)
.set({ .set({
subMonthlyUsage: sql` fixedUsage: sql`
CASE CASE
WHEN ${UserTable.timeSubMonthlyUsageUpdated} >= ${cycleStart} AND ${UserTable.timeSubMonthlyUsageUpdated} < ${cycleEnd} THEN ${UserTable.subMonthlyUsage} + ${cost} WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}
ELSE ${cost} ELSE ${cost}
END END
`, `,
timeSubMonthlyUsageUpdated: sql`now()`, timeFixedUpdated: sql`now()`,
subIntervalUsage: sql` rollingUsage: sql`
CASE CASE
WHEN FLOOR(UNIX_TIMESTAMP(${UserTable.timeSubIntervalUsageUpdated}) / (${BlackData.get().intervalLength} * 3600)) = FLOOR(UNIX_TIMESTAMP(now()) / (${BlackData.get().intervalLength} * 3600)) THEN ${UserTable.subIntervalUsage} + ${cost} WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost}
ELSE ${cost} ELSE ${cost}
END END
`, `,
timeSubIntervalUsageUpdated: sql`now()`, timeRollingUpdated: sql`now()`,
}) })
.where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))), .where(
and(
eq(SubscriptionTable.workspaceID, authInfo.workspaceID),
eq(SubscriptionTable.userID, authInfo.user.id),
),
),
] ]
})() })()
: [ : [
@@ -0,0 +1,13 @@
CREATE TABLE `subscription` (
`id` varchar(30) NOT NULL,
`workspace_id` varchar(30) NOT NULL,
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`time_deleted` timestamp(3),
`user_id` varchar(30) NOT NULL,
`rolling_usage` bigint,
`fixed_usage` bigint,
`time_rolling_updated` timestamp(3),
`time_fixed_updated` timestamp(3),
CONSTRAINT `subscription_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
);
@@ -0,0 +1,6 @@
CREATE INDEX `workspace_user_id` ON `subscription` (`workspace_id`,`user_id`);--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `time_subscribed`;--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `sub_interval_usage`;--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `sub_monthly_usage`;--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `sub_time_interval_usage_updated`;--> statement-breakpoint
ALTER TABLE `user` DROP COLUMN `sub_time_monthly_usage_updated`;
@@ -0,0 +1,2 @@
DROP INDEX `workspace_user_id` ON `subscription`;--> statement-breakpoint
ALTER TABLE `subscription` ADD CONSTRAINT `workspace_user_id` UNIQUE(`workspace_id`,`user_id`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -323,6 +323,27 @@
"when": 1767765497502, "when": 1767765497502,
"tag": "0045_cuddly_diamondback", "tag": "0045_cuddly_diamondback",
"breakpoints": true "breakpoints": true
},
{
"idx": 46,
"version": "5",
"when": 1767912262458,
"tag": "0046_charming_black_bolt",
"breakpoints": true
},
{
"idx": 47,
"version": "5",
"when": 1767916965243,
"tag": "0047_huge_omega_red",
"breakpoints": true
},
{
"idx": 48,
"version": "5",
"when": 1767917785224,
"tag": "0048_mean_frank_castle",
"breakpoints": true
} }
] ]
} }
+117 -2
View File
@@ -1,8 +1,11 @@
import { Database, eq, sql, inArray } from "../src/drizzle/index.js" import { Database, and, eq, sql } from "../src/drizzle/index.js"
import { AuthTable } from "../src/schema/auth.sql.js" import { AuthTable } from "../src/schema/auth.sql.js"
import { UserTable } from "../src/schema/user.sql.js" import { UserTable } from "../src/schema/user.sql.js"
import { BillingTable, PaymentTable, UsageTable } from "../src/schema/billing.sql.js" import { BillingTable, PaymentTable, SubscriptionTable, UsageTable } from "../src/schema/billing.sql.js"
import { WorkspaceTable } from "../src/schema/workspace.sql.js" import { WorkspaceTable } from "../src/schema/workspace.sql.js"
import { BlackData } from "../src/black.js"
import { centsToMicroCents } from "../src/util/price.js"
import { getWeekBounds } from "../src/util/date.js"
// get input from command line // get input from command line
const identifier = process.argv[2] const identifier = process.argv[2]
@@ -56,6 +59,44 @@ async function printWorkspace(workspaceID: string) {
printHeader(`Workspace "${workspace.name}" (${workspace.id})`) printHeader(`Workspace "${workspace.name}" (${workspace.id})`)
await printTable("Users", (tx) =>
tx
.select({
authEmail: AuthTable.subject,
inviteEmail: UserTable.email,
role: UserTable.role,
timeSeen: UserTable.timeSeen,
monthlyLimit: UserTable.monthlyLimit,
monthlyUsage: UserTable.monthlyUsage,
timeDeleted: UserTable.timeDeleted,
fixedUsage: SubscriptionTable.fixedUsage,
rollingUsage: SubscriptionTable.rollingUsage,
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
timeSubscriptionCreated: SubscriptionTable.timeCreated,
})
.from(UserTable)
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
.where(eq(UserTable.workspaceID, workspace.id))
.then((rows) =>
rows.map((row) => {
const subStatus = getSubscriptionStatus(row)
return {
email: (row.timeDeleted ? "❌ " : "") + (row.authEmail ?? row.inviteEmail),
role: row.role,
timeSeen: formatDate(row.timeSeen),
monthly: formatMonthlyUsage(row.monthlyUsage, row.monthlyLimit),
subscribed: formatDate(row.timeSubscriptionCreated),
subWeekly: subStatus.weekly,
subRolling: subStatus.rolling,
rateLimited: subStatus.rateLimited,
retryIn: subStatus.retryIn,
}
}),
),
)
await printTable("Billing", (tx) => await printTable("Billing", (tx) =>
tx tx
.select({ .select({
@@ -124,6 +165,80 @@ async function printWorkspace(workspaceID: string) {
) )
} }
function formatMicroCents(value: number | null | undefined) {
if (value === null || value === undefined) return null
return `$${(value / 100000000).toFixed(2)}`
}
function formatDate(value: Date | null | undefined) {
if (!value) return null
return value.toISOString().split("T")[0]
}
function formatMonthlyUsage(usage: number | null | undefined, limit: number | null | undefined) {
const usageText = formatMicroCents(usage) ?? "$0.00"
if (limit === null || limit === undefined) return `${usageText} / no limit`
return `${usageText} / $${limit.toFixed(2)}`
}
function formatRetryTime(seconds: number) {
const days = Math.floor(seconds / 86400)
if (days >= 1) return `${days} day${days > 1 ? "s" : ""}`
const hours = Math.floor(seconds / 3600)
const minutes = Math.ceil((seconds % 3600) / 60)
if (hours >= 1) return `${hours}hr ${minutes}min`
return `${minutes}min`
}
function getSubscriptionStatus(row: {
timeSubscriptionCreated: Date | null
fixedUsage: number | null
rollingUsage: number | null
timeFixedUpdated: Date | null
timeRollingUpdated: Date | null
}) {
if (!row.timeSubscriptionCreated) {
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
}
const black = BlackData.get()
const now = new Date()
const week = getWeekBounds(now)
const fixedLimit = black.fixedLimit ? centsToMicroCents(black.fixedLimit * 100) : null
const rollingLimit = black.rollingLimit ? centsToMicroCents(black.rollingLimit * 100) : null
const rollingWindowMs = (black.rollingWindow ?? 5) * 3600 * 1000
// Calculate current weekly usage (reset if outside current week)
const currentWeekly =
row.fixedUsage && row.timeFixedUpdated && row.timeFixedUpdated >= week.start ? row.fixedUsage : 0
// Calculate current rolling usage
const windowStart = new Date(now.getTime() - rollingWindowMs)
const currentRolling =
row.rollingUsage && row.timeRollingUpdated && row.timeRollingUpdated >= windowStart ? row.rollingUsage : 0
// Check rate limiting
const isWeeklyLimited = fixedLimit !== null && currentWeekly >= fixedLimit
const isRollingLimited = rollingLimit !== null && currentRolling >= rollingLimit
let retryIn: string | null = null
if (isWeeklyLimited) {
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
retryIn = formatRetryTime(retryAfter)
} else if (isRollingLimited && row.timeRollingUpdated) {
const retryAfter = Math.ceil((row.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
retryIn = formatRetryTime(retryAfter)
}
return {
weekly: fixedLimit !== null ? `${formatMicroCents(currentWeekly)} / $${black.fixedLimit}` : null,
rolling: rollingLimit !== null ? `${formatMicroCents(currentRolling)} / $${black.rollingLimit}` : null,
rateLimited: isWeeklyLimited || isRollingLimited ? "yes" : "no",
retryIn,
}
}
function printHeader(title: string) { function printHeader(title: string) {
console.log() console.log()
console.log("─".repeat(title.length)) console.log("─".repeat(title.length))
@@ -1,35 +1,35 @@
import { Billing } from "../src/billing.js" import { Billing } from "../src/billing.js"
import { Database, eq, and, sql } from "../src/drizzle/index.js" import { and, Database, eq, isNull, sql } from "../src/drizzle/index.js"
import { AuthTable } from "../src/schema/auth.sql.js"
import { UserTable } from "../src/schema/user.sql.js" import { UserTable } from "../src/schema/user.sql.js"
import { BillingTable, PaymentTable } from "../src/schema/billing.sql.js" import { BillingTable, PaymentTable, SubscriptionTable } from "../src/schema/billing.sql.js"
import { Identifier } from "../src/identifier.js" import { Identifier } from "../src/identifier.js"
import { centsToMicroCents } from "../src/util/price.js" import { centsToMicroCents } from "../src/util/price.js"
import { AuthTable } from "../src/schema/auth.sql.js"
const workspaceID = process.argv[2] const workspaceID = process.argv[2]
const email = process.argv[3] const email = process.argv[3]
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 onboard-zen-black.ts <workspaceID> <email>")
process.exit(1) process.exit(1)
} }
// Look up the Stripe customer by email // Look up the Stripe customer by email
const customers = await Billing.stripe().customers.list({ email, limit: 1 }) const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] })
const customer = customers.data[0] if (!customers.data) {
if (!customer) {
console.error(`Error: No Stripe customer found for email ${email}`) console.error(`Error: No Stripe customer found for email ${email}`)
process.exit(1) process.exit(1)
} }
const customerID = customer.id const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000)
if (!customer) {
// Get the subscription id console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`)
const subscriptions = await Billing.stripe().subscriptions.list({ customer: customerID, limit: 1 })
const subscription = subscriptions.data[0]
if (!subscription) {
console.error(`Error: Customer ${customerID} does not have a subscription`)
process.exit(1) process.exit(1)
} }
const customerID = customer.id
const subscription = customer.subscriptions!.data[0]
const subscriptionID = subscription.id const subscriptionID = subscription.id
// Validate the subscription is $200 // Validate the subscription is $200
@@ -90,29 +90,21 @@ const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.re
const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null
const paymentMethodType = paymentMethod?.type ?? null const paymentMethodType = paymentMethod?.type ?? null
// Look up the user by email via AuthTable // Look up the user in the workspace
const auth = await Database.use((tx) => const users = await Database.use((tx) =>
tx tx
.select({ accountID: AuthTable.accountID }) .select({ id: UserTable.id, email: AuthTable.subject })
.from(AuthTable) .from(UserTable)
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email))) .innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
.then((rows) => rows[0]), .where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
) )
if (!auth) { if (users.length === 0) {
console.error(`Error: No user found with email ${email}`) console.error(`Error: No users found in workspace ${workspaceID}`)
process.exit(1) process.exit(1)
} }
const user = users.length === 1 ? users[0] : users.find((u) => u.email === email)
// Look up the user in the workspace
const user = await Database.use((tx) =>
tx
.select({ id: UserTable.id })
.from(UserTable)
.where(and(eq(UserTable.workspaceID, workspaceID), eq(UserTable.accountID, auth.accountID)))
.then((rows) => rows[0]),
)
if (!user) { if (!user) {
console.error(`Error: User with email ${email} is not a member of workspace ${workspaceID}`) console.error(`Error: User with email ${email} not found in workspace ${workspaceID}`)
process.exit(1) process.exit(1)
} }
@@ -136,13 +128,12 @@ await Database.transaction(async (tx) => {
}) })
.where(eq(BillingTable.workspaceID, workspaceID)) .where(eq(BillingTable.workspaceID, workspaceID))
// Set current time as timeSubscribed on user // Create a row in subscription table
await tx await tx.insert(SubscriptionTable).values({
.update(UserTable) workspaceID,
.set({ id: Identifier.create("subscription"),
timeSubscribed: sql`now()`, userID: user.id,
}) })
.where(eq(UserTable.id, user.id))
// Create a row in payments table // Create a row in payments table
await tx.insert(PaymentTable).values({ await tx.insert(PaymentTable).values({
+3 -3
View File
@@ -4,9 +4,9 @@ import { Resource } from "@opencode-ai/console-resource"
export namespace BlackData { export namespace BlackData {
const Schema = z.object({ const Schema = z.object({
monthlyLimit: z.number().int(), fixedLimit: z.number().int(),
intervalLimit: z.number().int(), rollingLimit: z.number().int(),
intervalLength: z.number().int(), rollingWindow: z.number().int(),
}) })
export const validate = fn(Schema, (input) => { export const validate = fn(Schema, (input) => {
+1
View File
@@ -11,6 +11,7 @@ export namespace Identifier {
model: "mod", model: "mod",
payment: "pay", payment: "pay",
provider: "prv", provider: "prv",
subscription: "sub",
usage: "usg", usage: "usg",
user: "usr", user: "usr",
workspace: "wrk", workspace: "wrk",
@@ -30,6 +30,20 @@ export const BillingTable = mysqlTable(
], ],
) )
export const SubscriptionTable = mysqlTable(
"subscription",
{
...workspaceColumns,
...timestamps,
userID: ulid("user_id").notNull(),
rollingUsage: bigint("rolling_usage", { mode: "number" }),
fixedUsage: bigint("fixed_usage", { mode: "number" }),
timeRollingUpdated: utc("time_rolling_updated"),
timeFixedUpdated: utc("time_fixed_updated"),
},
(table) => [...workspaceIndexes(table), uniqueIndex("workspace_user_id").on(table.workspaceID, table.userID)],
)
export const PaymentTable = mysqlTable( export const PaymentTable = mysqlTable(
"payment", "payment",
{ {
@@ -18,12 +18,6 @@ export const UserTable = mysqlTable(
monthlyLimit: int("monthly_limit"), monthlyLimit: int("monthly_limit"),
monthlyUsage: bigint("monthly_usage", { mode: "number" }), monthlyUsage: bigint("monthly_usage", { mode: "number" }),
timeMonthlyUsageUpdated: utc("time_monthly_usage_updated"), timeMonthlyUsageUpdated: utc("time_monthly_usage_updated"),
// subscription
timeSubscribed: utc("time_subscribed"),
subIntervalUsage: bigint("sub_interval_usage", { mode: "number" }),
subMonthlyUsage: bigint("sub_monthly_usage", { mode: "number" }),
timeSubIntervalUsageUpdated: utc("sub_time_interval_usage_updated"),
timeSubMonthlyUsageUpdated: utc("sub_time_monthly_usage_updated"),
}, },
(table) => [ (table) => [
...workspaceIndexes(table), ...workspaceIndexes(table),
+9
View File
@@ -0,0 +1,9 @@
export function getWeekBounds(date: Date) {
const dayOfWeek = date.getUTCDay()
const start = new Date(date)
start.setUTCDate(date.getUTCDate() - dayOfWeek)
start.setUTCHours(0, 0, 0, 0)
const end = new Date(start)
end.setUTCDate(start.getUTCDate() + 7)
return { start, end }
}