wip: zen black
This commit is contained in:
@@ -101,15 +101,26 @@ export const stripeWebhook = new stripe.WebhookEndpoint("StripeWebhookEndpoint",
|
|||||||
const zenProduct = new stripe.Product("ZenBlack", {
|
const zenProduct = new stripe.Product("ZenBlack", {
|
||||||
name: "OpenCode Black",
|
name: "OpenCode Black",
|
||||||
})
|
})
|
||||||
const zenPrice = new stripe.Price("ZenBlackPrice", {
|
const zenPriceProps = {
|
||||||
product: zenProduct.id,
|
product: zenProduct.id,
|
||||||
unitAmount: 20000,
|
|
||||||
currency: "usd",
|
currency: "usd",
|
||||||
recurring: {
|
recurring: {
|
||||||
interval: "month",
|
interval: "month",
|
||||||
intervalCount: 1,
|
intervalCount: 1,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
const zenPrice200 = new stripe.Price("ZenBlackPrice", { ...zenPriceProps, unitAmount: 20000, })
|
||||||
|
const zenPrice100 = new stripe.Price("ZenBlack100Price", { ...zenPriceProps, unitAmount: 10000, })
|
||||||
|
const zenPrice20 = new stripe.Price("ZenBlack20Price", { ...zenPriceProps, unitAmount: 2000, })
|
||||||
|
const ZEN_BLACK_PRICE = new sst.Linkable("ZEN_BLACK_PRICE", {
|
||||||
|
properties: {
|
||||||
|
product: zenProduct.id,
|
||||||
|
plan200: zenPrice200.id,
|
||||||
|
plan100: zenPrice100.id,
|
||||||
|
plan20: zenPrice20.id,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
const ZEN_BLACK_LIMITS = new sst.Secret("ZEN_BLACK_LIMITS")
|
||||||
|
|
||||||
const ZEN_MODELS = [
|
const ZEN_MODELS = [
|
||||||
new sst.Secret("ZEN_MODELS1"),
|
new sst.Secret("ZEN_MODELS1"),
|
||||||
@@ -121,7 +132,6 @@ const ZEN_MODELS = [
|
|||||||
new sst.Secret("ZEN_MODELS7"),
|
new sst.Secret("ZEN_MODELS7"),
|
||||||
new sst.Secret("ZEN_MODELS8"),
|
new sst.Secret("ZEN_MODELS8"),
|
||||||
]
|
]
|
||||||
const ZEN_BLACK = new sst.Secret("ZEN_BLACK")
|
|
||||||
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
const STRIPE_SECRET_KEY = new sst.Secret("STRIPE_SECRET_KEY")
|
||||||
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
const STRIPE_PUBLISHABLE_KEY = new sst.Secret("STRIPE_PUBLISHABLE_KEY")
|
||||||
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
const AUTH_API_URL = new sst.Linkable("AUTH_API_URL", {
|
||||||
@@ -164,7 +174,8 @@ new sst.cloudflare.x.SolidStart("Console", {
|
|||||||
EMAILOCTOPUS_API_KEY,
|
EMAILOCTOPUS_API_KEY,
|
||||||
AWS_SES_ACCESS_KEY_ID,
|
AWS_SES_ACCESS_KEY_ID,
|
||||||
AWS_SES_SECRET_ACCESS_KEY,
|
AWS_SES_SECRET_ACCESS_KEY,
|
||||||
ZEN_BLACK,
|
ZEN_BLACK_PRICE,
|
||||||
|
ZEN_BLACK_LIMITS,
|
||||||
new sst.Secret("ZEN_SESSION_SECRET"),
|
new sst.Secret("ZEN_SESSION_SECRET"),
|
||||||
...ZEN_MODELS,
|
...ZEN_MODELS,
|
||||||
...($dev
|
...($dev
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"
|
|||||||
import { Show } from "solid-js"
|
import { Show } from "solid-js"
|
||||||
import { Billing } from "@opencode-ai/console-core/billing.js"
|
import { Billing } from "@opencode-ai/console-core/billing.js"
|
||||||
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
import { Database, eq, and, isNull } from "@opencode-ai/console-core/drizzle/index.js"
|
||||||
import { SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
import { BillingTable, SubscriptionTable } from "@opencode-ai/console-core/schema/billing.sql.js"
|
||||||
import { Actor } from "@opencode-ai/console-core/actor.js"
|
import { Actor } from "@opencode-ai/console-core/actor.js"
|
||||||
import { Black } from "@opencode-ai/console-core/black.js"
|
import { Black } from "@opencode-ai/console-core/black.js"
|
||||||
import { withActor } from "~/context/auth.withActor"
|
import { withActor } from "~/context/auth.withActor"
|
||||||
@@ -20,19 +20,24 @@ const querySubscription = query(async (workspaceID: string) => {
|
|||||||
fixedUsage: SubscriptionTable.fixedUsage,
|
fixedUsage: SubscriptionTable.fixedUsage,
|
||||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||||
|
subscription: BillingTable.subscription,
|
||||||
})
|
})
|
||||||
.from(SubscriptionTable)
|
.from(BillingTable)
|
||||||
|
.innerJoin(SubscriptionTable, eq(SubscriptionTable.workspaceID, BillingTable.workspaceID))
|
||||||
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
|
.where(and(eq(SubscriptionTable.workspaceID, Actor.workspace()), isNull(SubscriptionTable.timeDeleted)))
|
||||||
.then((r) => r[0]),
|
.then((r) => r[0]),
|
||||||
)
|
)
|
||||||
if (!row) return null
|
if (!row.subscription) return null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
plan: row.subscription.plan,
|
||||||
rollingUsage: Black.analyzeRollingUsage({
|
rollingUsage: Black.analyzeRollingUsage({
|
||||||
|
plan: row.subscription.plan,
|
||||||
usage: row.rollingUsage ?? 0,
|
usage: row.rollingUsage ?? 0,
|
||||||
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
timeUpdated: row.timeRollingUpdated ?? new Date(),
|
||||||
}),
|
}),
|
||||||
weeklyUsage: Black.analyzeWeeklyUsage({
|
weeklyUsage: Black.analyzeWeeklyUsage({
|
||||||
|
plan: row.subscription.plan,
|
||||||
usage: row.fixedUsage ?? 0,
|
usage: row.fixedUsage ?? 0,
|
||||||
timeUpdated: row.timeFixedUpdated ?? new Date(),
|
timeUpdated: row.timeFixedUpdated ?? new Date(),
|
||||||
}),
|
}),
|
||||||
@@ -89,10 +94,13 @@ export function BlackSection() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section class={styles.root}>
|
<section class={styles.root}>
|
||||||
|
<Show when={subscription()}>
|
||||||
|
{(sub) => (
|
||||||
|
<>
|
||||||
<div data-slot="section-title">
|
<div data-slot="section-title">
|
||||||
<h2>Subscription</h2>
|
<h2>Subscription</h2>
|
||||||
<div data-slot="title-row">
|
<div data-slot="title-row">
|
||||||
<p>You are subscribed to OpenCode Black for $200 per month.</p>
|
<p>You are subscribed to OpenCode Black for ${sub().plan} per month.</p>
|
||||||
<button
|
<button
|
||||||
data-color="primary"
|
data-color="primary"
|
||||||
disabled={sessionSubmission.pending || store.sessionRedirecting}
|
disabled={sessionSubmission.pending || store.sessionRedirecting}
|
||||||
@@ -102,8 +110,6 @@ export function BlackSection() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={subscription()}>
|
|
||||||
{(sub) => (
|
|
||||||
<div data-slot="usage">
|
<div data-slot="usage">
|
||||||
<div data-slot="usage-item">
|
<div data-slot="usage-item">
|
||||||
<div data-slot="usage-header">
|
<div data-slot="usage-header">
|
||||||
@@ -126,6 +132,7 @@ export function BlackSection() {
|
|||||||
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
<span data-slot="reset-time">Resets in {formatResetTime(sub().weeklyUsage.resetInSec)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default function () {
|
|||||||
<div data-page="workspace-[id]">
|
<div data-page="workspace-[id]">
|
||||||
<div data-slot="sections">
|
<div data-slot="sections">
|
||||||
<Show when={sessionInfo()?.isAdmin}>
|
<Show when={sessionInfo()?.isAdmin}>
|
||||||
<Show when={billingInfo()?.subscriptionID}>
|
<Show when={billingInfo()?.subscriptionID || billingInfo()?.timeSubscriptionBooked}>
|
||||||
<BlackSection />
|
<BlackSection />
|
||||||
</Show>
|
</Show>
|
||||||
<BillingSection />
|
<BillingSection />
|
||||||
|
|||||||
@@ -111,6 +111,8 @@ export const queryBillingInfo = query(async (workspaceID: string) => {
|
|||||||
reloadError: billing.reloadError,
|
reloadError: billing.reloadError,
|
||||||
timeReloadError: billing.timeReloadError,
|
timeReloadError: billing.timeReloadError,
|
||||||
subscriptionID: billing.subscriptionID,
|
subscriptionID: billing.subscriptionID,
|
||||||
|
subscriptionPlan: billing.subscriptionPlan,
|
||||||
|
timeSubscriptionBooked: billing.timeSubscriptionBooked,
|
||||||
}
|
}
|
||||||
}, workspaceID)
|
}, workspaceID)
|
||||||
}, "billing.get")
|
}, "billing.get")
|
||||||
|
|||||||
@@ -417,6 +417,7 @@ export async function handler(
|
|||||||
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
timeMonthlyUsageUpdated: BillingTable.timeMonthlyUsageUpdated,
|
||||||
reloadTrigger: BillingTable.reloadTrigger,
|
reloadTrigger: BillingTable.reloadTrigger,
|
||||||
timeReloadLockedTill: BillingTable.timeReloadLockedTill,
|
timeReloadLockedTill: BillingTable.timeReloadLockedTill,
|
||||||
|
subscription: BillingTable.subscription,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
id: UserTable.id,
|
id: UserTable.id,
|
||||||
@@ -488,10 +489,9 @@ export async function handler(
|
|||||||
if (modelInfo.allowAnonymous) return
|
if (modelInfo.allowAnonymous) return
|
||||||
|
|
||||||
// Validate subscription billing
|
// Validate subscription billing
|
||||||
if (authInfo.subscription) {
|
if (authInfo.billing.subscription && authInfo.subscription) {
|
||||||
const black = BlackData.get()
|
|
||||||
const sub = authInfo.subscription
|
const sub = authInfo.subscription
|
||||||
const now = new Date()
|
const plan = authInfo.billing.subscription.plan
|
||||||
|
|
||||||
const formatRetryTime = (seconds: number) => {
|
const formatRetryTime = (seconds: number) => {
|
||||||
const days = Math.floor(seconds / 86400)
|
const days = Math.floor(seconds / 86400)
|
||||||
@@ -505,6 +505,7 @@ export async function handler(
|
|||||||
// Check weekly limit
|
// Check weekly limit
|
||||||
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
if (sub.fixedUsage && sub.timeFixedUpdated) {
|
||||||
const result = Black.analyzeWeeklyUsage({
|
const result = Black.analyzeWeeklyUsage({
|
||||||
|
plan,
|
||||||
usage: sub.fixedUsage,
|
usage: sub.fixedUsage,
|
||||||
timeUpdated: sub.timeFixedUpdated,
|
timeUpdated: sub.timeFixedUpdated,
|
||||||
})
|
})
|
||||||
@@ -518,6 +519,7 @@ export async function handler(
|
|||||||
// Check rolling limit
|
// Check rolling limit
|
||||||
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
if (sub.rollingUsage && sub.timeRollingUpdated) {
|
||||||
const result = Black.analyzeRollingUsage({
|
const result = Black.analyzeRollingUsage({
|
||||||
|
plan,
|
||||||
usage: sub.rollingUsage,
|
usage: sub.rollingUsage,
|
||||||
timeUpdated: sub.timeRollingUpdated,
|
timeUpdated: sub.timeRollingUpdated,
|
||||||
})
|
})
|
||||||
@@ -666,7 +668,8 @@ 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 black = BlackData.get()
|
const plan = authInfo.billing.subscription!.plan
|
||||||
|
const black = BlackData.get({ plan })
|
||||||
const week = getWeekBounds(new Date())
|
const week = getWeekBounds(new Date())
|
||||||
const rollingWindowSeconds = black.rollingWindow * 3600
|
const rollingWindowSeconds = black.rollingWindow * 3600
|
||||||
return [
|
return [
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Database, and, eq, sql } 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, SubscriptionTable, UsageTable } from "../src/schema/billing.sql.js"
|
import { BillingTable, PaymentTable, SubscriptionTable, SubscriptionPlan, 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 { BlackData } from "../src/black.js"
|
||||||
import { centsToMicroCents } from "../src/util/price.js"
|
import { centsToMicroCents } from "../src/util/price.js"
|
||||||
@@ -86,8 +86,10 @@ async function printWorkspace(workspaceID: string) {
|
|||||||
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
timeFixedUpdated: SubscriptionTable.timeFixedUpdated,
|
||||||
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
timeRollingUpdated: SubscriptionTable.timeRollingUpdated,
|
||||||
timeSubscriptionCreated: SubscriptionTable.timeCreated,
|
timeSubscriptionCreated: SubscriptionTable.timeCreated,
|
||||||
|
subscription: BillingTable.subscription,
|
||||||
})
|
})
|
||||||
.from(UserTable)
|
.from(UserTable)
|
||||||
|
.innerJoin(BillingTable, eq(BillingTable.workspaceID, workspace.id))
|
||||||
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
.leftJoin(AuthTable, and(eq(UserTable.accountID, AuthTable.accountID), eq(AuthTable.provider, "email")))
|
||||||
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
|
.leftJoin(SubscriptionTable, eq(SubscriptionTable.userID, UserTable.id))
|
||||||
.where(eq(UserTable.workspaceID, workspace.id))
|
.where(eq(UserTable.workspaceID, workspace.id))
|
||||||
@@ -223,17 +225,20 @@ function formatRetryTime(seconds: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSubscriptionStatus(row: {
|
function getSubscriptionStatus(row: {
|
||||||
|
subscription: {
|
||||||
|
plan: typeof SubscriptionPlan[number]
|
||||||
|
} | null
|
||||||
timeSubscriptionCreated: Date | null
|
timeSubscriptionCreated: Date | null
|
||||||
fixedUsage: number | null
|
fixedUsage: number | null
|
||||||
rollingUsage: number | null
|
rollingUsage: number | null
|
||||||
timeFixedUpdated: Date | null
|
timeFixedUpdated: Date | null
|
||||||
timeRollingUpdated: Date | null
|
timeRollingUpdated: Date | null
|
||||||
}) {
|
}) {
|
||||||
if (!row.timeSubscriptionCreated) {
|
if (!row.timeSubscriptionCreated || !row.subscription) {
|
||||||
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
|
return { weekly: null, rolling: null, rateLimited: null, retryIn: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
const black = BlackData.get()
|
const black = BlackData.get({ plan: row.subscription.plan })
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const week = getWeekBounds(now)
|
const week = getWeekBounds(now)
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ const root = path.resolve(process.cwd(), "..", "..", "..")
|
|||||||
// read the secret
|
// read the secret
|
||||||
const ret = await $`bun sst secret list`.cwd(root).text()
|
const ret = await $`bun sst secret list`.cwd(root).text()
|
||||||
const lines = ret.split("\n")
|
const lines = ret.split("\n")
|
||||||
const value = lines.find((line) => line.startsWith("ZEN_BLACK"))?.split("=")[1]
|
const value = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1]
|
||||||
if (!value) throw new Error("ZEN_BLACK not found")
|
if (!value) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||||
|
|
||||||
// validate value
|
// validate value
|
||||||
BlackData.validate(JSON.parse(value))
|
BlackData.validate(JSON.parse(value))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_BLACK ${value} --stage ${stage}`
|
await $`bun sst secret set ZEN_BLACK_LIMITS ${value} --stage ${stage}`
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import { BlackData } from "../src/black"
|
|||||||
const root = path.resolve(process.cwd(), "..", "..", "..")
|
const root = path.resolve(process.cwd(), "..", "..", "..")
|
||||||
const secrets = await $`bun sst secret list`.cwd(root).text()
|
const secrets = await $`bun sst secret list`.cwd(root).text()
|
||||||
|
|
||||||
// read the line starting with "ZEN_BLACK"
|
// read value
|
||||||
const lines = secrets.split("\n")
|
const lines = secrets.split("\n")
|
||||||
const oldValue = lines.find((line) => line.startsWith("ZEN_BLACK"))?.split("=")[1]
|
const oldValue = lines.find((line) => line.startsWith("ZEN_BLACK_LIMITS"))?.split("=")[1] ?? "{}"
|
||||||
if (!oldValue) throw new Error("ZEN_BLACK not found")
|
if (!oldValue) throw new Error("ZEN_BLACK_LIMITS not found")
|
||||||
|
|
||||||
// store the prettified json to a temp file
|
// store the prettified json to a temp file
|
||||||
const filename = `black-${Date.now()}.json`
|
const filename = `black-${Date.now()}.json`
|
||||||
@@ -25,4 +25,4 @@ const newValue = JSON.stringify(JSON.parse(await tempFile.text()))
|
|||||||
BlackData.validate(JSON.parse(newValue))
|
BlackData.validate(JSON.parse(newValue))
|
||||||
|
|
||||||
// update the secret
|
// update the secret
|
||||||
await $`bun sst secret set ZEN_BLACK ${newValue}`
|
await $`bun sst secret set ZEN_BLACK_LIMITS ${newValue}`
|
||||||
|
|||||||
@@ -3,33 +3,49 @@ import { fn } from "./util/fn"
|
|||||||
import { Resource } from "@opencode-ai/console-resource"
|
import { Resource } from "@opencode-ai/console-resource"
|
||||||
import { centsToMicroCents } from "./util/price"
|
import { centsToMicroCents } from "./util/price"
|
||||||
import { getWeekBounds } from "./util/date"
|
import { getWeekBounds } from "./util/date"
|
||||||
|
import { SubscriptionPlan } from "./schema/billing.sql"
|
||||||
|
|
||||||
export namespace BlackData {
|
export namespace BlackData {
|
||||||
const Schema = z.object({
|
const Schema = z.object({
|
||||||
fixedLimit: z.number().int(),
|
"200": z.object({
|
||||||
rollingLimit: z.number().int(),
|
fixedLimit: z.number().int(),
|
||||||
rollingWindow: z.number().int(),
|
rollingLimit: z.number().int(),
|
||||||
|
rollingWindow: z.number().int(),
|
||||||
|
}),
|
||||||
|
"100": z.object({
|
||||||
|
fixedLimit: z.number().int(),
|
||||||
|
rollingLimit: z.number().int(),
|
||||||
|
rollingWindow: z.number().int(),
|
||||||
|
}),
|
||||||
|
"20": z.object({
|
||||||
|
fixedLimit: z.number().int(),
|
||||||
|
rollingLimit: z.number().int(),
|
||||||
|
rollingWindow: z.number().int(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const validate = fn(Schema, (input) => {
|
export const validate = fn(Schema, (input) => {
|
||||||
return input
|
return input
|
||||||
})
|
})
|
||||||
|
|
||||||
export const get = fn(z.void(), () => {
|
export const get = fn(z.object({
|
||||||
const json = JSON.parse(Resource.ZEN_BLACK.value)
|
plan: z.enum(SubscriptionPlan),
|
||||||
return Schema.parse(json)
|
}), ({ plan }) => {
|
||||||
|
const json = JSON.parse(Resource.ZEN_BLACK_LIMITS.value)
|
||||||
|
return Schema.parse(json)[plan]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Black {
|
export namespace Black {
|
||||||
export const analyzeRollingUsage = fn(
|
export const analyzeRollingUsage = fn(
|
||||||
z.object({
|
z.object({
|
||||||
|
plan: z.enum(SubscriptionPlan),
|
||||||
usage: z.number().int(),
|
usage: z.number().int(),
|
||||||
timeUpdated: z.date(),
|
timeUpdated: z.date(),
|
||||||
}),
|
}),
|
||||||
({ usage, timeUpdated }) => {
|
({ plan, usage, timeUpdated }) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const black = BlackData.get()
|
const black = BlackData.get({ plan })
|
||||||
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
const rollingWindowMs = black.rollingWindow * 3600 * 1000
|
||||||
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
|
const rollingLimitInMicroCents = centsToMicroCents(black.rollingLimit * 100)
|
||||||
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
const windowStart = new Date(now.getTime() - rollingWindowMs)
|
||||||
@@ -59,11 +75,12 @@ export namespace Black {
|
|||||||
|
|
||||||
export const analyzeWeeklyUsage = fn(
|
export const analyzeWeeklyUsage = fn(
|
||||||
z.object({
|
z.object({
|
||||||
|
plan:z.enum(SubscriptionPlan),
|
||||||
usage: z.number().int(),
|
usage: z.number().int(),
|
||||||
timeUpdated: z.date(),
|
timeUpdated: z.date(),
|
||||||
}),
|
}),
|
||||||
({ usage, timeUpdated }) => {
|
({ plan, usage, timeUpdated }) => {
|
||||||
const black = BlackData.get()
|
const black = BlackData.get({ plan })
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const week = getWeekBounds(now)
|
const week = getWeekBounds(now)
|
||||||
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
|
const fixedLimitInMicroCents = centsToMicroCents(black.fixedLimit * 100)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { bigint, boolean, index, int, json, mysqlEnum, mysqlTable, uniqueIndex,
|
|||||||
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
import { timestamps, ulid, utc, workspaceColumns } from "../drizzle/types"
|
||||||
import { workspaceIndexes } from "./workspace.sql"
|
import { workspaceIndexes } from "./workspace.sql"
|
||||||
|
|
||||||
|
export const SubscriptionPlan = ["20", "100", "200"] as const
|
||||||
export const BillingTable = mysqlTable(
|
export const BillingTable = mysqlTable(
|
||||||
"billing",
|
"billing",
|
||||||
{
|
{
|
||||||
@@ -28,7 +29,7 @@ export const BillingTable = mysqlTable(
|
|||||||
plan: "20" | "100" | "200"
|
plan: "20" | "100" | "200"
|
||||||
}>(),
|
}>(),
|
||||||
subscriptionID: varchar("subscription_id", { length: 28 }),
|
subscriptionID: varchar("subscription_id", { length: 28 }),
|
||||||
subscriptionPlan: mysqlEnum("subscription_plan", ["20", "100", "200"] as const),
|
subscriptionPlan: mysqlEnum("subscription_plan", SubscriptionPlan),
|
||||||
timeSubscriptionBooked: utc("time_subscription_booked"),
|
timeSubscriptionBooked: utc("time_subscription_booked"),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
|
|||||||
9
packages/console/core/sst-env.d.ts
vendored
9
packages/console/core/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
9
packages/console/function/sst-env.d.ts
vendored
9
packages/console/function/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
9
packages/console/resource/sst-env.d.ts
vendored
9
packages/console/resource/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
9
packages/enterprise/sst-env.d.ts
vendored
9
packages/enterprise/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
9
packages/function/sst-env.d.ts
vendored
9
packages/function/sst-env.d.ts
vendored
@@ -118,10 +118,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
9
sst-env.d.ts
vendored
9
sst-env.d.ts
vendored
@@ -144,10 +144,17 @@ declare module "sst" {
|
|||||||
"type": "sst.cloudflare.StaticSite"
|
"type": "sst.cloudflare.StaticSite"
|
||||||
"url": string
|
"url": string
|
||||||
}
|
}
|
||||||
"ZEN_BLACK": {
|
"ZEN_BLACK_LIMITS": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
}
|
}
|
||||||
|
"ZEN_BLACK_PRICE": {
|
||||||
|
"plan100": string
|
||||||
|
"plan20": string
|
||||||
|
"plan200": string
|
||||||
|
"product": string
|
||||||
|
"type": "sst.sst.Linkable"
|
||||||
|
}
|
||||||
"ZEN_MODELS1": {
|
"ZEN_MODELS1": {
|
||||||
"type": "sst.sst.Secret"
|
"type": "sst.sst.Secret"
|
||||||
"value": string
|
"value": string
|
||||||
|
|||||||
Reference in New Issue
Block a user