chore(wip): initial scaffolding of update sub v2
This commit is contained in:
@@ -5,8 +5,8 @@ import { getFreeTrialForAttach } from "./fetchAutumnUtils/getFreeTrialForAttach"
|
||||
import { getProductsForAttach } from "./fetchAutumnUtils/getProductsForAttach";
|
||||
import { overrideProduct } from "./fetchAutumnUtils/overrideProduct";
|
||||
import { resolveAttachActions } from "./fetchAutumnUtils/resolveAttachActions/resolveAttachActions";
|
||||
import { fetchStripeCustomerForAttach } from "./fetchStripeUtils/fetchStripeCustomerForAttach";
|
||||
import { fetchStripeSubForAttach } from "./fetchStripeUtils/fetchStripeSubForAttach";
|
||||
import { fetchStripeCustomerForBilling } from "./fetchStripeUtils/fetchStripeCustomerForBilling";
|
||||
import { fetchStripeSubscriptionForBilling } from "./fetchStripeUtils/fetchStripeSubscriptionForBilling";
|
||||
|
||||
export const fetchAttachContext = async ({
|
||||
ctx,
|
||||
@@ -67,17 +67,19 @@ export const fetchAttachContext = async ({
|
||||
// 5. Get sub update context
|
||||
|
||||
// 5. Get stripe sub
|
||||
const stripeSub = await fetchStripeSubForAttach({
|
||||
const stripeSub = await fetchStripeSubscriptionForBilling({
|
||||
ctx,
|
||||
fullCus,
|
||||
products: newFullProducts,
|
||||
});
|
||||
|
||||
// 6. Get stripe customer
|
||||
const { stripeCus, paymentMethod, now } = await fetchStripeCustomerForAttach({
|
||||
ctx,
|
||||
fullCus,
|
||||
});
|
||||
const { stripeCus, paymentMethod, now } = await fetchStripeCustomerForBilling(
|
||||
{
|
||||
ctx,
|
||||
fullCus,
|
||||
},
|
||||
);
|
||||
|
||||
const cusProductActions = resolveAttachActions({
|
||||
fullCus,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../../../../external/stripe/stripeCusUtils";
|
||||
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
|
||||
|
||||
export const fetchStripeCustomerForAttach = async ({
|
||||
export const fetchStripeCustomerForBilling = async ({
|
||||
ctx,
|
||||
fullCus,
|
||||
}: {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { createStripeCli } from "../../../../../external/connect/createStripeCli";
|
||||
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
|
||||
|
||||
export const fetchStripeSubForAttach = async ({
|
||||
export const fetchStripeSubscriptionForBilling = async ({
|
||||
ctx,
|
||||
fullCus,
|
||||
products,
|
||||
@@ -1,22 +1,8 @@
|
||||
import {
|
||||
cusProductToProduct,
|
||||
InternalError,
|
||||
SubscriptionUpdateV0ParamsSchema,
|
||||
secondsToMs,
|
||||
} from "@autumn/shared";
|
||||
import { SubscriptionUpdateV0ParamsSchema } from "@autumn/shared";
|
||||
import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||
import { CusService } from "../../../customers/CusService";
|
||||
import { EntitlementService } from "../../../products/entitlements/EntitlementService";
|
||||
import { PriceService } from "../../../products/prices/PriceService";
|
||||
import { cusProductToExistingUsages } from "../../billingUtils/handleExistingUsages/cusProductToExistingUsages";
|
||||
import { initFullCusProduct } from "../../billingUtils/initFullCusProduct/initFullCusProduct";
|
||||
import { buildAutumnLineItems } from "../compute/computeAutumnUtils/buildAutumnLineItems";
|
||||
import { buildStripeSubAction } from "../compute/computeStripeUtils/buildStripeSubAction";
|
||||
import { executeCusProductActions } from "../execute/executeAutumnActions/executeCusProductActions";
|
||||
import { executeStripeSubAction } from "../execute/executeStripeSubAction";
|
||||
import { overrideProduct } from "../fetch/fetchAutumnUtils/overrideProduct";
|
||||
import { fetchStripeCustomerForAttach } from "../fetch/fetchStripeUtils/fetchStripeCustomerForAttach";
|
||||
import { fetchStripeSubForAttach } from "../fetch/fetchStripeUtils/fetchStripeSubForAttach";
|
||||
import { computeSubscriptionUpdatePlan } from "../subscriptionUpdate/compute/computeSubscriptionUpdatePlan";
|
||||
import { executeSubscriptionUpdate } from "../subscriptionUpdate/execute/executeSubscriptionUpdate";
|
||||
import { fetchApiSubscriptionUpdateContext } from "../subscriptionUpdate/fetch/fetchApiSubscriptionUpdateContext";
|
||||
|
||||
export const handleApiSubscriptionUpdate = createRoute({
|
||||
body: SubscriptionUpdateV0ParamsSchema,
|
||||
@@ -24,140 +10,20 @@ export const handleApiSubscriptionUpdate = createRoute({
|
||||
const ctx = c.get("ctx");
|
||||
const body = c.req.valid("json");
|
||||
|
||||
const { db, org, env } = ctx;
|
||||
const { customer_id: customerId, product_id: planId } = body;
|
||||
|
||||
// 1. Fetch the context
|
||||
/**
|
||||
* const apiSubscriptionUpdateContext = await fetchApiSubscriptionUpdateContext({
|
||||
ctx,
|
||||
body,
|
||||
});
|
||||
1. Full customer
|
||||
2. Target customer product
|
||||
3. Stripe subscription (if applicable)
|
||||
4. Stripe schedule (if applicable)
|
||||
5. Stripe customer
|
||||
6. Payment method (if applicable)
|
||||
7. Test clock frozen time (if applicable)
|
||||
8. shouldDoUpdateQuantity -- if feature quantities is passed in, and there are no custom items, then we go down update quantity plan, if not do the compute update subscription plan
|
||||
|
||||
* if (apiSubscriptionUpdateContext.intent === "update_quantity") {
|
||||
* const updateQuantityPlan = computeUpdateQuantityPlan({ ctx, context });
|
||||
* } else if (apiSubscriptionUpdateContext.intent === "update_plan") {
|
||||
* const updatePlanPlan = computeUpdateSubscriptionPlan({ ctx, context });
|
||||
* }
|
||||
* await executeBillingPlan({ ctx, plan });
|
||||
*/
|
||||
|
||||
const fullCus = await CusService.getFull({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
withSubs: true,
|
||||
withEntities: true,
|
||||
});
|
||||
|
||||
// 1. Find target customer product
|
||||
const targetCusProduct = fullCus.customer_products.find(
|
||||
(cp) => cp.product.id === planId,
|
||||
const updateSubscriptionContext = await fetchApiSubscriptionUpdateContext(
|
||||
ctx,
|
||||
body,
|
||||
);
|
||||
|
||||
if (!targetCusProduct) {
|
||||
throw new InternalError({
|
||||
message: `[api subscription update] Target cus product not found: ${planId}`,
|
||||
});
|
||||
}
|
||||
|
||||
const ongoingCusProductAction = {
|
||||
action: "expire" as const,
|
||||
cusProduct: targetCusProduct,
|
||||
};
|
||||
|
||||
const curFullProduct = cusProductToProduct({
|
||||
cusProduct: targetCusProduct!,
|
||||
const subscriptionUpdatePlan = computeSubscriptionUpdatePlan(ctx, {
|
||||
updateSubscriptionContext,
|
||||
params: body,
|
||||
});
|
||||
|
||||
const {
|
||||
fullProducts: [newFullProduct],
|
||||
customPrices,
|
||||
customEnts,
|
||||
} = await overrideProduct({
|
||||
ctx,
|
||||
newItems: body.items,
|
||||
products: [curFullProduct],
|
||||
});
|
||||
|
||||
await EntitlementService.insert({
|
||||
db,
|
||||
data: customEnts,
|
||||
});
|
||||
|
||||
await PriceService.insert({
|
||||
db,
|
||||
data: customPrices,
|
||||
});
|
||||
|
||||
const newCusProduct = initFullCusProduct({
|
||||
ctx,
|
||||
fullCus,
|
||||
initContext: {
|
||||
fullCus,
|
||||
product: newFullProduct,
|
||||
featureQuantities: [],
|
||||
replaceables: [],
|
||||
existingUsages: cusProductToExistingUsages({
|
||||
cusProduct: targetCusProduct,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Get stripe subscription
|
||||
const stripeSub = await fetchStripeSubForAttach({
|
||||
ctx,
|
||||
fullCus,
|
||||
products: [],
|
||||
targetCusProductId: targetCusProduct.id,
|
||||
});
|
||||
|
||||
const { stripeCus, paymentMethod, testClockFrozenTime } =
|
||||
await fetchStripeCustomerForAttach({
|
||||
ctx,
|
||||
fullCus,
|
||||
});
|
||||
|
||||
const billingCycleAnchor = secondsToMs(stripeSub?.billing_cycle_anchor);
|
||||
|
||||
// 2. Cases: update feature quantity, update plan entirely
|
||||
const autumnLineItems = buildAutumnLineItems({
|
||||
ctx,
|
||||
newCusProducts: [newCusProduct],
|
||||
ongoingCusProductAction,
|
||||
billingCycleAnchor,
|
||||
testClockFrozenTime,
|
||||
});
|
||||
|
||||
const stripeSubAction = buildStripeSubAction({
|
||||
ctx,
|
||||
stripeSub: stripeSub!,
|
||||
fullCus,
|
||||
paymentMethod,
|
||||
ongoingCusProductAction,
|
||||
newCusProducts: [newCusProduct],
|
||||
});
|
||||
|
||||
ctx.logger.info("Executing stripe sub action");
|
||||
await executeStripeSubAction({
|
||||
ctx,
|
||||
stripeSubAction,
|
||||
});
|
||||
|
||||
ctx.logger.info("Executing cus product actions");
|
||||
await executeCusProductActions({
|
||||
ctx,
|
||||
ongoingCusProductAction,
|
||||
newCusProducts: [newCusProduct],
|
||||
await executeSubscriptionUpdate(ctx, {
|
||||
params: body,
|
||||
updateSubscriptionContext,
|
||||
subscriptionUpdatePlan,
|
||||
});
|
||||
|
||||
return c.json({ success: true }, 200);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// const {
|
||||
// fullProducts: [newFullProduct],
|
||||
// customPrices,
|
||||
// customEnts,
|
||||
// } = await overrideProduct({
|
||||
// ctx,
|
||||
// newItems: body.items,
|
||||
// products: [product],
|
||||
// });
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { SubscriptionUpdateV0Params } from "@shared/index";
|
||||
import { SubscriptionUpdateIntentEnum } from "./computeSubscriptionUpdateSchema";
|
||||
|
||||
/**
|
||||
* Compute the intent for a subscription update
|
||||
* @param params - The parameters for the subscription update
|
||||
* @returns The intent for the subscription update
|
||||
*/
|
||||
export const computeSubscriptionUpdateIntent = (
|
||||
params: SubscriptionUpdateV0Params,
|
||||
): SubscriptionUpdateIntentEnum => {
|
||||
if (params.options?.length && !params.items?.length)
|
||||
return SubscriptionUpdateIntentEnum.UpdateQuantity;
|
||||
return SubscriptionUpdateIntentEnum.UpdatePlan;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { SubscriptionUpdateV0Params } from "@shared/index";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import type { SubscriptionUpdatePlan } from "../../types";
|
||||
import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema";
|
||||
import { computeSubscriptionUpdateIntent } from "./computeSubscriptionUpdateIntent";
|
||||
import { getComputeSubscriptionUpdatePlanIntentMap } from "./computeSubscriptionUpdatePlanIntentMap";
|
||||
|
||||
/**
|
||||
* Compute the subscription update plan
|
||||
* @param ctx - The context
|
||||
* @param params - The parameters for the subscription update
|
||||
* @returns The subscription update plan
|
||||
*/
|
||||
export const computeSubscriptionUpdatePlan = (
|
||||
ctx: AutumnContext,
|
||||
{
|
||||
updateSubscriptionContext,
|
||||
params,
|
||||
}: {
|
||||
updateSubscriptionContext: UpdateSubscriptionContext;
|
||||
params: SubscriptionUpdateV0Params;
|
||||
},
|
||||
): SubscriptionUpdatePlan => {
|
||||
const intent = computeSubscriptionUpdateIntent(params);
|
||||
const computePlan = getComputeSubscriptionUpdatePlanIntentMap(intent);
|
||||
|
||||
return computePlan(ctx, { updateSubscriptionContext, params });
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
ErrCode,
|
||||
RecaseError,
|
||||
type SubscriptionUpdateV0Params,
|
||||
} from "@shared/index";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import type { SubscriptionUpdatePlan } from "../../types";
|
||||
import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema";
|
||||
import { computeSubscriptionUpdateQuantityPlan } from "./computeSubscriptionUpdateQuantityPlan";
|
||||
import { SubscriptionUpdateIntentEnum } from "./computeSubscriptionUpdateSchema";
|
||||
|
||||
export type ComputeSubscriptionUpdatePlan = (
|
||||
ctx: AutumnContext,
|
||||
{
|
||||
updateSubscriptionContext,
|
||||
params,
|
||||
}: {
|
||||
updateSubscriptionContext: UpdateSubscriptionContext;
|
||||
params: SubscriptionUpdateV0Params;
|
||||
},
|
||||
) => SubscriptionUpdatePlan;
|
||||
|
||||
export type ComputeSubscriptionUpdatePlanIntentMap = Partial<
|
||||
Record<SubscriptionUpdateIntentEnum, ComputeSubscriptionUpdatePlan>
|
||||
>;
|
||||
|
||||
/**
|
||||
* Map of intent to function to compute the subscription update plan
|
||||
*/
|
||||
const computeSubscriptionUpdatePlanIntentMap: ComputeSubscriptionUpdatePlanIntentMap =
|
||||
{
|
||||
[SubscriptionUpdateIntentEnum.UpdateQuantity]:
|
||||
computeSubscriptionUpdateQuantityPlan,
|
||||
};
|
||||
|
||||
export const getComputeSubscriptionUpdatePlanIntentMap = (
|
||||
intent: SubscriptionUpdateIntentEnum,
|
||||
): ComputeSubscriptionUpdatePlan => {
|
||||
const plan = computeSubscriptionUpdatePlanIntentMap[intent];
|
||||
|
||||
if (!plan) {
|
||||
throw new RecaseError({
|
||||
message: `[Compute Subscription Update] Invalid intent: ${intent}`,
|
||||
code: ErrCode.InvalidInputs,
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
return plan;
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import {
|
||||
OngoingCusProductActionEnum,
|
||||
type SubscriptionUpdateV0Params,
|
||||
secondsToMs,
|
||||
} from "@shared/index";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { cusProductToExistingUsages } from "@/internal/billing/billingUtils/handleExistingUsages/cusProductToExistingUsages";
|
||||
import { initFullCusProduct } from "@/internal/billing/billingUtils/initFullCusProduct/initFullCusProduct";
|
||||
import { buildAutumnLineItems } from "../../compute/computeAutumnUtils/buildAutumnLineItems";
|
||||
import { buildStripeSubAction } from "../../compute/computeStripeUtils/buildStripeSubAction";
|
||||
import {
|
||||
SubscriptionUpdateQuantityAction,
|
||||
type SubscriptionUpdateQuantityPlan,
|
||||
} from "../../types";
|
||||
import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema";
|
||||
import { SubscriptionUpdateIntentEnum } from "./computeSubscriptionUpdateSchema";
|
||||
|
||||
export const computeSubscriptionUpdateQuantityPlan = (
|
||||
ctx: AutumnContext,
|
||||
{
|
||||
updateSubscriptionContext,
|
||||
params,
|
||||
}: {
|
||||
updateSubscriptionContext: UpdateSubscriptionContext;
|
||||
params: SubscriptionUpdateV0Params;
|
||||
},
|
||||
): SubscriptionUpdateQuantityPlan => {
|
||||
const { options } = params;
|
||||
const {
|
||||
customerProduct,
|
||||
fullCustomer,
|
||||
stripeSubscription,
|
||||
testClockFrozenTime,
|
||||
product,
|
||||
paymentMethod,
|
||||
} = updateSubscriptionContext;
|
||||
|
||||
const featureQuantities = {
|
||||
old: customerProduct.options,
|
||||
new: options || [],
|
||||
};
|
||||
|
||||
const isUpgrade =
|
||||
featureQuantities.new[0].quantity > featureQuantities.old[0].quantity;
|
||||
|
||||
const action = isUpgrade
|
||||
? SubscriptionUpdateQuantityAction.Upgrade
|
||||
: SubscriptionUpdateQuantityAction.Downgrade;
|
||||
|
||||
const billingCycleAnchor = secondsToMs(
|
||||
stripeSubscription?.billing_cycle_anchor,
|
||||
);
|
||||
|
||||
const ongoingCusProductAction = {
|
||||
action: OngoingCusProductActionEnum.Expire,
|
||||
cusProduct: customerProduct,
|
||||
};
|
||||
|
||||
const autumnLineItems = buildAutumnLineItems({
|
||||
ctx,
|
||||
newCusProducts: [customerProduct],
|
||||
ongoingCusProductAction,
|
||||
billingCycleAnchor,
|
||||
testClockFrozenTime,
|
||||
});
|
||||
|
||||
const newCustomerProduct = initFullCusProduct({
|
||||
ctx,
|
||||
fullCus: fullCustomer,
|
||||
initContext: {
|
||||
fullCus: fullCustomer,
|
||||
product,
|
||||
featureQuantities: [],
|
||||
replaceables: [],
|
||||
existingUsages: cusProductToExistingUsages({
|
||||
cusProduct: customerProduct,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const stripeSubscriptionAction = buildStripeSubAction({
|
||||
ctx,
|
||||
stripeSub: stripeSubscription!,
|
||||
fullCus: fullCustomer,
|
||||
paymentMethod,
|
||||
ongoingCusProductAction,
|
||||
newCusProducts: [newCustomerProduct],
|
||||
});
|
||||
|
||||
return {
|
||||
intent: SubscriptionUpdateIntentEnum.UpdateQuantity,
|
||||
customEntitlements: [],
|
||||
customPrices: [],
|
||||
featureQuantities,
|
||||
action,
|
||||
autumnLineItems,
|
||||
stripeSubscriptionAction,
|
||||
ongoingCusProductAction,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum SubscriptionUpdateIntentEnum {
|
||||
UpdateQuantity = "update_quantity",
|
||||
UpdatePlan = "update_plan",
|
||||
}
|
||||
|
||||
export const ComputeSubscriptionUpdateResultSchema = z.object({
|
||||
intent: SubscriptionUpdateIntentEnum,
|
||||
});
|
||||
|
||||
export type ComputeSubscriptionUpdateResult = z.infer<
|
||||
typeof ComputeSubscriptionUpdateResultSchema
|
||||
>;
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { SubscriptionUpdateV0Params } from "@shared/index";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService";
|
||||
import { PriceService } from "@/internal/products/prices/PriceService";
|
||||
import { executeCusProductActions } from "../../execute/executeAutumnActions/executeCusProductActions";
|
||||
import { executeStripeSubAction } from "../../execute/executeStripeSubAction";
|
||||
import type { SubscriptionUpdatePlan } from "../../types";
|
||||
import type { UpdateSubscriptionContext } from "../fetch/updateSubscriptionContextSchema";
|
||||
|
||||
export const executeSubscriptionUpdate = async (
|
||||
ctx: AutumnContext,
|
||||
{
|
||||
params,
|
||||
updateSubscriptionContext,
|
||||
subscriptionUpdatePlan,
|
||||
}: {
|
||||
params: SubscriptionUpdateV0Params;
|
||||
updateSubscriptionContext: UpdateSubscriptionContext;
|
||||
subscriptionUpdatePlan: SubscriptionUpdatePlan;
|
||||
},
|
||||
) => {
|
||||
const { db, logger } = ctx;
|
||||
const { customerProduct } = updateSubscriptionContext;
|
||||
const {
|
||||
customEntitlements,
|
||||
customPrices,
|
||||
ongoingCusProductAction,
|
||||
stripeSubscriptionAction,
|
||||
} = subscriptionUpdatePlan;
|
||||
|
||||
await EntitlementService.insert({
|
||||
db,
|
||||
data: customEntitlements,
|
||||
});
|
||||
|
||||
await PriceService.insert({
|
||||
db,
|
||||
data: customPrices,
|
||||
});
|
||||
|
||||
logger.info("Executing stripe sub action");
|
||||
await executeStripeSubAction({
|
||||
ctx,
|
||||
stripeSubAction: stripeSubscriptionAction,
|
||||
});
|
||||
|
||||
logger.info("Executing cus product actions");
|
||||
await executeCusProductActions({
|
||||
ctx,
|
||||
ongoingCusProductAction,
|
||||
newCusProducts: [customerProduct],
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
cusProductToProduct,
|
||||
InternalError,
|
||||
type SubscriptionUpdateV0Params,
|
||||
} from "@shared/index";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { CusService } from "../../../../customers/CusService";
|
||||
import { fetchStripeCustomerForBilling } from "../../fetch/fetchStripeUtils/fetchStripeCustomerForBilling";
|
||||
import { fetchStripeSubscriptionForBilling } from "../../fetch/fetchStripeUtils/fetchStripeSubscriptionForBilling";
|
||||
import type { UpdateSubscriptionContext } from "./updateSubscriptionContextSchema";
|
||||
|
||||
/**
|
||||
* Fetch the context for updating a subscription
|
||||
* @param ctx - The context
|
||||
* @param body - The body of the request
|
||||
* @returns The update subscription context
|
||||
* @example
|
||||
* const context = await fetchApiSubscriptionUpdateContext(ctx, params);
|
||||
*
|
||||
* Returns:
|
||||
* 1. Full customer
|
||||
* 2. Target customer product
|
||||
* 3. Stripe subscription (if applicable)
|
||||
* 4. Stripe schedule (if applicable)
|
||||
* 5. Stripe customer
|
||||
* 6. Payment method (if applicable)
|
||||
* 7. Test clock frozen time (if applicable)
|
||||
*/
|
||||
export const fetchApiSubscriptionUpdateContext = async (
|
||||
ctx: AutumnContext,
|
||||
params: SubscriptionUpdateV0Params,
|
||||
): Promise<UpdateSubscriptionContext> => {
|
||||
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,
|
||||
});
|
||||
|
||||
const targetCustomerProduct = fullCustomer.customer_products.find(
|
||||
(cp) => cp.product.id === productId,
|
||||
);
|
||||
|
||||
if (!targetCustomerProduct) {
|
||||
throw new InternalError({
|
||||
message: `[API Subscription Update] Target customer product not found: ${productId}`,
|
||||
});
|
||||
}
|
||||
|
||||
const targetProduct = cusProductToProduct({
|
||||
cusProduct: targetCustomerProduct,
|
||||
});
|
||||
|
||||
const stripeSubscription = await fetchStripeSubscriptionForBilling({
|
||||
ctx,
|
||||
fullCus: fullCustomer,
|
||||
products: [],
|
||||
targetCusProductId: targetCustomerProduct.id,
|
||||
});
|
||||
|
||||
const {
|
||||
stripeCus: stripeCustomer,
|
||||
paymentMethod,
|
||||
testClockFrozenTime,
|
||||
} = await fetchStripeCustomerForBilling({
|
||||
ctx,
|
||||
fullCus: fullCustomer,
|
||||
});
|
||||
|
||||
return {
|
||||
fullCustomer,
|
||||
product: targetProduct,
|
||||
customerProduct: targetCustomerProduct,
|
||||
stripeSubscription,
|
||||
stripeCustomer,
|
||||
paymentMethod,
|
||||
testClockFrozenTime,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { FullCusProduct, FullCustomer, FullProduct } from "@shared/index";
|
||||
import type Stripe from "stripe";
|
||||
|
||||
export type UpdateSubscriptionContext = {
|
||||
fullCustomer: FullCustomer;
|
||||
product: FullProduct;
|
||||
customerProduct: FullCusProduct;
|
||||
stripeSubscription?: Stripe.Subscription;
|
||||
stripeSubscriptionSchedule?: Stripe.SubscriptionSchedule;
|
||||
stripeCustomer: Stripe.Customer;
|
||||
paymentMethod?: Stripe.PaymentMethod;
|
||||
testClockFrozenTime?: number;
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import type {
|
||||
AttachBodyV1,
|
||||
Entitlement,
|
||||
FeatureOptions,
|
||||
FreeTrial,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
LineItem,
|
||||
OngoingCusProductAction,
|
||||
Price,
|
||||
ScheduledCusProductAction,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
@@ -67,3 +70,31 @@ export type AttachPlan = {
|
||||
stripeSubAction: StripeSubAction;
|
||||
stripeInvoiceAction?: StripeInvoiceAction;
|
||||
};
|
||||
|
||||
export type BillingPlan = {
|
||||
intent: "attach" | "update_quantity" | "update_plan" | "cancel" | "one_off";
|
||||
};
|
||||
|
||||
export type BaseSubscriptionUpdatePlan = BillingPlan & {
|
||||
intent: "update_quantity" | "update_plan";
|
||||
customEntitlements: Entitlement[];
|
||||
customPrices: Price[];
|
||||
autumnLineItems: LineItem[];
|
||||
stripeSubscriptionAction: StripeSubAction;
|
||||
ongoingCusProductAction: OngoingCusProductAction;
|
||||
};
|
||||
|
||||
export enum SubscriptionUpdateQuantityAction {
|
||||
Upgrade = "upgrade",
|
||||
Downgrade = "downgrade",
|
||||
}
|
||||
|
||||
export type SubscriptionUpdateQuantityPlan = BaseSubscriptionUpdatePlan & {
|
||||
featureQuantities: {
|
||||
old: FeatureOptions[];
|
||||
new: FeatureOptions[];
|
||||
};
|
||||
action: SubscriptionUpdateQuantityAction;
|
||||
};
|
||||
|
||||
export type SubscriptionUpdatePlan = SubscriptionUpdateQuantityPlan;
|
||||
|
||||
@@ -35,6 +35,6 @@
|
||||
"@utils/*": ["../shared/utils/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src", "tests", "scripts", "emails", "experiments"],
|
||||
"include": ["src", "tests", "scripts", "emails", "experiments", "src/internal/billing/v2/subscriptionUpdate", "../shared/api/billing/subscriptionUpdate/compute", "../shared/api/billing/subscriptionUpdate/fetch"],
|
||||
"exclude": ["node_modules", "dist", "tests/archives"]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { FeatureOptionsSchema } from "../../../models/cusProductModels/cusProduc
|
||||
import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels";
|
||||
import { CustomerDataSchema } from "../../common/customerData";
|
||||
import { EntityDataSchema } from "../../models";
|
||||
|
||||
export const SubscriptionUpdateV0ParamsSchema = z.object({
|
||||
// Customer / Entity Info
|
||||
customer_id: z.string(),
|
||||
@@ -12,14 +13,14 @@ export const SubscriptionUpdateV0ParamsSchema = z.object({
|
||||
customer_data: CustomerDataSchema.optional(),
|
||||
entity_data: EntityDataSchema.optional(),
|
||||
|
||||
options: z.array(FeatureOptionsSchema).nullish(),
|
||||
options: z.array(FeatureOptionsSchema).nullish(), // used for update quantity etc (in api - feature_quantities)
|
||||
|
||||
invoice: z.boolean().optional(),
|
||||
enable_product_immediately: z.boolean().optional(),
|
||||
finalize_invoice: z.boolean().optional(),
|
||||
|
||||
// Reset billing cycle anchor?
|
||||
items: z.array(ProductItemSchema).optional(),
|
||||
items: z.array(ProductItemSchema).optional(), // used for custom configuration of a plan (in api - plan_override)
|
||||
reset_billing_cycle_anchor: z.boolean().optional(),
|
||||
new_billing_subscription: z.boolean().optional(),
|
||||
prorate_billing: z.boolean().optional(),
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import z from "zod/v4";
|
||||
import { FullCusProductSchema } from "../cusProductModels/cusProductModels";
|
||||
|
||||
export enum OngoingCusProductActionEnum {
|
||||
Expire = "expire",
|
||||
Cancel = "cancel",
|
||||
Uncancel = "uncancel",
|
||||
}
|
||||
|
||||
// What happens to the CURRENT active cus product
|
||||
export const OngoingCusProductActionSchema = z.object({
|
||||
action: z.literal(["expire", "cancel", "uncancel"]),
|
||||
action: z.enum(OngoingCusProductActionEnum),
|
||||
cusProduct: FullCusProductSchema,
|
||||
});
|
||||
export type OngoingCusProductAction = z.infer<
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
"@utils/*": ["./utils/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./**/*"],
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../server/src/internal/billing/v2/update-subscription"
|
||||
],
|
||||
"types": ["node"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user