kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
254 wiersze
7.2 KiB
TypeScript
254 wiersze
7.2 KiB
TypeScript
import {
|
|
type StripeSubscriptionItemIdMap,
|
|
stripeSubscriptionItemIdMapSchema
|
|
} from '@agentic/platform-types'
|
|
import { relations } from '@fisch0920/drizzle-orm'
|
|
import {
|
|
boolean,
|
|
index,
|
|
jsonb,
|
|
pgTable,
|
|
text
|
|
} from '@fisch0920/drizzle-orm/pg-core'
|
|
import { z } from '@hono/zod-openapi'
|
|
|
|
import { env } from '@/lib/env'
|
|
|
|
import {
|
|
consumerIdSchema,
|
|
deploymentIdSchema,
|
|
projectIdSchema,
|
|
userIdSchema
|
|
} from '../schemas'
|
|
import {
|
|
consumerPrimaryId,
|
|
createInsertSchema,
|
|
createSelectSchema,
|
|
createUpdateSchema,
|
|
deploymentId,
|
|
projectId,
|
|
stripeId,
|
|
timestamps,
|
|
userId
|
|
} from './common'
|
|
import { deployments, deploymentSelectSchema } from './deployment'
|
|
import { projects, projectSelectSchema } from './project'
|
|
import { users, userSelectSchema } from './user'
|
|
|
|
// TODO: Consumers should be valid for any enabled project like in RapidAPI and GCP.
|
|
// This may require a separate model to aggregate User Applications.
|
|
// https://docs.rapidapi.com/docs/keys#section-different-api-keys-per-application
|
|
|
|
/**
|
|
* A `Consumer` represents a user who has subscribed to a `Project` and is used
|
|
* to track usage and billing.
|
|
*
|
|
* Consumers are linked to a corresponding Stripe Customer and Subscription.
|
|
* The Stripe customer will either be the user's default Stripe Customer if the
|
|
* project uses the default Agentic platform account, or a customer on the project
|
|
* owner's connected Stripe account if the project has Stripe Connect enabled.
|
|
*/
|
|
export const consumers = pgTable(
|
|
'consumers',
|
|
{
|
|
...consumerPrimaryId,
|
|
...timestamps,
|
|
|
|
// API key for this consumer
|
|
// (called "token" for backwards compatibility)
|
|
token: text().notNull(),
|
|
|
|
// The slug of the PricingPlan in the target deployment that this consumer
|
|
// is subscribed to.
|
|
plan: text(),
|
|
|
|
// Whether the consumer has made at least one successful API call after
|
|
// initializing their subscription.
|
|
activated: boolean().default(false).notNull(),
|
|
|
|
// TODO: Re-add coupon support
|
|
// coupon: text(),
|
|
|
|
// only used during initial creation
|
|
source: text(),
|
|
|
|
userId: userId()
|
|
.notNull()
|
|
.references(() => users.id),
|
|
|
|
// The project this user is subscribed to
|
|
projectId: projectId()
|
|
.notNull()
|
|
.references(() => projects.id, {
|
|
onDelete: 'cascade'
|
|
}),
|
|
|
|
// The specific deployment this user is subscribed to, since pricing can
|
|
// change across deployment versions)
|
|
deploymentId: deploymentId()
|
|
.notNull()
|
|
.references(() => deployments.id, {
|
|
onDelete: 'cascade'
|
|
}),
|
|
|
|
// Stripe subscription status (synced via webhooks). Should move from
|
|
// `incomplete` to `active` after the first successful payment.
|
|
stripeStatus: text().default('incomplete').notNull(),
|
|
|
|
// Whether the consumer's subscription is currently active, depending on
|
|
// `stripeStatus`.
|
|
isStripeSubscriptionActive: boolean().default(true).notNull(),
|
|
|
|
// Main Stripe Subscription id
|
|
_stripeSubscriptionId: stripeId(),
|
|
|
|
// [pricingPlanLineItemSlug: string]: string
|
|
_stripeSubscriptionItemIdMap: jsonb()
|
|
.$type<StripeSubscriptionItemIdMap>()
|
|
.default({})
|
|
.notNull(),
|
|
|
|
// Denormalized from User or possibly separate for stripe connect
|
|
// TODO: is this necessary?
|
|
_stripeCustomerId: stripeId().notNull()
|
|
},
|
|
(table) => [
|
|
index('consumer_token_idx').on(table.token),
|
|
index('consumer_userId_idx').on(table.userId),
|
|
index('consumer_projectId_idx').on(table.projectId),
|
|
index('consumer_deploymentId_idx').on(table.deploymentId),
|
|
index('consumer_isStripeSubscriptionActive_idx').on(
|
|
table.isStripeSubscriptionActive
|
|
),
|
|
index('consumer_createdAt_idx').on(table.createdAt),
|
|
index('consumer_updatedAt_idx').on(table.updatedAt),
|
|
index('consumer_deletedAt_idx').on(table.deletedAt)
|
|
]
|
|
)
|
|
|
|
export const consumersRelations = relations(consumers, ({ one }) => ({
|
|
user: one(users, {
|
|
fields: [consumers.userId],
|
|
references: [users.id]
|
|
}),
|
|
project: one(projects, {
|
|
fields: [consumers.projectId],
|
|
references: [projects.id]
|
|
}),
|
|
deployment: one(deployments, {
|
|
fields: [consumers.deploymentId],
|
|
references: [deployments.id]
|
|
})
|
|
}))
|
|
|
|
export const consumerSelectBaseSchema = createSelectSchema(consumers, {
|
|
id: consumerIdSchema,
|
|
userId: userIdSchema,
|
|
projectId: projectIdSchema,
|
|
deploymentId: deploymentIdSchema,
|
|
|
|
_stripeSubscriptionItemIdMap: stripeSubscriptionItemIdMapSchema
|
|
})
|
|
.omit({
|
|
_stripeSubscriptionId: true,
|
|
_stripeSubscriptionItemIdMap: true,
|
|
_stripeCustomerId: true
|
|
})
|
|
.extend({
|
|
user: z
|
|
.lazy(() => userSelectSchema)
|
|
.optional()
|
|
.openapi('User', { type: 'object' }),
|
|
|
|
project: z
|
|
.lazy(() => projectSelectSchema)
|
|
.optional()
|
|
.openapi('Project', { type: 'object' }),
|
|
|
|
// deployment: z
|
|
// .lazy(() => deploymentSelectSchema)
|
|
// .optional()
|
|
// .openapi('Deployment', { type: 'object' })
|
|
|
|
// TODO: Improve the self-referential typing here that `@hono/zod-openapi`
|
|
// trips up on.
|
|
deployment: z
|
|
.any()
|
|
.refine(
|
|
(deployment): boolean =>
|
|
!deployment || deploymentSelectSchema.safeParse(deployment).success,
|
|
{
|
|
message: 'Invalid lastDeployment'
|
|
}
|
|
)
|
|
.transform((deployment): any => {
|
|
if (!deployment) return undefined
|
|
return deploymentSelectSchema.parse(deployment)
|
|
})
|
|
.optional()
|
|
})
|
|
|
|
// These are all derived virtual URLs that are not stored in the database
|
|
export const derivedConsumerFields = {
|
|
/**
|
|
* A private admin URL for managing the customer's subscription. This URL
|
|
* is only accessible by the customer.
|
|
*
|
|
* @example https://agentic.so/app/consumers/cons_123
|
|
*/
|
|
adminUrl: z
|
|
.string()
|
|
.url()
|
|
.describe(
|
|
"A private admin URL for managing the customer's subscription. This URL is only accessible by the customer."
|
|
)
|
|
} as const
|
|
|
|
export const consumerSelectSchema = consumerSelectBaseSchema
|
|
.transform((consumer) => ({
|
|
...consumer,
|
|
adminUrl: `${env.AGENTIC_WEB_BASE_URL}/app/consumers/${consumer.id}`
|
|
}))
|
|
.pipe(consumerSelectBaseSchema.extend(derivedConsumerFields).strip())
|
|
.describe(
|
|
`A Consumer represents a user who has subscribed to a Project and is used
|
|
to track usage and billing.
|
|
|
|
Consumers are linked to a corresponding Stripe Customer and Subscription.
|
|
The Stripe customer will either be the user's default Stripe Customer if the
|
|
project uses the default Agentic platform account, or a customer on the project
|
|
owner's connected Stripe account if the project has Stripe Connect enabled.`
|
|
)
|
|
.openapi('Consumer')
|
|
|
|
export const consumerAdminSelectSchema = consumerSelectBaseSchema
|
|
.extend({
|
|
_stripeCustomerId: z.string().nonempty()
|
|
})
|
|
.transform((consumer) => ({
|
|
...consumer,
|
|
adminUrl: `${env.AGENTIC_WEB_BASE_URL}/app/consumers/${consumer.id}`
|
|
}))
|
|
.openapi('AdminConsumer')
|
|
|
|
export const consumerInsertSchema = createInsertSchema(consumers, {
|
|
deploymentId: deploymentIdSchema.optional(),
|
|
|
|
plan: z.string().nonempty()
|
|
})
|
|
.pick({
|
|
plan: true,
|
|
source: true,
|
|
deploymentId: true
|
|
})
|
|
.strict()
|
|
|
|
export const consumerUpdateSchema = createUpdateSchema(consumers, {
|
|
deploymentId: deploymentIdSchema.optional()
|
|
})
|
|
.pick({
|
|
plan: true,
|
|
deploymentId: true
|
|
})
|
|
.strict()
|