11import type { APIEvent } from "@solidjs/start/server"
22import { and , Database , eq , isNull , lt , or , sql } from "@opencode-ai/console-core/drizzle/index.js"
33import { 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"
55import { centsToMicroCents } from "@opencode-ai/console-core/util/price.js"
6+ import { getWeekBounds } from "@opencode-ai/console-core/util/date.js"
67import { Identifier } from "@opencode-ai/console-core/identifier.js"
78import { Billing } from "@opencode-ai/console-core/billing.js"
89import { 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 : [
0 commit comments