feat: getting closer to stripe perfection <3

pull/715/head
Travis Fischer 2025-05-18 17:58:51 +07:00
rodzic 353f0ff46a
commit cd77eef88b
8 zmienionych plików z 269 dodań i 203 usunięć

Wyświetl plik

@ -11,6 +11,10 @@ import { z } from '@hono/zod-openapi'
import { deployments, deploymentSelectSchema } from './deployment' import { deployments, deploymentSelectSchema } from './deployment'
import { projects, projectSelectSchema } from './project' import { projects, projectSelectSchema } from './project'
import {
type StripeSubscriptionItemIdMap,
stripeSubscriptionItemIdMapSchema
} from './types'
import { users, userSelectSchema } from './user' import { users, userSelectSchema } from './user'
import { import {
createInsertSchema, createInsertSchema,
@ -89,9 +93,9 @@ export const consumers = pgTable(
// Main Stripe Subscription id // Main Stripe Subscription id
_stripeSubscriptionId: stripeId(), _stripeSubscriptionId: stripeId(),
// [lineItemSlug: string]: string // [pricingPlanLineItemSlug: string]: string
_stripeSubscriptionLineItemIdMap: jsonb() _stripeSubscriptionItemIdMap: jsonb()
.$type<Record<string, string>>() .$type<StripeSubscriptionItemIdMap>()
.default({}) .default({})
.notNull(), .notNull(),
@ -132,7 +136,7 @@ export const consumerRelationsSchema: z.ZodType<ConsumerRelationFields> =
z.enum(['user', 'project', 'deployment']) z.enum(['user', 'project', 'deployment'])
export const consumerSelectSchema = createSelectSchema(consumers, { export const consumerSelectSchema = createSelectSchema(consumers, {
_stripeSubscriptionLineItemIdMap: z.record(z.string(), z.string()), _stripeSubscriptionItemIdMap: stripeSubscriptionItemIdMapSchema,
deploymentId: (schema) => deploymentId: (schema) =>
schema.refine((id) => validators.deploymentId(id), { schema.refine((id) => validators.deploymentId(id), {
@ -146,7 +150,7 @@ export const consumerSelectSchema = createSelectSchema(consumers, {
}) })
.omit({ .omit({
_stripeSubscriptionId: true, _stripeSubscriptionId: true,
_stripeSubscriptionLineItemIdMap: true, _stripeSubscriptionItemIdMap: true,
_stripeCustomerId: true _stripeCustomerId: true
}) })
.extend({ .extend({

Wyświetl plik

@ -12,10 +12,10 @@ import { z } from '@hono/zod-openapi'
import { projects } from './project' import { projects } from './project'
import { teams, teamSelectSchema } from './team' import { teams, teamSelectSchema } from './team'
import { import {
type PricingPlanList,
pricingPlanListSchema
// type Coupon, // type Coupon,
// couponSchema, // couponSchema,
type PricingPlanMap,
pricingPlanMapSchema
} from './types' } from './types'
import { users, userSelectSchema } from './user' import { users, userSelectSchema } from './user'
import { import {
@ -64,8 +64,8 @@ export const deployments = pgTable(
// Backend API URL // Backend API URL
_url: text().notNull(), _url: text().notNull(),
// Record<string, PricingPlan> // Array<PricingPlan>
pricingPlanMap: jsonb().$type<PricingPlanMap>().notNull() pricingPlans: jsonb().$type<PricingPlanList>().notNull()
// coupons: jsonb().$type<Coupon[]>().default([]).notNull() // coupons: jsonb().$type<Coupon[]>().default([]).notNull()
}, },
@ -107,7 +107,7 @@ export const deploymentSelectSchema = createSelectSchema(deployments, {
// build: z.object({}), // build: z.object({}),
// env: z.object({}), // env: z.object({}),
pricingPlanMap: pricingPlanMapSchema pricingPlans: pricingPlanListSchema
// coupons: z.array(couponSchema) // coupons: z.array(couponSchema)
}) })
.omit({ .omit({
@ -145,11 +145,9 @@ export const deploymentInsertSchema = createInsertSchema(deployments, {
_url: (schema) => schema.url(), _url: (schema) => schema.url(),
// TODO: should this public resource be decoupled from the internal pricing pricingPlans: pricingPlanListSchema
// plan structure?
pricingPlanMap: pricingPlanMapSchema
// TODO // TODOp
// coupons: z.array(couponSchema).optional() // coupons: z.array(couponSchema).optional()
}) })
.omit({ id: true, createdAt: true, updatedAt: true }) .omit({ id: true, createdAt: true, updatedAt: true })

Wyświetl plik

@ -95,7 +95,16 @@ export const pricingPlanLineItemHashSchema = z
export const pricingPlanLineItemSlugSchema = z export const pricingPlanLineItemSlugSchema = z
.string() .string()
.nonempty() .nonempty()
.describe('PricingPlanLineItem slug') .describe(
'PricingPlanLineItem slug which acts as a unique lookup key for LineItems across deployments. They must be lower and kebab-cased ("base", "requests", "image-transformations").'
)
export const pricingPlanSlugSchema = z
.string()
.nonempty()
.describe(
'PricingPlan slug which acts as a unique lookup key for PricingPlans across deployments. They must be lower and kebab-cased and should have the interval as a suffix ("free", "starter-monthly", "pro-annual").'
)
export const stripePriceIdMapSchema = z export const stripePriceIdMapSchema = z
.record(pricingPlanLineItemHashSchema, z.string().describe('Stripe Price id')) .record(pricingPlanLineItemHashSchema, z.string().describe('Stripe Price id'))
@ -256,7 +265,13 @@ export type PricingPlanLineItem = z.infer<typeof pricingPlanLineItemSchema>
export const pricingPlanSchema = z export const pricingPlanSchema = z
.object({ .object({
name: z.string().nonempty().openapi('name', { example: 'Starter Monthly' }), name: z.string().nonempty().openapi('name', { example: 'Starter Monthly' }),
slug: z.string().nonempty().openapi('slug', { example: 'starter-monthly' }), slug: z
.string()
.nonempty()
.describe(
'PricingPlan slug ("free", "starter-monthly", "pro-annual", etc)'
)
.openapi('slug', { example: 'starter-monthly' }),
/** /**
* The frequency at which a subscription is billed. * The frequency at which a subscription is billed.
@ -325,27 +340,69 @@ export const stripeProductIdMapSchema = z
.openapi('StripeProductIdMap') .openapi('StripeProductIdMap')
export type StripeProductIdMap = z.infer<typeof stripeProductIdMapSchema> export type StripeProductIdMap = z.infer<typeof stripeProductIdMapSchema>
export const pricingPlanMapSchema = z export const pricingPlanListSchema = z
.record( .array(pricingPlanSchema)
z .nonempty({
.string()
.describe(
'PricingPlan slug ("free", "starter-monthly", "pro-annual", etc)'
),
pricingPlanSchema
)
.refine((data) => Object.keys(data).length > 0, {
message: 'Must contain at least one PricingPlan' message: 'Must contain at least one PricingPlan'
}) })
.describe('Map from PricingPlan slug to PricingPlan') .refine(
export type PricingPlanMap = z.infer<typeof pricingPlanMapSchema> (pricingPlans) => {
const slugs = new Set(pricingPlans.map((p) => p.slug))
return slugs.size === pricingPlans.length
},
{
message: `Invalid PricingPlanList: duplicate PricingPlan slugs`
}
)
.refine(
(pricingPlans) => {
const pricingPlanLineItemSlugMap: Record<string, PricingPlanLineItem[]> =
{}
for (const pricingPlan of pricingPlans) {
for (const lineItem of pricingPlan.lineItems) {
if (!pricingPlanLineItemSlugMap[lineItem.slug]) {
pricingPlanLineItemSlugMap[lineItem.slug] = []
}
// TODO pricingPlanLineItemSlugMap[lineItem.slug]!.push(lineItem)
// export const _stripeSubscriptionLineItemIdMapSchema = z }
// .record(pricingPlanLineItemHashSchema, z.string().describe('Stripe LineItem id')) }
// .describe('Map from internal PricingPlanLineItem **hash** to Stripe LineItem id')
// .openapi('StripeSubscriptionLineItemMap') for (const lineItems of Object.values(pricingPlanLineItemSlugMap)) {
// export type StripeSubscriptionLineItemMap = z.infer<typeof stripeSubscriptionLineItemMapSchema> if (lineItems.length <= 1) continue
const lineItem0 = lineItems[0]!
for (let i = 1; i < lineItems.length; ++i) {
const lineItem = lineItems[i]!
if (lineItem.usageType !== lineItem0.usageType) {
return false
}
}
}
return true
},
{
message: `Invalid PricingPlanList: all pricing plans which contain the same LineItems (by slug) must have the same usage type (licensed or metered).`
}
)
.describe('List of PricingPlans')
export type PricingPlanList = z.infer<typeof pricingPlanListSchema>
export const stripeSubscriptionItemIdMapSchema = z
.record(
pricingPlanLineItemSlugSchema,
z.string().describe('Stripe Subscription Item id')
)
.describe(
'Map from internal PricingPlanLineItem **slug** to Stripe Subscription Item id'
)
.openapi('StripeSubscriptionItemIdMap')
export type StripeSubscriptionItemIdMap = z.infer<
typeof stripeSubscriptionItemIdMapSchema
>
// export const couponSchema = z // export const couponSchema = z
// .object({ // .object({

Wyświetl plik

@ -20,7 +20,7 @@ import type {
PricingInterval, PricingInterval,
PricingPlan, PricingPlan,
PricingPlanLineItem, PricingPlanLineItem,
PricingPlanMap PricingPlanList
} from './types' } from './types'
const usernameAndTeamSlugLength = 64 as const const usernameAndTeamSlugLength = 64 as const
@ -164,15 +164,35 @@ export function getPricingPlanLineItemHashForStripePrice({
return `price:${pricingPlan.slug}:${pricingPlanLineItem.slug}:${hash}` return `price:${pricingPlan.slug}:${pricingPlanLineItem.slug}:${hash}`
} }
export function getStripePriceIdForPricingPlanLineItem({
pricingPlan,
pricingPlanLineItem,
project
}: {
pricingPlan: PricingPlan
pricingPlanLineItem: PricingPlanLineItem
project: RawProject
}): string | undefined {
const pricingPlanLineItemHash = getPricingPlanLineItemHashForStripePrice({
pricingPlan,
pricingPlanLineItem,
project
})
return project._stripePriceIdMap[pricingPlanLineItemHash]
}
export function getPricingPlansByInterval({ export function getPricingPlansByInterval({
pricingInterval, pricingInterval,
pricingPlanMap pricingPlans
}: { }: {
pricingInterval: PricingInterval pricingInterval: PricingInterval
pricingPlanMap: PricingPlanMap pricingPlans: PricingPlanList
}): PricingPlan[] { }): PricingPlan[] {
return Object.values(pricingPlanMap).filter( return pricingPlans.filter(
(pricingPlan) => pricingPlan.interval === pricingInterval (pricingPlan) =>
pricingPlan.interval === undefined ||
pricingPlan.interval === pricingInterval
) )
} }

Wyświetl plik

@ -108,7 +108,7 @@ export async function upsertConsumer(
) )
if (plan) { if (plan) {
const pricingPlan = deployment.pricingPlanMap[plan] const pricingPlan = deployment.pricingPlans.find((p) => p.slug === plan)
assert( assert(
pricingPlan, pricingPlan,
400, 400,

Wyświetl plik

@ -242,13 +242,13 @@ export async function upsertStripePricing({
// Validate deployment pricing plans to ensure they contain at least one valid // Validate deployment pricing plans to ensure they contain at least one valid
// plan per pricing interval configured on the project. // plan per pricing interval configured on the project.
// TODO: move some of this `pricingPlanMap` validation to a separate function? // TODO: move some of this `pricingPlans` validation to a separate function?
// We really wouldn't want to create some resources and then fail partway when // We really wouldn't want to create some resources and then fail partway when
// this validation or some of the validation above fails. // this validation or some of the validation above fails.
for (const pricingInterval of project.pricingIntervals) { for (const pricingInterval of project.pricingIntervals) {
const pricingPlans = getPricingPlansByInterval({ const pricingPlans = getPricingPlansByInterval({
pricingInterval, pricingInterval,
pricingPlanMap: deployment.pricingPlanMap pricingPlans: deployment.pricingPlans
}) })
assert( assert(
@ -258,7 +258,7 @@ export async function upsertStripePricing({
) )
} }
for (const pricingPlan of Object.values(deployment.pricingPlanMap)) { for (const pricingPlan of deployment.pricingPlans) {
for (const pricingPlanLineItem of pricingPlan.lineItems) { for (const pricingPlanLineItem of pricingPlan.lineItems) {
upserts.push(() => upserts.push(() =>
upsertStripeResourcesForPricingPlanLineItem({ upsertStripeResourcesForPricingPlanLineItem({

Wyświetl plik

@ -10,6 +10,7 @@ import {
type RawUser, type RawUser,
schema schema
} from '@/db' } from '@/db'
import { getStripePriceIdForPricingPlanLineItem } from '@/db/schema'
import { stripe } from '@/lib/stripe' import { stripe } from '@/lib/stripe'
import { assert } from '@/lib/utils' import { assert } from '@/lib/utils'
@ -62,16 +63,43 @@ export async function upsertStripeSubscription(
if (consumer._stripeSubscriptionId) { if (consumer._stripeSubscriptionId) {
// customer has an existing subscription // customer has an existing subscription
const existing = await stripe.subscriptions.retrieve( const existingStripeSubscription = await stripe.subscriptions.retrieve(
consumer._stripeSubscriptionId, consumer._stripeSubscriptionId,
...stripeConnectParams ...stripeConnectParams
) )
const existingItems = existing.items.data const existingStripeSubscriptionItems =
existingStripeSubscription.items.data
logger.debug() logger.debug()
logger.debug('existing subscription', JSON.stringify(existing, null, 2)) logger.debug(
'existing stripe subscription',
JSON.stringify(existingStripeSubscription, null, 2)
)
logger.debug() logger.debug()
const update: Stripe.SubscriptionUpdateParams = {} assert(
existingStripeSubscription.metadata?.userId === consumer.userId,
500,
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata for consumer "${consumer.id}"`
)
assert(
existingStripeSubscription.metadata?.consumerId === consumer.id,
500,
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata for consumer "${consumer.id}"`
)
assert(
existingStripeSubscription.metadata?.projectId === project.id,
500,
`Error updating stripe subscription: invalid existing subscription "${existingStripeSubscription.id}" metadata for project "${project.id}"`
)
const updateParams: Stripe.SubscriptionUpdateParams = {
metadata: {
userId: consumer.userId,
consumerId: consumer.id,
projectId: project.id,
deployment: deployment.id
}
}
if (plan) { if (plan) {
assert( assert(
@ -80,62 +108,50 @@ export async function upsertStripeSubscription(
`Unable to update stripe subscription for invalid pricing plan "${plan}"` `Unable to update stripe subscription for invalid pricing plan "${plan}"`
) )
let items: Stripe.SubscriptionUpdateParams.Item[] = [ const items: Stripe.SubscriptionUpdateParams.Item[] =
{ pricingPlan.lineItems.map((lineItem) => {
plan: pricingPlan.stripeBasePlanId, const priceId = getStripePriceIdForPricingPlanLineItem({
id: consumer.stripeSubscriptionBaseItemId pricingPlan,
}, pricingPlanLineItem: lineItem,
{ project
plan: pricingPlan.stripeRequestPlanId, })
id: consumer.stripeSubscriptionRequestItemId assert(
} priceId,
] 500,
`Error updating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line item "${lineItem.slug}"`
)
for (const metric of pricingPlan.metrics) { // An existing Stripe Subscription Item may or may not exist for this
const { slug: metricSlug } = metric // LineItem. It should exist if this is an update to an existing
logger.debug({ // LineItem. It won't exist if it's a new LineItem.
metricSlug, const id = consumer._stripeSubscriptionItemIdMap[lineItem.slug]
plan: pricingPlan.stripeMetricPlans[metricSlug],
id: consumer.stripeSubscriptionMetricItems[metricSlug] return {
price: priceId,
id,
metadata: {
lineItemSlug: lineItem.slug
}
}
}) })
items.push({ // Sanity check that LineItems we think should exist are all present in
plan: pricingPlan.stripeMetricPlans[metricSlug]!, // the current subscription's items.
id: consumer.stripeSubscriptionMetricItems[metricSlug]
})
}
const invalidItems = items.filter((item) => !item.plan)
if (plan && invalidItems.length) {
logger.error('billing warning found invalid items', invalidItems)
}
items = items.filter((item) => item.plan)
for (const item of items) { for (const item of items) {
if (item.id) { if (item.id) {
const existingItem = existingItems.find( const existingItem = existingStripeSubscriptionItems.find(
(existingItem) => item.id === existingItem.id (existingItem) => item.id === existingItem.id
) )
if (!existingItem) { assert(
logger.error( existingItem,
'billing warning found new item that has a subscription item id but should not', 500,
{ item } `Error updating stripe subscription: invalid pricing plan "${plan}" missing existing Subscription Item for "${item.id}"`
) )
delete item.id
}
} }
} }
// TODO: We should never use clear_usage because it causes us to lose money. for (const existingItem of existingStripeSubscriptionItems) {
// A customer could downgrade their subscription at the end of a pay period
// and this would clear all usage for their period, effectively allowing them
// to hack the service for free usage.
// The solution to this problem is to always have an equivalent free plan for
// every paid plan.
for (const existingItem of existingItems) {
const updatedItem = items.find((item) => item.id === existingItem.id) const updatedItem = items.find((item) => item.id === existingItem.id)
if (!updatedItem) { if (!updatedItem) {
@ -144,10 +160,6 @@ export async function upsertStripeSubscription(
deleted: true deleted: true
} }
if (existingItem.plan.usage_type === 'metered') {
deletedItem.clear_usage = true
}
items.push(deletedItem) items.push(deletedItem)
} }
} }
@ -164,63 +176,88 @@ export async function upsertStripeSubscription(
} }
} }
update.items = items updateParams.items = items
if (pricingPlan.trialPeriodDays) { if (pricingPlan.trialPeriodDays) {
update.trial_end = const trialEnd =
Math.trunc(Date.now() / 1000) + Math.trunc(Date.now() / 1000) +
24 * 60 * 60 * pricingPlan.trialPeriodDays 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.debug('subscription', action, { items }) logger.debug('subscription', action, { items })
} else { } else {
update.cancel_at_period_end = true updateParams.cancel_at_period_end = true
} }
if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) { if (project.isStripeConnectEnabled && project.applicationFeePercent > 0) {
update.application_fee_percent = project.applicationFeePercent updateParams.application_fee_percent = project.applicationFeePercent
} }
subscription = await stripe.subscriptions.update( subscription = await stripe.subscriptions.update(
consumer._stripeSubscriptionId, consumer._stripeSubscriptionId,
update, updateParams,
...stripeConnectParams ...stripeConnectParams
) )
// TODO: this will cancel the subscription without resolving current usage / invoices // TODO: this will cancel the subscription without resolving current usage / invoices
// await stripe.subscriptions.del(consumer.stripeSubscription) // await stripe.subscriptions.del(consumer.stripeSubscription)
} else { } else {
// Creating a new subscription for this consumer for the first time.
assert( assert(
pricingPlan, pricingPlan,
404, 404,
`Unable to update stripe subscription for invalid pricing plan "${plan}"` `Unable to update stripe subscription for invalid pricing plan "${plan}"`
) )
let items: Stripe.SubscriptionCreateParams.Item[] = [ const items: Stripe.SubscriptionCreateParams.Item[] =
{ pricingPlan.lineItems.map((lineItem) => {
plan: pricingPlan.stripeBasePlanId const priceId = getStripePriceIdForPricingPlanLineItem({
}, pricingPlan,
{ pricingPlanLineItem: lineItem,
plan: pricingPlan.stripeRequestPlanId project
} })
] assert(
priceId,
500,
`Error creating stripe subscription: missing expected Stripe Price for plan "${pricingPlan.slug}" line item "${lineItem.slug}"`
)
for (const metric of pricingPlan.metrics) { // An existing Stripe Subscription Item may or may not exist for this
const { slug: metricSlug } = metric // LineItem. It should exist if this is an update to an existing
items.push({ // LineItem. It won't exist if it's a new LineItem.
plan: pricingPlan.stripeMetricPlans[metricSlug]! 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,
metadata: {
lineItemSlug: lineItem.slug
}
}
}) })
}
items = items.filter((item) => item.plan)
assert( assert(
items.length, items.length,
500, 500,
`Error creating stripe subscription for invalid plan "${pricingPlan.slug}"` `Error creating stripe subscription: invalid plan "${plan}"`
) )
const createParams: Stripe.SubscriptionCreateParams = { const createParams: Stripe.SubscriptionCreateParams = {
customer: stripeCustomerId, customer: stripeCustomerId,
description: `Agentic subscription to project "${project.id}"`,
// TODO: coupons // TODO: coupons
// coupon: filterConsumerCoupon(ctx, consumer, deployment), // coupon: filterConsumerCoupon(ctx, consumer, deployment),
items, items,
@ -249,105 +286,54 @@ export async function upsertStripeSubscription(
consumer._stripeSubscriptionId = subscription.id consumer._stripeSubscriptionId = subscription.id
} }
// ----------------------------------------------------
// Same codepath for updating, creating, and cancelling
// ----------------------------------------------------
assert(subscription, 500, 'Missing stripe subscription') assert(subscription, 500, 'Missing stripe subscription')
logger.debug('subscription', subscription) logger.debug('subscription', subscription)
const consumerUpdate: ConsumerUpdate = consumer const consumerUpdate: ConsumerUpdate = consumer
consumerUpdate.stripeStatus = subscription.status
if (plan) { // if (!plan) {
consumerUpdate.stripeStatus = subscription.status // TODO: we cancel at the end of the billing interval, so we shouldn't
} else { // invalidate the stripe subscription just yet. That should happen via
// TODO // webhook. And we should never set `_stripeSubscriptionId` to `null`.
consumerUpdate._stripeSubscriptionId = null // consumerUpdate._stripeSubscriptionId = null
consumerUpdate.stripeStatus = 'cancelled' // consumerUpdate.stripeStatus = 'cancelled'
} // }
if (pricingPlan?.stripeBasePlanId) { if (pricingPlan) {
const subscriptionItem = subscription.items.data.find( for (const lineItem of pricingPlan.lineItems) {
(item) => item.plan.id === pricingPlan.stripeBasePlanId const stripeSubscriptionItemId =
) consumer._stripeSubscriptionItemIdMap[lineItem.slug]
assert(
subscriptionItem,
500,
`Error initializing stripe subscription for base plan "${subscription.id}"`
)
consumerUpdate.stripeSubscriptionBaseItemId = subscriptionItem.id const stripeSubscriptionItem: Stripe.SubscriptionItem | undefined =
assert( subscription.items.data.find((item) =>
consumerUpdate.stripeSubscriptionBaseItemId, stripeSubscriptionItemId
500, ? item.id === stripeSubscriptionItemId
`Error initializing stripe subscription for base plan [${subscription.id}]` : item.metadata?.lineItemSlug === lineItem.slug
)
} else {
// TODO
consumerUpdate.stripeSubscriptionBaseItemId = null
}
if (pricingPlan?.stripeRequestPlanId) {
const subscriptionItem = subscription.items.data.find(
(item) => item.plan.id === pricingPlan.stripeRequestPlanId
)
assert(
subscriptionItem,
500,
`Error initializing stripe subscription for metric "requests" on plan "${subscription.id}"`
)
consumerUpdate.stripeSubscriptionRequestItemId = subscriptionItem.id
assert(
consumerUpdate.stripeSubscriptionRequestItemId,
500,
`Error initializing stripe subscription for metric "requests" on plan "${subscription.id}"`
)
} else {
// TODO
consumerUpdate.stripeSubscriptionRequestItemId = null
}
const metricSlugs = (
pricingPlan?.metrics.map((metric) => metric.slug) ?? []
).concat(Object.keys(consumer.stripeSubscriptionMetricItems))
const isMetricInPricingPlan = (metricSlug: string) =>
pricingPlan?.metrics.find((metric) => metric.slug === metricSlug)
for (const metricSlug of metricSlugs) {
logger.debug({
metricSlug,
pricingPlan
})
const metricPlan = pricingPlan?.stripeMetricPlans[metricSlug]
if (metricPlan) {
const subscriptionItem: Stripe.SubscriptionItem | undefined =
subscription.items.data.find((item) => item.plan.id === metricPlan)
if (isMetricInPricingPlan(metricSlug)) {
assert(
subscriptionItem,
500,
`Error initializing stripe subscription for metric "${metricSlug}" on plan [${subscription.id}]`
) )
consumerUpdate.stripeSubscriptionMetricItems![metricSlug] = assert(
subscriptionItem.id stripeSubscriptionItem,
assert( 500,
consumerUpdate.stripeSubscriptionMetricItems![metricSlug], `Error post-processing stripe subscription for line item "${lineItem.slug}" on plan "${pricingPlan.slug}"`
500, )
`Error initializing stripe subscription for metric "${metricSlug}" on plan [${subscription.id}]`
) consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug] =
} stripeSubscriptionItem.id
} else { assert(
// TODO consumerUpdate._stripeSubscriptionItemIdMap![lineItem.slug],
delete consumerUpdate.stripeSubscriptionMetricItems![metricSlug] 500,
`Error post-processing stripe subscription for line item "${lineItem.slug}" on plan "${pricingPlan.slug}"`
)
} }
} }
logger.debug() logger.debug()
logger.debug('consumer update', { logger.debug('consumer update', consumerUpdate)
...consumer,
...consumerUpdate
})
const [updatedConsumer] = await db const [updatedConsumer] = await db
.update(schema.consumers) .update(schema.consumers)

Wyświetl plik

@ -11,7 +11,8 @@ export default [
}, },
rules: { rules: {
...drizzle.configs.recommended.rules, ...drizzle.configs.recommended.rules,
'no-console': 'error' 'no-console': 'error',
'unicorn/no-array-reduce': 'off'
} }
} }
] ]