test: add vite unit test action and remove from .gitignore

This commit is contained in:
Charlie Lamb
2026-02-25 10:21:27 +00:00
parent 7dd8dd11b7
commit 4da64eebc9
8 changed files with 1149 additions and 0 deletions

25
.github/workflows/vite-unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Vite Unit Tests
on:
pull_request:
jobs:
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.2
- name: Install dependencies
run: bun install
- name: Run unit tests
run: bun test tests/
working-directory: vite

1
.gitignore vendored
View File

@@ -20,6 +20,7 @@ supabase.sh
**/.env*
tests/
!server/tests
!vite/tests
.secrets
# **/.npmrc

View File

@@ -0,0 +1,159 @@
import { describe, expect, test } from "bun:test";
import { type ProductV2, UsageModel } from "@autumn/shared";
import { buildAttachRequestBody } from "@/components/forms/attach-v2/hooks/useAttachRequestBody";
function makeProduct({ items }: { items: ProductV2["items"] }): ProductV2 {
return {
id: "prod_test",
name: "Test Product",
is_add_on: false,
is_default: false,
version: 1,
group: null,
env: "sandbox" as any,
items,
created_at: Date.now(),
};
}
const baseParams = {
customerId: "cus_123",
entityId: undefined,
items: null,
version: undefined,
trialLength: null,
trialDuration: "day" as const,
trialEnabled: false,
trialCardRequired: false,
planSchedule: null,
billingBehavior: null,
newBillingSubscription: false,
discounts: [],
};
describe("buildAttachRequestBody — billing_units handling", () => {
test("should pass display quantities through as-is, not multiply by billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = buildAttachRequestBody({
...baseParams,
product,
prepaidOptions: { messages: 5000 },
});
expect(result?.options).toEqual([
{ feature_id: "messages", quantity: 5000 },
]);
});
test("should not divide display quantities by billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = buildAttachRequestBody({
...baseParams,
product,
prepaidOptions: { messages: 5000 },
});
// Must NOT be 5 (5000 / 1000)
expect(result?.options?.[0]?.quantity).not.toBe(5);
// Must NOT be 5,000,000 (5000 * 1000)
expect(result?.options?.[0]?.quantity).not.toBe(5000000);
expect(result?.options?.[0]?.quantity).toBe(5000);
});
test("should handle multiple prepaid features with different billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
{
feature_id: "tokens",
usage_model: UsageModel.Prepaid,
billing_units: 500,
},
],
});
const result = buildAttachRequestBody({
...baseParams,
product,
prepaidOptions: { messages: 10000, tokens: 2500 },
});
expect(result?.options).toEqual([
{ feature_id: "messages", quantity: 10000 },
{ feature_id: "tokens", quantity: 2500 },
]);
});
test("should omit options when prepaidOptions is empty", () => {
const product = makeProduct({
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = buildAttachRequestBody({
...baseParams,
product,
prepaidOptions: {},
});
expect(result?.options).toBeUndefined();
});
test("should return null when customerId is missing", () => {
const product = makeProduct({
items: [
{
feature_id: "messages",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = buildAttachRequestBody({
...baseParams,
customerId: undefined,
product,
prepaidOptions: { messages: 5000 },
});
expect(result).toBeNull();
});
test("should return null when product is missing", () => {
const result = buildAttachRequestBody({
...baseParams,
product: undefined,
prepaidOptions: { messages: 5000 },
});
expect(result).toBeNull();
});
});

View File

@@ -0,0 +1,391 @@
import { describe, expect, test } from "bun:test";
import {
CouponDurationType,
type Reward,
type RewardProgram,
RewardType,
} from "@autumn/shared";
import type Stripe from "stripe";
import {
buildDiscountOptions,
filterDiscountRewards,
formatCouponDiscount,
rewardToOption,
stripeCouponToOption,
} from "@/components/forms/attach-v2/utils/discountOptionUtils";
function makeReward(overrides: Partial<Reward> & { id: string }): Reward {
return {
name: null,
promo_codes: [],
type: RewardType.PercentageDiscount,
free_product_id: null,
discount_config: null,
free_product_config: null,
internal_id: `rew_${overrides.id}`,
org_id: "org_test",
env: "sandbox",
created_at: Date.now(),
...overrides,
};
}
function makeStripeCoupon(
overrides: Partial<Stripe.Coupon> & { id: string },
): Stripe.Coupon {
return {
object: "coupon",
created: Math.floor(Date.now() / 1000),
livemode: false,
metadata: {},
name: null,
percent_off: null,
amount_off: null,
currency: null,
duration: "once",
duration_in_months: null,
valid: true,
max_redemptions: null,
redeem_by: null,
times_redeemed: 0,
...overrides,
} as Stripe.Coupon;
}
function makeRewardProgram(
overrides: Partial<RewardProgram> & {
internal_reward_id: string;
product_ids: string[];
},
): RewardProgram {
return {
internal_id: `rp_${overrides.internal_reward_id}`,
org_id: "org_test",
env: "sandbox",
created_at: Date.now(),
...overrides,
} as RewardProgram;
}
describe("filterDiscountRewards", () => {
test("should keep percentage and fixed discount rewards", () => {
const rewards = [
makeReward({ id: "pct", type: RewardType.PercentageDiscount }),
makeReward({ id: "fix", type: RewardType.FixedDiscount }),
];
const result = filterDiscountRewards(rewards);
expect(result).toHaveLength(2);
});
test("should filter out free product and invoice credits rewards", () => {
const rewards = [
makeReward({ id: "pct", type: RewardType.PercentageDiscount }),
makeReward({ id: "free", type: RewardType.FreeProduct }),
makeReward({ id: "credits", type: RewardType.InvoiceCredits }),
];
const result = filterDiscountRewards(rewards);
expect(result).toHaveLength(1);
expect(result[0].id).toBe("pct");
});
test("should return empty array when no discount rewards exist", () => {
const rewards = [makeReward({ id: "free", type: RewardType.FreeProduct })];
expect(filterDiscountRewards(rewards)).toHaveLength(0);
});
});
describe("rewardToOption", () => {
test("should use reward name as label", () => {
const reward = makeReward({ id: "r1", name: "20% Off" });
const option = rewardToOption(reward);
expect(option).toEqual({
id: "r1",
label: "20% Off",
sublabel: undefined,
source: "autumn",
});
});
test("should fall back to id when name is null", () => {
const reward = makeReward({ id: "r1", name: null });
expect(rewardToOption(reward).label).toBe("r1");
});
test("should include first promo code as sublabel", () => {
const reward = makeReward({
id: "r1",
name: "Discount",
promo_codes: [{ code: "SAVE20" }, { code: "SAVE30" }],
});
expect(rewardToOption(reward).sublabel).toBe("SAVE20");
});
});
describe("formatCouponDiscount", () => {
test("should format percent_off", () => {
const coupon = makeStripeCoupon({ id: "c1", percent_off: 25 });
expect(formatCouponDiscount(coupon)).toBe("25% off");
});
test("should format amount_off in cents to currency display", () => {
const coupon = makeStripeCoupon({
id: "c1",
amount_off: 1000,
currency: "usd",
});
expect(formatCouponDiscount(coupon)).toBe("10 USD off");
});
test("should default to USD when no currency", () => {
const coupon = makeStripeCoupon({ id: "c1", amount_off: 500 });
expect(formatCouponDiscount(coupon)).toBe("5 USD off");
});
test("should return empty string when no discount values set", () => {
const coupon = makeStripeCoupon({ id: "c1" });
expect(formatCouponDiscount(coupon)).toBe("");
});
});
describe("stripeCouponToOption", () => {
test("should use coupon name as label", () => {
const coupon = makeStripeCoupon({
id: "c1",
name: "Holiday Sale",
percent_off: 15,
});
const option = stripeCouponToOption(coupon);
expect(option).toEqual({
id: "c1",
label: "Holiday Sale",
sublabel: "15% off",
source: "stripe",
});
});
test("should fall back to id when name is null", () => {
const coupon = makeStripeCoupon({ id: "c1", name: null });
expect(stripeCouponToOption(coupon).label).toBe("c1");
});
});
describe("buildDiscountOptions", () => {
test("should return autumn rewards mapped to options", () => {
const rewards = [
makeReward({
id: "r1",
name: "10% Off",
type: RewardType.PercentageDiscount,
discount_config: {
discount_value: 10,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: true,
},
}),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms: [],
stripeCoupons: [],
productId: undefined,
});
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "r1",
label: "10% Off",
sublabel: undefined,
source: "autumn",
});
});
test("should return stripe-only coupons mapped to options", () => {
const stripeCoupons = [
makeStripeCoupon({ id: "sc1", name: "Stripe Deal", percent_off: 20 }),
];
const result = buildDiscountOptions({
rewards: [],
rewardPrograms: [],
stripeCoupons,
productId: undefined,
});
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: "sc1",
label: "Stripe Deal",
sublabel: "20% off",
source: "stripe",
});
});
test("should deduplicate stripe coupons that match autumn reward IDs", () => {
const rewards = [
makeReward({
id: "shared_id",
name: "Autumn Discount",
type: RewardType.PercentageDiscount,
discount_config: {
discount_value: 10,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: true,
},
}),
];
const stripeCoupons = [
makeStripeCoupon({
id: "shared_id",
name: "Same Coupon In Stripe",
percent_off: 10,
}),
makeStripeCoupon({
id: "stripe_only",
name: "Stripe Only",
percent_off: 30,
}),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms: [],
stripeCoupons,
productId: undefined,
});
expect(result).toHaveLength(2);
// First should be the Autumn reward version
expect(result[0].id).toBe("shared_id");
expect(result[0].source).toBe("autumn");
// Second should be the stripe-only coupon
expect(result[1].id).toBe("stripe_only");
expect(result[1].source).toBe("stripe");
});
test("should deduplicate against all autumn rewards, not just discount-type ones", () => {
// A free product reward that shares an ID with a Stripe coupon should still deduplicate
const rewards = [
makeReward({
id: "free_reward_id",
type: RewardType.FreeProduct,
}),
];
const stripeCoupons = [
makeStripeCoupon({
id: "free_reward_id",
name: "Coupon",
percent_off: 5,
}),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms: [],
stripeCoupons,
productId: undefined,
});
// Free product reward is filtered from autumn options, AND
// the stripe coupon with the same ID is deduped away
expect(result).toHaveLength(0);
});
test("should filter non-discount reward types from autumn options", () => {
const rewards = [
makeReward({
id: "discount",
type: RewardType.PercentageDiscount,
discount_config: {
discount_value: 10,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: true,
},
}),
makeReward({ id: "free", type: RewardType.FreeProduct }),
makeReward({ id: "credits", type: RewardType.InvoiceCredits }),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms: [],
stripeCoupons: [],
productId: undefined,
});
expect(result).toHaveLength(1);
expect(result[0].id).toBe("discount");
});
test("should filter autumn rewards by product when productId is provided", () => {
const rewards = [
makeReward({
id: "all_products",
name: "All Products",
type: RewardType.PercentageDiscount,
discount_config: {
discount_value: 10,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: true,
},
}),
makeReward({
id: "specific",
name: "Specific",
type: RewardType.FixedDiscount,
discount_config: {
discount_value: 500,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: false,
},
}),
];
const rewardPrograms = [
makeRewardProgram({
internal_reward_id: "rew_specific",
product_ids: ["prod_other"],
}),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms,
stripeCoupons: [],
productId: "prod_target",
});
// Only the apply_to_all reward should pass; the specific one is linked to prod_other
expect(result).toHaveLength(1);
expect(result[0].id).toBe("all_products");
});
test("should place autumn options before stripe options", () => {
const rewards = [
makeReward({
id: "autumn1",
name: "A",
type: RewardType.PercentageDiscount,
discount_config: {
discount_value: 10,
duration_type: CouponDurationType.OneOff,
duration_value: 0,
apply_to_all: true,
},
}),
];
const stripeCoupons = [
makeStripeCoupon({ id: "stripe1", name: "B", percent_off: 5 }),
];
const result = buildDiscountOptions({
rewards,
rewardPrograms: [],
stripeCoupons,
productId: undefined,
});
expect(result[0].source).toBe("autumn");
expect(result[1].source).toBe("stripe");
});
test("should return empty array when no rewards or coupons", () => {
const result = buildDiscountOptions({
rewards: [],
rewardPrograms: [],
stripeCoupons: [],
productId: undefined,
});
expect(result).toHaveLength(0);
});
});

View File

@@ -0,0 +1,156 @@
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", () => {
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 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
});
test("should not multiply or divide quantity by billing_units", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { 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", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 200 }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5200 }]);
});
test("should skip items where quantity has not changed", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 1000 },
initialPrepaidOptions: { messages: 1000 },
});
expect(result).toEqual([]);
});
test("should handle multiple features with different billing_units", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [
{ feature_id: "messages", included_usage: 0 },
{ feature_id: "tokens", included_usage: 100 },
],
prepaidOptions: { messages: 10000, tokens: 2500 },
initialPrepaidOptions: { messages: 5000, tokens: 1000 },
});
expect(result).toEqual([
{ feature_id: "messages", quantity: 10000 },
{ feature_id: "tokens", quantity: 2600 },
]);
});
test("should include new prepaid items from items array that are not in prepaidItems", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: 0 }],
prepaidOptions: { messages: 5000, tokens: 3000 },
initialPrepaidOptions: { messages: 1000 },
items: [
{
feature_id: "tokens",
usage_model: UsageModel.Prepaid,
included_usage: 50,
},
],
});
expect(result).toEqual([
{ feature_id: "messages", quantity: 5000 },
{ feature_id: "tokens", quantity: 3050 },
]);
});
test("should not duplicate items already in prepaidItems when also in items array", () => {
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,
},
],
});
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
});
test("should skip non-prepaid items from items array", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [],
prepaidOptions: { storage: 100 },
initialPrepaidOptions: {},
items: [
{
feature_id: "storage",
usage_model: UsageModel.PayPerUse,
included_usage: 0,
},
],
});
expect(result).toEqual([]);
});
test("should return empty array when no quantities changed", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [
{ feature_id: "messages", included_usage: 0 },
{ feature_id: "tokens", included_usage: 0 },
],
prepaidOptions: { messages: 1000, tokens: 500 },
initialPrepaidOptions: { messages: 1000, tokens: 500 },
});
expect(result).toEqual([]);
});
test("should handle included_usage as 'inf' by treating as 0", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [{ feature_id: "messages", included_usage: "inf" }],
prepaidOptions: { messages: 5000 },
initialPrepaidOptions: { messages: 1000 },
});
// typeof "inf" !== "number", so includedUsage defaults to 0
expect(result).toEqual([{ feature_id: "messages", quantity: 5000 }]);
});
test("should use feature.internal_id as fallback when feature_id is null", () => {
const result = buildUpdateSubscriptionOptions({
prepaidItems: [
{
feature_id: null,
feature: { internal_id: "int_messages" },
included_usage: 0,
},
],
prepaidOptions: { int_messages: 3000 },
initialPrepaidOptions: { int_messages: 1000 },
});
expect(result).toEqual([{ feature_id: "int_messages", quantity: 3000 }]);
});
});

View File

@@ -0,0 +1,59 @@
import { describe, expect, test } from "bun:test";
import {
getArrowByType,
getDateByType,
setDateByType,
} from "@/components/general/timePickerUtils";
describe("timePickerUtils seconds support", () => {
test("setDateByType sets seconds when type is seconds", () => {
const date = new Date("2026-01-01T10:15:20.000Z");
const updatedDate = setDateByType({
date,
value: "45",
type: "seconds",
});
expect(updatedDate.getSeconds()).toBe(45);
});
test("setDateByType clamps invalid seconds values", () => {
const date = new Date("2026-01-01T10:15:20.000Z");
const updatedDate = setDateByType({
date,
value: "99",
type: "seconds",
});
expect(updatedDate.getSeconds()).toBe(59);
});
test("getDateByType returns zero-padded seconds", () => {
const date = new Date("2026-01-01T10:15:07.000Z");
const value = getDateByType({
date,
type: "seconds",
});
expect(value).toBe("07");
});
test("getArrowByType wraps forward and backward for seconds", () => {
const incremented = getArrowByType({
value: "59",
step: 1,
type: "seconds",
});
const decremented = getArrowByType({
value: "00",
step: -1,
type: "seconds",
});
expect(incremented).toBe("00");
expect(decremented).toBe("59");
});
});

View File

@@ -0,0 +1,296 @@
import { describe, expect, test } from "bun:test";
import {
getPrepaidDisplayQuantity,
type ProductV2,
UsageModel,
} from "@autumn/shared";
import {
backendToDisplayQuantity,
convertPrepaidOptionsToFeatureOptions,
} from "@/utils/billing/prepaidQuantityUtils";
function makeProduct({ items }: { items: ProductV2["items"] }): ProductV2 {
return {
id: "prod_test",
name: "Test Product",
is_add_on: false,
is_default: false,
version: 1,
group: null,
env: "sandbox" as any,
items,
created_at: Date.now(),
};
}
describe("backendToDisplayQuantity", () => {
test("should multiply backend quantity by billing_units", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "messages", quantity: 1 }],
prepaidItems: [{ feature_id: "messages", billing_units: 1000 }],
});
expect(result).toEqual({ messages: 1000 });
});
test("should handle multiple features with different billing_units", () => {
const result = backendToDisplayQuantity({
backendOptions: [
{ feature_id: "messages", quantity: 10 },
{ feature_id: "tokens", quantity: 5 },
],
prepaidItems: [
{ feature_id: "messages", billing_units: 1000 },
{ feature_id: "tokens", billing_units: 500 },
],
});
expect(result).toEqual({ messages: 10000, tokens: 2500 });
});
test("should default to billing_units=1 when nullish", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "messages", quantity: 5 }],
prepaidItems: [{ feature_id: "messages", billing_units: null }],
});
expect(result).toEqual({ messages: 5 });
});
test("should default to 0 when feature has no backend option", () => {
const result = backendToDisplayQuantity({
backendOptions: [],
prepaidItems: [{ feature_id: "messages", billing_units: 1000 }],
});
expect(result).toEqual({ messages: 0 });
});
test("should ignore backend options not in prepaidItems", () => {
const result = backendToDisplayQuantity({
backendOptions: [
{ feature_id: "messages", quantity: 10 },
{ feature_id: "unknown", quantity: 99 },
],
prepaidItems: [{ feature_id: "messages", billing_units: 1000 }],
});
expect(result).toEqual({ messages: 10000 });
});
test("should return empty record when no prepaid items", () => {
const result = backendToDisplayQuantity({
backendOptions: [{ feature_id: "messages", quantity: 10 }],
prepaidItems: [],
});
expect(result).toEqual({});
});
});
describe("convertPrepaidOptionsToFeatureOptions", () => {
test("should pass quantity through as-is for prepaid items (inclusive of billing units)", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_credits: 10000 },
product,
});
expect(result).toEqual([{ feature_id: "feat_credits", quantity: 10000 }]);
});
test("should not multiply quantity by billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_credits: 5000 },
product,
});
// Must NOT be 5,000,000 (5000 * 1000)
expect(result).toEqual([{ feature_id: "feat_credits", quantity: 5000 }]);
});
test("should not divide quantity by billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_credits: 5000 },
product,
});
// Must NOT be 5 (5000 / 1000)
expect(result).toEqual([{ feature_id: "feat_credits", quantity: 5000 }]);
});
test("should pass quantity through for non-prepaid items", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_storage",
usage_model: UsageModel.PayPerUse,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_storage: 5000 },
product,
});
expect(result).toEqual([{ feature_id: "feat_storage", quantity: 5000 }]);
});
test("should handle multiple features with different billing_units", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
{
feature_id: "feat_tokens",
usage_model: UsageModel.Prepaid,
billing_units: 500,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: {
feat_credits: 10000,
feat_tokens: 2500,
},
product,
});
expect(result).toEqual([
{ feature_id: "feat_credits", quantity: 10000 },
{ feature_id: "feat_tokens", quantity: 2500 },
]);
});
test("should return undefined when product is undefined", () => {
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_credits: 10000 },
product: undefined,
});
expect(result).toBeUndefined();
});
test("should return undefined when prepaidOptions is empty", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: {},
product,
});
expect(result).toBeUndefined();
});
test("should handle zero quantity", () => {
const product = makeProduct({
items: [
{
feature_id: "feat_credits",
usage_model: UsageModel.Prepaid,
billing_units: 1000,
},
],
});
const result = convertPrepaidOptionsToFeatureOptions({
prepaidOptions: { feat_credits: 0 },
product,
});
expect(result).toEqual([{ feature_id: "feat_credits", quantity: 0 }]);
});
});
describe("getPrepaidDisplayQuantity", () => {
test("should multiply quantity by billingUnits", () => {
const result = getPrepaidDisplayQuantity({
quantity: 1,
billingUnits: 1000,
});
expect(result).toBe(1000);
});
test("should handle larger pack counts", () => {
const result = getPrepaidDisplayQuantity({
quantity: 5,
billingUnits: 1000,
});
expect(result).toBe(5000);
});
test("should default to billingUnits=1 when null", () => {
const result = getPrepaidDisplayQuantity({
quantity: 7,
billingUnits: null,
});
expect(result).toBe(7);
});
test("should default to billingUnits=1 when undefined", () => {
const result = getPrepaidDisplayQuantity({
quantity: 7,
billingUnits: undefined,
});
expect(result).toBe(7);
});
test("should return 0 when quantity is 0", () => {
const result = getPrepaidDisplayQuantity({
quantity: 0,
billingUnits: 1000,
});
expect(result).toBe(0);
});
test("should be identity when billingUnits is 1", () => {
const result = getPrepaidDisplayQuantity({
quantity: 42,
billingUnits: 1,
});
expect(result).toBe(42);
});
});

View File

@@ -0,0 +1,62 @@
import { describe, expect, test } from "bun:test";
import { AppEnv } from "@autumn/shared";
import { getStripeInvoiceLink } from "@/utils/linkUtils";
describe("getStripeInvoiceLink", () => {
const baseArgs = {
env: AppEnv.Sandbox,
accountId: "acct_123",
};
test("works when stripeInvoice is an object with id", () => {
const result = getStripeInvoiceLink({
stripeInvoice: { id: "in_abc123" },
...baseArgs,
});
expect(result).toBe(
"https://dashboard.stripe.com/acct_123/test/invoices/in_abc123",
);
expect(result).not.toContain("undefined");
});
test("works when stripeInvoice is an object with stripe_id", () => {
const result = getStripeInvoiceLink({
stripeInvoice: { stripe_id: "in_abc123" },
...baseArgs,
});
expect(result).toBe(
"https://dashboard.stripe.com/acct_123/test/invoices/in_abc123",
);
expect(result).not.toContain("undefined");
});
test("works when stripeInvoice is a string (attach v2 flow)", () => {
const result = getStripeInvoiceLink({
stripeInvoice: "in_abc123",
...baseArgs,
});
expect(result).toBe(
"https://dashboard.stripe.com/acct_123/test/invoices/in_abc123",
);
expect(result).not.toContain("undefined");
});
test("works with live env (no /test prefix)", () => {
const result = getStripeInvoiceLink({
stripeInvoice: "in_abc123",
env: AppEnv.Live,
accountId: "acct_123",
});
expect(result).toBe(
"https://dashboard.stripe.com/acct_123/invoices/in_abc123",
);
});
test("works without accountId", () => {
const result = getStripeInvoiceLink({
stripeInvoice: "in_abc123",
env: AppEnv.Sandbox,
});
expect(result).toBe("https://dashboard.stripe.com/test/invoices/in_abc123");
});
});