chore: fix update sub volume based bug

This commit is contained in:
Charlie Lamb
2026-03-27 21:46:59 +00:00
parent e9c96e827e
commit 7281372fd3
10 changed files with 340 additions and 151 deletions

View File

@@ -11,6 +11,38 @@ import { STAGGER_ITEM_LAYOUT } from "@/components/forms/update-subscription-v2/c
import type { UseUpdateSubscriptionForm } from "@/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionForm";
import { LAYOUT_TRANSITION } from "@/components/v2/sheets/SharedSheetComponents";
export function getPlanItemPrepaidQuantity({
featureId,
prepaidOptions,
initialPrepaidOptions,
existingOptions,
features,
}: {
featureId: string;
prepaidOptions: Record<string, number>;
initialPrepaidOptions: Record<string, number>;
existingOptions?: FeatureOptions[];
features: Feature[];
}) {
const formQuantity = prepaidOptions[featureId];
if (formQuantity !== undefined) return formQuantity;
const initialQuantity = initialPrepaidOptions[featureId];
if (initialQuantity !== undefined) return initialQuantity;
if (!existingOptions) return undefined;
const featureForOptions = features?.find((f) => f.id === featureId);
if (!featureForOptions) return undefined;
const prepaidOption = featureToOptions({
feature: featureForOptions,
options: existingOptions,
});
return prepaidOption?.quantity;
}
export function PlanItemRow({
item,
index,
@@ -41,17 +73,13 @@ export function PlanItemRow({
const featureId = item.feature_id;
const isPrepaid = item.usage_model === UsageModel.Prepaid;
let currentPrepaidQuantity: number | undefined;
if (isPrepaid) {
currentPrepaidQuantity = prepaidOptions[featureId];
} else if (existingOptions) {
const featureForOptions = features?.find((f) => f.id === featureId);
const prepaidOption = featureToOptions({
feature: featureForOptions,
options: existingOptions,
});
currentPrepaidQuantity = prepaidOption?.quantity;
}
const currentPrepaidQuantity = getPlanItemPrepaidQuantity({
featureId,
prepaidOptions,
initialPrepaidOptions,
existingOptions: isPrepaid ? undefined : existingOptions,
features,
});
const initialPrepaidQuantity = isPrepaid
? initialPrepaidOptions[featureId]

View File

@@ -214,6 +214,8 @@ export function SubscriptionItemRow({
const showPrepaidOutside = isPrepaid && form && featureId && !hasEditableEdit;
const inputQuantity = prepaidQuantity ?? 0;
const billingUnitStep = item.billing_units ?? 1;
const minPrepaidQuantity =
typeof item.included_usage === "number" ? item.included_usage : 0;
const { roundedQuantity, normalizedBillingUnits, shouldShowRoundingHint } =
getPrepaidQuantityTooltipData({
inputQuantity,
@@ -302,7 +304,7 @@ export function SubscriptionItemRow({
{(field) => (
<field.QuantityField
label=""
min={0}
min={minPrepaidQuantity}
step={billingUnitStep}
hideFieldInfo
/>
@@ -345,7 +347,7 @@ export function SubscriptionItemRow({
);
const prepaidTooltipContent = (
<div className="flex flex-col gap-1">
<p>Quantity is exclusive of included usage.</p>
<p>Quantity includes included usage.</p>
{shouldShowRoundingHint && (
<p>
Rounded up to {roundedQuantity} to match {normalizedBillingUnits}-unit

View File

@@ -169,27 +169,6 @@ export function UpdateSubscriptionFormProvider({
const initialPrepaidOptions = defaultValues?.prepaidOptions ?? {};
const initialBillingBehavior = defaultValues?.billingBehavior ?? null;
const hasChanges = useHasSubscriptionChanges({
formValues,
initialPrepaidOptions,
initialBillingBehavior,
prepaidItems,
customerProduct,
currentVersion,
originalItems,
features,
});
const changedPrepaidOptions = useMemo(() => {
const changed: Record<string, number> = {};
for (const [featureId, quantity] of Object.entries(prepaidOptions)) {
if (quantity !== initialPrepaidOptions[featureId]) {
changed[featureId] = quantity;
}
}
return Object.keys(changed).length > 0 ? changed : undefined;
}, [prepaidOptions, initialPrepaidOptions]);
const productWithFormItems = useMemo((): FrontendProduct | undefined => {
if (!effectiveProduct) return undefined;
@@ -203,6 +182,63 @@ export function UpdateSubscriptionFormProvider({
});
}, [effectiveProduct, formValues]);
const currentPrepaidItems = useMemo(
() =>
(productWithFormItems?.items ?? []).filter(
(item) => item.usage_model === "prepaid" && item.feature_id,
),
[productWithFormItems?.items],
);
const normalizedPrepaidOptions = useMemo(() => {
const normalizedOptions = { ...prepaidOptions };
for (const item of currentPrepaidItems) {
if (!item.feature_id) continue;
const minQuantity =
typeof item.included_usage === "number" ? item.included_usage : 0;
const currentQuantity = normalizedOptions[item.feature_id];
if (currentQuantity === undefined || currentQuantity < minQuantity) {
normalizedOptions[item.feature_id] = minQuantity;
}
}
return normalizedOptions;
}, [prepaidOptions, currentPrepaidItems]);
const normalizedFormValues = useMemo(
() => ({
...formValues,
prepaidOptions: normalizedPrepaidOptions,
}),
[formValues, normalizedPrepaidOptions],
);
const hasChanges = useHasSubscriptionChanges({
formValues: normalizedFormValues,
initialPrepaidOptions,
initialBillingBehavior,
prepaidItems,
customerProduct,
currentVersion,
originalItems,
features,
});
const changedPrepaidOptions = useMemo(() => {
const changed: Record<string, number> = {};
for (const [featureId, quantity] of Object.entries(
normalizedPrepaidOptions,
)) {
if (quantity !== initialPrepaidOptions[featureId]) {
changed[featureId] = quantity;
}
}
return Object.keys(changed).length > 0 ? changed : undefined;
}, [normalizedPrepaidOptions, initialPrepaidOptions]);
const baseProduct = useMemo((): FrontendProduct | undefined => {
if (!product) return undefined;
return productV2ToFrontendProduct({ product: product as ProductV2 });
@@ -217,9 +253,9 @@ export function UpdateSubscriptionFormProvider({
return getProductWithSupportedFormValues({
baseProduct: base,
formValues,
formValues: normalizedFormValues,
});
}, [effectiveProduct, formValues]);
}, [effectiveProduct, normalizedFormValues]);
const hasBillingChanges = useHasBillingChanges({
baseProduct: baseProduct as FrontendProduct,
@@ -237,6 +273,7 @@ export function UpdateSubscriptionFormProvider({
const { buildRequestBody } = useUpdateSubscriptionRequestBody({
updateSubscriptionFormContext: formContext,
form,
currentPrepaidItems,
});
// Build the preview body reactively — formValues triggers recomputation,
@@ -340,7 +377,7 @@ export function UpdateSubscriptionFormProvider({
() => ({
formContext,
form,
formValues,
formValues: normalizedFormValues,
features,
trialState,
originalItems,

View File

@@ -1,90 +1,106 @@
import {
type ProductItem,
type UpdateSubscriptionV0Params,
UsageModel,
} from "@autumn/shared";
import type { ProductItem, UpdateSubscriptionV0Params } from "@autumn/shared";
import { useCallback } from "react";
import type { UpdateSubscriptionFormContext } from "../context/UpdateSubscriptionFormProvider";
import { getFreeTrial } from "../utils/getFreeTrial";
import type { UseUpdateSubscriptionForm } from "./useUpdateSubscriptionForm";
type PrepaidItemInput = {
feature_id?: string | null;
feature?: { internal_id?: string } | null;
included_usage?: number | "inf" | null;
};
/** Pure function to build update subscription options from prepaid form values. Extracted for testability. */
export function buildUpdateSubscriptionOptions({
prepaidItems,
prepaidOptions,
initialPrepaidOptions,
items,
initialBackendQuantities,
}: {
prepaidItems: {
feature_id?: string | null;
feature?: { internal_id?: string } | null;
included_usage?: number | "inf" | null;
}[];
prepaidItems: PrepaidItemInput[];
prepaidOptions: Record<string, number>;
initialPrepaidOptions: Record<string, number>;
items?: ProductItem[] | null;
initialBackendQuantities: Record<string, number>;
}): { feature_id: string; quantity: number }[] {
const options = prepaidItems
const getFeatureId = ({ item }: { item: PrepaidItemInput }) =>
item.feature_id ?? item.feature?.internal_id ?? "";
const getIncludedUsage = ({ item }: { item?: PrepaidItemInput | null }) =>
typeof item?.included_usage === "number" ? item.included_usage : 0;
const normalizeQuantity = ({
quantity,
includedUsage,
}: {
quantity: number;
includedUsage: number;
}) => Math.max(quantity, includedUsage);
const getPurchasedQuantity = ({
totalQuantity,
includedUsage,
}: {
totalQuantity: number;
includedUsage: number;
}) => Math.max(0, totalQuantity - includedUsage);
return prepaidItems
.map((item) => {
const featureId = item.feature_id ?? item.feature?.internal_id ?? "";
const featureId = getFeatureId({ item });
const inputQuantity = prepaidOptions[featureId];
const initialQuantity = initialPrepaidOptions[featureId];
const includedUsage =
typeof item.included_usage === "number" ? item.included_usage : 0;
const initialQuantity = initialPrepaidOptions[featureId] ?? 0;
const normalizedInputQuantity =
inputQuantity === undefined || inputQuantity === null
? undefined
: normalizeQuantity({
quantity: inputQuantity,
includedUsage: getIncludedUsage({ item }),
});
const currentIncludedUsage = getIncludedUsage({ item });
const purchasedQuantityChanged =
getPurchasedQuantity({
totalQuantity: normalizedInputQuantity ?? initialQuantity,
includedUsage: currentIncludedUsage,
}) !== (initialBackendQuantities[featureId] ?? 0);
if (
inputQuantity !== undefined &&
inputQuantity !== null &&
normalizedInputQuantity !== undefined &&
normalizedInputQuantity !== null &&
featureId &&
inputQuantity !== initialQuantity
(normalizedInputQuantity !== initialQuantity ||
purchasedQuantityChanged)
) {
return {
feature_id: featureId,
quantity: inputQuantity + includedUsage,
quantity: normalizedInputQuantity,
};
}
return null;
})
.filter((o): o is { feature_id: string; quantity: number } => o !== null);
if (items && items.length > 0) {
const existingFeatureIds = new Set(options.map((o) => o.feature_id));
for (const item of items) {
if (
item.usage_model === UsageModel.Prepaid &&
item.feature_id &&
!existingFeatureIds.has(item.feature_id)
) {
const inputQuantity = prepaidOptions[item.feature_id];
const includedUsage =
typeof item.included_usage === "number" ? item.included_usage : 0;
if (inputQuantity !== undefined && inputQuantity !== null) {
options.push({
feature_id: item.feature_id,
quantity: inputQuantity + includedUsage,
});
}
}
}
}
return options;
}
export function useUpdateSubscriptionRequestBody({
updateSubscriptionFormContext,
form,
currentPrepaidItems,
}: {
updateSubscriptionFormContext: UpdateSubscriptionFormContext;
form: UseUpdateSubscriptionForm;
currentPrepaidItems: ProductItem[];
}) {
const { customerId, product, entityId, customerProduct, prepaidItems } =
const { customerId, product, entityId, customerProduct } =
updateSubscriptionFormContext;
const initialPrepaidOptions =
form.options.defaultValues?.prepaidOptions ?? {};
const initialBackendQuantities = customerProduct.options.reduce(
(acc, option) => {
acc[option.feature_id] = option.quantity;
return acc;
},
{} as Record<string, number>,
);
const initialVersion = form.options.defaultValues?.version;
const buildRequestBody = useCallback((): UpdateSubscriptionV0Params => {
@@ -123,10 +139,10 @@ export function useUpdateSubscriptionRequestBody({
}
const options = buildUpdateSubscriptionOptions({
prepaidItems,
prepaidItems: currentPrepaidItems,
prepaidOptions,
initialPrepaidOptions,
items,
initialBackendQuantities,
});
const freeTrial = getFreeTrial({
@@ -152,9 +168,11 @@ export function useUpdateSubscriptionRequestBody({
entityId,
customerProduct.id,
customerProduct.internal_product_id,
customerProduct.options,
initialVersion,
prepaidItems,
currentPrepaidItems,
initialPrepaidOptions,
initialBackendQuantities,
]);
return { buildRequestBody };

View File

@@ -2,9 +2,9 @@ import type { BillingBehavior } from "@autumn/shared";
import {
AppEnv,
type CreateFreeTrial,
type FeatureOptions,
type ProductItem,
type ProductV2,
UsageModel,
} from "@autumn/shared";
import { useMemo } from "react";
import type {
@@ -78,28 +78,9 @@ export function useUpdateSubscriptionBodyBuilder(
mergedParams.version ??
(storeProduct?.id ? storeProduct.version : undefined);
// Convert prepaidOptions to options array
const options = mergedParams.prepaidOptions
? Object.entries(mergedParams.prepaidOptions).map(
([featureId, quantity]) => {
const prepaidItem = product?.items.find(
(item) =>
item.feature_id === featureId &&
item.usage_model === UsageModel.Prepaid,
);
const includedUsage =
prepaidItem && typeof prepaidItem.included_usage === "number"
? prepaidItem.included_usage
: 0;
return {
feature_id: featureId,
quantity: (quantity || 0) + includedUsage,
};
},
)
: [];
const options = buildLegacyUpdateSubscriptionOptions({
prepaidOptions: mergedParams.prepaidOptions,
});
// Build the body using getUpdateSubscriptionBody (includes freeTrial support)
return getUpdateSubscriptionBody({
@@ -133,3 +114,16 @@ export function useUpdateSubscriptionBodyBuilder(
return { updateSubscriptionBody, buildUpdateSubscriptionBody };
}
export function buildLegacyUpdateSubscriptionOptions({
prepaidOptions,
}: {
prepaidOptions?: Record<string, number>;
}): FeatureOptions[] {
if (!prepaidOptions) return [];
return Object.entries(prepaidOptions).map(([featureId, quantity]) => ({
feature_id: featureId,
quantity: quantity || 0,
}));
}

View File

@@ -13,7 +13,11 @@ export function backendToDisplayQuantity({
prepaidItems,
}: {
backendOptions: { feature_id: string; quantity: number }[];
prepaidItems: { feature_id?: string | null; billing_units?: number | null }[];
prepaidItems: {
feature_id?: string | null;
billing_units?: number | null;
included_usage?: number | "inf" | null;
}[];
}): Record<string, number> {
const backendLookup = backendOptions.reduce(
(acc, option) => {
@@ -28,10 +32,13 @@ export function backendToDisplayQuantity({
if (!item.feature_id) return acc;
const backendQuantity = backendLookup[item.feature_id] ?? 0;
acc[item.feature_id] = getPrepaidDisplayQuantity({
quantity: backendQuantity,
billingUnits: item.billing_units,
});
const includedUsage =
typeof item.included_usage === "number" ? item.included_usage : 0;
acc[item.feature_id] =
getPrepaidDisplayQuantity({
quantity: backendQuantity,
billingUnits: item.billing_units,
}) + includedUsage;
return acc;
},
{} as Record<string, number>,

View File

@@ -0,0 +1,28 @@
import { describe, expect, test } from "bun:test";
import { getPlanItemPrepaidQuantity } from "@/components/forms/shared/plan-items/PlanItemRow";
describe("getPlanItemPrepaidQuantity", () => {
test("should prefer the form quantity over existing backend options", () => {
const result = getPlanItemPrepaidQuantity({
featureId: "AI_CREDITS",
prepaidOptions: { AI_CREDITS: 750 },
initialPrepaidOptions: { AI_CREDITS: 750 },
existingOptions: [{ feature_id: "AI_CREDITS", quantity: 500 }],
features: [],
});
expect(result).toBe(750);
});
test("should fall back to the initial quantity before existing options", () => {
const result = getPlanItemPrepaidQuantity({
featureId: "AI_CREDITS",
prepaidOptions: {},
initialPrepaidOptions: { AI_CREDITS: 750 },
existingOptions: [{ feature_id: "AI_CREDITS", quantity: 500 }],
features: [],
});
expect(result).toBe(750);
});
});

View File

@@ -1,13 +1,13 @@
import { describe, expect, test } from "bun:test";
import { UsageModel } from "@autumn/shared";
import { buildUpdateSubscriptionOptions } from "@/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody";
describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
describe("buildUpdateSubscriptionOptions — included usage handling", () => {
test("should pass display quantities through, not multiply by billing_units", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
initialBackendQuantities: { messages: 1000 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
@@ -18,20 +18,55 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
initialBackendQuantities: { messages: 1000 },
});
// Must NOT be 5,000,000 (5000 * 1000) or 5 (5000 / 1000)
expect(result[0]?.quantity).toBe(5000);
});
test("should add included_usage to quantity", () => {
test("should pass inclusive quantities through unchanged", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 200 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
initialBackendQuantities: { messages: 800 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5200 }]);
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
});
test("should skip unchanged inclusive quantities", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "AI_CREDITS", included_usage: 250 }],
prepaidOptions: { AI_CREDITS: 750 },
initialPrepaidOptions: { AI_CREDITS: 750 },
initialBackendQuantities: { AI_CREDITS: 500 },
});
expect(result).toEqual([]);
});
test("should serialize inclusive 750 as 750", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "AI_CREDITS", included_usage: 250 }],
prepaidOptions: { AI_CREDITS: 750 },
initialPrepaidOptions: { AI_CREDITS: 500 },
initialBackendQuantities: { AI_CREDITS: 250 },
});
expect(result).toEqual([{ feature_id: "AI_CREDITS", quantity: 750 }]);
});
test("should serialize inclusive 1000 as 1000", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "AI_CREDITS", included_usage: 250 }],
prepaidOptions: { AI_CREDITS: 1000 },
initialPrepaidOptions: { AI_CREDITS: 750 },
initialBackendQuantities: { AI_CREDITS: 500 },
});
expect(result).toEqual([{ feature_id: "AI_CREDITS", quantity: 1000 }]);
});
test("should skip items where quantity has not changed", () => {
@@ -39,6 +74,7 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 1000 },
initialPrepaidOptions: { messages: 1000 },
initialBackendQuantities: { messages: 1000 },
});
expect(result).toEqual([]);
@@ -52,63 +88,49 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
],
prepaidOptions: { messages: 10000, tokens: 2500 },
initialPrepaidOptions: { messages: 5000, tokens: 1000 },
initialBackendQuantities: { messages: 5000, tokens: 900 },
});
expect(result).toEqual([
{ feature_id: "messages", quantity: 10000 },
{ feature_id: "tokens", quantity: 2600 },
{ feature_id: "tokens", quantity: 2500 },
]);
});
test("should include new prepaid items from items array that are not in prepaidItems", () => {
test("should include new prepaid items from the current plan", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidItems: [
{ feature_id: "messages", included_usage: 0 },
{ feature_id: "tokens", included_usage: 50 },
],
prepaidOptions: { messages: 5000, tokens: 3000 },
initialPrepaidOptions: { messages: 1000 },
items: [
{
feature_id: "tokens",
usage_model: UsageModel.Prepaid,
included_usage: 50,
},
],
initialBackendQuantities: { messages: 1000 },
});
expect(result).toEqual([
{ feature_id: "messages", quantity: 5000 },
{ feature_id: "tokens", quantity: 3050 },
{ feature_id: "tokens", quantity: 3000 },
]);
});
test("should not duplicate items already in prepaidItems when also in items array", () => {
test("should resend the same total when included usage changes", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
included_usage: 0,
},
],
prepaidItems: [{ feature_id: "AI_CREDITS", included_usage: 500 }],
prepaidOptions: { AI_CREDITS: 750 },
initialPrepaidOptions: { AI_CREDITS: 750 },
initialBackendQuantities: { AI_CREDITS: 500 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
expect(result).toEqual([{ feature_id: "AI_CREDITS", quantity: 750 }]);
});
test("should skip non-prepaid items from items array", () => {
test("should ignore prepaid options for removed current items", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [],
prepaidOptions: { storage: 100 },
initialPrepaidOptions: {},
items: [
{
feature_id: "storage",
usage_model: UsageModel.PayPerUse,
included_usage: 0,
},
],
initialBackendQuantities: { storage: 100 },
});
expect(result).toEqual([]);
@@ -122,6 +144,7 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
],
prepaidOptions: { messages: 1000, tokens: 500 },
initialPrepaidOptions: { messages: 1000, tokens: 500 },
initialBackendQuantities: { messages: 1000, tokens: 500 },
});
expect(result).toEqual([]);
@@ -132,12 +155,24 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
prepaidItems: [{ feature_id: "messages", included_usage: "inf" }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
initialBackendQuantities: { messages: 1000 },
});
// typeof "inf" !== "number", so includedUsage defaults to 0
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
});
test("should clamp totals below the current included usage", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 200 }],
prepaidOptions: { messages: 150 },
initialPrepaidOptions: { messages: 0 },
initialBackendQuantities: { messages: 0 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 200 }]);
});
test("should use feature.internal_id as fallback when feature_id is null", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [
@@ -149,6 +184,7 @@ describe("buildUpdateSubscriptionOptions — billing_units handling", () => {
],
prepaidOptions: { int_messages: 3000 },
initialPrepaidOptions: { int_messages: 1000 },
initialBackendQuantities: { int_messages: 1000 },
});
expect(result).toEqual([{ feature_id: "int_messages", quantity: 3000 }]);

View File

@@ -0,0 +1,18 @@
import { describe, expect, test } from "bun:test";
import { buildLegacyUpdateSubscriptionOptions } from "@/components/forms/update-subscription/use-update-subscription-body-builder";
describe("buildLegacyUpdateSubscriptionOptions", () => {
test("should pass displayed prepaid quantities through unchanged", () => {
const result = buildLegacyUpdateSubscriptionOptions({
prepaidOptions: { AI_CREDITS: 750 },
});
expect(result).toEqual([{ feature_id: "AI_CREDITS", quantity: 750 }]);
});
test("should return an empty array when prepaid options are missing", () => {
const result = buildLegacyUpdateSubscriptionOptions({});
expect(result).toEqual([]);
});
});

View File

@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test";
import {
AppEnv,
getPrepaidDisplayQuantity,
type ProductV2,
UsageModel,
@@ -17,7 +18,7 @@ function makeProduct({ items }: { items: ProductV2["items"] }): ProductV2 {
is_default: false,
version: 1,
group: null,
env: "sandbox" as any,
env: AppEnv.Sandbox,
items,
created_at: Date.now(),
};
@@ -48,6 +49,26 @@ describe("backendToDisplayQuantity", () => {
expect(result).toEqual({ messages: 10000, tokens: 2500 });
});
test("should add included_usage on top of the purchased quantity", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "credits", quantity: 500 }],
prepaidItems: [{ feature_id: "credits", included_usage: 250 }],
});
expect(result).toEqual({ credits: 750 });
});
test("should add included_usage after expanding billing_units", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "credits", quantity: 1 }],
prepaidItems: [
{ feature_id: "credits", billing_units: 500, included_usage: 250 },
],
});
expect(result).toEqual({ credits: 750 });
});
test("should default to billing_units=1 when nullish", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "messages", quantity: 5 }],