267 lines
7.5 KiB
TypeScript
267 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
BillingDiscountType,
|
|
BillingModel,
|
|
PricingPlanType,
|
|
RecurringInterval,
|
|
} from "../../types";
|
|
import {
|
|
calculatePriceDiscount,
|
|
calculateRecurringDiscount,
|
|
formatPrice,
|
|
getHighestDiscountForPrice,
|
|
getPlanPrice,
|
|
getPriceWithHighestDiscount,
|
|
} from "../price";
|
|
|
|
import type { Discount, PricingPlan, PricingPlanPrice } from "../../types";
|
|
|
|
const MONTHLY_PRICE: PricingPlanPrice = {
|
|
id: "price_monthly",
|
|
type: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
amount: 1000, // $10.00
|
|
currency: "USD",
|
|
custom: false,
|
|
};
|
|
|
|
const YEARLY_PRICE: PricingPlanPrice = {
|
|
id: "price_yearly",
|
|
type: BillingModel.RECURRING,
|
|
interval: RecurringInterval.YEAR,
|
|
amount: 10000, // $100.00
|
|
currency: "USD",
|
|
custom: false,
|
|
};
|
|
|
|
const LIFETIME_PRICE: PricingPlanPrice = {
|
|
id: "price_lifetime",
|
|
type: BillingModel.ONE_TIME,
|
|
amount: 30000, // $300.00
|
|
currency: "USD",
|
|
custom: false,
|
|
};
|
|
|
|
const EUR_PRICE: PricingPlanPrice = {
|
|
id: "price_eur",
|
|
type: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
amount: 900, // €9.00
|
|
currency: "EUR",
|
|
custom: false,
|
|
};
|
|
|
|
const PRICES: PricingPlanPrice[] = [
|
|
MONTHLY_PRICE,
|
|
YEARLY_PRICE,
|
|
LIFETIME_PRICE,
|
|
EUR_PRICE,
|
|
];
|
|
|
|
const PLAN: PricingPlan = {
|
|
id: PricingPlanType.PREMIUM,
|
|
name: "Premium Plan",
|
|
description: "Best for professionals",
|
|
badge: null,
|
|
prices: PRICES,
|
|
};
|
|
|
|
const PERCENT_DISCOUNT: Discount = {
|
|
code: "SAVE10",
|
|
type: BillingDiscountType.PERCENT,
|
|
off: 10,
|
|
appliesTo: ["price_monthly", "price_yearly"],
|
|
};
|
|
|
|
const AMOUNT_DISCOUNT: Discount = {
|
|
code: "MINUS5",
|
|
type: BillingDiscountType.AMOUNT,
|
|
off: 500, // $5.00
|
|
appliesTo: ["price_monthly"],
|
|
};
|
|
|
|
const DISCOUNTS: Discount[] = [PERCENT_DISCOUNT, AMOUNT_DISCOUNT];
|
|
|
|
describe("getPlanPrice", () => {
|
|
it("should return the correct recurring price for a given interval", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
});
|
|
expect(price).toEqual(MONTHLY_PRICE);
|
|
});
|
|
|
|
it("should return the correct one-time price", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.ONE_TIME,
|
|
});
|
|
expect(price).toEqual(LIFETIME_PRICE);
|
|
});
|
|
|
|
it("should match currency if provided", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
currency: "USD",
|
|
});
|
|
expect(price).toEqual(MONTHLY_PRICE);
|
|
});
|
|
|
|
it("should match currency case-insensitively", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
currency: "usd",
|
|
});
|
|
expect(price).toEqual(MONTHLY_PRICE);
|
|
});
|
|
|
|
it("should return specific currency price when multiple exist", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.RECURRING,
|
|
interval: RecurringInterval.MONTH,
|
|
currency: "EUR",
|
|
});
|
|
expect(price).toEqual(EUR_PRICE);
|
|
});
|
|
|
|
it("should return undefined if no matching price found", () => {
|
|
const price = getPlanPrice(PLAN, {
|
|
model: BillingModel.RECURRING,
|
|
interval: RecurringInterval.WEEK, // No weekly price in mock
|
|
});
|
|
expect(price).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("formatPrice", () => {
|
|
it("should format currency correctly using Intl.NumberFormat", () => {
|
|
const formatted = formatPrice({ amount: 1234, currency: "USD" }, "en-US");
|
|
expect(formatted).toBe("$12.3");
|
|
});
|
|
|
|
it("should handle round numbers without decimals if specified in implementation", () => {
|
|
const formatted = formatPrice({ amount: 1000, currency: "USD" }, "en-US");
|
|
expect(formatted).toBe("$10");
|
|
});
|
|
|
|
it("should handle default language", () => {
|
|
const formatted = formatPrice({ amount: 1000, currency: "USD" });
|
|
// Should not throw and return a string
|
|
expect(typeof formatted).toBe("string");
|
|
});
|
|
});
|
|
|
|
describe("calculateRecurringDiscount", () => {
|
|
it("should calculate discount for yearly billing compared to monthly", () => {
|
|
const result = calculateRecurringDiscount(PLAN, RecurringInterval.YEAR);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.percentage).toBe(7);
|
|
expect(result?.discounted.id).toBe("price_yearly");
|
|
expect(result?.original.amount).toBe(10800);
|
|
});
|
|
|
|
it("should return null if price not found", () => {
|
|
const result = calculateRecurringDiscount(PLAN, RecurringInterval.WEEK);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("should return null if minPrice is missing/invalid", () => {
|
|
// Plan with no recurring prices
|
|
const emptyPlan: PricingPlan = { ...PLAN, prices: [] };
|
|
const result = calculateRecurringDiscount(
|
|
emptyPlan,
|
|
RecurringInterval.YEAR,
|
|
);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("calculatePriceDiscount", () => {
|
|
it("should calculate percentage discount", () => {
|
|
const price = MONTHLY_PRICE; // 1000
|
|
const discount = PERCENT_DISCOUNT; // 10%
|
|
|
|
const result = calculatePriceDiscount(price, discount);
|
|
expect(result).toBeDefined();
|
|
expect(result?.type).toBe(BillingDiscountType.PERCENT);
|
|
expect(result?.percentage).toBe(10);
|
|
expect(result?.discounted.amount).toBe(900); // 1000 - 10%
|
|
});
|
|
|
|
it("should calculate amount discount", () => {
|
|
const price = MONTHLY_PRICE; // 1000
|
|
const discount = AMOUNT_DISCOUNT; // 500 off
|
|
|
|
const result = calculatePriceDiscount(price, discount);
|
|
expect(result).toBeDefined();
|
|
expect(result?.type).toBe(BillingDiscountType.AMOUNT);
|
|
expect(result?.percentage).toBe(50); // 500/1000 * 100
|
|
expect(result?.discounted.amount).toBe(500);
|
|
});
|
|
|
|
it("should return null for custom price", () => {
|
|
const customPrice: PricingPlanPrice = {
|
|
id: "custom",
|
|
custom: true,
|
|
label: "Contact Us",
|
|
href: "/contact",
|
|
type: BillingModel.ONE_TIME,
|
|
currency: "USD",
|
|
};
|
|
const result = calculatePriceDiscount(customPrice, PERCENT_DISCOUNT);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getHighestDiscountForPrice", () => {
|
|
it("should select the discount that gives the biggest reduction", () => {
|
|
// Price: 1000
|
|
// Percent discount: 10% -> 100 off -> 900
|
|
// Amount discount: 500 off -> 500 off -> 500
|
|
const best = getHighestDiscountForPrice(MONTHLY_PRICE, DISCOUNTS);
|
|
expect(best).toBe(AMOUNT_DISCOUNT);
|
|
});
|
|
|
|
it("should return undefined if no discount applies", () => {
|
|
const noDiscountPrice: PricingPlanPrice = {
|
|
...MONTHLY_PRICE,
|
|
id: "price_other",
|
|
};
|
|
const result = getHighestDiscountForPrice(noDiscountPrice, DISCOUNTS);
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
it("should return correct discount when comparison involves amount 0", () => {
|
|
// Edge case where discount makes it free or similar
|
|
const fullDiscount: Discount = {
|
|
code: "FREE",
|
|
type: BillingDiscountType.PERCENT,
|
|
off: 100,
|
|
appliesTo: ["price_monthly"],
|
|
};
|
|
const discounts = [...DISCOUNTS, fullDiscount];
|
|
const best = getHighestDiscountForPrice(MONTHLY_PRICE, discounts);
|
|
expect(best).toBe(fullDiscount);
|
|
});
|
|
});
|
|
|
|
describe("getPriceWithHighestDiscount", () => {
|
|
it("should return the price configuration that yields the biggest saving", () => {
|
|
const result = getPriceWithHighestDiscount([PLAN], DISCOUNTS);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result?.id).toBe(YEARLY_PRICE.id);
|
|
expect(result?.discount?.code).toBe("SAVE10");
|
|
});
|
|
|
|
it("should return null if no prices have discounts", () => {
|
|
const result = getPriceWithHighestDiscount([PLAN], []);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.discount).toBeUndefined();
|
|
});
|
|
});
|