mirror of
https://github.com/anomalyco/opencode.git
synced 2026-02-01 22:48:16 +00:00
wip: zen
wip: zen
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import type { APIEvent } from "@solidjs/start/server"
|
||||
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 { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
|
||||
import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.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 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 { 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 { 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 { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
|
||||
import { Identifier } from "@opencode-ai/console-core/identifier.js"
|
||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||
@@ -415,11 +416,11 @@ export async function handler(
|
||||
timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
|
||||
},
|
||||
subscription: {
|
||||
timeSubscribed: UserTable.timeSubscribed,
|
||||
subIntervalUsage: UserTable.subIntervalUsage,
|
||||
subMonthlyUsage: UserTable.subMonthlyUsage,
|
||||
timeSubIntervalUsageUpdated: UserTable.timeSubIntervalUsageUpdated,
|
||||
timeSubMonthlyUsageUpdated: UserTable.timeSubMonthlyUsageUpdated,
|
||||
id: SubscriptionTable.id,
|
||||
rollingUsage: SubscriptionTable.rollingUsage,
|
||||
fixedUsage: SubscriptionTable.fixedUsage,
|
||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||
},
|
||||
provider: {
|
||||
credentials: ProviderTable.credentials,
|
||||
@@ -440,6 +441,14 @@ export async function handler(
|
||||
)
|
||||
: 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)))
|
||||
.then((rows) => rows[0]),
|
||||
)
|
||||
@@ -448,7 +457,7 @@ export async function handler(
|
||||
logger.metric({
|
||||
api_key: data.apiKey,
|
||||
workspace: data.workspaceID,
|
||||
isSubscription: data.subscription.timeSubscribed ? true : false,
|
||||
isSubscription: data.subscription ? true : false,
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -456,7 +465,7 @@ export async function handler(
|
||||
workspaceID: data.workspaceID,
|
||||
billing: data.billing,
|
||||
user: data.user,
|
||||
subscription: data.subscription.timeSubscribed ? data.subscription : undefined,
|
||||
subscription: data.subscription,
|
||||
provider: data.provider,
|
||||
isFree: FREE_WORKSPACES.includes(data.workspaceID),
|
||||
isDisabled: !!data.timeDisabled,
|
||||
@@ -484,23 +493,11 @@ export async function handler(
|
||||
return `${minutes}min`
|
||||
}
|
||||
|
||||
// Check monthly limit (based on subscription billing cycle)
|
||||
if (
|
||||
sub.subMonthlyUsage &&
|
||||
sub.timeSubMonthlyUsageUpdated &&
|
||||
sub.subMonthlyUsage >= centsToMicroCents(black.monthlyLimit * 100)
|
||||
) {
|
||||
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)
|
||||
// Check weekly limit
|
||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||
const week = getWeekBounds(now)
|
||||
if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
|
||||
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
||||
retryAfter,
|
||||
@@ -508,14 +505,12 @@ export async function handler(
|
||||
}
|
||||
}
|
||||
|
||||
// Check interval limit
|
||||
const intervalMs = black.intervalLength * 3600 * 1000
|
||||
if (sub.subIntervalUsage && sub.timeSubIntervalUsageUpdated) {
|
||||
const currentInterval = Math.floor(now.getTime() / intervalMs)
|
||||
const usageInterval = Math.floor(sub.timeSubIntervalUsageUpdated.getTime() / intervalMs)
|
||||
if (currentInterval === usageInterval && sub.subIntervalUsage >= centsToMicroCents(black.intervalLimit * 100)) {
|
||||
const nextInterval = (currentInterval + 1) * intervalMs
|
||||
const retryAfter = Math.ceil((nextInterval - now.getTime()) / 1000)
|
||||
// Check rolling limit
|
||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||
if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
|
||||
const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
|
||||
throw new SubscriptionError(
|
||||
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
|
||||
retryAfter,
|
||||
@@ -661,38 +656,34 @@ export async function handler(
|
||||
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
|
||||
...(authInfo.subscription
|
||||
? (() => {
|
||||
const now = new Date()
|
||||
const subscribeDay = authInfo.subscription.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),
|
||||
)
|
||||
const black = BlackData.get()
|
||||
const week = getWeekBounds(new Date())
|
||||
const rollingWindowSeconds = black.rollingWindow * 3600
|
||||
return [
|
||||
db
|
||||
.update(UserTable)
|
||||
.update(SubscriptionTable)
|
||||
.set({
|
||||
subMonthlyUsage: sql`
|
||||
fixedUsage: sql`
|
||||
CASE
|
||||
WHEN ${UserTable.timeSubMonthlyUsageUpdated} >= ${cycleStart} AND ${UserTable.timeSubMonthlyUsageUpdated} < ${cycleEnd} THEN ${UserTable.subMonthlyUsage} + ${cost}
|
||||
WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}
|
||||
ELSE ${cost}
|
||||
END
|
||||
`,
|
||||
timeSubMonthlyUsageUpdated: sql`now()`,
|
||||
subIntervalUsage: sql`
|
||||
timeFixedUpdated: sql`now()`,
|
||||
rollingUsage: sql`
|
||||
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}
|
||||
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`)
|
||||
);
|
||||
6
packages/console/core/migrations/0047_huge_omega_red.sql
Normal file
6
packages/console/core/migrations/0047_huge_omega_red.sql
Normal 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`;
|
||||
@@ -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`);
|
||||
1307
packages/console/core/migrations/meta/0046_snapshot.json
Normal file
1307
packages/console/core/migrations/meta/0046_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1281
packages/console/core/migrations/meta/0047_snapshot.json
Normal file
1281
packages/console/core/migrations/meta/0047_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
1281
packages/console/core/migrations/meta/0048_snapshot.json
Normal file
1281
packages/console/core/migrations/meta/0048_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -323,6 +323,27 @@
|
||||
"when": 1767765497502,
|
||||
"tag": "0045_cuddly_diamondback",
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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 { 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 { BlackData } from "../src/black.js"
|
||||
import { centsToMicroCents } from "../src/util/price.js"
|
||||
import { getWeekBounds } from "../src/util/date.js"
|
||||
|
||||
// get input from command line
|
||||
const identifier = process.argv[2]
|
||||
@@ -56,6 +59,44 @@ async function printWorkspace(workspaceID: string) {
|
||||
|
||||
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) =>
|
||||
tx
|
||||
.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) {
|
||||
console.log()
|
||||
console.log("─".repeat(title.length))
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
import { Billing } from "../src/billing.js"
|
||||
import { Database, eq, and, sql } from "../src/drizzle/index.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
import { and, Database, eq, isNull, sql } from "../src/drizzle/index.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 { centsToMicroCents } from "../src/util/price.js"
|
||||
import { AuthTable } from "../src/schema/auth.sql.js"
|
||||
|
||||
const workspaceID = process.argv[2]
|
||||
const email = process.argv[3]
|
||||
|
||||
console.log(`Onboarding workspace ${workspaceID} for email ${email}`)
|
||||
|
||||
if (!workspaceID || !email) {
|
||||
console.error("Usage: bun onboard-zen-black.ts <workspaceID> <email>")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Look up the Stripe customer by email
|
||||
const customers = await Billing.stripe().customers.list({ email, limit: 1 })
|
||||
const customer = customers.data[0]
|
||||
if (!customer) {
|
||||
const customers = await Billing.stripe().customers.list({ email, limit: 10, expand: ["data.subscriptions"] })
|
||||
if (!customers.data) {
|
||||
console.error(`Error: No Stripe customer found for email ${email}`)
|
||||
process.exit(1)
|
||||
}
|
||||
const customerID = customer.id
|
||||
|
||||
// Get the subscription id
|
||||
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`)
|
||||
const customer = customers.data.find((c) => c.subscriptions?.data[0]?.items.data[0]?.price.unit_amount === 20000)
|
||||
if (!customer) {
|
||||
console.error(`Error: No Stripe customer found for email ${email} with $200 subscription`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const customerID = customer.id
|
||||
const subscription = customer.subscriptions!.data[0]
|
||||
const subscriptionID = subscription.id
|
||||
|
||||
// Validate the subscription is $200
|
||||
@@ -90,29 +90,21 @@ const paymentMethod = paymentMethodID ? await Billing.stripe().paymentMethods.re
|
||||
const paymentMethodLast4 = paymentMethod?.card?.last4 ?? null
|
||||
const paymentMethodType = paymentMethod?.type ?? null
|
||||
|
||||
// Look up the user by email via AuthTable
|
||||
const auth = await Database.use((tx) =>
|
||||
// Look up the user in the workspace
|
||||
const users = await Database.use((tx) =>
|
||||
tx
|
||||
.select({ accountID: AuthTable.accountID })
|
||||
.from(AuthTable)
|
||||
.where(and(eq(AuthTable.provider, "email"), eq(AuthTable.subject, email)))
|
||||
.then((rows) => rows[0]),
|
||||
.select({ id: UserTable.id, email: AuthTable.subject })
|
||||
.from(UserTable)
|
||||
.innerJoin(AuthTable, and(eq(AuthTable.accountID, UserTable.accountID), eq(AuthTable.provider, "email")))
|
||||
.where(and(eq(UserTable.workspaceID, workspaceID), isNull(UserTable.timeDeleted))),
|
||||
)
|
||||
if (!auth) {
|
||||
console.error(`Error: No user found with email ${email}`)
|
||||
if (users.length === 0) {
|
||||
console.error(`Error: No users found in workspace ${workspaceID}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 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]),
|
||||
)
|
||||
const user = users.length === 1 ? users[0] : users.find((u) => u.email === email)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -136,13 +128,12 @@ await Database.transaction(async (tx) => {
|
||||
})
|
||||
.where(eq(BillingTable.workspaceID, workspaceID))
|
||||
|
||||
// Set current time as timeSubscribed on user
|
||||
await tx
|
||||
.update(UserTable)
|
||||
.set({
|
||||
timeSubscribed: sql`now()`,
|
||||
})
|
||||
.where(eq(UserTable.id, user.id))
|
||||
// Create a row in subscription table
|
||||
await tx.insert(SubscriptionTable).values({
|
||||
workspaceID,
|
||||
id: Identifier.create("subscription"),
|
||||
userID: user.id,
|
||||
})
|
||||
|
||||
// Create a row in payments table
|
||||
await tx.insert(PaymentTable).values({
|
||||
|
||||
@@ -4,9 +4,9 @@ import { Resource } from "@opencode-ai/console-resource"
|
||||
|
||||
export namespace BlackData {
|
||||
const Schema = z.object({
|
||||
monthlyLimit: z.number().int(),
|
||||
intervalLimit: z.number().int(),
|
||||
intervalLength: z.number().int(),
|
||||
fixedLimit: z.number().int(),
|
||||
rollingLimit: z.number().int(),
|
||||
rollingWindow: z.number().int(),
|
||||
})
|
||||
|
||||
export const validate = fn(Schema, (input) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export namespace Identifier {
|
||||
model: "mod",
|
||||
payment: "pay",
|
||||
provider: "prv",
|
||||
subscription: "sub",
|
||||
usage: "usg",
|
||||
user: "usr",
|
||||
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(
|
||||
"payment",
|
||||
{
|
||||
|
||||
@@ -18,12 +18,6 @@ export const UserTable = mysqlTable(
|
||||
monthlyLimit: int("monthly_limit"),
|
||||
monthlyUsage: bigint("monthly_usage", { mode: "number" }),
|
||||
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) => [
|
||||
...workspaceIndexes(table),
|
||||
|
||||
9
packages/console/core/src/util/date.ts
Normal file
9
packages/console/core/src/util/date.ts
Normal 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 }
|
||||
}
|
||||
Reference in New Issue
Block a user