feat: WIP stripe billing refactor update for 2025

pull/715/head
Travis Fischer 2025-05-18 00:22:47 +07:00
rodzic ca90a37d8c
commit b9b3e6c26b
6 zmienionych plików z 153 dodań i 124 usunięć

Wyświetl plik

@ -13,9 +13,9 @@ import { assert, parseZodSchema } from '@/lib/utils'
import { consumerTokenParamsSchema, populateConsumerSchema } from './schemas'
const route = createRoute({
description: 'Gets a consumer',
tags: ['consumers'],
operationId: 'getConsumer',
description: 'Gets a consumer by API token',
tags: ['admin', 'consumers'],
operationId: 'adminGetConsumerByToken',
method: 'get',
path: 'admin/consumers/tokens/{token}',
security: openapiAuthenticatedSecuritySchemas,
@ -51,7 +51,7 @@ export function registerV1AdminConsumersGetConsumerByToken(
...Object.fromEntries(populate.map((field) => [field, true]))
}
})
assert(consumer, 404, `Consumer token not found "${token}"`)
assert(consumer, 404, `API token not found "${token}"`)
return c.json(parseZodSchema(schema.consumerSelectSchema, consumer))
})

Wyświetl plik

@ -43,34 +43,34 @@ apiV1.openAPIRegistry.registerComponent('securitySchemes', 'Bearer', {
})
// Public routes
const pub = new OpenAPIHono()
const publicRouter = new OpenAPIHono()
// Private, authenticated routes
const pri = new OpenAPIHono<AuthenticatedEnv>()
const privateRouter = new OpenAPIHono<AuthenticatedEnv>()
registerHealthCheck(pub)
registerHealthCheck(publicRouter)
// Users crud
registerV1UsersGetUser(pri)
registerV1UsersUpdateUser(pri)
registerV1UsersGetUser(privateRouter)
registerV1UsersUpdateUser(privateRouter)
// Teams crud
registerV1TeamsCreateTeam(pri)
registerV1TeamsListTeams(pri)
registerV1TeamsGetTeam(pri)
registerV1TeamsDeleteTeam(pri)
registerV1TeamsUpdateTeam(pri)
registerV1TeamsCreateTeam(privateRouter)
registerV1TeamsListTeams(privateRouter)
registerV1TeamsGetTeam(privateRouter)
registerV1TeamsDeleteTeam(privateRouter)
registerV1TeamsUpdateTeam(privateRouter)
// Team members crud
registerV1TeamsMembersCreateTeamMember(pri)
registerV1TeamsMembersUpdateTeamMember(pri)
registerV1TeamsMembersDeleteTeamMember(pri)
registerV1TeamsMembersCreateTeamMember(privateRouter)
registerV1TeamsMembersUpdateTeamMember(privateRouter)
registerV1TeamsMembersDeleteTeamMember(privateRouter)
// Projects crud
registerV1ProjectsCreateProject(pri)
registerV1ProjectsListProjects(pri)
registerV1ProjectsGetProject(pri)
registerV1ProjectsUpdateProject(pri)
registerV1ProjectsCreateProject(privateRouter)
registerV1ProjectsListProjects(privateRouter)
registerV1ProjectsGetProject(privateRouter)
registerV1ProjectsUpdateProject(privateRouter)
// TODO
// pub.get('/projects/alias/:alias(.+)', require('./projects').readByAlias)
@ -84,17 +84,42 @@ registerV1ProjectsUpdateProject(pri)
// )
// Consumers crud
registerV1ConsumersGetConsumer(pri)
registerV1ConsumersGetConsumer(privateRouter)
// Webhook event handlers
registerV1StripeWebhook(pub)
registerV1StripeWebhook(publicRouter)
// Admin routes
registerV1AdminConsumersGetConsumerByToken(pri)
registerV1AdminConsumersGetConsumerByToken(privateRouter)
// Setup routes and middleware
apiV1.route('/', pub)
apiV1.route('/', publicRouter)
apiV1.use(middleware.authenticate)
apiV1.use(middleware.team)
apiV1.use(middleware.me)
apiV1.route('/', pri)
apiV1.route('/', privateRouter)
// API route types to be used by Hono's RPC client.
// Should include all routes except for internal and admin routes.
export type ApiRoutes =
| ReturnType<typeof registerHealthCheck>
// Users
| ReturnType<typeof registerV1UsersGetUser>
| ReturnType<typeof registerV1UsersUpdateUser>
// Teams
| ReturnType<typeof registerV1TeamsCreateTeam>
| ReturnType<typeof registerV1TeamsListTeams>
| ReturnType<typeof registerV1TeamsGetTeam>
| ReturnType<typeof registerV1TeamsDeleteTeam>
| ReturnType<typeof registerV1TeamsUpdateTeam>
// Team members
| ReturnType<typeof registerV1TeamsMembersCreateTeamMember>
| ReturnType<typeof registerV1TeamsMembersUpdateTeamMember>
| ReturnType<typeof registerV1TeamsMembersDeleteTeamMember>
// Projects
| ReturnType<typeof registerV1ProjectsCreateProject>
| ReturnType<typeof registerV1ProjectsListProjects>
| ReturnType<typeof registerV1ProjectsGetProject>
| ReturnType<typeof registerV1ProjectsUpdateProject>
// Consumers
| ReturnType<typeof registerV1ConsumersGetConsumer>

Wyświetl plik

@ -94,9 +94,9 @@ export const projects = pgTable(
// .notNull(),
// Stripe billing Products associated with this project across deployments,
// mapping from PricingPlanMetric **slug** to Stripe Product id.
// mapping from PricingPlanLineItem **slug** to Stripe Product id.
// NOTE: This map uses slugs as keys, unlike `_stripePriceIdMap`, because
// Stripe Products are agnostic to the PricingPlanMetric config. This is
// Stripe Products are agnostic to the PricingPlanLineItem config. This is
// important for them to be shared across deployments even if the pricing
// details change.
_stripeProductIdMap: jsonb()
@ -105,16 +105,16 @@ export const projects = pgTable(
.notNull(),
// Stripe billing Prices associated with this project, mapping from unique
// PricingPlanMetric **hash** to Stripe Price id.
// PricingPlanLineItem **hash** to Stripe Price id.
// NOTE: This map uses hashes as keys, because Stripe Prices are dependent
// on the PricingPlanMetric config. This is important for them to be shared
// on the PricingPlanLineItem config. This is important for them to be shared
// across deployments even if the pricing details change.
_stripePriceIdMap: jsonb().$type<StripePriceIdMap>().default({}).notNull(),
// Stripe billing Metrics associated with this project, mapping from unique
// PricingPlanMetric **slug** to Stripe Meter id.
// Stripe billing LineItems associated with this project, mapping from unique
// PricingPlanLineItem **slug** to Stripe Meter id.
// NOTE: This map uses slugs as keys, unlike `_stripePriceIdMap`, because
// Stripe Products are agnostic to the PricingPlanMetric config. This is
// Stripe Products are agnostic to the PricingPlanLineItem config. This is
// important for them to be shared across deployments even if the pricing
// details change.
_stripeMeterIdMap: jsonb().$type<StripeMeterIdMap>().default({}).notNull(),

Wyświetl plik

@ -89,29 +89,29 @@ export const pricingIntervalSchema = z
.openapi('PricingInterval')
export type PricingInterval = z.infer<typeof pricingIntervalSchema>
export const pricingPlanMetricHashSchema = z
export const pricingPlanLineItemHashSchema = z
.string()
.nonempty()
.describe('Internal PricingPlanMetric hash')
.describe('Internal PricingPlanLineItem hash')
export const pricingPlanMetricSlugSchema = z
export const pricingPlanLineItemSlugSchema = z
.string()
.nonempty()
.describe('PricingPlanMetric slug')
.describe('PricingPlanLineItem slug')
export const stripePriceIdMapSchema = z
.record(pricingPlanMetricHashSchema, z.string().describe('Stripe Price id'))
.describe('Map from internal PricingPlanMetric **hash** to Stripe Price id')
.record(pricingPlanLineItemHashSchema, z.string().describe('Stripe Price id'))
.describe('Map from internal PricingPlanLineItem **hash** to Stripe Price id')
.openapi('StripePriceIdMap')
export type StripePriceIdMap = z.infer<typeof stripePriceIdMapSchema>
export const stripeMeterIdMapSchema = z
.record(pricingPlanMetricHashSchema, z.string().describe('Stripe Meter id'))
.describe('Map from internal PricingPlanMetric **slug** to Stripe Meter id')
.record(pricingPlanLineItemHashSchema, z.string().describe('Stripe Meter id'))
.describe('Map from internal PricingPlanLineItem **slug** to Stripe Meter id')
.openapi('StripeMeterIdMap')
export type StripeMeterIdMap = z.infer<typeof stripeMeterIdMapSchema>
const commonPricingPlanMetricSchema = z.object({
const commonPricingPlanLineItemSchema = z.object({
/**
* Slugs act as the primary key for metrics. They should be lower and
* kebab-cased ("base", "requests", "image-transformations").
@ -132,21 +132,21 @@ const commonPricingPlanMetricSchema = z.object({
})
/**
* PricingPlanMetrics represent a single line-item in a Stripe Subscription.
* PricingPlanLineItems represent a single line-item in a Stripe Subscription.
*
* They map to a Stripe billing `Price` and possibly a corresponding Stripe
* `Metric` for metered usage.
* `Meter` for metered usage.
*/
export const pricingPlanMetricSchema = z
export const pricingPlanLineItemSchema = z
.discriminatedUnion('usageType', [
commonPricingPlanMetricSchema.merge(
commonPricingPlanLineItemSchema.merge(
z.object({
usageType: z.literal('licensed'),
amount: z.number().nonnegative()
})
),
commonPricingPlanMetricSchema.merge(
commonPricingPlanLineItemSchema.merge(
z.object({
usageType: z.literal('metered'),
unitLabel: z.string().optional(),
@ -189,7 +189,7 @@ export const pricingPlanMetricSchema = z
defaultAggregation: z
.object({
/**
* Specifies how events are aggregated for a Stripe Metric.
* Specifies how events are aggregated for a Stripe Meter.
* Allowed values are `count` to count the number of events, `sum`
* to sum each event's value and `last` to take the last event's
* value in the window.
@ -235,14 +235,14 @@ export const pricingPlanMetricSchema = z
return data
})
.describe(
'PricingPlanMetrics represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Metric` for metered usage.'
'PricingPlanLineItems represent a single line-item in a Stripe Subscription. They map to a Stripe billing `Price` and possibly a corresponding Stripe `Meter` for metered usage.'
)
.openapi('PricingPlanMetric')
export type PricingPlanMetric = z.infer<typeof pricingPlanMetricSchema>
.openapi('PricingPlanLineItem')
export type PricingPlanLineItem = z.infer<typeof pricingPlanLineItemSchema>
/**
* Represents the config for a Stripe subscription with one or more
* PricingPlanMetrics as line-items.
* PricingPlanLineItems as line-items.
*/
export const pricingPlanSchema = z
.object({
@ -261,7 +261,7 @@ export const pricingPlanSchema = z
trialPeriodDays: z.number().nonnegative().optional(),
metricsMap: z
.record(pricingPlanMetricSlugSchema, pricingPlanMetricSchema)
.record(pricingPlanLineItemSlugSchema, pricingPlanLineItemSchema)
.refine((metricsMap) => {
// Stripe Checkout currently supports a max of 20 line items per
// subscription.
@ -279,14 +279,17 @@ export const pricingPlanSchema = z
return data
})
.describe(
'Represents the config for a Stripe subscription with one or more PricingPlanMetrics as line-items.'
'Represents the config for a Stripe subscription with one or more PricingPlanLineItems as line-items.'
)
.openapi('PricingPlan')
export type PricingPlan = z.infer<typeof pricingPlanSchema>
export const stripeProductIdMapSchema = z
.record(pricingPlanMetricSlugSchema, z.string().describe('Stripe Product id'))
.describe('Map from PricingPlanMetric **slug** to Stripe Product id')
.record(
pricingPlanLineItemSlugSchema,
z.string().describe('Stripe Product id')
)
.describe('Map from PricingPlanLineItem **slug** to Stripe Product id')
.openapi('StripeProductIdMap')
export type StripeProductIdMap = z.infer<typeof stripeProductIdMapSchema>

Wyświetl plik

@ -19,8 +19,8 @@ import type { RawProject } from '../types'
import type {
PricingInterval,
PricingPlan,
PricingPlanMap,
PricingPlanMetric
PricingPlanLineItem,
PricingPlanMap
} from './types'
const usernameAndTeamSlugLength = 64 as const
@ -128,18 +128,19 @@ export const { createInsertSchema, createSelectSchema, createUpdateSchema } =
})
/**
* Gets the hash used to uniquely map a PricingPlanMetric to its corresponding
* Stripe Price in a stable way across deployments within a project.
* Gets the hash used to uniquely map a PricingPlanLineItem to its
* corresponding Stripe Price in a stable way across deployments within a
* project.
*
* This hash is used as the key for the `Project._stripePriceIdMap`.
*/
export function getPricingPlanMetricHashForStripePrice({
export function getPricingPlanLineItemHashForStripePrice({
pricingPlan,
pricingPlanMetric,
pricingPlanLineItem,
project
}: {
pricingPlan: PricingPlan
pricingPlanMetric: PricingPlanMetric
pricingPlanLineItem: PricingPlanLineItem
project: RawProject
}) {
// TODO: use pricingPlan.slug as well here?
@ -154,13 +155,13 @@ export function getPricingPlanMetricHashForStripePrice({
// - 'price:requests:<hash>'
const hash = hashObject({
...pricingPlanMetric,
...pricingPlanLineItem,
projectId: project.id,
stripeAccountId: project._stripeAccountId,
currency: project.pricingCurrency
})
return `price:${pricingPlan.slug}:${pricingPlanMetric.slug}:${hash}`
return `price:${pricingPlan.slug}:${pricingPlanLineItem.slug}:${hash}`
}
export function getPricingPlansByInterval({

Wyświetl plik

@ -4,10 +4,10 @@ import pAll from 'p-all'
import { db, eq, type RawDeployment, type RawProject, schema } from '@/db'
import {
getLabelForPricingInterval,
getPricingPlanMetricHashForStripePrice,
getPricingPlanLineItemHashForStripePrice,
getPricingPlansByInterval,
type PricingPlan,
type PricingPlanMetric
type PricingPlanLineItem
} from '@/db/schema'
import { stripe } from '@/lib/stripe'
import { assert } from '@/lib/utils'
@ -46,31 +46,31 @@ export async function upsertStripePricing({
: []
let dirty = false
async function upsertStripeResourcesForPricingPlanMetric({
async function upsertStripeResourcesForPricingPlanLineItem({
pricingPlan,
pricingPlanMetric
pricingPlanLineItem
}: {
pricingPlan: PricingPlan
pricingPlanMetric: PricingPlanMetric
pricingPlanLineItem: PricingPlanLineItem
}) {
const { slug: pricingPlanSlug } = pricingPlan
const { slug: pricingPlanMetricSlug } = pricingPlanMetric
const { slug: pricingPlanLineItemSlug } = pricingPlanLineItem
// Upsert the Stripe Product
if (!project._stripeProductIdMap[pricingPlanMetricSlug]) {
if (!project._stripeProductIdMap[pricingPlanLineItemSlug]) {
const productParams: Stripe.ProductCreateParams = {
name: `${project.id} ${pricingPlanMetricSlug}`,
name: `${project.id} ${pricingPlanLineItemSlug}`,
type: 'service',
metadata: {
projectId: project.id,
pricingPlanMetricSlug
pricingPlanLineItemSlug
}
}
if (pricingPlanMetric.usageType === 'licensed') {
productParams.unit_label = pricingPlanMetric.label
if (pricingPlanLineItem.usageType === 'licensed') {
productParams.unit_label = pricingPlanLineItem.label
} else {
productParams.unit_label = pricingPlanMetric.unitLabel
productParams.unit_label = pricingPlanLineItem.unitLabel
}
const product = await stripe.products.create(
@ -78,23 +78,23 @@ export async function upsertStripePricing({
...stripeConnectParams
)
project._stripeProductIdMap[pricingPlanMetricSlug] = product.id
project._stripeProductIdMap[pricingPlanLineItemSlug] = product.id
dirty = true
}
assert(project._stripeProductIdMap[pricingPlanMetricSlug])
assert(project._stripeProductIdMap[pricingPlanLineItemSlug])
if (pricingPlanMetric.usageType === 'metered') {
if (pricingPlanLineItem.usageType === 'metered') {
// Upsert the Stripe Meter
if (!project._stripeMeterIdMap[pricingPlanMetricSlug]) {
if (!project._stripeMeterIdMap[pricingPlanLineItemSlug]) {
const stripeMeter = await stripe.billing.meters.create(
{
display_name: `${project.id} ${pricingPlanMetric.label || pricingPlanMetricSlug}`,
event_name: `meter-${project.id}-${pricingPlanMetricSlug}`,
display_name: `${project.id} ${pricingPlanLineItem.label || pricingPlanLineItemSlug}`,
event_name: `meter-${project.id}-${pricingPlanLineItemSlug}`,
// TODO: This currently isn't taken into account for the slug, so if it
// changes across deployments, the meter will not be updated.
default_aggregation: {
formula: pricingPlanMetric.defaultAggregation?.formula ?? 'sum'
formula: pricingPlanLineItem.defaultAggregation?.formula ?? 'sum'
},
customer_mapping: {
event_payload_key: 'stripe_customer_id',
@ -107,37 +107,37 @@ export async function upsertStripePricing({
...stripeConnectParams
)
project._stripeMeterIdMap[pricingPlanMetricSlug] = stripeMeter.id
project._stripeMeterIdMap[pricingPlanLineItemSlug] = stripeMeter.id
dirty = true
}
assert(project._stripeMeterIdMap[pricingPlanMetricSlug])
assert(project._stripeMeterIdMap[pricingPlanLineItemSlug])
} else {
assert(pricingPlanMetric.usageType === 'licensed', 400)
assert(pricingPlanLineItem.usageType === 'licensed', 400)
assert(
!project._stripeMeterIdMap[pricingPlanMetricSlug],
!project._stripeMeterIdMap[pricingPlanLineItemSlug],
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": licensed pricing plan metrics cannot replace a previous metered pricing plan metric. Use a different pricing plan metric slug for the new licensed plan.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": licensed pricing plan metrics cannot replace a previous metered pricing plan metric. Use a different pricing plan metric slug for the new licensed plan.`
)
}
const pricingPlanMetricHashForStripePrice =
getPricingPlanMetricHashForStripePrice({
const pricingPlanLineItemHashForStripePrice =
getPricingPlanLineItemHashForStripePrice({
pricingPlan,
pricingPlanMetric,
pricingPlanLineItem,
project
})
// Upsert the Stripe Price
if (!project._stripePriceIdMap[pricingPlanMetricHashForStripePrice]) {
if (!project._stripePriceIdMap[pricingPlanLineItemHashForStripePrice]) {
const interval =
pricingPlanMetric.interval ?? project.defaultPricingInterval
pricingPlanLineItem.interval ?? project.defaultPricingInterval
const nickname = [
'price',
project.id,
pricingPlanMetricSlug,
pricingPlanLineItemSlug,
getLabelForPricingInterval(interval)
]
.filter(Boolean)
@ -145,7 +145,7 @@ export async function upsertStripePricing({
const priceParams: Stripe.PriceCreateParams = {
nickname,
product: project._stripeProductIdMap[pricingPlanMetricSlug],
product: project._stripeProductIdMap[pricingPlanLineItemSlug],
currency: project.pricingCurrency,
recurring: {
interval,
@ -153,35 +153,35 @@ export async function upsertStripePricing({
// TODO: support this
interval_count: 1,
usage_type: pricingPlanMetric.usageType,
usage_type: pricingPlanLineItem.usageType,
meter: project._stripeMeterIdMap[pricingPlanMetricSlug]
meter: project._stripeMeterIdMap[pricingPlanLineItemSlug]
},
metadata: {
projectId: project.id,
pricingPlanMetricSlug
pricingPlanLineItemSlug
}
}
if (pricingPlanMetric.usageType === 'licensed') {
priceParams.unit_amount_decimal = pricingPlanMetric.amount.toFixed(12)
if (pricingPlanLineItem.usageType === 'licensed') {
priceParams.unit_amount_decimal = pricingPlanLineItem.amount.toFixed(12)
} else {
priceParams.billing_scheme = pricingPlanMetric.billingScheme
priceParams.billing_scheme = pricingPlanLineItem.billingScheme
if (pricingPlanMetric.billingScheme === 'tiered') {
if (pricingPlanLineItem.billingScheme === 'tiered') {
assert(
pricingPlanMetric.tiers?.length,
pricingPlanLineItem.tiers?.length,
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": tiered billing schemes must have at least one tier.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": tiered billing schemes must have at least one tier.`
)
assert(
!pricingPlanMetric.transformQuantity,
!pricingPlanLineItem.transformQuantity,
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": tiered billing schemes cannot have transformQuantity.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": tiered billing schemes cannot have transformQuantity.`
)
priceParams.tiers_mode = pricingPlanMetric.tiersMode
priceParams.tiers = pricingPlanMetric.tiers!.map((tierData) => {
priceParams.tiers_mode = pricingPlanLineItem.tiersMode
priceParams.tiers = pricingPlanLineItem.tiers!.map((tierData) => {
const tier: Stripe.PriceCreateParams.Tier = {
up_to: tierData.upTo
}
@ -198,28 +198,28 @@ export async function upsertStripePricing({
})
} else {
assert(
pricingPlanMetric.billingScheme === 'per_unit',
pricingPlanLineItem.billingScheme === 'per_unit',
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": invalid billing scheme.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": invalid billing scheme.`
)
assert(
pricingPlanMetric.unitAmount !== undefined,
pricingPlanLineItem.unitAmount !== undefined,
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": unitAmount is required for per_unit billing schemes.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": unitAmount is required for per_unit billing schemes.`
)
assert(
!pricingPlanMetric.tiers,
!pricingPlanLineItem.tiers,
400,
`Invalid pricing plan metric "${pricingPlanMetricSlug}" for pricing plan "${pricingPlanSlug}": per_unit billing schemes cannot have tiers.`
`Invalid pricing plan metric "${pricingPlanLineItemSlug}" for pricing plan "${pricingPlanSlug}": per_unit billing schemes cannot have tiers.`
)
priceParams.unit_amount_decimal =
pricingPlanMetric.unitAmount.toFixed(12)
pricingPlanLineItem.unitAmount.toFixed(12)
if (pricingPlanMetric.transformQuantity) {
if (pricingPlanLineItem.transformQuantity) {
priceParams.transform_quantity = {
divide_by: pricingPlanMetric.transformQuantity.divideBy,
round: pricingPlanMetric.transformQuantity.round
divide_by: pricingPlanLineItem.transformQuantity.divideBy,
round: pricingPlanLineItem.transformQuantity.round
}
}
}
@ -230,12 +230,12 @@ export async function upsertStripePricing({
...stripeConnectParams
)
project._stripePriceIdMap[pricingPlanMetricHashForStripePrice] =
project._stripePriceIdMap[pricingPlanLineItemHashForStripePrice] =
stripePrice.id
dirty = true
}
assert(project._stripePriceIdMap[pricingPlanMetricHashForStripePrice])
assert(project._stripePriceIdMap[pricingPlanLineItemHashForStripePrice])
}
const upserts: Array<() => Promise<void>> = []
@ -259,11 +259,11 @@ export async function upsertStripePricing({
}
for (const pricingPlan of Object.values(deployment.pricingPlanMap)) {
for (const pricingPlanMetric of Object.values(pricingPlan.metricsMap)) {
for (const pricingPlanLineItem of Object.values(pricingPlan.metricsMap)) {
upserts.push(() =>
upsertStripeResourcesForPricingPlanMetric({
upsertStripeResourcesForPricingPlanLineItem({
pricingPlan,
pricingPlanMetric
pricingPlanLineItem
})
)
}