fix allocated tests

This commit is contained in:
John Yeo
2026-03-02 15:17:08 +00:00
parent 79370db01e
commit d0fd053fc9
6 changed files with 222 additions and 392 deletions

View File

@@ -47,7 +47,10 @@ export const computeUpdateCustomerEntitlementPlan = ({
// Downgrade case
if (previousOverage <= 0) {
// Just return
return undefined;
return {
customerEntitlement,
balanceChange: 0,
};
} else {
// Plan for downgrade
const customerPrice = cusEntToCusPrice({

View File

@@ -1,127 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
LegacyVersion,
OnDecrease,
OnIncrease,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { getV2Balance } from "../../testBalanceUtils";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 1,
config: {
on_increase: OnIncrease.BillImmediately,
on_decrease: OnDecrease.None,
},
});
export const pro = constructProduct({
items: [userItem],
type: "pro",
});
const testCase = "track-paid-allocated4";
describe(`${chalk.yellowBright(`${testCase}: Testing track usage for cont use (without overage)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
const autumnV2: AutumnInt = new AutumnInt({ version: ApiVersion.V2_0 });
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
});
let usage = 0;
test("should attach pro", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should track +1 and have no new invoice", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: 1,
});
usage += 1;
await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
});
test("should track -1 and have no new invoice", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: -1,
});
usage -= 1;
await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
});
// Verify balance values reflect the replaceable (unused) logic
const v2Balance = await getV2Balance({
customerId,
featureId: TestFeature.Users,
});
expect(v2Balance).toMatchObject({
granted_balance: 1,
purchased_balance: 0,
current_balance: 1,
usage: 0,
});
});
});

View File

@@ -1,227 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectSubQuantityCorrect,
expectUpcomingItemsCorrect,
} from "@tests/utils/expectUtils/expectContUseUtils.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { getV2Balance } from "../../testBalanceUtils";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 1,
config: {
on_increase: OnIncrease.ProrateNextCycle,
on_decrease: OnDecrease.ProrateNextCycle,
},
});
export const pro = constructProduct({
items: [userItem],
type: "pro",
});
const testCase = "track-paid-allocated5";
describe(`${chalk.yellowBright(`${testCase}: Testing track usage for cont use, prorate next cycle`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let curUnix = Date.now();
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
let usage = 0;
test("should attach pro", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should create track +3 usage and have correct invoice", async () => {
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addWeeks(new Date(), 2).getTime(),
waitForSeconds: 30,
});
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: 3,
});
usage += 3;
const { stripeSubs, fullCus } = await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus,
stripeSubs,
curUnix,
expectedNumItems: 1,
unitPrice: userItem.price!,
quantity: 2,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
const v2Balance = await getV2Balance({
customerId,
featureId: TestFeature.Users,
});
expect(v2Balance).toMatchObject({
granted_balance: 1,
purchased_balance: 2,
current_balance: 0,
usage: 3,
});
});
test("should track -1 and have no new invoice", async () => {
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
waitForSeconds: 30,
});
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: -1,
});
usage -= 1;
const { stripeSubs, fullCus } = await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus,
stripeSubs,
unitPrice: userItem.price!,
curUnix,
expectedNumItems: 2,
quantity: -1,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
const v2Balance = await getV2Balance({
customerId,
featureId: TestFeature.Users,
});
expect(v2Balance).toMatchObject({
granted_balance: 1,
purchased_balance: 1,
current_balance: 0,
usage: 2,
});
});
test("should track -1 and have no new invoice", async () => {
const quantity = -1;
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: quantity,
});
usage += quantity;
const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus,
stripeSubs,
unitPrice: userItem.price!,
curUnix,
expectedNumItems: 3,
quantity,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
const v2Balance = await getV2Balance({
customerId,
featureId: TestFeature.Users,
});
expect(v2Balance).toMatchObject({
granted_balance: 1,
purchased_balance: 0,
current_balance: 0,
usage: 1,
});
});
});

View File

@@ -5,12 +5,10 @@ import {
OnIncrease,
ProductItemFeatureType,
} from "@autumn/shared";
import { calculateProratedDiff } from "@tests/integration/billing/utils/proration/calculateProratedDiff.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import {
expectSubQuantityCorrect,
expectUpcomingItemsCorrect,
} from "@tests/utils/expectUtils/expectContUseUtils.js";
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
import { getSubsFromCusId } from "@tests/utils/expectUtils/expectSubUtils.js";
import { timeout } from "@tests/utils/genUtils.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
@@ -211,28 +209,36 @@ test.concurrent(`${chalk.yellowBright("legacy-set-usage2: ProrateNextCycle sub q
await timeout(15000);
let usage = 3;
const usage1 = 3;
const { stripeSubs, fullCus } = await expectSubQuantityCorrect({
const { stripeSubs } = await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
usage: usage1,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus,
stripeSubs,
curUnix,
expectedNumItems: 1,
unitPrice: userItem.price!,
quantity: 2, // 3 usage - 1 included = 2 overage
const stripeCustomerId = stripeSubs[0].customer as string;
// Step 1: overage went 0 → 2. Credit for $0 old is filtered; 1 deferred invoice item created.
const proratedCharge1 = await calculateProratedDiff({
customerId,
advancedTo: curUnix,
oldAmount: 0,
newAmount: 2 * userItem.price!,
});
const items1 = await ctx.stripeCli.invoiceItems.list({
customer: stripeCustomerId,
});
expect(items1.data.length).toBe(1);
expect(
Math.abs(items1.data[0].amount - Math.round(proratedCharge1 * 100)),
).toBeLessThanOrEqual(1);
const customer1 = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expect(customer1.invoices!.length).toBe(1);
@@ -250,7 +256,7 @@ test.concurrent(`${chalk.yellowBright("legacy-set-usage2: ProrateNextCycle sub q
value: 2,
});
const result2 = await expectSubQuantityCorrect({
await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
@@ -260,16 +266,25 @@ test.concurrent(`${chalk.yellowBright("legacy-set-usage2: ProrateNextCycle sub q
usage: 2,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus: result2.fullCus,
stripeSubs: result2.stripeSubs,
unitPrice: userItem.price!,
curUnix,
expectedNumItems: 2,
quantity: -1, // decrease by 1 seat
// Step 2: overage went 2 → 1. Two deferred items: credit for old (2 overage) + charge for new (1 overage).
// Net of the 2 newest items = prorated diff from 2×$50 → 1×$50.
const proratedDiff2 = await calculateProratedDiff({
customerId,
advancedTo: curUnix,
oldAmount: 2 * userItem.price!,
newAmount: 1 * userItem.price!,
});
const items2 = await ctx.stripeCli.invoiceItems.list({
customer: stripeCustomerId,
});
expect(items2.data.length).toBe(3);
const netStep2Cents = items2.data[0].amount + items2.data[1].amount;
expect(
Math.abs(netStep2Cents - Math.round(proratedDiff2 * 100)),
).toBeLessThanOrEqual(1);
const customer2 = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expect(customer2.invoices!.length).toBe(1);
@@ -280,28 +295,35 @@ test.concurrent(`${chalk.yellowBright("legacy-set-usage2: ProrateNextCycle sub q
value: 4,
});
usage = 4;
const result3 = await expectSubQuantityCorrect({
await expectSubQuantityCorrect({
stripeCli: ctx.stripeCli,
productId: pro.id,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
usage,
usage: 4,
});
await expectUpcomingItemsCorrect({
stripeCli: ctx.stripeCli,
fullCus: result3.fullCus,
stripeSubs: result3.stripeSubs,
unitPrice: userItem.price!,
curUnix,
expectedNumItems: 3,
quantity: 2, // +2 seats from usage=2 to usage=4
// Step 3: overage went 1 → 3. Two deferred items: credit for old (1 overage) + charge for new (3 overage).
// Net of the 2 newest items = prorated diff from 1×$50 → 3×$50.
const proratedDiff3 = await calculateProratedDiff({
customerId,
advancedTo: curUnix,
oldAmount: 1 * userItem.price!,
newAmount: 3 * userItem.price!,
});
const items3 = await ctx.stripeCli.invoiceItems.list({
customer: stripeCustomerId,
});
expect(items3.data.length).toBe(5);
const netStep3Cents = items3.data[0].amount + items3.data[1].amount;
expect(
Math.abs(netStep3Cents - Math.round(proratedDiff3 * 100)),
).toBeLessThanOrEqual(1);
const customer3 = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expect(customer3.invoices!.length).toBe(1);
}, 300_000); // Longer timeout for Stripe test clock operations
}); // Longer timeout for Stripe test clock operations

View File

@@ -75,6 +75,31 @@ test(`${chalk.yellowBright("bill-imm1: track within included usage creates no in
await expectCustomerInvoiceCorrect({ customerId, count: 1 });
await expectStripeSubscriptionCorrect({ ctx, customerId });
await autumnV2.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: -1,
});
expect(trackRes.balance).toMatchObject({
granted_balance: 1,
purchased_balance: 0,
current_balance: 1,
usage: 0,
});
await expectFeatureCachedAndDb({
autumn: autumnV1,
customerId,
featureId: TestFeature.Users,
balance: 1,
usage: 0,
});
await expectCustomerInvoiceCorrect({ customerId, count: 1 });
await expectStripeSubscriptionCorrect({ ctx, customerId });
});
// ═══════════════════════════════════════════════════════════════════

View File

@@ -10,8 +10,10 @@ import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/e
import { expectFeatureCachedAndDb } from "@tests/integration/billing/utils/expectFeatureCachedAndDb.js";
import { expectStripeSubscriptionCorrect } from "@tests/integration/billing/utils/expectStripeSubCorrect/expectStripeSubscriptionCorrect.js";
import { calculateProratedDiff } from "@tests/integration/billing/utils/proration/calculateProratedDiff.js";
import { getStripeSubscription } from "@tests/integration/billing/utils/stripeSubscriptionUtils.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { products } from "@tests/utils/fixtures/products.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import { advanceToNextInvoice } from "@tests/utils/testAttachUtils/testAttachUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
@@ -193,3 +195,135 @@ test(`${chalk.yellowBright("prorate-nc2: mid-cycle decrease creates no immediate
await expectStripeSubscriptionCorrect({ ctx, customerId });
});
// ═══════════════════════════════════════════════════════════════════
// prorate-nc3: Track +3 then -1 — verify pending invoice items
//
// Checks that each track creates correct prorated pending invoice
// items on the Stripe subscription. A decrease creates two items
// (credit for old allocation + charge for new allocation).
// ═══════════════════════════════════════════════════════════════════
test(`${chalk.yellowBright("prorate-nc3: pending invoice items match prorated amounts after increase and decrease")}`, async () => {
const pro = products.pro({ id: "pro", items: [userItem] });
const { customerId, autumnV1, autumnV2, testClockId, advancedTo } =
await initScenario({
customerId: "prorate-nc3",
setup: [
s.customer({ testClock: true, paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [
s.attach({ productId: pro.id }),
s.advanceTestClock({ weeks: 2 }),
],
});
// ─── Step 1: Track +3 (1 included + 2 overage) ───
const trackRes1: TrackResponseV2 = await autumnV2.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: 3,
});
expect(trackRes1.balance).toMatchObject({
granted_balance: 1,
purchased_balance: 2,
current_balance: 0,
usage: 3,
});
await expectFeatureCachedAndDb({
autumn: autumnV1,
customerId,
featureId: TestFeature.Users,
balance: -2,
usage: 3,
});
await expectCustomerInvoiceCorrect({ customerId, count: 1 });
await expectStripeSubscriptionCorrect({ ctx, customerId });
const proratedOverage = await calculateProratedDiff({
customerId,
advancedTo,
oldAmount: 0,
newAmount: 2 * PRICE_PER_SEAT,
});
const { stripeCli, stripeCustomerId } = await getStripeSubscription({
customerId,
});
const itemsAfterIncrease = await stripeCli.invoiceItems.list({
customer: stripeCustomerId,
});
expect(itemsAfterIncrease.data.length).toBe(1);
expect(itemsAfterIncrease.data[0].amount).toBe(
Math.round(proratedOverage * 100),
);
// ─── Step 2: Advance 1 week, then track -1 ───
const advancedTo2 = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId: testClockId!,
startingFrom: new Date(advancedTo),
numberOfWeeks: 1,
waitForSeconds: 30,
});
const trackRes2: TrackResponseV2 = await autumnV2.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: -1,
});
expect(trackRes2.balance).toMatchObject({
granted_balance: 1,
purchased_balance: 1,
current_balance: 0,
usage: 2,
});
await expectFeatureCachedAndDb({
autumn: autumnV1,
customerId,
featureId: TestFeature.Users,
balance: -1,
usage: 2,
});
await expectCustomerInvoiceCorrect({ customerId, count: 1 });
await expectStripeSubscriptionCorrect({ ctx, customerId });
const proratedDecrease = await calculateProratedDiff({
customerId,
advancedTo: advancedTo2,
oldAmount: 2 * PRICE_PER_SEAT,
newAmount: 1 * PRICE_PER_SEAT,
});
const itemsAfterDecrease = await stripeCli.invoiceItems.list({
customer: stripeCustomerId,
});
// 3 total: 1 from the increase + 2 from the decrease (credit + charge)
expect(itemsAfterDecrease.data.length).toBe(3);
// Newest 2 items are from the decrease (Stripe returns newest first)
const decreaseItems = itemsAfterDecrease.data.slice(0, 2);
const hasCredit = decreaseItems.some((item) => item.amount < 0);
const hasCharge = decreaseItems.some((item) => item.amount > 0);
expect(hasCredit).toBe(true);
expect(hasCharge).toBe(true);
const decreaseNetCents = decreaseItems.reduce(
(sum, item) => sum + item.amount,
0,
);
expect(decreaseNetCents).toBe(Math.round(proratedDecrease * 100));
});