236 lines
6.4 KiB
TypeScript
236 lines
6.4 KiB
TypeScript
import {
|
|
type ApiPlanItemV0,
|
|
ApiPlanItemV0Schema,
|
|
} from "@api/products/items/apiPlanItemV0.js";
|
|
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
|
import { Infinite } from "@models/productModels/productEnums.js";
|
|
import {
|
|
type ProductItem,
|
|
UsageModel,
|
|
} from "@models/productV2Models/productItemModels/productItemModels.js";
|
|
import { InternalError } from "../../api/models.js";
|
|
import type { Feature } from "../../models/featureModels/featureModels.js";
|
|
import { expandIncludes } from "../expandUtils.js";
|
|
import {
|
|
isBooleanFeature,
|
|
isContUseFeature,
|
|
} from "../featureUtils/convertFeatureUtils.js";
|
|
import { toApiFeature } from "../featureUtils.js";
|
|
import { getProductItemDisplay } from "../productDisplayUtils.js";
|
|
import { isFeaturePriceItem } from "../productV2Utils/productItemUtils/getItemType.js";
|
|
import { itemToBillingInterval } from "../productV2Utils/productItemUtils/itemIntervalUtils.js";
|
|
import { itemIntvToResetIntv } from "./planFeatureIntervals.js";
|
|
|
|
// const getFeaturePriceItemParams = ({
|
|
// item,
|
|
// feature,
|
|
// }: {
|
|
// item: ProductItem;
|
|
// feature: Feature;
|
|
// }) => {
|
|
|
|
// // 1. If
|
|
// // reset_interval: itemIntvToResetIntv(
|
|
// // item.interval!,
|
|
// // ) as ResetInterval,
|
|
// // ...(item.interval_count !== undefined &&
|
|
// // item.interval_count !== null
|
|
// // ? {
|
|
// // reset_interval_count: item.interval_count,
|
|
// // }
|
|
// // : {}),
|
|
// return {
|
|
// interval: item.interval,
|
|
// };
|
|
// };
|
|
|
|
const itemToReset = ({
|
|
item,
|
|
feature,
|
|
}: {
|
|
item: ProductItem;
|
|
feature: Feature;
|
|
}) => {
|
|
// 1. If continuous use or boolean, no reset
|
|
if (isContUseFeature({ feature }) || isBooleanFeature({ feature })) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
interval: itemIntvToResetIntv(item.interval ?? null),
|
|
interval_count:
|
|
item.interval_count !== 1 && typeof item.interval_count === "number"
|
|
? item.interval_count
|
|
: undefined,
|
|
reset_when_enabled: item.reset_usage_when_enabled ?? false,
|
|
} satisfies ApiPlanItemV0["reset"];
|
|
};
|
|
|
|
const itemToPlanFeaturePrice = ({ item }: { item: ProductItem }) => {
|
|
if (!isFeaturePriceItem(item)) {
|
|
return null;
|
|
}
|
|
|
|
const includedUsage =
|
|
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
|
const maxPurchase = item.usage_limit
|
|
? item.usage_limit - includedUsage
|
|
: null;
|
|
|
|
const price =
|
|
item.tiers && item.tiers.length === 1 ? item.tiers[0].amount : item.price;
|
|
|
|
const tiers =
|
|
item.tiers && item.tiers.length > 1
|
|
? item.tiers.map((tier) => ({
|
|
to: tier.to,
|
|
amount: tier.amount,
|
|
}))
|
|
: undefined;
|
|
|
|
return {
|
|
amount: price ?? undefined,
|
|
tiers: tiers,
|
|
|
|
interval: itemToBillingInterval({ item }),
|
|
interval_count:
|
|
item.interval_count !== 1 && typeof item.interval_count === "number"
|
|
? item.interval_count
|
|
: undefined,
|
|
|
|
billing_units: item.billing_units ?? 1,
|
|
usage_model: item.usage_model || UsageModel.PayPerUse,
|
|
max_purchase: maxPurchase,
|
|
} satisfies ApiPlanItemV0["price"];
|
|
};
|
|
|
|
const itemToPlanFeatureRollover = ({ item }: { item: ProductItem }) => {
|
|
if (!item.config?.rollover) return undefined;
|
|
|
|
return {
|
|
max: item.config.rollover.max ?? null,
|
|
expiry_duration_type: item.config.rollover.duration,
|
|
expiry_duration_length: item.config.rollover.length,
|
|
} satisfies ApiPlanItemV0["rollover"];
|
|
};
|
|
|
|
const itemToPlanFeatureProration = ({ item }: { item: ProductItem }) => {
|
|
if (!item.config?.on_increase || !item.config?.on_decrease) return undefined;
|
|
|
|
if (!isFeaturePriceItem(item)) return undefined;
|
|
|
|
return {
|
|
on_increase: item.config.on_increase,
|
|
on_decrease: item.config.on_decrease,
|
|
} satisfies ApiPlanItemV0["proration"];
|
|
};
|
|
|
|
export const itemsToPlanFeatures = ({
|
|
items,
|
|
features,
|
|
expand = [],
|
|
}: {
|
|
items: ProductItem[];
|
|
features: Feature[];
|
|
expand?: string[];
|
|
}): ApiPlanItemV0[] => {
|
|
if (!items) return [];
|
|
|
|
const shouldExpandFeature = expandIncludes({
|
|
expand,
|
|
includes: [CusExpand.PlanFeaturesFeature],
|
|
});
|
|
|
|
return items.map((item) => {
|
|
const feature = features.find((f) => f.id === item.feature_id);
|
|
if (!item.feature_id || !feature) {
|
|
throw new InternalError({
|
|
message:
|
|
"Converting item to plan feature: item has no feature ID or feature not found",
|
|
});
|
|
}
|
|
|
|
// 1. Granted balance
|
|
const grantedBalance =
|
|
item.included_usage === Infinite ? 0 : (item.included_usage ?? 0);
|
|
|
|
const reset = itemToReset({ item, feature });
|
|
const price = itemToPlanFeaturePrice({ item });
|
|
const rollover = itemToPlanFeatureRollover({ item });
|
|
const proration = itemToPlanFeatureProration({ item });
|
|
|
|
// Convert feature to API format if expand requested
|
|
const apiFeature = shouldExpandFeature
|
|
? toApiFeature({ feature })
|
|
: undefined;
|
|
|
|
return ApiPlanItemV0Schema.parse({
|
|
feature_id: item.feature_id,
|
|
feature: apiFeature,
|
|
granted_balance: grantedBalance,
|
|
unlimited: item.included_usage === Infinite,
|
|
|
|
reset,
|
|
price,
|
|
|
|
rollover,
|
|
proration,
|
|
|
|
display: getProductItemDisplay({ item, features }),
|
|
|
|
// Other fields
|
|
// entity_feature_id: item.entity_feature_id,
|
|
} satisfies ApiPlanItemV0);
|
|
});
|
|
};
|
|
|
|
// // Conditionally set reset_interval OR price.interval (mutually exclusive)
|
|
// // If has pricing: interval goes in price
|
|
// // If no pricing: interval goes in reset_interval
|
|
// ...(!hasPrice && item.interval
|
|
// ? {
|
|
// reset_interval: itemIntvToResetIntv(
|
|
// item.interval!,
|
|
// ) as ResetInterval,
|
|
// ...(item.interval_count !== undefined &&
|
|
// item.interval_count !== null
|
|
// ? {
|
|
// reset_interval_count: item.interval_count,
|
|
// }
|
|
// : {}),
|
|
// }
|
|
// : {}),
|
|
|
|
// ...(hasPrice
|
|
// ? (() => {
|
|
// // Check if this is a single tier to infinity (stored as tier but should be flat amount)
|
|
// const isSingleTierToInf =
|
|
// item.tiers &&
|
|
// item.tiers.length === 1 &&
|
|
// item.tiers[0].to === TierInfinite;
|
|
|
|
// return {
|
|
// price: {
|
|
// interval: (item.interval || "month") as BillingInterval,
|
|
// billing_units: item.billing_units ?? 1,
|
|
// usage_model: (item.usage_model || "pay_per_use") as UsageModel,
|
|
// max_purchase: 1,
|
|
// // If single tier to infinity, extract amount; otherwise use item.price
|
|
// amount: isSingleTierToInf
|
|
// ? item.tiers![0].amount
|
|
// : item.price || 0,
|
|
// // Only include tiers if multi-tier
|
|
// ...(item.tiers && item.tiers.length > 1
|
|
// ? {
|
|
// tiers: item.tiers.map((tier) => ({
|
|
// to: tier.to === TierInfinite ? TierInfinite : tier.to,
|
|
// amount: tier.amount,
|
|
// })),
|
|
// }
|
|
// : {}),
|
|
// interval_count: item.interval_count ?? undefined,
|
|
// },
|
|
// };
|
|
// })()
|
|
// : {}),
|