fix: normalize billing request items
This commit is contained in:
@@ -10,6 +10,7 @@ import type {
|
||||
TrialOnEnd,
|
||||
} from "@autumn/shared";
|
||||
import { useMemo } from "react";
|
||||
import { normalizeBillingRequestItems } from "@/components/forms/shared/utils/normalizeBillingRequestItems";
|
||||
import { getFreeTrial } from "@/components/forms/update-subscription-v2/utils/getFreeTrial";
|
||||
import { convertPrepaidOptionsToFeatureOptions } from "@/utils/billing/prepaidQuantityUtils";
|
||||
import type { FormCustomLineItem } from "../attachFormSchema";
|
||||
@@ -103,10 +104,13 @@ export function buildAttachRequestBody({
|
||||
}
|
||||
|
||||
if (items !== null) {
|
||||
body.items = items.map((item) => ({
|
||||
...item,
|
||||
interval: (item.interval ?? null) as ProductItemInterval | null,
|
||||
}));
|
||||
const normalizedItems = normalizeBillingRequestItems({ items });
|
||||
if (normalizedItems) {
|
||||
body.items = normalizedItems.map((item) => ({
|
||||
...item,
|
||||
interval: (item.interval ?? null) as ProductItemInterval | null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (version !== undefined) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ProductItem } from "@autumn/shared";
|
||||
|
||||
type DraftProductItem = Omit<ProductItem, "price"> & {
|
||||
price?: ProductItem["price"] | "";
|
||||
};
|
||||
|
||||
const isEmptyStandalonePriceDraft = (item: DraftProductItem) =>
|
||||
item.price === "" &&
|
||||
item.feature_id == null &&
|
||||
item.price_id == null &&
|
||||
item.entitlement_id == null &&
|
||||
item.price_config == null &&
|
||||
!item.tiers?.length;
|
||||
|
||||
export function normalizeBillingRequestItems({
|
||||
items,
|
||||
}: {
|
||||
items?: ProductItem[] | null;
|
||||
}): ProductItem[] | undefined {
|
||||
if (!items?.length) return undefined;
|
||||
|
||||
const normalizedItems = (items as DraftProductItem[]).flatMap((item) => {
|
||||
if (isEmptyStandalonePriceDraft(item)) return [];
|
||||
if (item.price !== "") return [item as ProductItem];
|
||||
|
||||
const { price: _price, ...itemWithoutDraftPrice } = item;
|
||||
return [itemWithoutDraftPrice as ProductItem];
|
||||
});
|
||||
|
||||
return normalizedItems.length > 0 ? normalizedItems : undefined;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
UpdateSubscriptionV0Params,
|
||||
} from "@autumn/shared";
|
||||
import { useCallback } from "react";
|
||||
import { normalizeBillingRequestItems } from "@/components/forms/shared/utils/normalizeBillingRequestItems";
|
||||
import type { UpdateSubscriptionFormContext } from "../context/UpdateSubscriptionFormProvider";
|
||||
import { getFreeTrial } from "../utils/getFreeTrial";
|
||||
import type { UseUpdateSubscriptionForm } from "./useUpdateSubscriptionForm";
|
||||
@@ -181,7 +182,7 @@ export function useUpdateSubscriptionRequestBody({
|
||||
...base,
|
||||
options: options.length > 0 ? options : undefined,
|
||||
free_trial: freeTrial,
|
||||
items: items && items.length > 0 ? items : undefined,
|
||||
items: normalizeBillingRequestItems({ items }),
|
||||
version: version !== initialVersion ? version : undefined,
|
||||
billing_behavior: billingBehavior || undefined,
|
||||
billing_cycle_anchor: resetBillingCycle ? "now" : undefined,
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type ProductV2, UsageModel } from "@autumn/shared";
|
||||
import {
|
||||
type ProductItem,
|
||||
ProductItemInterval,
|
||||
type ProductV2,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { addDays } from "date-fns";
|
||||
import { buildAttachRequestBody } from "@/components/forms/attach-v2/hooks/useAttachRequestBody";
|
||||
|
||||
@@ -222,3 +227,44 @@ describe("buildAttachRequestBody — starts_at handling", () => {
|
||||
expect(result?.starts_at).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildAttachRequestBody — items serialization", () => {
|
||||
test("drops empty fixed-price draft rows before serialization", () => {
|
||||
const product = makeProduct({
|
||||
items: [{ price: 20, interval: ProductItemInterval.Month }],
|
||||
});
|
||||
const items = [
|
||||
{
|
||||
feature_id: "AI_CREDITS",
|
||||
price: null,
|
||||
tiers: [{ amount: 0.01, to: "inf" }],
|
||||
},
|
||||
{
|
||||
price: "" as unknown as number,
|
||||
feature_id: null,
|
||||
price_id: null,
|
||||
entitlement_id: null,
|
||||
interval: ProductItemInterval.Month,
|
||||
interval_count: 1,
|
||||
tiers: null,
|
||||
price_config: null,
|
||||
},
|
||||
] satisfies ProductItem[];
|
||||
|
||||
const result = buildAttachRequestBody({
|
||||
...baseParams,
|
||||
product,
|
||||
prepaidOptions: {},
|
||||
items,
|
||||
});
|
||||
|
||||
expect(result?.items).toEqual([
|
||||
{
|
||||
feature_id: "AI_CREDITS",
|
||||
price: null,
|
||||
tiers: [{ amount: 0.01, to: "inf" }],
|
||||
interval: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProductItemInterval } from "@autumn/shared";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { type ProductItem, ProductItemInterval } from "@autumn/shared";
|
||||
import { normalizeBillingRequestItems } from "@/components/forms/shared/utils/normalizeBillingRequestItems";
|
||||
import { buildUpdateSubscriptionOptions } from "@/components/forms/update-subscription-v2/hooks/useUpdateSubscriptionRequestBody";
|
||||
|
||||
describe("buildUpdateSubscriptionOptions — included usage handling", () => {
|
||||
@@ -221,3 +222,98 @@ describe("buildUpdateSubscriptionOptions — included usage handling", () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeBillingRequestItems", () => {
|
||||
test("drops empty fixed-price draft rows before serialization", () => {
|
||||
const items = [
|
||||
{
|
||||
feature_id: "AI_CREDITS",
|
||||
price: null,
|
||||
tiers: [{ amount: 0.01, to: "inf" }],
|
||||
},
|
||||
{
|
||||
price: "" as unknown as number,
|
||||
feature_id: null,
|
||||
price_id: null,
|
||||
entitlement_id: null,
|
||||
interval: ProductItemInterval.Month,
|
||||
interval_count: 1,
|
||||
tiers: null,
|
||||
billing_units: null,
|
||||
usage_model: null,
|
||||
included_usage: null,
|
||||
config: null,
|
||||
feature_type: null,
|
||||
entity_feature_id: null,
|
||||
price_config: null,
|
||||
},
|
||||
{
|
||||
price: "" as unknown as number,
|
||||
feature_id: null,
|
||||
price_id: null,
|
||||
entitlement_id: null,
|
||||
interval: ProductItemInterval.Month,
|
||||
interval_count: 1,
|
||||
tiers: null,
|
||||
billing_units: null,
|
||||
usage_model: null,
|
||||
included_usage: null,
|
||||
config: null,
|
||||
feature_type: null,
|
||||
entity_feature_id: null,
|
||||
price_config: null,
|
||||
},
|
||||
{
|
||||
feature_id: "SSO",
|
||||
price: null,
|
||||
},
|
||||
] satisfies ProductItem[];
|
||||
|
||||
expect(normalizeBillingRequestItems({ items })).toEqual([
|
||||
items[0],
|
||||
items[3],
|
||||
]);
|
||||
});
|
||||
|
||||
test("omits draft string prices from non-empty items", () => {
|
||||
const items = [
|
||||
{
|
||||
feature_id: "seats",
|
||||
price: "" as unknown as number,
|
||||
included_usage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
},
|
||||
] satisfies ProductItem[];
|
||||
|
||||
expect(normalizeBillingRequestItems({ items })).toEqual([
|
||||
{
|
||||
feature_id: "seats",
|
||||
included_usage: 10,
|
||||
interval: ProductItemInterval.Month,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns undefined when only empty draft rows are present", () => {
|
||||
const items = [
|
||||
{
|
||||
price: "" as unknown as number,
|
||||
feature_id: null,
|
||||
price_id: null,
|
||||
entitlement_id: null,
|
||||
interval: ProductItemInterval.Month,
|
||||
interval_count: 1,
|
||||
tiers: null,
|
||||
billing_units: null,
|
||||
usage_model: null,
|
||||
included_usage: null,
|
||||
config: null,
|
||||
feature_type: null,
|
||||
entity_feature_id: null,
|
||||
price_config: null,
|
||||
},
|
||||
] satisfies ProductItem[];
|
||||
|
||||
expect(normalizeBillingRequestItems({ items })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user