Files
cfw-autumn/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNextCyclePreview.ts
John Yeo b4e5edb2d4 wip
2026-01-14 15:15:18 +00:00

74 lines
2.0 KiB
TypeScript

import {
type BillingPreviewResponse,
cp,
cusProductsToPrices,
getCycleEnd,
getSmallestInterval,
sumValues,
} from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv";
import type { BillingContext } from "@/internal/billing/v2/billingContext";
import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan";
import { customerProductToLineItems } from "../lineItems/customerProductToLineItems";
export const billingPlanToNextCyclePreview = ({
ctx,
billingContext,
billingPlan,
}: {
ctx: AutumnContext;
billingContext: BillingContext;
billingPlan: BillingPlan;
}): BillingPreviewResponse["next_cycle"] => {
// 1. Return undefined if billing cycle anchor is now
const { billingCycleAnchorMs } = billingContext;
if (billingCycleAnchorMs === "now") return undefined;
const { insertCustomerProducts, updateCustomerProduct } = billingPlan.autumn;
// 2. Get cycle end and if none, return undefined
const allCustomerProducts = [
...insertCustomerProducts,
...(updateCustomerProduct ? [updateCustomerProduct] : []),
];
const customerProducts = allCustomerProducts.filter(
(customerProduct) =>
cp(customerProduct).paid().recurring().hasActiveStatus().valid,
);
const prices = cusProductsToPrices({
cusProducts: customerProducts,
filters: { excludeOneOffPrices: true },
});
const smallestInterval = getSmallestInterval({ prices });
if (!smallestInterval) return undefined;
const nextCycleStart = getCycleEnd({
anchor: billingCycleAnchorMs,
interval: smallestInterval.interval,
intervalCount: smallestInterval.intervalCount,
now: billingContext.currentEpochMs,
});
const autumnLineItems = customerProducts.flatMap((customerProduct) =>
customerProductToLineItems({
ctx,
customerProduct: customerProduct,
billingContext: {
...billingContext,
currentEpochMs: nextCycleStart,
},
direction: "charge",
}),
);
const total = sumValues(autumnLineItems.map((line) => line.finalAmount));
return {
starts_at: nextCycleStart,
total,
};
};