kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: replace enabled with deletedAt for soft deletes
rodzic
1303926c9d
commit
2484e7efdb
|
@ -3,7 +3,7 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
|||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { schema } from '@/db'
|
||||
import { upsertConsumer } from '@/lib/billing/upsert-consumer'
|
||||
import { upsertConsumer } from '@/lib/consumers/upsert-consumer'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createRoute, type OpenAPIHono } from '@hono/zod-openapi'
|
|||
|
||||
import type { AuthenticatedEnv } from '@/lib/types'
|
||||
import { schema } from '@/db'
|
||||
import { upsertConsumer } from '@/lib/billing/upsert-consumer'
|
||||
import { upsertConsumer } from '@/lib/consumers/upsert-consumer'
|
||||
import {
|
||||
openapiAuthenticatedSecuritySchemas,
|
||||
openapiErrorResponse404,
|
||||
|
|
|
@ -3,6 +3,7 @@ import type Stripe from 'stripe'
|
|||
import { assert, HttpError } from '@agentic/platform-core'
|
||||
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import { setConsumerStripeSubscriptionStatus } from '@/lib/consumers/utils'
|
||||
import { env, isStripeLive } from '@/lib/env'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
|
||||
|
@ -10,13 +11,6 @@ const relevantStripeEvents = new Set<Stripe.Event.Type>([
|
|||
'customer.subscription.updated'
|
||||
])
|
||||
|
||||
const stripeValidSubscriptionStatuses = new Set([
|
||||
'active',
|
||||
'trialing',
|
||||
'incomplete',
|
||||
'past_due'
|
||||
])
|
||||
|
||||
export function registerV1StripeWebhook(app: OpenAPIHono) {
|
||||
return app.post('/webhooks/stripe', async (ctx) => {
|
||||
const body = await ctx.req.text()
|
||||
|
@ -81,15 +75,13 @@ export function registerV1StripeWebhook(app: OpenAPIHono) {
|
|||
|
||||
if (consumer.stripeStatus !== subscription.status) {
|
||||
consumer.stripeStatus = subscription.status
|
||||
consumer.enabled =
|
||||
consumer.plan === 'free' ||
|
||||
stripeValidSubscriptionStatuses.has(consumer.stripeStatus)
|
||||
setConsumerStripeSubscriptionStatus(consumer)
|
||||
|
||||
await db
|
||||
.update(schema.consumers)
|
||||
.set({
|
||||
stripeStatus: consumer.stripeStatus,
|
||||
enabled: consumer.enabled
|
||||
isStripeSubscriptionActive: consumer.isStripeSubscriptionActive
|
||||
})
|
||||
.where(eq(schema.consumers.id, consumer.id))
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
// The only place we allow `@agentic/platform-db` imports is in this directory.
|
||||
|
||||
|
||||
import {
|
||||
drizzle,
|
||||
postgres,
|
||||
|
@ -16,5 +16,4 @@ const postgresClient =
|
|||
|
||||
export const db = drizzle({ client: postgresClient, schema })
|
||||
|
||||
|
||||
export * from '@agentic/platform-db'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
|
||||
// The only place we allow `@agentic/platform-db` imports is in this directory.
|
||||
|
||||
|
||||
export * from '@agentic/platform-db'
|
||||
|
|
|
@ -3,11 +3,11 @@ import { assert } from '@agentic/platform-core'
|
|||
|
||||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import {
|
||||
type ConsumerUpdate,
|
||||
db,
|
||||
eq,
|
||||
getStripePriceIdForPricingPlanLineItem,
|
||||
type RawConsumer,
|
||||
type RawConsumerUpdate,
|
||||
type RawDeployment,
|
||||
type RawProject,
|
||||
type RawUser,
|
||||
|
@ -15,6 +15,8 @@ import {
|
|||
} from '@/db'
|
||||
import { stripe } from '@/lib/stripe'
|
||||
|
||||
import { setConsumerStripeSubscriptionStatus } from '../consumers/utils'
|
||||
|
||||
export async function upsertStripeSubscription(
|
||||
ctx: AuthenticatedContext,
|
||||
{
|
||||
|
@ -292,8 +294,9 @@ export async function upsertStripeSubscription(
|
|||
assert(subscription, 500, 'Missing stripe subscription')
|
||||
logger.debug('subscription', subscription)
|
||||
|
||||
const consumerUpdate: ConsumerUpdate = consumer
|
||||
const consumerUpdate: RawConsumerUpdate = consumer
|
||||
consumerUpdate.stripeStatus = subscription.status
|
||||
setConsumerStripeSubscriptionStatus(consumerUpdate)
|
||||
|
||||
// if (!plan) {
|
||||
// TODO: we cancel at the end of the billing interval, so we shouldn't
|
||||
|
|
|
@ -4,13 +4,12 @@ import { parseFaasIdentifier } from '@agentic/platform-validators'
|
|||
import type { AuthenticatedContext } from '@/lib/types'
|
||||
import { and, db, eq, schema } from '@/db'
|
||||
import { acl } from '@/lib/acl'
|
||||
import { upsertStripeConnectCustomer } from '@/lib/billing/upsert-stripe-connect-customer'
|
||||
import { upsertStripeCustomer } from '@/lib/billing/upsert-stripe-customer'
|
||||
import { upsertStripePricing } from '@/lib/billing/upsert-stripe-pricing'
|
||||
import { upsertStripeSubscription } from '@/lib/billing/upsert-stripe-subscription'
|
||||
import { createConsumerToken } from '@/lib/create-consumer-token'
|
||||
|
||||
import { upsertStripeConnectCustomer } from './upsert-stripe-connect-customer'
|
||||
import { upsertStripeCustomer } from './upsert-stripe-customer'
|
||||
import { upsertStripePricing } from './upsert-stripe-pricing'
|
||||
import { upsertStripeSubscription } from './upsert-stripe-subscription'
|
||||
|
||||
export async function upsertConsumer(
|
||||
c: AuthenticatedContext,
|
||||
{
|
||||
|
@ -80,7 +79,7 @@ export async function upsertConsumer(
|
|||
|
||||
assert(
|
||||
!existingConsumer ||
|
||||
!existingConsumer.enabled ||
|
||||
!existingConsumer.isStripeSubscriptionActive ||
|
||||
existingConsumer.plan !== plan ||
|
||||
existingConsumer.deploymentId !== deploymentId,
|
||||
409,
|
||||
|
@ -102,9 +101,9 @@ export async function upsertConsumer(
|
|||
`Project not found "${projectId}" for deployment "${deploymentId}"`
|
||||
)
|
||||
assert(
|
||||
deployment.enabled,
|
||||
!deployment.deletedAt,
|
||||
410,
|
||||
`Deployment has been disabled by its owner "${deployment.id}"`
|
||||
`Deployment has been deleted by its owner "${deployment.id}"`
|
||||
)
|
||||
|
||||
if (plan) {
|
|
@ -0,0 +1,20 @@
|
|||
import type { RawConsumerUpdate } from '@/db'
|
||||
|
||||
const stripeValidSubscriptionStatuses = new Set([
|
||||
'active',
|
||||
'trialing',
|
||||
'incomplete',
|
||||
'past_due'
|
||||
])
|
||||
|
||||
export function setConsumerStripeSubscriptionStatus(
|
||||
consumer: Pick<
|
||||
RawConsumerUpdate,
|
||||
'plan' | 'stripeStatus' | 'isStripeSubscriptionActive'
|
||||
>
|
||||
) {
|
||||
consumer.isStripeSubscriptionActive =
|
||||
consumer.plan === 'free' ||
|
||||
(!!consumer.stripeStatus &&
|
||||
stripeValidSubscriptionStatuses.has(consumer.stripeStatus))
|
||||
}
|
|
@ -87,7 +87,8 @@ export const timestamps = {
|
|||
createdAt: timestamp().notNull().defaultNow(),
|
||||
updatedAt: timestamp()
|
||||
.notNull()
|
||||
.default(sql`now()`)
|
||||
.default(sql`now()`),
|
||||
deletedAt: timestamp()
|
||||
}
|
||||
|
||||
export const userRoleEnum = pgEnum('UserRole', ['user', 'admin'])
|
||||
|
|
|
@ -59,9 +59,6 @@ export const consumers = pgTable(
|
|||
// initializing their subscription.
|
||||
activated: boolean().default(false).notNull(),
|
||||
|
||||
// Whether the consumer's subscription is currently active
|
||||
enabled: boolean().default(true).notNull(),
|
||||
|
||||
// TODO: Re-add coupon support
|
||||
// coupon: text(),
|
||||
|
||||
|
@ -90,6 +87,10 @@ export const consumers = pgTable(
|
|||
// Stripe subscription status (synced via webhooks)
|
||||
stripeStatus: text(),
|
||||
|
||||
// Whether the consumer's subscription is currently active, depending on
|
||||
// `stripeStatus`.
|
||||
isStripeSubscriptionActive: boolean().default(true).notNull(),
|
||||
|
||||
// Main Stripe Subscription id
|
||||
_stripeSubscriptionId: stripeId(),
|
||||
|
||||
|
@ -108,8 +109,12 @@ export const consumers = pgTable(
|
|||
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_updatedAt_idx').on(table.updatedAt),
|
||||
index('consumer_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ export const deployments = pgTable(
|
|||
hash: text().notNull(),
|
||||
version: text(),
|
||||
|
||||
enabled: boolean().default(true).notNull(),
|
||||
published: boolean().default(false).notNull(),
|
||||
|
||||
description: text().default('').notNull(),
|
||||
|
@ -82,11 +81,11 @@ export const deployments = pgTable(
|
|||
index('deployment_userId_idx').on(table.userId),
|
||||
index('deployment_teamId_idx').on(table.teamId),
|
||||
index('deployment_projectId_idx').on(table.projectId),
|
||||
index('deployment_enabled_idx').on(table.enabled),
|
||||
index('deployment_published_idx').on(table.published),
|
||||
index('deployment_version_idx').on(table.version),
|
||||
index('deployment_createdAt_idx').on(table.createdAt),
|
||||
index('deployment_updatedAt_idx').on(table.updatedAt)
|
||||
index('deployment_updatedAt_idx').on(table.updatedAt),
|
||||
index('deployment_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
@ -190,7 +189,7 @@ NOTE: Agentic currently only supports \`external\` API servers. If you'd like to
|
|||
|
||||
export const deploymentUpdateSchema = createUpdateSchema(deployments)
|
||||
.pick({
|
||||
enabled: true,
|
||||
deletedAt: true,
|
||||
description: true
|
||||
})
|
||||
.strict()
|
||||
|
|
|
@ -58,7 +58,8 @@ export const logEntries = pgTable(
|
|||
index('log_entry_deploymentId_idx').on(table.deploymentId),
|
||||
index('log_entry_consumerId_idx').on(table.consumerId),
|
||||
index('log_entry_createdAt_idx').on(table.createdAt),
|
||||
index('log_entry_updatedAt_idx').on(table.updatedAt)
|
||||
index('log_entry_updatedAt_idx').on(table.updatedAt),
|
||||
index('log_entry_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -126,7 +126,8 @@ export const projects = pgTable(
|
|||
index('project_teamId_idx').on(table.teamId),
|
||||
index('project_alias_idx').on(table.alias),
|
||||
index('project_createdAt_idx').on(table.createdAt),
|
||||
index('project_updatedAt_idx').on(table.updatedAt)
|
||||
index('project_updatedAt_idx').on(table.updatedAt),
|
||||
index('project_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -44,7 +44,8 @@ export const teamMembers = pgTable(
|
|||
index('team_member_team_idx').on(table.teamId),
|
||||
index('team_member_slug_idx').on(table.teamSlug),
|
||||
index('team_member_createdAt_idx').on(table.createdAt),
|
||||
index('team_member_updatedAt_idx').on(table.updatedAt)
|
||||
index('team_member_updatedAt_idx').on(table.updatedAt),
|
||||
index('team_member_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -33,7 +33,8 @@ export const teams = pgTable(
|
|||
(table) => [
|
||||
uniqueIndex('team_slug_idx').on(table.slug),
|
||||
index('team_createdAt_idx').on(table.createdAt),
|
||||
index('team_updatedAt_idx').on(table.updatedAt)
|
||||
index('team_updatedAt_idx').on(table.updatedAt),
|
||||
index('team_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -60,7 +60,8 @@ export const users = pgTable(
|
|||
uniqueIndex('user_emailConfirmToken_idx').on(table.emailConfirmToken),
|
||||
uniqueIndex('user_passwordResetToken_idx').on(table.passwordResetToken),
|
||||
index('user_createdAt_idx').on(table.createdAt),
|
||||
index('user_updatedAt_idx').on(table.updatedAt)
|
||||
index('user_updatedAt_idx').on(table.updatedAt),
|
||||
index('user_deletedAt_idx').on(table.deletedAt)
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
import { expectTypeOf, test } from 'vitest'
|
||||
|
||||
import type { LogEntry, RawLogEntry, RawUser, User } from './types'
|
||||
import type {
|
||||
Consumer,
|
||||
LogEntry,
|
||||
RawConsumer,
|
||||
RawConsumerUpdate,
|
||||
RawLogEntry,
|
||||
RawUser,
|
||||
User
|
||||
} from './types'
|
||||
|
||||
type UserKeys = Exclude<keyof User & keyof RawUser, 'authProviders'>
|
||||
type LogEntryKeys = keyof RawLogEntry & keyof LogEntry
|
||||
type ConsumerKeys = keyof RawConsumer & keyof Consumer
|
||||
|
||||
test('User types are compatible', () => {
|
||||
expectTypeOf<RawUser>().toExtend<User>()
|
||||
|
@ -18,3 +27,17 @@ test('LogEntry types are compatible', () => {
|
|||
RawLogEntry[LogEntryKeys]
|
||||
>()
|
||||
})
|
||||
|
||||
test('Consumer types are compatible', () => {
|
||||
expectTypeOf<RawConsumer>().toExtend<Consumer>()
|
||||
|
||||
expectTypeOf<Consumer[ConsumerKeys]>().toEqualTypeOf<
|
||||
RawConsumer[ConsumerKeys]
|
||||
>()
|
||||
|
||||
// Ensure that we can pass any Consumer as a RawConsumerUpdate
|
||||
expectTypeOf<Consumer>().toExtend<RawConsumerUpdate>()
|
||||
|
||||
// Ensure that we can pass any RawConsumer as a RawConsumerUpdate
|
||||
expectTypeOf<RawConsumer>().toExtend<RawConsumerUpdate>()
|
||||
})
|
||||
|
|
|
@ -68,7 +68,7 @@ export type RawConsumer = Simplify<
|
|||
deployment?: RawDeployment | null // TODO: remove null (requires drizzle-orm changes)
|
||||
}
|
||||
>
|
||||
export type ConsumerUpdate = Partial<
|
||||
export type RawConsumerUpdate = Partial<
|
||||
Omit<
|
||||
InferInsertModel<typeof schema.consumers>,
|
||||
'id' | 'projectId' | 'userId' | 'deploymentId'
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
- **replace Project.id and Deployment.id with cuids**
|
||||
- move others to `alias` or `publicIdentifier`?
|
||||
- won't work with hono routing? test this
|
||||
- add prefixes to model ids
|
||||
|
||||
---
|
||||
|
||||
|
@ -27,8 +28,6 @@
|
|||
- https://github.com/NangoHQ/nango
|
||||
- https://github.com/transitive-bullshit?submit=Search&q=oauth&tab=stars&type=&sort=&direction=&submit=Search
|
||||
- clerk / workos / auth0
|
||||
- db
|
||||
- replace `enabled` with soft `deletedAt` timestamp
|
||||
- consider switching to [consola](https://github.com/unjs/consola) for logging?
|
||||
|
||||
## License
|
||||
|
|
Ładowanie…
Reference in New Issue