feat: 🎸 frontend locks

This commit is contained in:
amianthus
2026-02-19 17:35:46 +00:00
parent 5f685cf9a3
commit c865b37e35
3 changed files with 35 additions and 4 deletions

View File

@@ -15,10 +15,10 @@ import {
RecaseError,
type RolloverConfig,
RolloverExpiryDurationType,
TiersType,
UsageModel,
} from "@autumn/shared";
import { createFeaturesFromItems } from "@server/internal/products/product-items/createFeaturesFromItems";
import { StatusCodes } from "http-status-codes";
import {
isBooleanFeatureItem,
@@ -173,6 +173,18 @@ const validateProductItem = ({
statusCode: StatusCodes.BAD_REQUEST,
});
}
if (
item.tiers_type === TiersType.VolumeBased &&
item.tiers.length > 1 &&
item.usage_model !== UsageModel.Prepaid
) {
throw new RecaseError({
message: `Volume-based pricing is only supported for prepaid items`,
code: ErrCode.InvalidInputs,
statusCode: StatusCodes.BAD_REQUEST,
});
}
}
if (
@@ -368,6 +380,7 @@ export const validateProductItems = ({
});
}
}
}
// 6. Can't have both weekly and monthly price items in the same product

View File

@@ -1,6 +1,6 @@
import { Infinite, type PriceTier } from "@autumn/shared";
import { Infinite, type PriceTier, TiersType, UsageModel } from "@autumn/shared";
import { PlusIcon, TrashSimpleIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { IconButton } from "@/components/v2/buttons/IconButton";
import { Input } from "@/components/v2/inputs/Input";
import {
@@ -87,6 +87,17 @@ export function PriceTiers() {
);
const [isEditing, setIsEditing] = useState<Record<string, boolean>>({});
// Auto-select prepaid when volume-based is active with multiple tiers
useEffect(() => {
if (
item?.tiers_type === TiersType.VolumeBased &&
(item?.tiers?.length ?? 0) > 1 &&
item?.usage_model !== UsageModel.Prepaid
) {
setItem({ ...item, usage_model: UsageModel.Prepaid });
}
}, [item?.tiers_type, item?.tiers?.length]);
if (!item) return null;
const tiers = item.tiers || [];

View File

@@ -3,6 +3,7 @@ import {
itemToBillingInterval,
nullish,
ProductItemInterval,
TiersType,
UsageModel,
} from "@autumn/shared";
import { FormLabel } from "@/components/v2/form/FormLabel";
@@ -16,6 +17,8 @@ export function PricedFeatureSettings() {
if (!item) return null;
const isOneOff = itemToBillingInterval({ item }) === BillingInterval.OneOff;
const isVolumeBased =
item.tiers_type === TiersType.VolumeBased && (item.tiers?.length ?? 0) > 1;
const handleUsageModelChange = (value: string) => {
const usageModel = value as UsageModel;
@@ -42,7 +45,11 @@ export function PricedFeatureSettings() {
label="Usage-based"
description={"Bill for how much the customer uses"}
disabledReason={
isOneOff ? "Usage based prices must have an interval." : undefined
isVolumeBased
? "Volume-based pricing requires prepaid billing."
: isOneOff
? "Usage based prices must have an interval."
: undefined
}
/>
<AreaRadioGroupItem