kopia lustrzana https://github.com/transitive-bullshit/chatgpt-api
feat: WIP stripe billing refactor update for 2025
rodzic
ca90a37d8c
commit
b9b3e6c26b
|
@ -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))
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue