Files
cfw-autumn/server/tests/unit-tests/billing/compute-feature-quantities/compute-subscription-update-feature-quantities.test.ts
2026-01-07 14:41:48 +00:00

746 lines
20 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type { UpdateSubscriptionV0Params } from "@autumn/shared";
import { createMockCtx } from "@tests/utils/mockUtils/contextMocks";
import { createMockCustomerProduct } from "@tests/utils/mockUtils/cusProductMocks";
import { createMockFeature } from "@tests/utils/mockUtils/featureMocks";
import {
createMockCustomerPrice,
createMockFixedPrice,
createMockPrepaidPrice,
} from "@tests/utils/mockUtils/priceMocks";
import { createMockFullProduct } from "@tests/utils/mockUtils/productMocks";
import chalk from "chalk";
import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext";
// ============ TESTS ============
describe(chalk.yellowBright("setupFeatureQuantitiesContext"), () => {
describe("basic quantity inheritance", () => {
test("1. current has quantity, new params has none → uses current", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 100,
},
],
customerPrices: [createMockCustomerPrice({ price })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
// No options provided
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
expect(result[0].quantity).toBe(100);
});
test("2. current has no quantity, new params has quantity → uses new", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 50 }],
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
expect(result[0].quantity).toBe(50);
});
test("3. both have quantity → uses new (new takes precedence)", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 100,
},
],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 200 }],
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
expect(result[0].quantity).toBe(200);
});
});
describe("multiple features", () => {
test("4. previous has all quantities, new has just one → updates only the specified one", () => {
const creditsFeature = createMockFeature({
id: "credits",
name: "Credits",
});
const seatsFeature = createMockFeature({
id: "seats",
name: "Seats",
});
const storageFeature = createMockFeature({
id: "storage",
name: "Storage",
});
const creditsPrice = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const seatsPrice = createMockPrepaidPrice({
id: "price_seats",
featureId: "seats",
});
const storagePrice = createMockPrepaidPrice({
id: "price_storage",
featureId: "storage",
});
const fullProduct = createMockFullProduct({
prices: [creditsPrice, seatsPrice, storagePrice],
});
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 100,
},
{
feature_id: "seats",
internal_feature_id: "internal_seats",
quantity: 5,
},
{
feature_id: "storage",
internal_feature_id: "internal_storage",
quantity: 50,
},
],
customerPrices: [
createMockCustomerPrice({ price: creditsPrice }),
createMockCustomerPrice({ price: seatsPrice }),
createMockCustomerPrice({ price: storagePrice }),
],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "seats", quantity: 10 }], // Only updating seats
};
const ctx = createMockCtx({
features: [creditsFeature, seatsFeature, storageFeature],
});
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(3);
const credits = result.find((r) => r.feature_id === "credits");
const seats = result.find((r) => r.feature_id === "seats");
const storage = result.find((r) => r.feature_id === "storage");
expect(credits?.quantity).toBe(100); // Unchanged
expect(seats?.quantity).toBe(10); // Updated
expect(storage?.quantity).toBe(50); // Unchanged
});
});
describe("billing units handling", () => {
test("5. new params quantity is rounded to billing units", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
billingUnits: 100, // Billing in units of 100
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 150 }], // Should round up to 200
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
// 150 rounded up to 200, then divided by billingUnits (100) = 2
expect(result[0].quantity).toBe(2);
});
test("exact billing unit multiple is not changed", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
billingUnits: 50,
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 200 }], // Exact multiple
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
// 200 / 50 = 4
expect(result[0].quantity).toBe(4);
});
test("small quantity rounds up to 1 billing unit", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
billingUnits: 1000,
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 1 }], // Should round to 1000
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
// 1 rounded to 1000, then divided by billingUnits (1000) = 1
expect(result[0].quantity).toBe(1);
});
test("converts current quantity from old billing units to new billing units", () => {
// Scenario: Customer has 5 packs of 100 credits (500 actual credits)
// New price uses packs of 250
// Expected: 500 credits → rounds to 500 (nearest 250) → 2 packs
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
// Old price: billing units of 100
const oldPrice = createMockPrepaidPrice({
id: "price_credits_old",
featureId: "credits",
billingUnits: 100,
});
// New price: billing units of 250
const newPrice = createMockPrepaidPrice({
id: "price_credits_new",
featureId: "credits",
billingUnits: 250,
});
const fullProduct = createMockFullProduct({ prices: [newPrice] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 5, // 5 packs of 100 = 500 actual credits
},
],
customerPrices: [createMockCustomerPrice({ price: oldPrice })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
// No options - should inherit from current
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
// Old: 5 packs * 100 = 500 credits
// Round 500 to nearest 250 = 500
// New: 500 / 250 = 2 packs
expect(result[0].quantity).toBe(2);
});
test("converts and rounds up when not exact multiple of new billing units", () => {
// Scenario: Customer has 3 packs of 100 credits (300 actual credits)
// New price uses packs of 250
// Expected: 300 credits → rounds to 500 (nearest 250) → 2 packs
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const oldPrice = createMockPrepaidPrice({
id: "price_credits_old",
featureId: "credits",
billingUnits: 100,
});
const newPrice = createMockPrepaidPrice({
id: "price_credits_new",
featureId: "credits",
billingUnits: 250,
});
const fullProduct = createMockFullProduct({ prices: [newPrice] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 3, // 3 packs of 100 = 300 actual credits
},
],
customerPrices: [createMockCustomerPrice({ price: oldPrice })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
// Old: 3 packs * 100 = 300 credits
// Round 300 to nearest 250 (ceiling) = 500
// New: 500 / 250 = 2 packs
expect(result[0].quantity).toBe(2);
});
test("no old customer price returns undefined (no quantity inherited)", () => {
// Scenario: No old price found - can't interpret the stored quantity
// Should return empty result (no feature quantities)
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const newPrice = createMockPrepaidPrice({
id: "price_credits_new",
featureId: "credits",
billingUnits: 250,
});
const fullProduct = createMockFullProduct({ prices: [newPrice] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 500,
},
],
// No customerPrices - can't interpret stored quantity
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
// No old price means we can't interpret the stored quantity
// So no feature quantity is inherited
expect(result).toHaveLength(0);
});
});
describe("price type filtering", () => {
test("skips non-prepaid prices (fixed prices)", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const fixedPrice = createMockFixedPrice({ id: "price_fixed" });
const prepaidPrice = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({
prices: [fixedPrice, prepaidPrice],
});
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 50 }],
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
// Only the prepaid price should be processed
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
});
});
describe("edge cases", () => {
test("empty prices array returns empty array", () => {
const fullProduct = createMockFullProduct({ prices: [] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
};
const ctx = createMockCtx({ features: [] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(0);
});
test("throws error when feature not found for price", () => {
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] });
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
};
// Empty features array - feature won't be found
const ctx = createMockCtx({ features: [] });
expect(() =>
setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
}),
).toThrow("Feature not found for price");
});
test("neither current nor new has quantity → feature not included", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({ options: [] }); // No current options
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
// No options in params either
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
// Since neither current nor new has quantity, nothing should be included
expect(result).toHaveLength(0);
});
test("params.options explicitly set to empty array → uses current quantities", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 100,
},
],
customerPrices: [createMockCustomerPrice({ price })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [], // Explicitly empty
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].feature_id).toBe("credits");
expect(result[0].quantity).toBe(100); // Falls back to current
});
test("new quantity of 0 is valid and replaces current", () => {
const feature = createMockFeature({
id: "credits",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "credits",
internal_feature_id: "internal_credits",
quantity: 100,
},
],
customerPrices: [createMockCustomerPrice({ price })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
options: [{ feature_id: "credits", quantity: 0 }],
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
// quantity: 0 is falsy, so paramsToFeatureOptions returns undefined
// Falls back to current quantity
expect(result).toHaveLength(1);
expect(result[0].quantity).toBe(100);
});
test("handles feature matched by internal_feature_id in current options", () => {
const feature = createMockFeature({
id: "credits",
internalId: "internal_credits_v2",
name: "Credits",
});
const price = createMockPrepaidPrice({
id: "price_credits",
featureId: "credits",
internalFeatureId: "internal_credits_v2",
});
const fullProduct = createMockFullProduct({ prices: [price] });
const cusProduct = createMockCustomerProduct({
options: [
{
feature_id: "old_credits_id", // Different feature_id
internal_feature_id: "internal_credits_v2", // Matches by internal_id
quantity: 75,
},
],
customerPrices: [createMockCustomerPrice({ price })],
});
const params: UpdateSubscriptionV0Params = {
customer_id: "cus_test",
product_id: "prod_test",
};
const ctx = createMockCtx({ features: [feature] });
const result = setupFeatureQuantitiesContext({
ctx,
featureQuantitiesParams: params,
fullProduct,
currentCustomerProduct: cusProduct,
});
expect(result).toHaveLength(1);
expect(result[0].quantity).toBe(75);
});
});
});