kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: stripe subscription flow improvements
rodzic
e598187b08
commit
72750d1361
|
@ -2,7 +2,16 @@ import type Stripe from 'stripe'
|
|||
import { assert, HttpError } from '@agentic/platform-core'
|
||||
|
||||
import type { HonoApp } from '@/lib/types'
|
||||
import { and, db, eq, type RawConsumer, schema } from '@/db'
|
||||
import {
|
||||
and,
|
||||
db,
|
||||
eq,
|
||||
getStripePriceIdForPricingPlanLineItem,
|
||||
type RawConsumer,
|
||||
type RawDeployment,
|
||||
type RawProject,
|
||||
schema
|
||||
} from '@/db'
|
||||
import { setConsumerStripeSubscriptionStatus } from '@/lib/consumers/utils'
|
||||
import { env } from '@/lib/env'
|
||||
import { stripe } from '@/lib/external/stripe'
|
||||
|
@ -88,15 +97,22 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
? subscriptionOrId
|
||||
: subscriptionOrId.id
|
||||
|
||||
const [subscription, consumer] = await Promise.all([
|
||||
const [subscription, consumer, deployment] = await Promise.all([
|
||||
// Make sure we have the full subscription instead of just the id
|
||||
typeof subscriptionOrId === 'string'
|
||||
? stripe.subscriptions.retrieve(subscriptionId)
|
||||
: subscriptionOrId,
|
||||
|
||||
db.query.consumers.findFirst({
|
||||
where: and(eq(schema.consumers.id, consumerId))
|
||||
})
|
||||
where: and(eq(schema.consumers.id, consumerId)),
|
||||
with: { project: true }
|
||||
}),
|
||||
|
||||
deploymentId
|
||||
? db.query.deployments.findFirst({
|
||||
where: and(eq(schema.deployments.id, deploymentId))
|
||||
})
|
||||
: undefined
|
||||
])
|
||||
assert(
|
||||
subscription,
|
||||
|
@ -104,6 +120,10 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
`stripe subscription "${subscriptionId}" not found`
|
||||
)
|
||||
assert(consumer, 404, `consumer "${consumerId}" not found`)
|
||||
if (deploymentId) {
|
||||
assert(deployment, 404, `deployment "${deploymentId}" not found`)
|
||||
}
|
||||
const { project } = consumer
|
||||
|
||||
// TODO: Treat this as a transaction...
|
||||
await Promise.all([
|
||||
|
@ -119,6 +139,8 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
// Sync our Consumer's state with the Stripe Subscription's state
|
||||
syncConsumerWithStripeSubscription({
|
||||
consumer,
|
||||
deployment,
|
||||
project,
|
||||
subscription,
|
||||
plan,
|
||||
userId,
|
||||
|
@ -176,14 +198,29 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
status: subscription.status
|
||||
})
|
||||
|
||||
const consumer = await db.query.consumers.findFirst({
|
||||
where: eq(schema.consumers.id, consumerId)
|
||||
})
|
||||
const [consumer, deployment] = await Promise.all([
|
||||
db.query.consumers.findFirst({
|
||||
where: eq(schema.consumers.id, consumerId),
|
||||
with: { project: true }
|
||||
}),
|
||||
|
||||
deploymentId
|
||||
? db.query.deployments.findFirst({
|
||||
where: and(eq(schema.deployments.id, deploymentId))
|
||||
})
|
||||
: undefined
|
||||
])
|
||||
assert(consumer, 404, `consumer "${consumerId}" not found`)
|
||||
if (deploymentId) {
|
||||
assert(deployment, 404, `deployment "${deploymentId}" not found`)
|
||||
}
|
||||
const { project } = consumer
|
||||
|
||||
// Sync our Consumer's state with the Stripe Subscription's state
|
||||
await syncConsumerWithStripeSubscription({
|
||||
consumer,
|
||||
deployment,
|
||||
project,
|
||||
subscription,
|
||||
plan,
|
||||
userId,
|
||||
|
@ -220,6 +257,8 @@ export function registerV1StripeWebhook(app: HonoApp) {
|
|||
*/
|
||||
export async function syncConsumerWithStripeSubscription({
|
||||
consumer,
|
||||
project,
|
||||
deployment,
|
||||
subscription,
|
||||
plan,
|
||||
userId,
|
||||
|
@ -227,12 +266,14 @@ export async function syncConsumerWithStripeSubscription({
|
|||
deploymentId
|
||||
}: {
|
||||
consumer: RawConsumer
|
||||
project: RawProject
|
||||
deployment?: RawDeployment
|
||||
subscription: Stripe.Subscription
|
||||
plan: string | null | undefined
|
||||
userId?: string
|
||||
projectId?: string
|
||||
deploymentId?: string
|
||||
}) {
|
||||
}): Promise<RawConsumer> {
|
||||
// These extra checks aren't really necessary, but they're nice sanity checks
|
||||
// to ensure metadata consistency with our consumer
|
||||
assert(
|
||||
|
@ -246,28 +287,65 @@ export async function syncConsumerWithStripeSubscription({
|
|||
`consumer "${consumer.id}" project "${consumer.projectId}" does not match stripe checkout metadata project "${projectId}"`
|
||||
)
|
||||
|
||||
if (
|
||||
consumer._stripeSubscriptionId !== subscription.id ||
|
||||
consumer.stripeStatus !== subscription.status ||
|
||||
consumer.plan !== plan ||
|
||||
consumer.deploymentId !== deploymentId
|
||||
) {
|
||||
consumer._stripeSubscriptionId = subscription.id
|
||||
consumer.stripeStatus = subscription.status
|
||||
consumer.plan = plan as any // TODO: types
|
||||
setConsumerStripeSubscriptionStatus(consumer)
|
||||
consumer._stripeSubscriptionId = subscription.id
|
||||
consumer.stripeStatus = subscription.status
|
||||
consumer.plan = plan as any // TODO: types
|
||||
setConsumerStripeSubscriptionStatus(consumer)
|
||||
|
||||
if (deploymentId) {
|
||||
consumer.deploymentId = deploymentId
|
||||
}
|
||||
|
||||
await db
|
||||
.update(schema.consumers)
|
||||
.set(consumer)
|
||||
.where(eq(schema.consumers.id, consumer.id))
|
||||
|
||||
// TODO: invoke provider webhooks
|
||||
// event.data.customer = consumer.getPublicDocument()
|
||||
// await invokeWebhooks(consumer.project, event)
|
||||
if (deploymentId) {
|
||||
consumer.deploymentId = deploymentId
|
||||
}
|
||||
|
||||
const pricingPlan = plan
|
||||
? deployment?.pricingPlans.find((p) => p.slug === plan)
|
||||
: undefined
|
||||
|
||||
if (pricingPlan) {
|
||||
for (const lineItem of pricingPlan.lineItems) {
|
||||
const stripeSubscriptionItemId =
|
||||
consumer._stripeSubscriptionItemIdMap[lineItem.slug]
|
||||
|
||||
const stripePriceId: string | undefined = stripeSubscriptionItemId
|
||||
? undefined
|
||||
: await getStripePriceIdForPricingPlanLineItem({
|
||||
pricingPlan,
|
||||
pricingPlanLineItem: lineItem,
|
||||
project
|
||||
})
|
||||
|
||||
const stripeSubscriptionItem: Stripe.SubscriptionItem | undefined =
|
||||
subscription.items.data.find((item) =>
|
||||
stripeSubscriptionItemId
|
||||
? item.id === stripeSubscriptionItemId
|
||||
: item.price.id === stripePriceId
|
||||
)
|
||||
|
||||
assert(
|
||||
stripeSubscriptionItem,
|
||||
500,
|
||||
`Error post-processing stripe subscription "${subscription.id}" for line-item "${lineItem.slug}" on plan "${pricingPlan.slug}"`
|
||||
)
|
||||
|
||||
consumer._stripeSubscriptionItemIdMap[lineItem.slug] =
|
||||
stripeSubscriptionItem.id
|
||||
assert(
|
||||
consumer._stripeSubscriptionItemIdMap[lineItem.slug],
|
||||
500,
|
||||
`Error post-processing stripe subscription "${subscription.id}" for line-item "${lineItem.slug}" on plan "${pricingPlan.slug}"`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const [updatedConsumer] = await db
|
||||
.update(schema.consumers)
|
||||
.set(consumer)
|
||||
.where(eq(schema.consumers.id, consumer.id))
|
||||
.returning()
|
||||
assert(updatedConsumer, 500, `consumer "${consumer.id}" not found`)
|
||||
|
||||
// TODO: invoke provider webhooks
|
||||
// event.data.customer = consumer.getPublicDocument()
|
||||
// await invokeWebhooks(consumer.project, event)
|
||||
|
||||
return updatedConsumer
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ export async function createStripeCheckoutSession(
|
|||
project: RawProject
|
||||
plan?: string
|
||||
}
|
||||
): Promise<Stripe.Checkout.Session> {
|
||||
): Promise<{ id: string; url: string }> {
|
||||
const logger = ctx.get('logger')
|
||||
const stripeConnectParams = project._stripeAccountId
|
||||
? [
|
||||
|
@ -49,168 +49,201 @@ export async function createStripeCheckoutSession(
|
|||
? deployment.pricingPlans.find((pricingPlan) => pricingPlan.slug === plan)
|
||||
: undefined
|
||||
|
||||
// const action: 'create' | 'update' | 'cancel' = consumer._stripeSubscriptionId
|
||||
// ? plan
|
||||
// ? 'update'
|
||||
// : 'cancel'
|
||||
// : 'create'
|
||||
let checkoutSession: Stripe.Checkout.Session | undefined
|
||||
const action: 'create' | 'update' | 'cancel' = consumer._stripeSubscriptionId
|
||||
? plan
|
||||
? 'update'
|
||||
: 'cancel'
|
||||
: 'create'
|
||||
|
||||
// TODO: test cancel => resubscribe flow
|
||||
|
||||
if (consumer._stripeSubscriptionId) {
|
||||
// // customer has an existing subscription
|
||||
// const existingStripeSubscription = await stripe.subscriptions.retrieve(
|
||||
// consumer._stripeSubscriptionId,
|
||||
// ...stripeConnectParams
|
||||
// )
|
||||
// const existingStripeSubscriptionItems =
|
||||
// existingStripeSubscription.items.data
|
||||
// logger.debug()
|
||||
// logger.debug(
|
||||
// 'existing stripe subscription',
|
||||
// JSON.stringify(existingStripeSubscription, null, 2)
|
||||
// )
|
||||
// logger.debug()
|
||||
// customer has an existing subscription
|
||||
const existingStripeSubscription = await stripe.subscriptions.retrieve(
|
||||
consumer._stripeSubscriptionId,
|
||||
...stripeConnectParams
|
||||
)
|
||||
const existingStripeSubscriptionItems =
|
||||
existingStripeSubscription.items.data
|
||||
logger.debug()
|
||||
logger.debug(
|
||||
'existing stripe subscription',
|
||||
JSON.stringify(existingStripeSubscription, null, 2)
|
||||
)
|
||||
logger.debug()
|
||||
|
||||
// assert(
|
||||
// existingStripeSubscription.metadata?.userId === consumer.userId,
|
||||
// 500,
|
||||
// `Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.userId for consumer "${consumer.id}"`
|
||||
// )
|
||||
// assert(
|
||||
// existingStripeSubscription.metadata?.consumerId === consumer.id,
|
||||
// 500,
|
||||
// `Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.consumerId for consumer "${consumer.id}"`
|
||||
// )
|
||||
// assert(
|
||||
// existingStripeSubscription.metadata?.projectId === project.id,
|
||||
// 500,
|
||||
// `Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.projectId for consumer "${consumer.id}"`
|
||||
// )
|
||||
assert(
|
||||
existingStripeSubscription.metadata?.userId === consumer.userId,
|
||||
500,
|
||||
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.userId for consumer "${consumer.id}"`
|
||||
)
|
||||
assert(
|
||||
existingStripeSubscription.metadata?.consumerId === consumer.id,
|
||||
500,
|
||||
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.consumerId for consumer "${consumer.id}"`
|
||||
)
|
||||
assert(
|
||||
existingStripeSubscription.metadata?.projectId === project.id,
|
||||
500,
|
||||
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata.projectId for consumer "${consumer.id}"`
|
||||
)
|
||||
|
||||
// const updateParams: Stripe.SubscriptionUpdateParams = {
|
||||
// collection_method: 'charge_automatically',
|
||||
// metadata: {
|
||||
// plan: plan ?? null,
|
||||
// consumerId: consumer.id,
|
||||
// userId: consumer.userId,
|
||||
// projectId: project.id,
|
||||
// deploymentId: deployment.id
|
||||
// }
|
||||
// }
|
||||
if (!plan) {
|
||||
const billingPortalSession = await stripe.billingPortal.sessions.create(
|
||||
{
|
||||
customer: stripeCustomerId,
|
||||
return_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers`,
|
||||
flow_data: {
|
||||
type: 'subscription_cancel',
|
||||
subscription_cancel: {
|
||||
subscription: consumer._stripeSubscriptionId
|
||||
},
|
||||
after_completion: {
|
||||
type: 'redirect',
|
||||
redirect: {
|
||||
return_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers/${consumer.id}?checkout=canceled`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...stripeConnectParams
|
||||
)
|
||||
|
||||
// if (plan) {
|
||||
// assert(
|
||||
// pricingPlan,
|
||||
// 404,
|
||||
// `Unable to update stripe subscription for invalid pricing plan "${plan}"`
|
||||
// )
|
||||
//
|
||||
// const items: Stripe.SubscriptionUpdateParams.Item[] = await Promise.all(
|
||||
// pricingPlan.lineItems.map(async (lineItem) => {
|
||||
// const priceId = await getStripePriceIdForPricingPlanLineItem({
|
||||
// pricingPlan,
|
||||
// pricingPlanLineItem: lineItem,
|
||||
// project
|
||||
// })
|
||||
// assert(
|
||||
// priceId,
|
||||
// 500,
|
||||
// `Error updating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line-item "${lineItem.slug}"`
|
||||
// )
|
||||
return {
|
||||
id: billingPortalSession.id,
|
||||
url: billingPortalSession.url
|
||||
}
|
||||
}
|
||||
|
||||
// // An existing Stripe Subscription Item may or may not exist for this
|
||||
// // LineItem. It should exist if this is an update to an existing
|
||||
// // LineItem. It won't exist if it's a new LineItem.
|
||||
// const id = consumer._stripeSubscriptionItemIdMap[lineItem.slug]
|
||||
const updateParams: Stripe.SubscriptionUpdateParams = {
|
||||
collection_method: 'charge_automatically',
|
||||
metadata: {
|
||||
plan: plan ?? null,
|
||||
consumerId: consumer.id,
|
||||
userId: consumer.userId,
|
||||
projectId: project.id,
|
||||
deploymentId: deployment.id
|
||||
}
|
||||
}
|
||||
|
||||
// return {
|
||||
// price: priceId,
|
||||
// id,
|
||||
// metadata: {
|
||||
// lineItemSlug: lineItem.slug
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// )
|
||||
assert(
|
||||
pricingPlan,
|
||||
404,
|
||||
`Unable to update stripe subscription for invalid pricing plan "${plan}"`
|
||||
)
|
||||
|
||||
// // Sanity check that LineItems we think should exist are all present in
|
||||
// // the current subscription's items.
|
||||
// for (const item of items) {
|
||||
// if (item.id) {
|
||||
// const existingItem = existingStripeSubscriptionItems.find(
|
||||
// (existingItem) => item.id === existingItem.id
|
||||
// )
|
||||
const items: Stripe.SubscriptionUpdateParams.Item[] = await Promise.all(
|
||||
pricingPlan.lineItems.map(async (lineItem) => {
|
||||
const priceId = await getStripePriceIdForPricingPlanLineItem({
|
||||
pricingPlan,
|
||||
pricingPlanLineItem: lineItem,
|
||||
project
|
||||
})
|
||||
assert(
|
||||
priceId,
|
||||
500,
|
||||
`Error updating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line-item "${lineItem.slug}"`
|
||||
)
|
||||
|
||||
// assert(
|
||||
// existingItem,
|
||||
// 500,
|
||||
// `Error updating stripe subscription: invalid pricing plan "${plan}" missing existing Subscription Item for "${item.id}"`
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
// An existing Stripe Subscription Item may or may not exist for this
|
||||
// LineItem. It should exist if this is an update to an existing
|
||||
// LineItem. It won't exist if it's a new LineItem.
|
||||
const id = consumer._stripeSubscriptionItemIdMap[lineItem.slug]
|
||||
|
||||
// for (const existingItem of existingStripeSubscriptionItems) {
|
||||
// const updatedItem = items.find((item) => item.id === existingItem.id)
|
||||
return {
|
||||
price: priceId,
|
||||
id,
|
||||
metadata: {
|
||||
lineItemSlug: lineItem.slug
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
// if (!updatedItem) {
|
||||
// const deletedItem: Stripe.SubscriptionUpdateParams.Item = {
|
||||
// id: existingItem.id,
|
||||
// deleted: true
|
||||
// }
|
||||
// Sanity check that LineItems we think should exist are all present in
|
||||
// the current subscription's items.
|
||||
for (const item of items) {
|
||||
if (item.id) {
|
||||
const existingItem = existingStripeSubscriptionItems.find(
|
||||
(existingItem) => item.id === existingItem.id
|
||||
)
|
||||
|
||||
// items.push(deletedItem)
|
||||
// }
|
||||
// }
|
||||
assert(
|
||||
existingItem,
|
||||
500,
|
||||
`Error updating stripe subscription: invalid pricing plan "${plan}" missing existing Subscription Item for "${item.id}"`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// assert(
|
||||
// items.length || !plan,
|
||||
// 500,
|
||||
// `Error updating stripe subscription "${consumer._stripeSubscriptionId}"`
|
||||
// )
|
||||
for (const existingItem of existingStripeSubscriptionItems) {
|
||||
const updatedItem = items.find((item) => item.id === existingItem.id)
|
||||
|
||||
// for (const item of items) {
|
||||
// if (!item.id) {
|
||||
// delete item.id
|
||||
// }
|
||||
// }
|
||||
if (!updatedItem) {
|
||||
const deletedItem: Stripe.SubscriptionUpdateParams.Item = {
|
||||
id: existingItem.id,
|
||||
deleted: true
|
||||
}
|
||||
|
||||
// updateParams.items = items
|
||||
items.push(deletedItem)
|
||||
}
|
||||
}
|
||||
|
||||
// if (pricingPlan.trialPeriodDays) {
|
||||
// const trialEnd =
|
||||
// Math.trunc(Date.now() / 1000) +
|
||||
// 24 * 60 * 60 * pricingPlan.trialPeriodDays
|
||||
assert(
|
||||
items.length || !plan,
|
||||
500,
|
||||
`Error updating stripe subscription "${consumer._stripeSubscriptionId}"`
|
||||
)
|
||||
|
||||
// // Reuse the existing trial end date if one exists. Otherwise, set a new
|
||||
// // one for the updated subscription.
|
||||
// updateParams.trial_end =
|
||||
// existingStripeSubscription.trial_end ?? trialEnd
|
||||
// } else if (existingStripeSubscription.trial_end) {
|
||||
// // If the existing subscription has a trial end date, but the updated
|
||||
// // subscription doesn't, we should end the trial now.
|
||||
// updateParams.trial_end = 'now'
|
||||
// }
|
||||
for (const item of items) {
|
||||
if (!item.id) {
|
||||
delete item.id
|
||||
}
|
||||
}
|
||||
|
||||
// logger.debug('subscription', action, { items })
|
||||
// } else {
|
||||
// updateParams.cancel_at_period_end = true
|
||||
// }
|
||||
updateParams.items = items
|
||||
|
||||
if (pricingPlan.trialPeriodDays) {
|
||||
const trialEnd =
|
||||
Math.trunc(Date.now() / 1000) +
|
||||
24 * 60 * 60 * pricingPlan.trialPeriodDays
|
||||
|
||||
// Reuse the existing trial end date if one exists. Otherwise, set a new
|
||||
// one for the updated subscription.
|
||||
updateParams.trial_end = existingStripeSubscription.trial_end ?? trialEnd
|
||||
} else if (existingStripeSubscription.trial_end) {
|
||||
// If the existing subscription has a trial end date, but the updated
|
||||
// subscription doesn't, we should end the trial now.
|
||||
updateParams.trial_end = 'now'
|
||||
}
|
||||
|
||||
logger.info('>>> subscription', action, { items })
|
||||
|
||||
// TODO: Stripe Connect
|
||||
// if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) {
|
||||
// updateParams.application_fee_percent = project.applicationFeePercent
|
||||
// }
|
||||
|
||||
assert(false, 500, 'TODO: update subscription => createCheckoutSession')
|
||||
const subscription = await stripe.subscriptions.update(
|
||||
consumer._stripeSubscriptionId,
|
||||
updateParams,
|
||||
...stripeConnectParams
|
||||
)
|
||||
|
||||
// subscription = await stripe.subscriptions.update(
|
||||
// consumer._stripeSubscriptionId,
|
||||
// updateParams,
|
||||
// ...stripeConnectParams
|
||||
// )
|
||||
logger.info('<<< subscription', action, subscription)
|
||||
|
||||
// TODO: this will cancel the subscription without resolving current usage / invoices
|
||||
// await stripe.subscriptions.del(consumer.stripeSubscription)
|
||||
const billingPortalSession = await stripe.billingPortal.sessions.create(
|
||||
{
|
||||
customer: stripeCustomerId,
|
||||
return_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers`
|
||||
},
|
||||
...stripeConnectParams
|
||||
)
|
||||
|
||||
return {
|
||||
id: billingPortalSession.id,
|
||||
url: billingPortalSession.url
|
||||
}
|
||||
} else {
|
||||
// Creating a new subscription for this consumer for the first time.
|
||||
assert(
|
||||
|
@ -222,6 +255,16 @@ export async function createStripeCheckoutSession(
|
|||
const items: Stripe.Checkout.SessionCreateParams.LineItem[] =
|
||||
await Promise.all(
|
||||
pricingPlan.lineItems.map(async (lineItem) => {
|
||||
// An existing Stripe Subscription Item may or may not exist for this
|
||||
// LineItem. It should exist if this is an update to an existing
|
||||
// LineItem. It won't exist if it's a new LineItem.
|
||||
const id = consumer._stripeSubscriptionItemIdMap[lineItem.slug]
|
||||
assert(
|
||||
!id,
|
||||
500,
|
||||
`Error creating stripe subscription: consumer contains a Stripe Subscription Item for LineItem "${lineItem.slug}" and pricing plan "${pricingPlan.slug}"`
|
||||
)
|
||||
|
||||
const priceId = await getStripePriceIdForPricingPlanLineItem({
|
||||
pricingPlan,
|
||||
pricingPlanLineItem: lineItem,
|
||||
|
@ -233,16 +276,6 @@ export async function createStripeCheckoutSession(
|
|||
`Error creating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line item "${lineItem.slug}"`
|
||||
)
|
||||
|
||||
// An existing Stripe Subscription Item may or may not exist for this
|
||||
// LineItem. It should exist if this is an update to an existing
|
||||
// LineItem. It won't exist if it's a new LineItem.
|
||||
const id = consumer._stripeSubscriptionItemIdMap[lineItem.slug]
|
||||
assert(
|
||||
!id,
|
||||
500,
|
||||
`Error creating stripe subscription: consumer contains a Stripe Subscription Item for LineItem "${lineItem.slug}" and pricing plan "${pricingPlan.slug}"`
|
||||
)
|
||||
|
||||
return {
|
||||
price: priceId,
|
||||
// TODO: Make this customizable
|
||||
|
@ -267,11 +300,21 @@ export async function createStripeCheckoutSession(
|
|||
success_url: `${env.AGENTIC_WEB_BASE_URL}/app/consumers/${consumer.id}?checkout=success&plan=${plan}`,
|
||||
cancel_url: `${env.AGENTIC_WEB_BASE_URL}/marketplace/projects/${project.identifier}?checkout=canceled`,
|
||||
submit_type: 'subscribe',
|
||||
saved_payment_method_options: {
|
||||
payment_method_save: 'enabled'
|
||||
},
|
||||
subscription_data: {
|
||||
description:
|
||||
pricingPlan.description ??
|
||||
`Subscription to ${project.name} ${pricingPlan.name}`,
|
||||
trial_period_days: pricingPlan.trialPeriodDays
|
||||
trial_period_days: pricingPlan.trialPeriodDays,
|
||||
metadata: {
|
||||
plan: plan ?? null,
|
||||
consumerId: consumer.id,
|
||||
userId: consumer.userId,
|
||||
projectId: project.id,
|
||||
deploymentId: deployment.id
|
||||
}
|
||||
// TODO: Stripe Connect
|
||||
// application_fee_percent: project.applicationFeePercent
|
||||
},
|
||||
|
@ -297,18 +340,15 @@ export async function createStripeCheckoutSession(
|
|||
// }
|
||||
|
||||
logger.debug('checkout session line_items', items)
|
||||
checkoutSession = await stripe.checkout.sessions.create(
|
||||
const checkoutSession = await stripe.checkout.sessions.create(
|
||||
checkoutSessionParams,
|
||||
...stripeConnectParams
|
||||
)
|
||||
assert(checkoutSession.url, 500, 'Missing stripe checkout session URL')
|
||||
|
||||
return {
|
||||
id: checkoutSession.id,
|
||||
url: checkoutSession.url
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
// Same codepath for updating, creating, and cancelling
|
||||
// ----------------------------------------------------
|
||||
|
||||
assert(checkoutSession, 500, 'Missing stripe checkout session')
|
||||
logger.debug('checkout session', checkoutSession)
|
||||
|
||||
return checkoutSession
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import type Stripe from 'stripe'
|
||||
import { assert } from '@agentic/platform-core'
|
||||
|
||||
import type { AuthenticatedHonoContext } from '@/lib/types'
|
||||
|
@ -32,7 +31,7 @@ export async function upsertConsumerStripeCheckout(
|
|||
consumerId?: string
|
||||
}
|
||||
): Promise<{
|
||||
checkoutSession: Stripe.Checkout.Session
|
||||
checkoutSession: { id: string; url: string }
|
||||
consumer: RawConsumer
|
||||
}> {
|
||||
assert(
|
||||
|
|
|
@ -40,7 +40,10 @@ export function AppConsumerIndex({ consumerId }: { consumerId: string }) {
|
|||
useEffect(() => {
|
||||
if (!ctx || !consumer || !firstLoadConsumer.current) return
|
||||
|
||||
if (checkout === 'success') {
|
||||
if (checkout === 'canceled') {
|
||||
firstLoadConsumer.current = false
|
||||
toast('Subscription canceled')
|
||||
} else if (checkout === 'success') {
|
||||
if (plan) {
|
||||
firstLoadConsumer.current = false
|
||||
toast(
|
||||
|
|
Ładowanie…
Reference in New Issue