Files
cfw-autumn/server/tests/integration/billing/update-subscription/version-update/version-discounts.test.ts
2026-01-15 15:04:37 +00:00

581 lines
14 KiB
TypeScript

/**
* Integration tests for discounts during version updates.
*
* These tests verify that existing discounts on subscriptions are correctly
* applied when updating to a new product version.
*/
import { expect, test } from "bun:test";
import {
applySubscriptionDiscount,
createAmountCoupon,
createPercentCoupon,
getStripeSubscription,
} from "@tests/integration/billing/utils/discounts/discountTestUtils";
import { TestFeature } from "@tests/setup/v2Features.js";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
import chalk from "chalk";
const billingUnits = 12;
const pricePerUnit = 10;
// ═══════════════════════════════════════════════════════════════════════════════
// VERSION DISCOUNTS: Discount preservation during version updates
// ═══════════════════════════════════════════════════════════════════════════════
// 7.1 Percent discount applied to version upgrade price increase
test.concurrent(`${chalk.yellowBright("version-discount: 20% off on price increase")}`, async () => {
const customerId = "version-disc-pct-inc";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit,
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 20,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
// Create v2 with higher price ($20 per unit instead of $10)
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20, // Doubled price
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Preview upgrade to v2
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Current: 5 units * $10 = $50
// New v2: 5 units * $20 = $100
// Upgrade generates: refund (-$50) + charge ($100)
// Discounts only apply to charges, not refunds
// Charge with 20% off: $100 * 0.8 = $80
// Total: -$50 + $80 = $30
const refundAmount = -50;
const discountedCharge = Math.round(100 * 0.8);
const expectedAmount = refundAmount + discountedCharge;
expect(preview.total).toBe(expectedAmount);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
});
// 7.2 Percent discount applied to version downgrade (credit)
test.concurrent(`${chalk.yellowBright("version-discount: 20% off on price decrease")}`, async () => {
const customerId = "version-disc-pct-dec";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20, // Start with higher price
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 20,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
// Create v2 with lower price ($10 per unit instead of $20)
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit, // Half price
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Preview downgrade to v2
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Current: 5 units * $20 = $100
// New v2: 5 units * $10 = $50
// Downgrade generates: refund (-$100) + charge ($50)
// Discounts only apply to charges, not refunds
// Charge with 20% off: $50 * 0.8 = $40
// Total: -$100 + $40 = -$60
const refundAmount = -100;
const discountedCharge = Math.round(50 * 0.8);
const expectedAmount = refundAmount + discountedCharge;
expect(preview.total).toBe(expectedAmount);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
});
// 7.3 Amount-off discount applied to version upgrade
test.concurrent(`${chalk.yellowBright("version-discount: $10 off on price increase")}`, async () => {
const customerId = "version-disc-amt-inc";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit,
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
// $10 off coupon (1000 cents)
const coupon = await createAmountCoupon({
stripeCli,
amountOffCents: 1000,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
// Create v2 with higher price ($20 per unit instead of $10)
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20, // Doubled price
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Preview upgrade to v2
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Current: 5 units * $10 = $50
// New v2: 5 units * $20 = $100
// Upgrade generates: refund (-$50) + charge ($100)
// Discounts only apply to charges, not refunds
// Charge with $10 off: $100 - $10 = $90
// Total: -$50 + $90 = $40
const refundAmount = -50;
const discountedCharge = 100 - 10;
const expectedAmount = refundAmount + discountedCharge;
expect(preview.total).toBe(expectedAmount);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
});
// 7.4 100% discount on version upgrade (free upgrade)
test.concurrent(`${chalk.yellowBright("version-discount: 100% off free upgrade")}`, async () => {
const customerId = "version-disc-100-free";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit,
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 100,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
// Create v2 with higher price ($20 per unit instead of $10)
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20, // Doubled price
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Preview upgrade to v2
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Current: 5 units * $10 = $50
// New v2: 5 units * $20 = $100
// Upgrade generates: refund (-$50) + charge ($100)
// Discounts only apply to charges, not refunds
// Charge with 100% off: $100 * 0 = $0
// Total: -$50 + $0 = -$50
expect(preview.total).toBe(-50);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
});
// 7.5 Multiple discounts stacking on version update
test.concurrent(`${chalk.yellowBright("version-discount: multiple discounts stack")}`, async () => {
const customerId = "version-disc-multi";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit,
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
// Apply multiple discounts: 20% + 10%
const coupon1 = await createPercentCoupon({
stripeCli,
percentOff: 20,
});
const coupon2 = await createPercentCoupon({
stripeCli,
percentOff: 10,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon1.id, coupon2.id],
});
// Create v2 with higher price ($20 per unit instead of $10)
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20, // Doubled price
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Preview upgrade to v2
const preview = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Current: 5 units * $10 = $50
// New v2: 5 units * $20 = $100
// Upgrade generates: refund (-$50) + charge ($100)
// Discounts only apply to charges, not refunds
// Charge with 20% then 10% off: $100 * 0.8 = $80, then $80 * 0.9 = $72
// Total: -$50 + $72 = $22
const refundAmount = -50;
const discountedCharge = Math.round(100 * 0.8 * 0.9);
const expectedAmount = refundAmount + discountedCharge;
expect(preview.total).toBe(expectedAmount);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
});
// 7.6 Discount preserved across multiple version updates
test.concurrent(`${chalk.yellowBright("version-discount: preserved across v1 to v2 to v3")}`, async () => {
const customerId = "version-disc-preserve";
const product = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: pricePerUnit, // $10 per unit
}),
],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [product] }),
],
actions: [
s.attach({
productId: product.id,
options: [
{ feature_id: TestFeature.Messages, quantity: 5 * billingUnits },
],
}),
],
});
const { stripeCli, subscription } = await getStripeSubscription({
customerId,
});
const coupon = await createPercentCoupon({
stripeCli,
percentOff: 25,
});
await applySubscriptionDiscount({
stripeCli,
subscriptionId: subscription.id,
couponIds: [coupon.id],
});
// Create v2 with $15 per unit
const productV2 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 15,
}),
],
});
await autumnV1.products.update(product.id, {
items: productV2.items,
});
// Update to v2
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 2,
});
// Create v3 with $20 per unit
const productV3 = products.base({
id: "prepaid",
items: [
items.prepaid({
featureId: TestFeature.Messages,
billingUnits,
price: 20,
}),
],
});
await autumnV1.products.update(product.id, {
items: productV3.items,
});
// Preview upgrade to v3
const previewV3 = await autumnV1.subscriptions.previewUpdate({
customer_id: customerId,
product_id: product.id,
version: 3,
});
// Current v2: 5 units * $15 = $75
// New v3: 5 units * $20 = $100
// Upgrade generates: refund (-$75) + charge ($100)
// Discounts only apply to charges, not refunds
// Charge with 25% off: $100 * 0.75 = $75
// Total: -$75 + $75 = $0
const refundAmount = -75;
const discountedCharge = Math.round(100 * 0.75);
const expectedAmount = refundAmount + discountedCharge;
expect(previewV3.total).toBe(expectedAmount);
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: product.id,
version: 3,
});
});