kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: add stripe billing portal session route
rodzic
22e15c5e84
commit
efe4b122d3
|
@ -0,0 +1,65 @@
|
||||||
|
import { assert } from '@agentic/platform-core'
|
||||||
|
import { createRoute, type OpenAPIHono, z } from '@hono/zod-openapi'
|
||||||
|
|
||||||
|
import type { AuthenticatedHonoEnv } from '@/lib/types'
|
||||||
|
import { db, eq, schema } from '@/db'
|
||||||
|
import { acl } from '@/lib/acl'
|
||||||
|
import { env } from '@/lib/env'
|
||||||
|
import { stripe } from '@/lib/external/stripe'
|
||||||
|
import {
|
||||||
|
openapiAuthenticatedSecuritySchemas,
|
||||||
|
openapiErrorResponse404,
|
||||||
|
openapiErrorResponse409,
|
||||||
|
openapiErrorResponse410,
|
||||||
|
openapiErrorResponses
|
||||||
|
} from '@/lib/openapi-utils'
|
||||||
|
|
||||||
|
import { consumerIdParamsSchema } from './schemas'
|
||||||
|
|
||||||
|
const route = createRoute({
|
||||||
|
description: 'Creates a Stripe billing portal session for a customer.',
|
||||||
|
tags: ['consumers'],
|
||||||
|
operationId: 'createConsumerBillingPortalSession',
|
||||||
|
method: 'post',
|
||||||
|
path: 'consumers/{consumerId}/billing-portal',
|
||||||
|
security: openapiAuthenticatedSecuritySchemas,
|
||||||
|
request: {
|
||||||
|
params: consumerIdParamsSchema
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
description: 'A billing portal session URL',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: z.object({
|
||||||
|
url: z.string().url()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...openapiErrorResponses,
|
||||||
|
...openapiErrorResponse404,
|
||||||
|
...openapiErrorResponse409,
|
||||||
|
...openapiErrorResponse410
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerV1CreateConsumerBillingPortalSession(
|
||||||
|
app: OpenAPIHono<AuthenticatedHonoEnv>
|
||||||
|
) {
|
||||||
|
return app.openapi(route, async (c) => {
|
||||||
|
const { consumerId } = c.req.valid('param')
|
||||||
|
const consumer = await db.query.consumers.findFirst({
|
||||||
|
where: eq(schema.consumers.id, consumerId)
|
||||||
|
})
|
||||||
|
assert(consumer, 404, `Consumer not found "${consumerId}"`)
|
||||||
|
await acl(c, consumer, { label: 'Consumer' })
|
||||||
|
|
||||||
|
const portalSession = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: consumer._stripeCustomerId,
|
||||||
|
return_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers/${consumerId}`
|
||||||
|
})
|
||||||
|
|
||||||
|
return c.json({ url: portalSession.url })
|
||||||
|
})
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { registerV1SignUpWithPassword } from './auth/sign-up-with-password'
|
||||||
import { registerV1AdminActivateConsumer } from './consumers/admin-activate-consumer'
|
import { registerV1AdminActivateConsumer } from './consumers/admin-activate-consumer'
|
||||||
import { registerV1AdminGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
|
import { registerV1AdminGetConsumerByToken } from './consumers/admin-get-consumer-by-token'
|
||||||
import { registerV1CreateConsumer } from './consumers/create-consumer'
|
import { registerV1CreateConsumer } from './consumers/create-consumer'
|
||||||
|
import { registerV1CreateConsumerBillingPortalSession } from './consumers/create-consumer-billing-portal-session'
|
||||||
import { registerV1CreateConsumerCheckoutSession } from './consumers/create-consumer-checkout-session'
|
import { registerV1CreateConsumerCheckoutSession } from './consumers/create-consumer-checkout-session'
|
||||||
import { registerV1GetConsumer } from './consumers/get-consumer'
|
import { registerV1GetConsumer } from './consumers/get-consumer'
|
||||||
import { registerV1GetConsumerByProjectIdentifier } from './consumers/get-consumer-by-project-identifier'
|
import { registerV1GetConsumerByProjectIdentifier } from './consumers/get-consumer-by-project-identifier'
|
||||||
|
@ -119,10 +120,11 @@ registerV1GetProject(privateRouter)
|
||||||
registerV1UpdateProject(privateRouter)
|
registerV1UpdateProject(privateRouter)
|
||||||
|
|
||||||
// Consumers
|
// Consumers
|
||||||
registerV1GetConsumerByProjectIdentifier(privateRouter) // msut be before `registerV1GetConsumer`
|
registerV1GetConsumerByProjectIdentifier(privateRouter) // must be before `registerV1GetConsumer`
|
||||||
registerV1GetConsumer(privateRouter)
|
registerV1GetConsumer(privateRouter)
|
||||||
registerV1CreateConsumer(privateRouter)
|
registerV1CreateConsumer(privateRouter)
|
||||||
registerV1CreateConsumerCheckoutSession(privateRouter)
|
registerV1CreateConsumerCheckoutSession(privateRouter)
|
||||||
|
registerV1CreateConsumerBillingPortalSession(privateRouter)
|
||||||
registerV1UpdateConsumer(privateRouter)
|
registerV1UpdateConsumer(privateRouter)
|
||||||
registerV1RefreshConsumerToken(privateRouter)
|
registerV1RefreshConsumerToken(privateRouter)
|
||||||
registerV1ListConsumers(privateRouter)
|
registerV1ListConsumers(privateRouter)
|
||||||
|
|
|
@ -254,6 +254,7 @@ export async function syncConsumerWithStripeSubscription({
|
||||||
) {
|
) {
|
||||||
consumer._stripeSubscriptionId = subscription.id
|
consumer._stripeSubscriptionId = subscription.id
|
||||||
consumer.stripeStatus = subscription.status
|
consumer.stripeStatus = subscription.status
|
||||||
|
consumer.plan = plan as any // TODO: types
|
||||||
setConsumerStripeSubscriptionStatus(consumer)
|
setConsumerStripeSubscriptionStatus(consumer)
|
||||||
|
|
||||||
if (deploymentId) {
|
if (deploymentId) {
|
||||||
|
|
|
@ -196,11 +196,15 @@ export async function upsertConsumerStripeCheckout(
|
||||||
// TODO: this function may mutate `consumer`
|
// TODO: this function may mutate `consumer`
|
||||||
await upsertStripeConnectCustomer({ stripeCustomer, consumer, project })
|
await upsertStripeConnectCustomer({ stripeCustomer, consumer, project })
|
||||||
|
|
||||||
logger.info('SUBSCRIPTION', existingConsumer ? 'UPDATE' : 'CREATE', {
|
logger.info(
|
||||||
project,
|
'CONSUMER STRIPE CHECKOUT',
|
||||||
deployment,
|
existingConsumer ? 'UPDATE' : 'CREATE',
|
||||||
consumer
|
{
|
||||||
})
|
project,
|
||||||
|
deployment,
|
||||||
|
consumer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const checkoutSession = await createStripeCheckoutSession(c, {
|
const checkoutSession = await createStripeCheckoutSession(c, {
|
||||||
consumer,
|
consumer,
|
||||||
|
|
|
@ -561,6 +561,22 @@ export class AgenticApiClient {
|
||||||
.json()
|
.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Stripe Billing Portal Session for a customer.
|
||||||
|
*/
|
||||||
|
async createConsumerBillingPortalSession({
|
||||||
|
consumerId,
|
||||||
|
...searchParams
|
||||||
|
}: OperationParameters<'createConsumerBillingPortalSession'>): Promise<{
|
||||||
|
url: string
|
||||||
|
}> {
|
||||||
|
return this.ky
|
||||||
|
.post(`v1/consumers/${consumerId}/billing-portal`, {
|
||||||
|
searchParams: sanitizeSearchParams(searchParams)
|
||||||
|
})
|
||||||
|
.json()
|
||||||
|
}
|
||||||
|
|
||||||
/** Refreshes a consumer's API token. */
|
/** Refreshes a consumer's API token. */
|
||||||
async refreshConsumerToken({
|
async refreshConsumerToken({
|
||||||
consumerId,
|
consumerId,
|
||||||
|
|
|
@ -353,6 +353,23 @@ export interface paths {
|
||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
|
"/v1/consumers/{consumerId}/billing-portal": {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path?: never;
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
get?: never;
|
||||||
|
put?: never;
|
||||||
|
/** @description Creates a Stripe billing portal session for a customer. */
|
||||||
|
post: operations["createConsumerBillingPortalSession"];
|
||||||
|
delete?: never;
|
||||||
|
options?: never;
|
||||||
|
head?: never;
|
||||||
|
patch?: never;
|
||||||
|
trace?: never;
|
||||||
|
};
|
||||||
"/v1/consumers/{consumerId}/refresh-token": {
|
"/v1/consumers/{consumerId}/refresh-token": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
@ -1935,6 +1952,38 @@ export interface operations {
|
||||||
410: components["responses"]["410"];
|
410: components["responses"]["410"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
createConsumerBillingPortalSession: {
|
||||||
|
parameters: {
|
||||||
|
query?: never;
|
||||||
|
header?: never;
|
||||||
|
path: {
|
||||||
|
/** @description Consumer ID */
|
||||||
|
consumerId: string;
|
||||||
|
};
|
||||||
|
cookie?: never;
|
||||||
|
};
|
||||||
|
requestBody?: never;
|
||||||
|
responses: {
|
||||||
|
/** @description A billing portal session URL */
|
||||||
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
/** Format: uri */
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
400: components["responses"]["400"];
|
||||||
|
401: components["responses"]["401"];
|
||||||
|
403: components["responses"]["403"];
|
||||||
|
404: components["responses"]["404"];
|
||||||
|
409: components["responses"]["409"];
|
||||||
|
410: components["responses"]["410"];
|
||||||
|
};
|
||||||
|
};
|
||||||
refreshConsumerToken: {
|
refreshConsumerToken: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|
|
@ -44,7 +44,6 @@
|
||||||
- consider using [neon serverless driver](https://orm.drizzle.team/docs/connect-neon) for production
|
- consider using [neon serverless driver](https://orm.drizzle.team/docs/connect-neon) for production
|
||||||
- can this also be used locally?
|
- can this also be used locally?
|
||||||
- may need to update our `drizzle-orm` fork
|
- may need to update our `drizzle-orm` fork
|
||||||
- figure out the best OSS license for launch
|
|
||||||
|
|
||||||
## TODO: Post-MVP
|
## TODO: Post-MVP
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue