Billing - Migrate from Stripe metering (#20298)
**Overall strategy** **1. Introduce “Billing V2” behind a workspace flag** Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing workspaces stay on the old behavior until they’re migrated or explicitly on V2. **2. Replace workflow metered SKUs with a resource-credit product** Conceptually, billable “workflow execution” usage is not the primary subscription line item anymore. Add a RESOURCE_CREDIT product (and keep WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and limits are expressed through credit buckets (e.g. price metadata like credit_amount), so one product can represent pooled credits instead of a narrow workflow-only meter. **3. Migrate subscriptions in two layers** Schema/catalog: persist extra price metadata (instance upgrade) so the server knows credit amounts and can match Stripe prices to the new model. Per workspace: the registered workspace command upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT prices (using existing Stripe schedule + BillingSubscriptionUpdateService stack), then treats the workspace as V2 (flag). Workspaces without that legacy item or without a subscription are skipped. **4. Unify subscription lifecycle + usage on the server** **5. Refresh the product surface in Settings** Test : - [x] Subscribe v1 + Update subscribe + Migrate - [x] Subscribe v2 + Update subscribe
This commit is contained in:
@@ -15,6 +15,8 @@ import { mockedPublicWorkspaceDataBySubdomain } from '~/testing/mock-data/public
|
||||
import { mockedUserData } from '~/testing/mock-data/users';
|
||||
|
||||
import { GET_PUBLIC_WORKSPACE_DATA_BY_DOMAIN } from '@/auth/graphql/queries/getPublicWorkspaceDataByDomain';
|
||||
import { BILLING_PORTAL_SESSION } from '@/settings/billing/graphql/queries/billingPortalSession';
|
||||
import { GET_METERED_PRODUCTS_USAGE } from '@/settings/billing/graphql/queries/getMeteredProductsUsage';
|
||||
import { LIST_PLANS } from '@/settings/billing/graphql/queries/listPlans';
|
||||
import { GET_ROLES } from '@/settings/roles/graphql/queries/getRolesQuery';
|
||||
import { mockBillingPlans } from '~/testing/mock-data/billing-plans';
|
||||
@@ -528,6 +530,42 @@ export const graphqlMocks = {
|
||||
data: mockBillingPlans,
|
||||
});
|
||||
}),
|
||||
graphql.query(getOperationName(GET_METERED_PRODUCTS_USAGE) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
getMeteredProductsUsage: [
|
||||
{
|
||||
__typename: 'BillingMeteredProductUsage',
|
||||
productKey: 'WORKFLOW_NODE_EXECUTION',
|
||||
usedCredits: 1000,
|
||||
grantedCredits: 500000,
|
||||
rolloverCredits: 0,
|
||||
totalGrantedCredits: 500000,
|
||||
unitPriceCents: 1,
|
||||
},
|
||||
{
|
||||
__typename: 'BillingMeteredProductUsage',
|
||||
productKey: 'RESOURCE_CREDIT',
|
||||
usedCredits: 0,
|
||||
grantedCredits: 10000,
|
||||
rolloverCredits: 0,
|
||||
totalGrantedCredits: 10000,
|
||||
unitPriceCents: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}),
|
||||
graphql.query(getOperationName(BILLING_PORTAL_SESSION) ?? '', () => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
billingPortalSession: {
|
||||
__typename: 'BillingSession',
|
||||
url: 'https://billing.stripe.com/p/mock-portal-session',
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
http.get('https://chat-assets.frontapp.com/v1/chat.bundle.js', () => {
|
||||
return HttpResponse.text(
|
||||
`
|
||||
|
||||
@@ -5,7 +5,7 @@ export const mockBillingPlans = {
|
||||
{
|
||||
__typename: 'BillingPlan',
|
||||
planKey: 'PRO',
|
||||
licensedProducts: [
|
||||
baseProducts: [
|
||||
{
|
||||
__typename: 'BillingLicensedProduct',
|
||||
name: 'Pro Plan',
|
||||
@@ -24,6 +24,7 @@ export const mockBillingPlans = {
|
||||
unitAmount: 1200,
|
||||
recurringInterval: 'Month',
|
||||
priceUsageType: 'LICENSED',
|
||||
creditAmount: null,
|
||||
},
|
||||
{
|
||||
__typename: 'BillingPriceLicensed',
|
||||
@@ -31,10 +32,12 @@ export const mockBillingPlans = {
|
||||
unitAmount: 10800,
|
||||
recurringInterval: 'Year',
|
||||
priceUsageType: 'LICENSED',
|
||||
creditAmount: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
resourceCreditProducts: [],
|
||||
meteredProducts: [
|
||||
{
|
||||
__typename: 'BillingMeteredProduct',
|
||||
@@ -295,7 +298,7 @@ export const mockBillingPlans = {
|
||||
{
|
||||
__typename: 'BillingPlan',
|
||||
planKey: 'ENTERPRISE',
|
||||
licensedProducts: [
|
||||
baseProducts: [
|
||||
{
|
||||
__typename: 'BillingLicensedProduct',
|
||||
name: 'Organization Plan',
|
||||
@@ -314,6 +317,7 @@ export const mockBillingPlans = {
|
||||
unitAmount: 2500,
|
||||
recurringInterval: 'Month',
|
||||
priceUsageType: 'LICENSED',
|
||||
creditAmount: null,
|
||||
},
|
||||
{
|
||||
__typename: 'BillingPriceLicensed',
|
||||
@@ -321,10 +325,12 @@ export const mockBillingPlans = {
|
||||
unitAmount: 22800,
|
||||
recurringInterval: 'Year',
|
||||
priceUsageType: 'LICENSED',
|
||||
creditAmount: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
resourceCreditProducts: [],
|
||||
meteredProducts: [
|
||||
{
|
||||
__typename: 'BillingMeteredProduct',
|
||||
|
||||
@@ -49,7 +49,7 @@ export const workspaceLogoUrl =
|
||||
|
||||
// Extract Pro monthly base product from mockBillingPlans to use in workspace billing mocks
|
||||
const PRO_PLAN = mockBillingPlans.listPlans.find((p) => p.planKey === 'PRO')!;
|
||||
const PRO_BASE_LICENSED_PRODUCT = PRO_PLAN?.licensedProducts?.[0]!;
|
||||
const PRO_BASE_LICENSED_PRODUCT = PRO_PLAN?.baseProducts?.[0]!;
|
||||
const PRO_BASE_MONTHLY_PRICE = PRO_BASE_LICENSED_PRODUCT?.prices?.find(
|
||||
(pr) => pr.recurringInterval === 'Month',
|
||||
)!;
|
||||
|
||||
Reference in New Issue
Block a user