Skip to content

Commit a32bedb

Browse files
committed
wip: zen
1 parent cf97633 commit a32bedb

File tree

17 files changed

+4222
-106
lines changed

17 files changed

+4222
-106
lines changed

packages/console/app/src/routes/stripe/webhook.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { Billing } from "@opencode-ai/console-core/billing.js"
22
import type { APIEvent } from "@solidjs/start/server"
33
import { and, Database, eq, sql } from "@opencode-ai/console-core/drizzle/index.js"
4-
import { BillingTable, PaymentTable } from "@opencode-ai/console-core/schema/billing.sql.js"
5-
import { UserTable } from "@opencode-ai/console-core/schema/user.sql.js"
4+
import { BillingTable, PaymentTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
65
import { Identifier } from "@opencode-ai/console-core/identifier.js"
76
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
87
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -380,7 +379,7 @@ export async function POST(input: APIEvent) {
380379
await Database.transaction(async (tx) => {
381380
await tx.update(BillingTable).set({ subscriptionID: null }).where(eq(BillingTable.workspaceID, workspaceID))
382381

383-
await tx.update(UserTable).set({ timeSubscribed: null }).where(eq(UserTable.workspaceID, workspaceID))
382+
await tx.delete(SubscriptionTable).where(eq(SubscriptionTable.workspaceID, workspaceID))
384383
})
385384
}
386385
})()

packages/console/app/src/routes/zen/util/handler.ts

Lines changed: 44 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import type { APIEvent } from "@solidjs/start/server"
22
import { and, Database, eq, isNull, lt, or, sql } from "@opencode-ai/console-core/drizzle/index.js"
33
import { KeyTable } from "@opencode-ai/console-core/schema/key.sql.js"
4-
import { BillingTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
4+
import { BillingTable, SubscriptionTable, UsageTable } from "@opencode-ai/console-core/schema/billing.sql.js"
55
import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
6+
import { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
67
import { Identifier } from "@opencode-ai/console-core/identifier.js"
78
import { Billing } from "@opencode-ai/console-core/billing.js"
89
import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -415,11 +416,11 @@ export async function handler(
415416
timeMonthlyUsageUpdated: UserTable.timeMonthlyUsageUpdated,
416417
},
417418
subscription: {
418-
timeSubscribed: UserTable.timeSubscribed,
419-
subIntervalUsage: UserTable.subIntervalUsage,
420-
subMonthlyUsage: UserTable.subMonthlyUsage,
421-
timeSubIntervalUsageUpdated: UserTable.timeSubIntervalUsageUpdated,
422-
timeSubMonthlyUsageUpdated: UserTable.timeSubMonthlyUsageUpdated,
419+
id: SubscriptionTable.id,
420+
rollingUsage: SubscriptionTable.rollingUsage,
421+
fixedUsage: SubscriptionTable.fixedUsage,
422+
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
423+
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
423424
},
424425
provider: {
425426
credentials: ProviderTable.credentials,
@@ -440,6 +441,14 @@ export async function handler(
440441
)
441442
: sql`false`,
442443
)
444+
.leftJoin(
445+
SubscriptionTable,
446+
and(
447+
eq(SubscriptionTable.workspaceID, KeyTable.workspaceID),
448+
eq(SubscriptionTable.userID, KeyTable.userID),
449+
isNull(SubscriptionTable.timeDeleted),
450+
),
451+
)
443452
.where(and(eq(KeyTable.key, apiKey), isNull(KeyTable.timeDeleted)))
444453
.then((rows) => rows[0]),
445454
)
@@ -448,15 +457,15 @@ export async function handler(
448457
logger.metric({
449458
api_key: data.apiKey,
450459
workspace: data.workspaceID,
451-
isSubscription: data.subscription.timeSubscribed ? true : false,
460+
isSubscription: data.subscription ? true : false,
452461
})
453462

454463
return {
455464
apiKeyId: data.apiKey,
456465
workspaceID: data.workspaceID,
457466
billing: data.billing,
458467
user: data.user,
459-
subscription: data.subscription.timeSubscribed ? data.subscription : undefined,
468+
subscription: data.subscription,
460469
provider: data.provider,
461470
isFree: FREE_WORKSPACES.includes(data.workspaceID),
462471
isDisabled: !!data.timeDisabled,
@@ -484,38 +493,24 @@ export async function handler(
484493
return `${minutes}min`
485494
}
486495

487-
// Check monthly limit (based on subscription billing cycle)
488-
if (
489-
sub.subMonthlyUsage &&
490-
sub.timeSubMonthlyUsageUpdated &&
491-
sub.subMonthlyUsage >= centsToMicroCents(black.monthlyLimit * 100)
492-
) {
493-
const subscribeDay = sub.timeSubscribed!.getUTCDate()
494-
const cycleStart = new Date(
495-
Date.UTC(
496-
now.getUTCFullYear(),
497-
now.getUTCDate() >= subscribeDay ? now.getUTCMonth() : now.getUTCMonth() - 1,
498-
subscribeDay,
499-
),
500-
)
501-
const cycleEnd = new Date(Date.UTC(cycleStart.getUTCFullYear(), cycleStart.getUTCMonth() + 1, subscribeDay))
502-
if (sub.timeSubMonthlyUsageUpdated >= cycleStart && sub.timeSubMonthlyUsageUpdated < cycleEnd) {
503-
const retryAfter = Math.ceil((cycleEnd.getTime() - now.getTime()) / 1000)
496+
// Check weekly limit
497+
if (sub.fixedUsage && sub.timeFixedUpdated) {
498+
const week = getWeekBounds(now)
499+
if (sub.timeFixedUpdated >= week.start && sub.fixedUsage >= centsToMicroCents(black.fixedLimit * 100)) {
500+
const retryAfter = Math.ceil((week.end.getTime() - now.getTime()) / 1000)
504501
throw new SubscriptionError(
505502
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
506503
retryAfter,
507504
)
508505
}
509506
}
510507

511-
// Check interval limit
512-
const intervalMs = black.intervalLength * 3600 * 1000
513-
if (sub.subIntervalUsage && sub.timeSubIntervalUsageUpdated) {
514-
const currentInterval = Math.floor(now.getTime() / intervalMs)
515-
const usageInterval = Math.floor(sub.timeSubIntervalUsageUpdated.getTime() / intervalMs)
516-
if (currentInterval === usageInterval && sub.subIntervalUsage >= centsToMicroCents(black.intervalLimit * 100)) {
517-
const nextInterval = (currentInterval + 1) * intervalMs
518-
const retryAfter = Math.ceil((nextInterval - now.getTime()) / 1000)
508+
// Check rolling limit
509+
if (sub.rollingUsage && sub.timeRollingUpdated) {
510+
const rollingWindowMs = black.rollingWindow * 3600 * 1000
511+
const windowStart = new Date(now.getTime() - rollingWindowMs)
512+
if (sub.timeRollingUpdated >= windowStart && sub.rollingUsage >= centsToMicroCents(black.rollingLimit * 100)) {
513+
const retryAfter = Math.ceil((sub.timeRollingUpdated.getTime() + rollingWindowMs - now.getTime()) / 1000)
519514
throw new SubscriptionError(
520515
`Subscription quota exceeded. Retry in ${formatRetryTime(retryAfter)}.`,
521516
retryAfter,
@@ -661,38 +656,34 @@ export async function handler(
661656
.where(and(eq(KeyTable.workspaceID, authInfo.workspaceID), eq(KeyTable.id, authInfo.apiKeyId))),
662657
...(authInfo.subscription
663658
? (() => {
664-
const now = new Date()
665-
const subscribeDay = authInfo.subscription.timeSubscribed!.getUTCDate()
666-
const cycleStart = new Date(
667-
Date.UTC(
668-
now.getUTCFullYear(),
669-
now.getUTCDate() >= subscribeDay ? now.getUTCMonth() : now.getUTCMonth() - 1,
670-
subscribeDay,
671-
),
672-
)
673-
const cycleEnd = new Date(
674-
Date.UTC(cycleStart.getUTCFullYear(), cycleStart.getUTCMonth() + 1, subscribeDay),
675-
)
659+
const black = BlackData.get()
660+
const week = getWeekBounds(new Date())
661+
const rollingWindowSeconds = black.rollingWindow * 3600
676662
return [
677663
db
678-
.update(UserTable)
664+
.update(SubscriptionTable)
679665
.set({
680-
subMonthlyUsage: sql`
666+
fixedUsage: sql`
681667
CASE
682-
WHEN ${UserTable.timeSubMonthlyUsageUpdated} >= ${cycleStart} AND ${UserTable.timeSubMonthlyUsageUpdated} < ${cycleEnd} THEN ${UserTable.subMonthlyUsage} + ${cost}
668+
WHEN ${SubscriptionTable.timeFixedUpdated} >= ${week.start} THEN ${SubscriptionTable.fixedUsage} + ${cost}
683669
ELSE ${cost}
684670
END
685671
`,
686-
timeSubMonthlyUsageUpdated: sql`now()`,
687-
subIntervalUsage: sql`
672+
timeFixedUpdated: sql`now()`,
673+
rollingUsage: sql`
688674
CASE
689-
WHEN FLOOR(UNIX_TIMESTAMP(${UserTable.timeSubIntervalUsageUpdated}) / (${BlackData.get().intervalLength} * 3600)) = FLOOR(UNIX_TIMESTAMP(now()) / (${BlackData.get().intervalLength} * 3600)) THEN ${UserTable.subIntervalUsage} + ${cost}
675+
WHEN UNIX_TIMESTAMP(${SubscriptionTable.timeRollingUpdated}) >= UNIX_TIMESTAMP(now()) - ${rollingWindowSeconds} THEN ${SubscriptionTable.rollingUsage} + ${cost}
690676
ELSE ${cost}
691677
END
692678
`,
693-
timeSubIntervalUsageUpdated: sql`now()`,
679+
timeRollingUpdated: sql`now()`,
694680
})
695-
.where(and(eq(UserTable.workspaceID, authInfo.workspaceID), eq(UserTable.id, authInfo.user.id))),
681+
.where(
682+
and(
683+
eq(SubscriptionTable.workspaceID, authInfo.workspaceID),
684+
eq(SubscriptionTable.userID, authInfo.user.id),
685+
),
686+
),
696687
]
697688
})()
698689
: [
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
CREATE TABLE `subscription` (
2+
`id` varchar(30) NOT NULL,
3+
`workspace_id` varchar(30) NOT NULL,
4+
`time_created` timestamp(3) NOT NULL DEFAULT (now()),
5+
`time_updated` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
6+
`time_deleted` timestamp(3),
7+
`user_id` varchar(30) NOT NULL,
8+
`rolling_usage` bigint,
9+
`fixed_usage` bigint,
10+
`time_rolling_updated` timestamp(3),
11+
`time_fixed_updated` timestamp(3),
12+
CONSTRAINT `subscription_workspace_id_id_pk` PRIMARY KEY(`workspace_id`,`id`)
13+
);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE INDEX `workspace_user_id` ON `subscription` (`workspace_id`,`user_id`);--> statement-breakpoint
2+
ALTER TABLE `user` DROP COLUMN `time_subscribed`;--> statement-breakpoint
3+
ALTER TABLE `user` DROP COLUMN `sub_interval_usage`;--> statement-breakpoint
4+
ALTER TABLE `user` DROP COLUMN `sub_monthly_usage`;--> statement-breakpoint
5+
ALTER TABLE `user` DROP COLUMN `sub_time_interval_usage_updated`;--> statement-breakpoint
6+
ALTER TABLE `user` DROP COLUMN `sub_time_monthly_usage_updated`;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
DROP INDEX `workspace_user_id` ON `subscription`;--> statement-breakpoint
2+
ALTER TABLE `subscription` ADD CONSTRAINT `workspace_user_id` UNIQUE(`workspace_id`,`user_id`);

0 commit comments

Comments
 (0)