Files
cfw-autumn/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts
2025-08-27 17:13:58 -07:00

184 lines
5.2 KiB
TypeScript

import {
AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
APIVersion,
AttachConfig,
AttachScenario,
SuccessCode,
} from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
attachToInsertParams,
isFreeProduct,
} from "@/internal/products/productUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
attachParamsToCurCusProduct,
paramsToCurSub,
paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import {
getCurrentPhaseIndex,
logPhases,
} from "../../mergeUtils/phaseUtils/phaseUtils.js";
export const handleScheduleFunction2 = async ({
req,
res,
attachParams,
config,
skipInsertCusProduct = false,
}: {
req: any;
res: any;
attachParams: AttachParams;
config: AttachConfig;
skipInsertCusProduct?: boolean;
}) => {
const logger = req.logtail;
const product = attachParams.products[0];
const { stripeCli } = attachParams;
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
const curSub = await paramsToCurSub({ attachParams });
const subItems = curSub?.items.data.filter((item) =>
subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item })
);
const expectedEnd = getLatestPeriodEnd({ subItems });
// 1. Cancel current subscription and fetch items from other cus products...?
let schedule = await paramsToCurSubSchedule({ attachParams });
const newProductFree = isFreeProduct(attachParams.prices);
if (schedule) {
const newItems = await paramsToScheduleItems({
req,
schedule: schedule!,
attachParams,
config,
billingPeriodEnd: expectedEnd!,
});
const currentPhaseIndex = getCurrentPhaseIndex({
schedule: { phases: newItems.phases } as any,
now: attachParams.now,
});
if (currentPhaseIndex == newItems.phases.length - 1) {
logger.info(
`SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`
);
await stripeCli.subscriptionSchedules.release(schedule!.id);
await CusProductService.updateByStripeScheduledId({
db: req.db,
stripeScheduledId: schedule!.id,
updates: { scheduled_ids: [] },
});
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
canceled: true,
canceled_at: Date.now(),
ended_at: expectedEnd * 1000,
},
});
schedule = undefined;
} else {
logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`);
schedule = await updateCurSchedule({
req,
attachParams,
schedule,
newPhases: newItems.phases || [],
sub: curSub!,
});
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
scheduled_ids: [schedule!.id],
canceled_at: Date.now(),
canceled: true,
ended_at: expectedEnd * 1000,
},
});
}
} else {
logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
schedule = await subToNewSchedule({
req,
sub: curSub!,
attachParams,
config,
endOfBillingPeriod: expectedEnd!,
});
await CusProductService.update({
db: req.db,
cusProductId: curCusProduct!.id,
updates: {
canceled: true,
canceled_at: Date.now(),
ended_at: expectedEnd * 1000,
},
});
}
if (!schedule) {
logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`);
await stripeCli.subscriptions.update(curSub!.id, {
cancel_at: expectedEnd!,
});
}
if (!skipInsertCusProduct) {
await createFullCusProduct({
db: req.db,
attachParams: attachToInsertParams(attachParams, product),
startsAt: expectedEnd! * 1000,
subscriptionScheduleIds: schedule ? [schedule.id] : [],
nextResetAt: expectedEnd! * 1000,
disableFreeTrial: true,
isDowngrade: true,
scenario: newProductFree
? AttachScenario.Cancel
: AttachScenario.Downgrade,
logger,
});
}
let apiVersion = attachParams.apiVersion || APIVersion.v1;
if (res) {
if (apiVersion >= APIVersion.v1_1) {
res.status(200).json(
AttachResultSchema.parse({
code: SuccessCode.DowngradeScheduled,
message: `Successfully downgraded from ${curCusProduct!.product.name} to ${product.name}`,
product_ids: [product.id],
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
})
);
} else {
res.status(200).json({
success: true,
});
}
}
};