Files
cfw-autumn/shared/api/customers/cusFeatures/changes/V1.2_CusFeatureChange.ts
2026-02-09 08:15:46 -08:00

309 lines
7.4 KiB
TypeScript

import { ApiFeatureType } from "@api/features/prevVersions/apiFeatureV0.js";
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
import {
AffectedResource,
defineVersionChange,
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
import type { EntInterval } from "@models/productModels/intervals/entitlementInterval.js";
import { resetIntvToEntIntv } from "@utils/productV2Utils/productItemUtils/convertProductItem/planItemIntervals.js";
import { Decimal } from "decimal.js";
import type { z } from "zod/v4";
import { FeatureType } from "../../../../models/featureModels/featureEnums.js";
import { sumValues } from "../../../../utils/utils.js";
import type { ApiFeatureV1 } from "../../../features/apiFeatureV1.js";
import {
type ApiBalance,
type ApiBalanceBreakdown,
ApiBalanceSchema,
} from "../apiBalance.js";
import {
type CusFeatureLegacyData,
CusFeatureLegacyDataSchema,
} from "../cusFeatureLegacyData.js";
import {
type ApiCusFeatureV3Breakdown,
ApiCusFeatureV3Schema,
} from "../previousVersions/apiCusFeatureV3.js";
// overage_allowed: z.boolean().nullish().meta({
// description: "Whether overage usage beyond the limit is allowed",
// example: true,
// }),
/**
* Transform feature from V2.0 format to V1.2 format
* Exported so it can be reused in other transformations (e.g., V1_2_CustomerChange)
*/
const resetToV3IntervalParams = ({
input,
feature,
unlimited,
}: {
input: ApiBalance | ApiBalanceBreakdown;
feature?: ApiFeatureV1;
unlimited: boolean;
}): {
interval: EntInterval | "multiple" | null;
interval_count: number | null;
next_reset_at: number | null;
} => {
const isBoolean = feature?.type === FeatureType.Boolean;
// 1. No reset
if (!input.reset)
return {
interval: null,
interval_count: isBoolean || unlimited ? null : 1,
next_reset_at: null,
};
// 2. Multiple interval
if (input.reset?.interval === "multiple") {
return {
interval: "multiple",
interval_count: null,
next_reset_at: null,
};
}
// 3. Single interval
return {
interval: resetIntvToEntIntv({ resetIntv: input.reset?.interval }),
interval_count: input.reset?.interval_count || 1,
next_reset_at: input.reset?.resets_at,
};
};
const toV3Type = ({ feature }: { feature?: ApiFeatureV1 }) => {
if (feature?.type === FeatureType.Boolean) {
return ApiFeatureType.Static;
} else if (feature?.type === FeatureType.Metered) {
if (feature.consumable) {
return ApiFeatureType.SingleUsage;
} else {
return ApiFeatureType.ContinuousUse;
}
} else if (feature?.type === FeatureType.CreditSystem) {
return ApiFeatureType.SingleUsage;
} else {
return ApiFeatureType.Static;
}
};
const toV3BalanceParams = ({
input,
feature,
unlimited,
legacyData,
isBreakdown = false,
}: {
input: ApiBalance | ApiBalanceBreakdown;
feature?: ApiFeatureV1;
unlimited: boolean;
legacyData?: CusFeatureLegacyData;
isBreakdown?: boolean;
}) => {
const isBoolean = feature?.type === FeatureType.Boolean;
if (isBoolean || unlimited) {
return {
includedUsage: 0,
balance: 0,
usage: 0,
overageAllowed: false,
usageLimit: undefined,
};
}
let prepaidQuantity = 0;
if (isBreakdown) {
prepaidQuantity = (input as ApiBalanceBreakdown).prepaid_quantity ?? 0;
} else {
prepaidQuantity = sumValues(
(input as ApiBalance).breakdown?.map((b) => b.prepaid_quantity) ?? [],
);
}
let overage = 0;
if (isBreakdown) {
overage = input.overage_allowed
? new Decimal(input.purchased_balance).toNumber()
: 0;
} else {
overage = new Decimal(input.purchased_balance)
.sub(prepaidQuantity)
.toNumber();
}
// 1. Get included usage
const includedUsage = new Decimal(input.granted_balance)
.add(prepaidQuantity)
.toNumber();
// 2. Balance
const balance = new Decimal(input.current_balance).sub(overage).toNumber();
// 3. Usage
const usage = new Decimal(input.usage).toNumber();
// 4. Overage allowed
let overageAllowed = input.overage_allowed ?? false;
if (overageAllowed && input.max_purchase) {
overageAllowed = false;
}
// 5. Usage limit
const usageLimit = input.max_purchase
? new Decimal(input.max_purchase).add(includedUsage).toNumber()
: undefined;
// 6. Expires at
const expiresAt =
"expires_at" in input ? (input as ApiBalanceBreakdown).expires_at : null;
return {
includedUsage,
balance,
usage,
overageAllowed,
usageLimit,
expiresAt,
};
};
export function transformBalanceToCusFeatureV3({
input,
legacyData,
}: {
input: z.infer<typeof ApiBalanceSchema>;
legacyData?: CusFeatureLegacyData;
}): z.infer<typeof ApiCusFeatureV3Schema> {
// 1. Is boolean feature
const feature = input.feature;
const isUnlimited = input.unlimited;
const { interval, interval_count, next_reset_at } = resetToV3IntervalParams({
input,
feature,
unlimited: isUnlimited,
});
const { includedUsage, balance, usage, overageAllowed, usageLimit } =
toV3BalanceParams({
input,
feature,
unlimited: isUnlimited,
legacyData,
});
let newBreakdown: ApiCusFeatureV3Breakdown[] | undefined;
if (input.breakdown && input.breakdown.length > 0) {
newBreakdown = input.breakdown.map((breakdown) => {
// const interval = resetIntvToEntIntv({
// resetIntv: breakdown.reset_interval,
// });
const { interval, interval_count, next_reset_at } =
resetToV3IntervalParams({
input: breakdown,
feature,
unlimited: isUnlimited,
});
const {
includedUsage,
balance,
usage,
overageAllowed,
usageLimit,
expiresAt,
} = toV3BalanceParams({
input: breakdown,
feature,
unlimited: isUnlimited,
legacyData,
isBreakdown: true,
});
return {
interval: interval === "multiple" || !interval ? null : interval,
interval_count: interval_count,
balance: balance,
usage: usage,
included_usage: includedUsage,
next_reset_at: next_reset_at,
usage_limit: usageLimit,
overage_allowed: overageAllowed,
expires_at: expiresAt,
} satisfies ApiCusFeatureV3Breakdown;
});
}
// 1. Included usage: granted_balance, or if prepaid, granted_balance + purchased_balance (?)
return {
id: input.feature_id,
type: toV3Type({ feature }),
name: input.feature?.name ?? null,
unlimited: input.unlimited,
included_usage: includedUsage,
balance: balance,
usage: usage,
usage_limit: usageLimit,
interval: interval,
interval_count: interval_count,
next_reset_at: next_reset_at,
overage_allowed: overageAllowed,
credit_schema: input.feature?.credit_schema
? input.feature.credit_schema.map((credit) => ({
feature_id: credit.metered_feature_id,
credit_amount: credit.credit_cost,
}))
: undefined,
breakdown: newBreakdown,
rollovers: input.rollovers,
};
}
/**
* V1_2_CusFeatureChange: Transforms customer feature response TO V1_2 format
*
* Applied when: targetVersion <= V1_2
*
* Breaking changes introduced in V2.0:
*
* 1. Simplified schema with minimal required fields
* 2. Optional feature object for expanded data
* 3. Removed verbose metadata and display fields
*
* Input: ApiCusFeature (V2.0+ format)
* Output: ApiCusFeatureV3 (V1.2 format)
*/
const V1_2_CusFeatureChange = defineVersionChange({
newVersion: ApiVersion.V2_0,
oldVersion: ApiVersion.V1_2,
description: [
"Simplified customer feature schema",
"Added optional feature object",
"Removed verbose fields",
],
affectedResources: [AffectedResource.Customer],
newSchema: ApiBalanceSchema,
oldSchema: ApiCusFeatureV3Schema,
legacyDataSchema: CusFeatureLegacyDataSchema,
transformResponse: transformBalanceToCusFeatureV3,
});