Files
cfw-autumn/server/tests/utils/fixtures/items.ts
2026-02-26 08:26:44 +00:00

745 lines
20 KiB
TypeScript

import {
BillingInterval,
type LimitedItem,
type ProductItemConfig,
ProductItemInterval,
type RolloverConfig,
TierBehavior,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import {
constructArrearItem,
constructArrearProratedItem,
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem";
// ═══════════════════════════════════════════════════════════════════
// BOOLEAN FEATURES
// ═══════════════════════════════════════════════════════════════════
/**
* Boolean feature - on/off access (no usage tracking)
* @returns Dashboard feature item
*/
const dashboard = () =>
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
});
/**
* Boolean feature - admin rights access
* @returns AdminRights feature item
*/
const adminRights = () =>
constructFeatureItem({
featureId: TestFeature.AdminRights,
isBoolean: true,
});
// ═══════════════════════════════════════════════════════════════════
// FREE METERED (included usage, resets monthly)
// ═══════════════════════════════════════════════════════════════════
/**
* Generic free metered feature - resets each billing cycle
* @param featureId - Feature ID
* @param includedUsage - Free usage allowance (default: 100)
*/
const free = ({
featureId,
includedUsage = 100,
}: {
featureId: string;
includedUsage?: number;
}): LimitedItem =>
constructFeatureItem({
featureId,
includedUsage,
}) as LimitedItem;
/**
* Monthly messages - resets each billing cycle
* @param includedUsage - Free usage allowance (default: 100)
* @param entityFeatureId - Entity feature ID for per-entity balances
* @param resetUsageWhenEnabled - Whether to reset usage when enabled (default: undefined, uses server default)
*/
const monthlyMessages = ({
includedUsage = 100,
entityFeatureId,
resetUsageWhenEnabled,
}: {
includedUsage?: number;
entityFeatureId?: string;
resetUsageWhenEnabled?: boolean;
} = {}): LimitedItem => {
const item = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage,
entityFeatureId,
}) as LimitedItem;
if (resetUsageWhenEnabled !== undefined) {
item.reset_usage_when_enabled = resetUsageWhenEnabled;
}
return item;
};
/**
* Monthly words - resets each billing cycle
* @param includedUsage - Free usage allowance (default: 100)
* @param entityFeatureId - Entity feature ID for per-entity balances
* @param resetUsageWhenEnabled - Whether to reset usage when enabled (default: undefined, uses server default)
*/
const monthlyWords = ({
includedUsage = 100,
entityFeatureId,
resetUsageWhenEnabled,
}: {
includedUsage?: number;
entityFeatureId?: string;
resetUsageWhenEnabled?: boolean;
} = {}): LimitedItem => {
const item = constructFeatureItem({
featureId: TestFeature.Words,
includedUsage,
entityFeatureId,
}) as LimitedItem;
if (resetUsageWhenEnabled !== undefined) {
item.reset_usage_when_enabled = resetUsageWhenEnabled;
}
return item;
};
/**
* Monthly credits - resets each billing cycle
* @param includedUsage - Free usage allowance (default: 100)
* @param rolloverConfig - Optional rollover configuration
*/
const monthlyCredits = ({
includedUsage = 100,
rolloverConfig,
}: {
includedUsage?: number;
rolloverConfig?: RolloverConfig;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage,
rolloverConfig,
}) as LimitedItem;
/**
* Unlimited messages - no usage cap
* @returns Unlimited messages feature item
*/
const unlimitedMessages = () =>
constructFeatureItem({
featureId: TestFeature.Messages,
unlimited: true,
});
/**
* Monthly users - resets each billing cycle
* @param includedUsage - Free usage allowance (default: 5)
*/
const monthlyUsers = ({
includedUsage = 5,
}: {
includedUsage?: number;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Users,
includedUsage,
}) as LimitedItem;
/**
* Free users - allocated seats with no price (free tier)
* @param includedUsage - Free seats included (default: 5)
*/
const freeUsers = ({
includedUsage = 5,
}: {
includedUsage?: number;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Users,
includedUsage,
}) as LimitedItem;
/**
* Free allocated users - allocated seats with no price, usage carries over on product switch
* @param includedUsage - Free seats included (default: 5)
* @param entityFeatureId - Entity feature ID for per-entity balances
*/
const freeAllocatedUsers = ({
includedUsage = 5,
entityFeatureId,
}: {
includedUsage?: number;
entityFeatureId?: string;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Users,
includedUsage,
entityFeatureId,
}) as LimitedItem;
/**
* Free allocated workflows - allocated workflows with no price, usage carries over on product switch
* @param includedUsage - Free workflows included (default: 5)
* @param entityFeatureId - Entity feature ID for per-entity balances
*/
const freeAllocatedWorkflows = ({
includedUsage = 5,
entityFeatureId,
}: {
includedUsage?: number;
entityFeatureId?: string;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Workflows,
includedUsage,
entityFeatureId,
}) as LimitedItem;
/**
* Lifetime messages - never resets (interval: null)
* @param includedUsage - One-time usage allowance (default: 100)
* @param entityFeatureId - Entity feature ID for per-entity balances
*/
const lifetimeMessages = ({
includedUsage = 100,
entityFeatureId,
}: {
includedUsage?: number;
entityFeatureId?: string;
} = {}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage,
interval: null,
entityFeatureId,
}) as LimitedItem;
/**
* Monthly messages with rollover - unused balance rolls over to next cycle
* @param includedUsage - Free usage allowance (default: 100)
* @param rolloverConfig - Rollover configuration (max, length, duration)
*/
const monthlyMessagesWithRollover = ({
includedUsage = 100,
rolloverConfig,
}: {
includedUsage?: number;
rolloverConfig: RolloverConfig;
}): LimitedItem =>
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage,
rolloverConfig,
}) as LimitedItem;
// ═══════════════════════════════════════════════════════════════════
// PREPAID (purchase units upfront)
// ═══════════════════════════════════════════════════════════════════
/**
* Generic prepaid item - purchase units upfront for any feature
*/
const prepaid = ({
featureId,
price = 10,
billingUnits = 100,
includedUsage = 0,
config,
entityFeatureId,
}: {
featureId: string;
price?: number;
billingUnits?: number;
includedUsage?: number;
config?: ProductItemConfig;
entityFeatureId?: string;
}): LimitedItem =>
constructPrepaidItem({
featureId,
price,
billingUnits,
includedUsage,
config,
entityFeatureId,
}) as LimitedItem;
/**
* Prepaid messages - purchase units upfront ($10/unit)
* @param includedUsage - Free units before purchase required (default: 0), billing units are 100
* @param entityFeatureId - Entity feature ID for per-entity balances
*/
const prepaidMessages = ({
includedUsage = 0,
billingUnits = 100,
price = 10,
config,
entityFeatureId,
}: {
includedUsage?: number;
billingUnits?: number;
price?: number;
config?: ProductItemConfig;
entityFeatureId?: string;
} = {}): LimitedItem =>
prepaid({
featureId: TestFeature.Messages,
price,
billingUnits,
includedUsage,
config,
entityFeatureId,
});
/**
* Prepaid users/seats - purchase seats upfront ($10/seat)
* @param includedUsage - Free seats before purchase required (default: 0)
*/
const prepaidUsers = ({
includedUsage = 0,
billingUnits = 1,
}: {
includedUsage?: number;
billingUnits?: number;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Users,
price: 10,
billingUnits,
includedUsage,
}) as LimitedItem;
/**
* Tiered prepaid messages - graduated pricing with tiers.
* Default tiers: 0-500 units at $10/pack, 501+ at $5/pack (100 units/pack).
* IMPORTANT: Last tier MUST have `to: "inf"` - Stripe requires a catch-all tier.
*/
const tieredPrepaidMessages = ({
includedUsage = 0,
billingUnits = 100,
tiers = [
{ to: 500, amount: 10 },
{ to: "inf", amount: 5 },
],
config,
}: {
includedUsage?: number;
billingUnits?: number;
tiers?: { to: number | "inf"; amount: number }[];
config?: ProductItemConfig;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Messages,
tiers: tiers as { to: number; amount: number }[],
billingUnits,
includedUsage,
config,
}) as LimitedItem;
/**
* Volume-priced prepaid messages — the entire purchased quantity is charged at
* the rate of whichever tier it falls into (not split across tiers).
* Default tiers: 0-500 units at $10/pack, 501+ at $5/pack (100 units/pack).
* IMPORTANT: Last tier MUST have `to: "inf"` - Stripe requires a catch-all tier.
*/
const volumePrepaidMessages = ({
includedUsage = 0,
billingUnits = 100,
tiers = [
{ to: 500, amount: 10 },
{ to: "inf", amount: 5 },
],
config,
}: {
includedUsage?: number;
billingUnits?: number;
tiers?: { to: number | "inf"; amount: number; flat_amount?: number | null }[];
config?: ProductItemConfig;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Messages,
tiers: tiers as {
to: number;
amount: number;
flat_amount?: number | null;
}[],
tierBehaviour: TierBehavior.VolumeBased,
billingUnits,
includedUsage,
config,
}) as LimitedItem;
// ═══════════════════════════════════════════════════════════════════
// ONE-OFF (interval: null, no recurring charges)
// ═══════════════════════════════════════════════════════════════════
/**
* One-off messages - purchase units once (no recurring charges)
* @param includedUsage - Free units included (default: 0)
* @param billingUnits - Units per pack (default: 100)
* @param price - Price per pack (default: 10)
*/
const oneOffMessages = ({
includedUsage = 0,
billingUnits = 100,
price = 10,
}: {
includedUsage?: number;
billingUnits?: number;
price?: number;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Messages,
price,
billingUnits,
includedUsage,
isOneOff: true,
}) as LimitedItem;
/**
* One-off words - purchase units once (no recurring charges)
* @param includedUsage - Free units included (default: 0)
* @param billingUnits - Units per pack (default: 100)
* @param price - Price per pack (default: 10)
*/
const oneOffWords = ({
includedUsage = 0,
billingUnits = 100,
price = 10,
}: {
includedUsage?: number;
billingUnits?: number;
price?: number;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Words,
price,
billingUnits,
includedUsage,
isOneOff: true,
}) as LimitedItem;
/**
* One-off storage - purchase units once (no recurring charges)
* @param includedUsage - Free units included (default: 0)
* @param billingUnits - Units per pack (default: 100)
* @param price - Price per pack (default: 10)
*/
const oneOffStorage = ({
includedUsage = 0,
billingUnits = 100,
price = 10,
}: {
includedUsage?: number;
billingUnits?: number;
price?: number;
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Storage,
price,
billingUnits,
includedUsage,
isOneOff: true,
}) as LimitedItem;
/**
* Tiered one-off messages - volume pricing with tiers (no recurring charges)
* Default tiers:
* - 0-500 units: $10/pack (100 units/pack)
* - 501+ units: $5/pack
*
* IMPORTANT: Last tier MUST have `to: "inf"` - Stripe requires a catch-all tier.
*
* @param includedUsage - Free units (default: 0)
* @param billingUnits - Units per pack (default: 100)
* @param tiers - Volume tiers (default: standard volume discount). Last tier must have `to: "inf"`.
*/
const tieredOneOffMessages = ({
includedUsage = 0,
billingUnits = 100,
tiers = [
{ to: 500, amount: 10 },
{ to: "inf", amount: 5 },
],
}: {
includedUsage?: number;
billingUnits?: number;
tiers?: { to: number | "inf"; amount: number }[];
} = {}): LimitedItem =>
constructPrepaidItem({
featureId: TestFeature.Messages,
tiers,
billingUnits,
includedUsage,
isOneOff: true,
}) as LimitedItem;
// ═══════════════════════════════════════════════════════════════════
// CONSUMABLE / PAY-PER-USE (overage pricing)
// ═══════════════════════════════════════════════════════════════════
/**
* Generic consumable item - pay-per-use overage for any feature
* @param featureId - Feature ID
* @param includedUsage - Free units before overage kicks in (default: 0)
* @param price - Price per billing unit (default: 0.1)
* @param billingUnits - Units per price (default: 1)
* @param entityFeatureId - Entity feature ID for per-entity balances
* @param interval - Billing interval (default: month)
* @param maxPurchase - Maximum overage allowed (usage_limit = maxPurchase + includedUsage)
*/
const consumable = ({
featureId,
includedUsage = 0,
price = 0.1,
billingUnits = 1,
entityFeatureId,
interval = ProductItemInterval.Month,
maxPurchase,
}: {
featureId: string;
includedUsage?: number;
price?: number;
billingUnits?: number;
entityFeatureId?: string;
interval?: ProductItemInterval;
maxPurchase?: number;
}): LimitedItem =>
constructArrearItem({
featureId,
includedUsage,
price,
billingUnits,
entityFeatureId,
interval,
usageLimit:
maxPurchase !== undefined ? maxPurchase + includedUsage : undefined,
}) as LimitedItem;
/**
* Consumable messages - pay-per-use overage ($0.10/unit)
* @param includedUsage - Free units before overage kicks in (default: 0)
* @param entityFeatureId - Entity feature ID for per-entity balances
* @param interval - Billing interval (default: month)
* @param maxPurchase - Maximum overage allowed (usage_limit = maxPurchase + includedUsage)
* @param price - Price per unit (default: 0.1)
*/
const consumableMessages = ({
includedUsage = 0,
entityFeatureId,
interval = ProductItemInterval.Month,
maxPurchase,
price = 0.1,
}: {
includedUsage?: number;
entityFeatureId?: string;
interval?: ProductItemInterval;
maxPurchase?: number;
price?: number;
} = {}): LimitedItem =>
consumable({
featureId: TestFeature.Messages,
includedUsage,
price,
billingUnits: 1,
entityFeatureId,
interval,
maxPurchase,
});
/**
* Consumable words - pay-per-use overage ($0.05/unit)
* @param includedUsage - Free units before overage kicks in (default: 0)
* @param entityFeatureId - Entity feature ID for per-entity balances
* @param interval - Billing interval (default: month)
*/
const consumableWords = ({
includedUsage = 0,
entityFeatureId,
interval = ProductItemInterval.Month,
}: {
includedUsage?: number;
entityFeatureId?: string;
interval?: ProductItemInterval;
} = {}): LimitedItem =>
consumable({
featureId: TestFeature.Words,
includedUsage,
price: 0.05,
billingUnits: 1,
entityFeatureId,
interval,
});
/**
* Tiered consumable messages - graduated pricing with tiers (pay-per-use).
* Default tiers: 0-500 units at $0.10/unit, 501+ at $0.05/unit.
* IMPORTANT: Last tier MUST have `to: "inf"` - Stripe requires a catch-all tier.
* Note: tier_behavior is undefined, defaulting to graduated pricing.
*/
const tieredConsumableMessages = ({
includedUsage = 0,
billingUnits = 1,
tiers = [
{ to: 500, amount: 0.1 },
{ to: "inf", amount: 0.05 },
],
}: {
includedUsage?: number;
billingUnits?: number;
tiers?: { to: number | "inf"; amount: number }[];
} = {}): LimitedItem =>
constructArrearItem({
featureId: TestFeature.Messages,
tiers: tiers as { to: number; amount: number }[],
billingUnits,
includedUsage,
}) as LimitedItem;
// ═══════════════════════════════════════════════════════════════════
// ALLOCATED / SEATS (prorated billing)
// ═══════════════════════════════════════════════════════════════════
/**
* Allocated users/seats - prorated billing on change ($10/seat)
* @param includedUsage - Free seats included (default: 0)
*/
const allocatedUsers = ({
includedUsage = 0,
}: {
includedUsage?: number;
} = {}): LimitedItem =>
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 10,
includedUsage,
}) as LimitedItem;
/**
* Allocated workflows - prorated billing on change ($10/workflow)
* @param includedUsage - Free workflows included (default: 0)
*/
const allocatedWorkflows = ({
includedUsage = 0,
}: {
includedUsage?: number;
} = {}): LimitedItem =>
constructArrearProratedItem({
featureId: TestFeature.Workflows,
pricePerUnit: 10,
includedUsage,
}) as LimitedItem;
/**
* Allocated messages - prorated billing on change ($10/unit)
* @param includedUsage - Free messages included (default: 0)
*/
const allocatedMessages = ({
includedUsage = 0,
}: {
includedUsage?: number;
} = {}): LimitedItem =>
constructArrearProratedItem({
featureId: TestFeature.Messages,
pricePerUnit: 10,
includedUsage,
}) as LimitedItem;
// ═══════════════════════════════════════════════════════════════════
// BASE PRICES
// ═══════════════════════════════════════════════════════════════════
/**
* Monthly base price item
* @param price - Monthly price (default: 20)
*/
const monthlyPrice = ({ price = 20 }: { price?: number } = {}) =>
constructPriceItem({
price,
interval: BillingInterval.Month,
});
/**
* Annual base price item
* @param price - Annual price (default: 200)
*/
const annualPrice = ({ price = 200 }: { price?: number } = {}) =>
constructPriceItem({
price,
interval: BillingInterval.Year,
});
/**
* One-off base price item (no recurring charges)
* @param price - One-time price (default: 50)
*/
const oneOffPrice = ({ price = 50 }: { price?: number } = {}) =>
constructPriceItem({
price,
interval: null,
});
// ═══════════════════════════════════════════════════════════════════
// EXPORT
// ═══════════════════════════════════════════════════════════════════
export const items = {
// Boolean
dashboard,
adminRights,
// Free metered
free,
monthlyMessages,
monthlyWords,
monthlyCredits,
monthlyUsers,
freeUsers,
freeAllocatedUsers,
freeAllocatedWorkflows,
unlimitedMessages,
lifetimeMessages,
monthlyMessagesWithRollover,
// Prepaid
prepaid,
prepaidMessages,
prepaidUsers,
tieredPrepaidMessages,
volumePrepaidMessages,
// One-off
oneOffMessages,
oneOffWords,
oneOffStorage,
tieredOneOffMessages,
// Consumable
consumable,
consumableMessages,
consumableWords,
tieredConsumableMessages,
// Allocated
allocatedUsers,
allocatedWorkflows,
allocatedMessages,
// Base prices
monthlyPrice,
annualPrice,
oneOffPrice,
} as const;