minor cleanup
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import type { FullCusProduct, FullCustomer } from "@autumn/shared";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv";
|
||||
import { fetchStripeCustomerForBilling } from "./fetchStripeCustomerForBilling";
|
||||
import { setupStripeDiscountsForBilling } from "./setupStripeDiscountsForBilling";
|
||||
import { fetchStripeSubscriptionForBilling } from "./fetchStripeSubscriptionForBilling";
|
||||
import { fetchStripeSubscriptionScheduleForBilling } from "./fetchStripeSubscriptionScheduleForBilling";
|
||||
import { setupStripeDiscountsForBilling } from "./setupStripeDiscountsForBilling";
|
||||
|
||||
export const setupStripeBillingContext = async ({
|
||||
ctx,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const DISCOUNT_TAG = "[inc. discount]";
|
||||
|
||||
/** Suffixes "[inc. discount]" to description if not already present */
|
||||
export const addDiscountTagToDescription = ({
|
||||
description,
|
||||
}: {
|
||||
description: string;
|
||||
}): string => {
|
||||
if (description.includes(DISCOUNT_TAG)) {
|
||||
return description;
|
||||
}
|
||||
return `${description} ${DISCOUNT_TAG}`;
|
||||
};
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
type LineItemDiscount,
|
||||
type StripeDiscountWithCoupon,
|
||||
stripeToAtmnAmount,
|
||||
sumValues,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { addDiscountTagToDescription } from "./addDiscountTagToDescription";
|
||||
import { discountAppliesToLineItem } from "./discountAppliesToLineItem";
|
||||
|
||||
/**
|
||||
@@ -33,8 +35,8 @@ export const applyAmountOffDiscountToLineItems = ({
|
||||
|
||||
// Filter to applicable CHARGE line items only
|
||||
// Discounts reduce what the customer pays, so only apply to charges
|
||||
const applicableChargeItems = lineItems.filter(
|
||||
(item) => discountAppliesToLineItem({ discount, lineItem: item }),
|
||||
const applicableChargeItems = lineItems.filter((item) =>
|
||||
discountAppliesToLineItem({ discount, lineItem: item }),
|
||||
);
|
||||
|
||||
if (applicableChargeItems.length === 0) return lineItems;
|
||||
@@ -43,9 +45,8 @@ export const applyAmountOffDiscountToLineItems = ({
|
||||
const discountMap = new Map<LineItem, number>();
|
||||
|
||||
// Distribute discount proportionally across charge items
|
||||
const total = applicableChargeItems.reduce(
|
||||
(sum, item) => sum + Math.abs(item.amount),
|
||||
0,
|
||||
const total = sumValues(
|
||||
applicableChargeItems.map((item) => Math.abs(item.amount)),
|
||||
);
|
||||
|
||||
if (total === 0) return lineItems;
|
||||
@@ -80,6 +81,9 @@ export const applyAmountOffDiscountToLineItems = ({
|
||||
|
||||
return {
|
||||
...item,
|
||||
description: addDiscountTagToDescription({
|
||||
description: item.description,
|
||||
}),
|
||||
discounts: [...existingDiscounts, newDiscount],
|
||||
finalAmount,
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
StripeDiscountWithCoupon,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { addDiscountTagToDescription } from "./addDiscountTagToDescription";
|
||||
import { discountAppliesToLineItem } from "./discountAppliesToLineItem";
|
||||
|
||||
/**
|
||||
@@ -60,6 +61,9 @@ export const applyPercentOffDiscountToLineItems = ({
|
||||
|
||||
return {
|
||||
...item,
|
||||
description: addDiscountTagToDescription({
|
||||
description: item.description,
|
||||
}),
|
||||
discounts: [...existingDiscounts, newDiscount],
|
||||
finalAmount,
|
||||
};
|
||||
|
||||
@@ -49,6 +49,7 @@ export const initCustomerV3 = async ({
|
||||
});
|
||||
|
||||
// 2. Create customer
|
||||
|
||||
try {
|
||||
await autumn.customers.delete(customerId);
|
||||
} catch (_error) {}
|
||||
|
||||
@@ -14,17 +14,15 @@ import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
getStripeSubscription,
|
||||
createPercentCoupon,
|
||||
applySubscriptionDiscount,
|
||||
createPercentCoupon,
|
||||
getStripeSubscription,
|
||||
} from "../../utils/discounts/discountTestUtils.js";
|
||||
|
||||
const billingUnits = 12;
|
||||
const pricePerUnit = 10;
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("entity: discount on single entity upgrade")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("entity: discount on single entity upgrade")}`, async () => {
|
||||
const customerId = "entity-single-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -97,12 +95,9 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("entity: both entities share subscription discount")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("entity: both entities share subscription discount")}`, async () => {
|
||||
const customerId = "entity-shared-discount";
|
||||
|
||||
const product = products.base({
|
||||
@@ -186,12 +181,9 @@ test.concurrent(
|
||||
|
||||
expect(preview0.total).toBe(expectedAmount);
|
||||
expect(preview1.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("entity: upgrade one entity while other unchanged")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("entity: upgrade one entity while other unchanged")}`, async () => {
|
||||
const customerId = "entity-partial-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -282,12 +274,9 @@ test.concurrent(
|
||||
const feature = entity1.features?.[TestFeature.Messages];
|
||||
|
||||
expect(feature?.balance).toBe(initialQuantity);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("entity: discount on entity product switch")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("entity: discount on entity product switch")}`, async () => {
|
||||
const customerId = "entity-product-switch";
|
||||
|
||||
const basicProduct = products.base({
|
||||
@@ -373,5 +362,4 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -14,10 +14,10 @@ import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
getStripeSubscription,
|
||||
createPercentCoupon,
|
||||
createAmountCoupon,
|
||||
applySubscriptionDiscount,
|
||||
createAmountCoupon,
|
||||
createPercentCoupon,
|
||||
getStripeSubscription,
|
||||
} from "../../utils/discounts/discountTestUtils.js";
|
||||
|
||||
const billingUnits = 12;
|
||||
@@ -27,9 +27,7 @@ const pricePerUnit = 10;
|
||||
// MIGRATED TESTS FROM subscription-discounts.test.ts
|
||||
// =============================================================================
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: two percent-off discounts (multiplicative)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: two percent-off discounts (multiplicative)")}`, async () => {
|
||||
const customerId = "stack-pct-pct";
|
||||
|
||||
const product = products.base({
|
||||
@@ -99,16 +97,13 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// NEW STACKING TESTS
|
||||
// =============================================================================
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: percent then amount (percent applied first)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: percent then amount (percent applied first)")}`, async () => {
|
||||
const customerId = "stack-pct-amt";
|
||||
|
||||
const product = products.base({
|
||||
@@ -172,12 +167,9 @@ test.concurrent(
|
||||
// Charge: $100, 30% off = $70, then $5 off = $65
|
||||
// Total: -$50 + $65 = $15
|
||||
expect(preview.total).toBe(15);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: two amount-off discounts (additive)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: two amount-off discounts (additive)")}`, async () => {
|
||||
const customerId = "stack-amt-amt";
|
||||
|
||||
const product = products.base({
|
||||
@@ -241,12 +233,9 @@ test.concurrent(
|
||||
// Charge: $100, $10 off = $90, $15 off = $75
|
||||
// Total: -$50 + $75 = $25
|
||||
expect(preview.total).toBe(25);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: three discounts (two percent + one amount)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: three discounts (two percent + one amount)")}`, async () => {
|
||||
const customerId = "stack-three";
|
||||
|
||||
const product = products.base({
|
||||
@@ -315,12 +304,9 @@ test.concurrent(
|
||||
// Charge: $100, 20% off = $80, 10% off = $72, $5 off = $67
|
||||
// Total: -$50 + $67 = $17
|
||||
expect(preview.total).toBe(17);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: order independence (amount listed before percent)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: order independence (amount listed before percent)")}`, async () => {
|
||||
const customerId = "stack-order-test";
|
||||
|
||||
const product = products.base({
|
||||
@@ -386,12 +372,9 @@ test.concurrent(
|
||||
// Charge: $100, 20% off = $80, $10 off = $70
|
||||
// Total: -$50 + $70 = $20
|
||||
expect(preview.total).toBe(20);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("stacking: total discount capped at charge amount")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("stacking: total discount capped at charge amount")}`, async () => {
|
||||
const customerId = "stack-cap-total";
|
||||
|
||||
const product = products.base({
|
||||
@@ -455,5 +438,4 @@ test.concurrent(
|
||||
// Charge: $100, 50% off = $50, $30 off = $20
|
||||
// Total: -$50 + $20 = -$30
|
||||
expect(preview.total).toBe(-30);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,24 +9,22 @@
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
import { applyProration } from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
getStripeSubscription,
|
||||
createPercentCoupon,
|
||||
applySubscriptionDiscount,
|
||||
createPercentCoupon,
|
||||
getStripeSubscription,
|
||||
} from "../../utils/discounts/discountTestUtils.js";
|
||||
|
||||
const billingUnits = 12;
|
||||
const pricePerUnit = 10;
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("proration: mid-cycle upgrade with discount")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("proration: mid-cycle upgrade with discount")}`, async () => {
|
||||
const customerId = "proration-mid-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -124,12 +122,9 @@ test.concurrent(
|
||||
|
||||
// Use toBeCloseTo due to proration timing precision differences
|
||||
expect(preview.total).toBeCloseTo(expectedAmount, 0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("proration: mid-cycle downgrade (refund not discounted)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("proration: mid-cycle downgrade (refund not discounted)")}`, async () => {
|
||||
const customerId = "proration-mid-downgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -198,9 +193,7 @@ test.concurrent(
|
||||
const preview = await autumnV1.subscriptions.previewUpdate({
|
||||
customer_id: customerId,
|
||||
product_id: product.id,
|
||||
options: [
|
||||
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
|
||||
],
|
||||
options: [{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits }],
|
||||
});
|
||||
|
||||
// Downgrade generates: refund (prorated -$100 for 10 units) + charge (prorated $50 for 5 units)
|
||||
@@ -229,12 +222,9 @@ test.concurrent(
|
||||
|
||||
// Use toBeCloseTo due to proration timing precision differences
|
||||
expect(preview.total).toBeCloseTo(expectedAmount, 0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("proration: discount on upgrade with proration")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("proration: discount on upgrade with proration")}`, async () => {
|
||||
const customerId = "proration-upgrade-discount";
|
||||
|
||||
const product = products.base({
|
||||
@@ -333,5 +323,4 @@ test.concurrent(
|
||||
const expectedAmount = -proratedRefund + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -62,9 +62,7 @@ const getStripeSubscription = async ({
|
||||
// PERCENT-OFF DISCOUNT TESTS
|
||||
// =============================================================================
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: 20% off subscription discount applied to upgrade")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: 20% off subscription discount applied to upgrade")}`, async () => {
|
||||
const customerId = "discount-20pct-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -126,12 +124,9 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: 50% off subscription discount")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: 50% off subscription discount")}`, async () => {
|
||||
const customerId = "discount-50pct-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -191,12 +186,9 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: 100% off subscription discount (free)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: 100% off subscription discount (free)")}`, async () => {
|
||||
const customerId = "discount-100pct-free";
|
||||
|
||||
const product = products.base({
|
||||
@@ -252,16 +244,13 @@ test.concurrent(
|
||||
// Charge with 100% off: $100 * 0 = $0
|
||||
// Total: -$50 + $0 = -$50
|
||||
expect(preview.total).toBe(-50);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// AMOUNT-OFF DISCOUNT TESTS
|
||||
// =============================================================================
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: $10 off amount discount applied to upgrade")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: $10 off amount discount applied to upgrade")}`, async () => {
|
||||
const customerId = "discount-10dollars-upgrade";
|
||||
|
||||
const product = products.base({
|
||||
@@ -324,12 +313,9 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: charge capped at zero when discount exceeds charge")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: charge capped at zero when discount exceeds charge")}`, async () => {
|
||||
const customerId = "discount-cap-at-zero";
|
||||
|
||||
const product = products.base({
|
||||
@@ -387,16 +373,13 @@ test.concurrent(
|
||||
// Charge is capped at 0 (100 - 100 = 0), but refund for unused still applies
|
||||
// Net = -$50 (refund) + $0 (discounted charge) = -$50
|
||||
expect(preview.total).toBe(-50);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// MULTIPLE DISCOUNT TESTS
|
||||
// =============================================================================
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: multiple discounts stack (20% + 10%)")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: multiple discounts stack (20% + 10%)")}`, async () => {
|
||||
const customerId = "discount-multiple-stack";
|
||||
|
||||
const product = products.base({
|
||||
@@ -464,12 +447,9 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test.concurrent(
|
||||
`${chalk.yellowBright("discount: promotion code applied to subscription")}`,
|
||||
async () => {
|
||||
test.concurrent(`${chalk.yellowBright("discount: promotion code applied to subscription")}`, async () => {
|
||||
const customerId = "discount-promo-code";
|
||||
|
||||
const product = products.base({
|
||||
@@ -539,5 +519,4 @@ test.concurrent(
|
||||
const expectedAmount = refundAmount + discountedCharge;
|
||||
|
||||
expect(preview.total).toBe(expectedAmount);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { KSUID } from "@owpz/ksuid";
|
||||
import { Decimal } from "decimal.js";
|
||||
|
||||
export const generateId = (prefix?: string): string => {
|
||||
const id = KSUID.random().toString();
|
||||
@@ -17,7 +18,7 @@ export const notNullish = <T>(value: T | null | undefined): value is T =>
|
||||
export const idRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
export const sumValues = (vals: number[]) => {
|
||||
return vals.reduce((acc, curr) => acc + curr, 0);
|
||||
return vals.reduce((acc, curr) => acc.add(curr), new Decimal(0)).toNumber();
|
||||
};
|
||||
|
||||
export const keyToTitle = (
|
||||
|
||||
Reference in New Issue
Block a user