feat(db): mesh data model — meshes, members, invites, audit log
- pgSchema "mesh" with 4 tables isolating the peer mesh domain - Enums: visibility, transport, tier, role - audit_log is metadata-only (E2E encryption enforced at broker/client) - Cascade on mesh delete, soft-delete via archivedAt/revokedAt Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
23
packages/billing/src/utils/env.ts
Normal file
23
packages/billing/src/utils/env.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { defineEnv } from "envin";
|
||||
import * as z from "zod";
|
||||
|
||||
import { envConfig } from "@turbostarter/shared/constants";
|
||||
|
||||
import { BillingModel } from "../types";
|
||||
|
||||
import type { Preset } from "envin/types";
|
||||
|
||||
export const sharedPreset = {
|
||||
id: "shared",
|
||||
server: {
|
||||
BILLING_MODEL: z
|
||||
.enum(BillingModel)
|
||||
.optional()
|
||||
.default(BillingModel.RECURRING),
|
||||
},
|
||||
} as const satisfies Preset;
|
||||
|
||||
export const sharedEnv = defineEnv({
|
||||
...envConfig,
|
||||
...sharedPreset,
|
||||
});
|
||||
1
packages/billing/src/utils/index.ts
Normal file
1
packages/billing/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./price";
|
||||
208
packages/billing/src/utils/price.ts
Normal file
208
packages/billing/src/utils/price.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
import { BillingDiscountType, BillingModel } from "../types";
|
||||
|
||||
import type {
|
||||
PricingPlanPrice,
|
||||
PricingPlan,
|
||||
RecurringInterval,
|
||||
Discount,
|
||||
} from "../types";
|
||||
|
||||
const INTERVAL_MULTIPLIER: Record<RecurringInterval, number> = {
|
||||
day: 7,
|
||||
week: 4,
|
||||
month: 12,
|
||||
year: 1,
|
||||
};
|
||||
|
||||
export const getPlanPrice = (
|
||||
plan: PricingPlan,
|
||||
options: {
|
||||
model: BillingModel;
|
||||
interval?: RecurringInterval;
|
||||
currency?: string;
|
||||
},
|
||||
) => {
|
||||
const filteredPrices =
|
||||
options.model === BillingModel.RECURRING && options.interval
|
||||
? plan.prices.filter(
|
||||
(price) =>
|
||||
price.type === BillingModel.RECURRING &&
|
||||
price.interval === options.interval,
|
||||
)
|
||||
: plan.prices.filter((price) => price.type === options.model);
|
||||
|
||||
return filteredPrices.find(
|
||||
(price) =>
|
||||
!price.currency ||
|
||||
!options.currency ||
|
||||
price.currency.toLowerCase() === options.currency.toLowerCase(),
|
||||
);
|
||||
};
|
||||
|
||||
export const formatPrice = (
|
||||
price: { amount: number; currency: string },
|
||||
lang?: string,
|
||||
) => {
|
||||
return new Intl.NumberFormat(lang, {
|
||||
style: "currency",
|
||||
currency: price.currency,
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 1,
|
||||
}).format(price.amount / 100);
|
||||
};
|
||||
|
||||
export const calculateRecurringDiscount = (
|
||||
product: PricingPlan,
|
||||
interval: RecurringInterval,
|
||||
) => {
|
||||
const recurringPrices = product.prices.filter(
|
||||
(price) => price.type === BillingModel.RECURRING,
|
||||
);
|
||||
const minPrice = recurringPrices.reduce((acc, price) => {
|
||||
if (
|
||||
"amount" in price &&
|
||||
price.amount < (acc && "amount" in acc ? acc.amount : 0)
|
||||
) {
|
||||
return price;
|
||||
}
|
||||
return acc;
|
||||
}, recurringPrices[0]);
|
||||
|
||||
const chosenPrice = recurringPrices.find(
|
||||
(price) => "interval" in price && price.interval === interval,
|
||||
);
|
||||
|
||||
if (!chosenPrice || !minPrice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const minMultiplierIndex = Object.entries(INTERVAL_MULTIPLIER).findIndex(
|
||||
([intervalKey]) =>
|
||||
"interval" in minPrice && intervalKey === minPrice.interval,
|
||||
);
|
||||
|
||||
const maxMultiplierIndex = Object.entries(INTERVAL_MULTIPLIER).findIndex(
|
||||
([intervalKey]) => intervalKey === interval,
|
||||
);
|
||||
|
||||
const multiplersToApply = Object.values(INTERVAL_MULTIPLIER).slice(
|
||||
minMultiplierIndex,
|
||||
maxMultiplierIndex,
|
||||
);
|
||||
|
||||
const minPriceInSameInterval =
|
||||
("amount" in minPrice ? minPrice.amount : 0) *
|
||||
multiplersToApply.reduce((acc, multiplier) => acc * multiplier, 1);
|
||||
|
||||
const discount = Math.round(
|
||||
(1 -
|
||||
("amount" in chosenPrice ? chosenPrice.amount : 0) /
|
||||
minPriceInSameInterval) *
|
||||
100,
|
||||
);
|
||||
|
||||
return {
|
||||
original: {
|
||||
...minPrice,
|
||||
amount: minPriceInSameInterval,
|
||||
},
|
||||
discounted: chosenPrice,
|
||||
percentage: isNaN(discount) ? 0 : discount,
|
||||
};
|
||||
};
|
||||
|
||||
export const calculatePriceDiscount = (
|
||||
price: PricingPlanPrice,
|
||||
discount: Discount,
|
||||
) => {
|
||||
if (price.custom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const amount = price.amount;
|
||||
|
||||
if (discount.type === BillingDiscountType.AMOUNT) {
|
||||
return {
|
||||
original: price,
|
||||
discounted: {
|
||||
...price,
|
||||
amount: amount - discount.off,
|
||||
},
|
||||
percentage: Math.floor((discount.off / amount) * 100),
|
||||
type: BillingDiscountType.AMOUNT,
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
original: price,
|
||||
discounted: {
|
||||
...price,
|
||||
amount: amount - (amount * discount.off) / 100,
|
||||
},
|
||||
percentage: discount.off,
|
||||
type: BillingDiscountType.PERCENT,
|
||||
} as const;
|
||||
};
|
||||
|
||||
export const getHighestDiscountForPrice = (
|
||||
price: PricingPlanPrice,
|
||||
discounts: Discount[],
|
||||
) => {
|
||||
const discountsForPrice = discounts.filter((d) =>
|
||||
d.appliesTo.includes(price.id),
|
||||
);
|
||||
|
||||
const [highestDiscount] = discountsForPrice.sort((a, b) => {
|
||||
const discountA = calculatePriceDiscount(price, a);
|
||||
const discountB = calculatePriceDiscount(price, b);
|
||||
|
||||
const amountA =
|
||||
(discountA?.original.amount ?? 0) - (discountA?.discounted.amount ?? 0);
|
||||
const amountB =
|
||||
(discountB?.original.amount ?? 0) - (discountB?.discounted.amount ?? 0);
|
||||
|
||||
return amountB - amountA;
|
||||
});
|
||||
|
||||
return highestDiscount;
|
||||
};
|
||||
|
||||
export const getPriceWithHighestDiscount = (
|
||||
plans: PricingPlan[],
|
||||
discounts: Discount[],
|
||||
) => {
|
||||
const pricesWithDiscounts = plans
|
||||
.flatMap((plan) => plan.prices)
|
||||
.map((price) => ({
|
||||
...price,
|
||||
discounts: discounts.filter((d) => d.appliesTo.includes(price.id)),
|
||||
}));
|
||||
|
||||
const [priceWithHighestDiscount] = pricesWithDiscounts.sort((a, b) => {
|
||||
const highestDiscountA = getHighestDiscountForPrice(a, discounts);
|
||||
const highestDiscountB = getHighestDiscountForPrice(b, discounts);
|
||||
|
||||
const discountA = highestDiscountA
|
||||
? calculatePriceDiscount(a, highestDiscountA)
|
||||
: null;
|
||||
const discountB = highestDiscountB
|
||||
? calculatePriceDiscount(b, highestDiscountB)
|
||||
: null;
|
||||
|
||||
const amountA =
|
||||
(discountA?.original.amount ?? 0) - (discountA?.discounted.amount ?? 0);
|
||||
const amountB =
|
||||
(discountB?.original.amount ?? 0) - (discountB?.discounted.amount ?? 0);
|
||||
|
||||
return amountB - amountA;
|
||||
});
|
||||
|
||||
if (!priceWithHighestDiscount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...priceWithHighestDiscount,
|
||||
discount: getHighestDiscountForPrice(priceWithHighestDiscount, discounts),
|
||||
};
|
||||
};
|
||||
266
packages/billing/src/utils/test/price.test.ts
Normal file
266
packages/billing/src/utils/test/price.test.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user