restructured context
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import type {
|
||||
Entitlement,
|
||||
FeatureOptions,
|
||||
FullCusProduct,
|
||||
FullProduct,
|
||||
Price,
|
||||
} from "@autumn/shared";
|
||||
import type { FullCustomer } from "@shared/models/cusModels/fullCusModel";
|
||||
import type Stripe from "stripe";
|
||||
@@ -32,6 +34,10 @@ export interface BillingContext {
|
||||
|
||||
// Feature quantities
|
||||
featureQuantities: FeatureOptions[];
|
||||
|
||||
// Unforunately, need to add custom prices, custom entitlements and free trial here, because it's determined in the setup step.
|
||||
customPrices: Price[];
|
||||
customEnts: Entitlement[];
|
||||
}
|
||||
|
||||
export interface UpdateSubscriptionBillingContext extends BillingContext {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { FullProduct, ProductItem } from "@autumn/shared";
|
||||
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
|
||||
import { getEntsWithFeature } from "../../../../products/entitlements/entitlementUtils";
|
||||
import { handleNewProductItems } from "../../../../products/product-items/productItemUtils/handleNewProductItems";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems";
|
||||
|
||||
export const computeCustomFullProduct = async ({
|
||||
export const setupCustomFullProduct = async ({
|
||||
ctx,
|
||||
customItems,
|
||||
currentFullProduct,
|
||||
@@ -11,10 +11,10 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { paramsToFeatureOptions } from "@/internal/billing/v2/compute/computeAutumnUtils/paramsToFeatureOptions";
|
||||
|
||||
/**
|
||||
* Parses feature quantities from params, iterating over all prepaid prices.
|
||||
* Sets up feature quantities context from params, iterating over all prepaid prices.
|
||||
* For each prepaid price, uses new quantity from params or falls back to existing subscription.
|
||||
*/
|
||||
export const parseFeatureQuantitiesParams = ({
|
||||
export const setupFeatureQuantitiesContext = ({
|
||||
ctx,
|
||||
featureQuantitiesParams,
|
||||
fullProduct,
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
|
||||
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
||||
import { CusService } from "@server/internal/customers/CusService";
|
||||
|
||||
export const setupFullCustomerContext = async ({
|
||||
ctx,
|
||||
params,
|
||||
autoCreateCustomer = false,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
params: UpdateSubscriptionV0Params;
|
||||
autoCreateCustomer?: boolean;
|
||||
}) => {
|
||||
const { db, org, env } = ctx;
|
||||
const { customer_id: customerId } = params;
|
||||
|
||||
const fullCustomer = await CusService.getFull({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
withSubs: true,
|
||||
withEntities: true,
|
||||
entityId: params.entity_id ?? undefined,
|
||||
});
|
||||
|
||||
return fullCustomer;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
CusProductStatus,
|
||||
cusProductToProduct,
|
||||
type UpdateSubscriptionV0Params,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@server/honoUtils/HonoEnv";
|
||||
@@ -9,8 +8,6 @@ import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutum
|
||||
import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan";
|
||||
import { computeCustomPlanFreeTrial } from "@/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanFreeTrial";
|
||||
import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct";
|
||||
import { parseFeatureQuantitiesParams } from "@/internal/billing/v2/utils/parseFeatureQuantitiesParams";
|
||||
import { computeCustomFullProduct } from "../../../compute/computeAutumnUtils/computeCustomFullProduct";
|
||||
|
||||
export const computeCustomPlan = async ({
|
||||
ctx,
|
||||
@@ -21,23 +18,10 @@ export const computeCustomPlan = async ({
|
||||
updateSubscriptionContext: UpdateSubscriptionBillingContext;
|
||||
params: UpdateSubscriptionV0Params;
|
||||
}) => {
|
||||
const { customerProduct } = updateSubscriptionContext;
|
||||
const { customerProduct, customPrices, customEnts } =
|
||||
updateSubscriptionContext;
|
||||
|
||||
const currentFullProduct = cusProductToProduct({
|
||||
cusProduct: customerProduct,
|
||||
});
|
||||
|
||||
const {
|
||||
fullProduct: customFullProduct,
|
||||
customPrices,
|
||||
customEnts,
|
||||
} = await computeCustomFullProduct({
|
||||
ctx,
|
||||
currentFullProduct,
|
||||
customItems: params.items,
|
||||
});
|
||||
|
||||
updateSubscriptionContext.fullProducts = [customFullProduct];
|
||||
const customFullProduct = updateSubscriptionContext.fullProducts[0];
|
||||
|
||||
// 2. Compute the custom trial details
|
||||
const { freeTrialPlan, customFreeTrial } = computeCustomPlanFreeTrial({
|
||||
@@ -50,13 +34,6 @@ export const computeCustomPlan = async ({
|
||||
updateSubscriptionContext.billingCycleAnchorMs = freeTrialPlan.trialEndsAt;
|
||||
}
|
||||
|
||||
updateSubscriptionContext.featureQuantities = parseFeatureQuantitiesParams({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct: customFullProduct,
|
||||
currentCustomerProduct: customerProduct,
|
||||
}); // re-parse feature quantities for new custom product
|
||||
|
||||
// 3. Compute the new customer product
|
||||
const newFullCustomerProduct = computeCustomPlanNewCustomerProduct({
|
||||
ctx,
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import {
|
||||
cusProductToProduct,
|
||||
InternalError,
|
||||
secondsToMs,
|
||||
type UpdateSubscriptionV0Params,
|
||||
} from "@autumn/shared";
|
||||
import { secondsToMs, type UpdateSubscriptionV0Params } from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext";
|
||||
import { CusService } from "../../../../customers/CusService";
|
||||
import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext";
|
||||
import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext";
|
||||
import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext";
|
||||
import type { UpdateSubscriptionBillingContext } from "../../billingContext";
|
||||
import { parseFeatureQuantitiesParams } from "../../utils/parseFeatureQuantitiesParams";
|
||||
import { findTargetCustomerProduct } from "./findTargetCustomerProduct";
|
||||
|
||||
/**
|
||||
* Fetch the context for updating a subscription
|
||||
@@ -24,32 +19,23 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
||||
ctx: AutumnContext;
|
||||
params: UpdateSubscriptionV0Params;
|
||||
}): Promise<UpdateSubscriptionBillingContext> => {
|
||||
const { db, org, env } = ctx;
|
||||
const { customer_id: customerId, product_id: productId } = params;
|
||||
|
||||
const fullCustomer = await CusService.getFull({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
withSubs: true,
|
||||
withEntities: true,
|
||||
entityId: params.entity_id ?? undefined,
|
||||
});
|
||||
|
||||
const targetCustomerProduct = findTargetCustomerProduct({
|
||||
const fullCustomer = await setupFullCustomerContext({
|
||||
ctx,
|
||||
params,
|
||||
fullCustomer,
|
||||
});
|
||||
|
||||
if (!targetCustomerProduct) {
|
||||
throw new InternalError({
|
||||
message: `[API Subscription Update] Target customer product not found: ${productId}`,
|
||||
const { customerProduct, fullProduct, customPrices, customEnts } =
|
||||
await setupUpdateSubscriptionProductContext({
|
||||
ctx,
|
||||
fullCustomer,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
const fullProduct = cusProductToProduct({
|
||||
cusProduct: targetCustomerProduct,
|
||||
const featureQuantities = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
currentCustomerProduct: customerProduct,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -61,14 +47,7 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
||||
} = await setupStripeBillingContext({
|
||||
ctx,
|
||||
fullCustomer,
|
||||
targetCustomerProduct,
|
||||
});
|
||||
|
||||
const featureQuantities = parseFeatureQuantitiesParams({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
currentCustomerProduct: targetCustomerProduct,
|
||||
targetCustomerProduct: customerProduct,
|
||||
});
|
||||
|
||||
const currentEpochMs = testClockFrozenTime ?? Date.now();
|
||||
@@ -88,7 +67,7 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
||||
return {
|
||||
fullCustomer,
|
||||
fullProducts: [fullProduct],
|
||||
customerProduct: targetCustomerProduct,
|
||||
customerProduct,
|
||||
stripeSubscription,
|
||||
stripeSubscriptionSchedule,
|
||||
stripeCustomer,
|
||||
@@ -98,5 +77,8 @@ export const setupUpdateSubscriptionBillingContext = async ({
|
||||
billingCycleAnchorMs: billingCycleAnchorMs ?? "now",
|
||||
invoiceMode,
|
||||
featureQuantities,
|
||||
|
||||
customPrices,
|
||||
customEnts,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
cusProductToProduct,
|
||||
type FullCustomer,
|
||||
InternalError,
|
||||
type UpdateSubscriptionV0Params,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { setupCustomFullProduct } from "../../setup/setupCustomFullProduct";
|
||||
import { findTargetCustomerProduct } from "./findTargetCustomerProduct";
|
||||
|
||||
export const setupUpdateSubscriptionProductContext = async ({
|
||||
ctx,
|
||||
fullCustomer,
|
||||
params,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
fullCustomer: FullCustomer;
|
||||
params: UpdateSubscriptionV0Params;
|
||||
}) => {
|
||||
const targetCustomerProduct = findTargetCustomerProduct({
|
||||
params,
|
||||
fullCustomer,
|
||||
});
|
||||
|
||||
if (!targetCustomerProduct) {
|
||||
throw new InternalError({
|
||||
message: `[API Subscription Update] Target customer product not found: ${params.product_id}`,
|
||||
});
|
||||
}
|
||||
|
||||
const fullProduct = cusProductToProduct({
|
||||
cusProduct: targetCustomerProduct,
|
||||
});
|
||||
|
||||
const {
|
||||
fullProduct: customFullProduct,
|
||||
customPrices,
|
||||
customEnts,
|
||||
} = await setupCustomFullProduct({
|
||||
ctx,
|
||||
currentFullProduct: fullProduct,
|
||||
customItems: params.items,
|
||||
});
|
||||
|
||||
return {
|
||||
customerProduct: targetCustomerProduct,
|
||||
fullProduct: customFullProduct,
|
||||
customPrices,
|
||||
customEnts,
|
||||
};
|
||||
};
|
||||
@@ -12,7 +12,7 @@ import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
/**
|
||||
* @deprecated Use parseFeatureQuantitiesParams from billing/v2/utils instead
|
||||
* @deprecated Use setupFeatureQuantitiesContext from billing/v2/setup instead
|
||||
*/
|
||||
export const mapOptionsList = ({
|
||||
optionsInput,
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
} from "@tests/utils/mockUtils/priceMocks";
|
||||
import { createMockFullProduct } from "@tests/utils/mockUtils/productMocks";
|
||||
import chalk from "chalk";
|
||||
import { parseFeatureQuantitiesParams } from "@/internal/billing/v2/utils/parseFeatureQuantitiesParams";
|
||||
import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext";
|
||||
|
||||
// ============ TESTS ============
|
||||
|
||||
describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
|
||||
describe("basic quantity inheritance", () => {
|
||||
test("1. current has quantity, new params has none → uses current", () => {
|
||||
const feature = createMockFeature({
|
||||
@@ -47,7 +47,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -81,7 +81,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -123,7 +123,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -203,7 +203,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
features: [creditsFeature, seatsFeature, storageFeature],
|
||||
});
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -246,7 +246,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -282,7 +282,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -317,7 +317,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -373,7 +373,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -429,7 +429,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -477,7 +477,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -517,7 +517,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -542,7 +542,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -570,7 +570,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
const ctx = createMockCtx({ features: [] });
|
||||
|
||||
expect(() =>
|
||||
parseFeatureQuantitiesParams({
|
||||
setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -601,7 +601,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -643,7 +643,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -686,7 +686,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
@@ -731,7 +731,7 @@ describe(chalk.yellowBright("parseFeatureQuantitiesParams"), () => {
|
||||
|
||||
const ctx = createMockCtx({ features: [feature] });
|
||||
|
||||
const result = parseFeatureQuantitiesParams({
|
||||
const result = setupFeatureQuantitiesContext({
|
||||
ctx,
|
||||
featureQuantitiesParams: params,
|
||||
fullProduct,
|
||||
|
||||
Reference in New Issue
Block a user