wip: zen
This commit is contained in:
Frank
2026-01-08 19:24:20 -05:00
parent cf97633d7d
commit 52fbd16e08
16 changed files with 4131 additions and 106 deletions

View File

@@ -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))
}) })
} }
})() })()

View File

@@ -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),
),
),
] ]
})() })()
: [ : [

View File

@@ -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`)
);

View File

@@ -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`;

View File

@@ -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

View File

@@ -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
} }
] ]
} }

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))

View File

@@ -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({

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) => {

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",

View File

@@ -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",
{ {

View File

@@ -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),

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 }
}