Files
cfw-autumn/shared/utils/cusProductUtils/convertCusProduct/cusProductToConvertedFeatureOptions.ts
2026-02-08 13:54:33 -08:00

93 lines
3.0 KiB
TypeScript

import type {
FeatureOptions,
FullCusProduct,
} from "@models/cusProductModels/cusProductModels";
import type { EntitlementWithFeature } from "@models/productModels/entModels/entModels";
import type { Price } from "@models/productModels/priceModels/priceModels";
import { roundUsageToNearestBillingUnit } from "@utils/billingUtils/usageUtils/roundUsageToNearestBillingUnit";
import { cusPriceToCusEnt } from "@utils/cusPriceUtils";
import { findPrepaidCusPriceByFeature } from "@utils/cusPriceUtils/findCusPriceUtils/findPrepaidCusPriceByFeature";
import { nullish } from "@utils/utils";
import { Decimal } from "decimal.js";
import { cusProductToFeatureOptions } from "./cusProductToFeatureOptions";
/**
* Converts purchased packs from an old customer product to packs in new billing units.
* Allowance (included usage) is NOT factored in here — it's handled by getStartingBalance.
*/
export const cusProductToConvertedFeatureOptions = ({
cusProduct,
entitlement,
newPrice,
}: {
cusProduct: FullCusProduct;
entitlement: EntitlementWithFeature;
newPrice: Price;
}): FeatureOptions | undefined => {
const feature = entitlement.feature;
const currentOption = cusProductToFeatureOptions({ cusProduct, feature });
if (nullish(currentOption?.quantity)) return undefined;
const oldCusPrice = findPrepaidCusPriceByFeature({
customerPrices: cusProduct.customer_prices,
feature,
});
if (!oldCusPrice)
// If no old price found, we can't interpret the stored quantity
return undefined;
const oldCustomerEntitlement = cusPriceToCusEnt({
cusPrice: oldCusPrice,
cusEnts: cusProduct.customer_entitlements,
});
const oldBillingUnits = oldCusPrice.price.config.billing_units ?? 1;
const newBillingUnits = newPrice.config.billing_units ?? 1;
// 1. Multiply by old billing units to get actual quantity
const actualQuantity = new Decimal(currentOption.quantity)
.mul(oldBillingUnits)
.toNumber();
// 2. Round to nearest new billing unit
const roundedQuantity = roundUsageToNearestBillingUnit({
usage: actualQuantity,
billingUnits: newBillingUnits,
});
// 3. Add current allowance
const oldAllowance = oldCustomerEntitlement?.entitlement?.allowance ?? 0;
const quantityWithOldAllowance = new Decimal(roundedQuantity)
.add(oldAllowance)
.toNumber();
// 4. Subtract new allowance
const newAllowance = entitlement.allowance ?? 0;
const quantityWithoutNewAllowance = Math.max(
0,
new Decimal(quantityWithOldAllowance).sub(newAllowance).toNumber(),
);
// 5. Round to nearest new billing unit
const roundedQuantityWithoutNewAllowance = roundUsageToNearestBillingUnit({
usage: quantityWithoutNewAllowance,
billingUnits: newBillingUnits,
});
// 3. Divide by new billing units
const convertedQuantity = new Decimal(roundedQuantityWithoutNewAllowance)
.div(newBillingUnits)
.toNumber();
// Clamp to 0 minimum - if new allowance exceeds old total, no additional packs needed
const finalQuantity = Math.max(0, convertedQuantity);
return {
internal_feature_id: feature.internal_id,
feature_id: feature.id,
quantity: finalQuantity,
};
};