This commit is contained in:
John Yeo
2026-02-07 10:42:03 -08:00
parent 84a083f960
commit 774ba56ece
18 changed files with 189 additions and 838 deletions

View File

@@ -8,7 +8,7 @@ BUN_PARALLEL_V2 \
'integration/billing/migrations' \
'integration/billing/cron' \
'integration/crud/customers' \
# 'integration/billing/attach' \
'integration/billing/attach' \
# 'integration/billing/attach' \

View File

@@ -4,23 +4,26 @@ source "$(dirname "$0")/config.sh"
export TEST_FILE_CONCURRENCY=6
# 'attach/migrations' \
BUN_PARALLEL_V2 \
'attach/basic' \
'attach/upgrade' \
'attach/downgrade' \
'attach/free' \
'attach/addOn' \
'attach/checkout' \
'attach/others' \
'attach/upgradeOld' \
'attach/response' \
'interval/upgrade' \
'interval/multiSub' \
# 'billing/new-billing-subscription' \
# 'billing/legacy/attach' \
# 'server/tests/attach/entities'
# --max=6
'billing/legacy/attach'
# 'billing/invoice-action-required' \
# BUN_PARALLEL_V2 \
# 'attach/basic' \
# 'attach/upgrade' \
# 'attach/downgrade' \
# 'attach/free' \
# 'attach/addOn' \
# 'attach/checkout' \
# 'attach/others' \
# 'attach/upgradeOld' \
# 'attach/response' \
# 'interval/upgrade' \
# 'interval/multiSub' \
# 'server/tests/attach/entities'
# # 'billing/new-billing-subscription' \
# # 'billing/legacy/attach' \
# # --max=6
# # 'billing/invoice-action-required' \

View File

@@ -111,6 +111,8 @@ export const handleAttach = createRoute({
const { products, customer } = attachParams;
console.log("ATTACH RESPONSE:", response);
const responseV1 = AttachResponseV1Schema.parse({
success: true,
product_ids: products.map((p) => p.id),

View File

@@ -20,7 +20,7 @@ export const buildAutumnLineItems = ({
const { logger } = ctx;
// For now, update subscription doesn't charge for existing usage.
const { lineItems: arrearLineItems, updateCustomerEntitlements } =
let { lineItems: arrearLineItems, updateCustomerEntitlements } =
deletedCustomerProduct && includeArrearLineItems
? customerProductToArrearLineItems({
ctx,
@@ -33,6 +33,8 @@ export const buildAutumnLineItems = ({
})
: { lineItems: [], updateCustomerEntitlements: [] };
arrearLineItems = arrearLineItems.filter((lineItem) => lineItem.amount !== 0);
// Get line items for ongoing cus product
const deletedLineItems = deletedCustomerProduct
? customerProductToLineItems({

View File

@@ -32,26 +32,11 @@ export const filterLineItemsForTrialTransition = ({
return lineItems;
}
return lineItems
.map((lineItem) => {
return lineItems.filter((lineItem) => {
const { billingTiming, direction, price } = lineItem.context;
const isPositive = lineItem.amount > 0;
const isRecurringPrice = !isOneOffPrice(price);
// Starting trial (!isTrialing → willBeTrialing):
// Mark in_advance positive recurring items as deferred (will be charged after trial)
if (willBeTrialing) {
if (billingTiming === "in_advance" && isPositive && isRecurringPrice) {
return { ...lineItem, deferredForTrial: true };
}
}
return lineItem;
})
.filter((lineItem) => {
const { billingTiming, direction } = lineItem.context;
const isPositive = lineItem.amount > 0;
// Ending trial (isTrialing → !willBeTrialing):
// Filter out refunds and in_arrear positive items (no refund for trial period, no arrear charges)
if (isTrialing) {
@@ -59,6 +44,13 @@ export const filterLineItemsForTrialTransition = ({
if (billingTiming === "in_arrear" && isPositive) return false;
}
// Starting trial (!isTrialing → willBeTrialing):
// Filter out in_advance positive items (no charge for upcoming trial period)
if (willBeTrialing) {
if (billingTiming === "in_advance" && isPositive && isRecurringPrice)
return false;
}
return true;
});
};

View File

@@ -15,10 +15,7 @@ import { attachParamsToMetadata } from "@/internal/billing/attach/utils/attachPa
import { billingActions } from "@/internal/billing/v2/actions/index.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { addIntervalToAnchor } from "@/internal/products/prices/billingIntervalUtils2.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
@@ -95,9 +92,7 @@ export const handlePaidProduct = async ({
checkout_url: billingResponse?.payment_url ?? undefined,
invoice: attachParams.invoiceOnly
? attachToInvoiceResponse({
invoice: billingResult?.stripe?.stripeInvoice || undefined,
})
? billingResult?.stripe?.stripeInvoice
: undefined,
});

View File

@@ -16,7 +16,8 @@ export const handleUpdateQuantityFunction = async ({
attachParams: AttachParams;
body: AttachBodyV0;
}) => {
const { billingResponse } = await billingActions.legacy.updateQuantity({
const { billingResponse, billingResult } =
await billingActions.legacy.updateQuantity({
ctx,
body,
attachParams,
@@ -25,7 +26,7 @@ export const handleUpdateQuantityFunction = async ({
return AttachFunctionResponseSchema.parse({
code: SuccessCode.FeaturesUpdated,
message: `Successfully updated quantity for features`,
invoice: billingResponse?.invoice,
invoice: billingResult?.stripe?.stripeInvoice,
checkout_url: billingResponse?.payment_url,
});

View File

@@ -6,7 +6,6 @@ import {
} from "@autumn/shared";
import { billingActions } from "@/internal/billing/v2/actions/index.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { attachToInvoiceResponse } from "@/internal/invoices/invoiceUtils.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
export const handleUpgradeFlow = async ({
@@ -268,9 +267,7 @@ export const handleUpgradeFlow = async ({
checkout_url: billingResponse?.payment_url,
invoice: attachParams.invoiceOnly
? attachToInvoiceResponse({
invoice: billingResult?.stripe?.stripeInvoice || undefined,
})
? billingResult?.stripe?.stripeInvoice
: undefined,
});
};

View File

@@ -20,7 +20,7 @@ import { products } from "@tests/utils/fixtures/products.js";
import { createTestContext } from "@tests/utils/testInitUtils/createTestContext";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
const SEED_PREFIX = "seed-cancel-test";
const SEED_PREFIX = "seed";
async function seedScenarios() {
// Clear and setup org first
@@ -40,7 +40,7 @@ async function seedScenarios() {
items.consumableWords({ includedUsage: 100 }), // 100 free words, then $0.05/word overage
items.allocatedWorkflows({ includedUsage: 3 }), // 3 free workflows, $10/workflow overage
items.prepaidMessages({
includedUsage: 50,
includedUsage: 100,
billingUnits: 100,
price: 10,
}), // 50 free messages, $10/100 pack
@@ -83,12 +83,26 @@ async function seedScenarios() {
],
});
const growthProduct = products.growth({
id: "growth",
items: [
items.consumableWords({ includedUsage: 1000 }),
items.allocatedWorkflows({ includedUsage: 10 }),
items.prepaidMessages({
includedUsage: 200,
billingUnits: 100,
price: 6,
}), // 200 free messages, $8/100 pack
],
});
const allProducts = [
proProduct,
recurringAddOn,
freeProduct,
oneTimeProduct,
premiumProduct,
growthProduct,
];
// ═══════════════════════════════════════════════════════════════════

View File

@@ -1,11 +1,4 @@
import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect.js";
import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect.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 { completeInvoiceCheckout } from "@tests/utils/stripeUtils/completeInvoiceCheckout.js";
import { test } from "bun:test";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
@@ -13,60 +6,23 @@ import chalk from "chalk";
* Test: Attach free default product, then attach pro with invoice mode
*/
test.concurrent(`${chalk.yellowBright("invoice-mode: free default then pro with invoice checkout")}`, async () => {
const messagesItem = items.monthlyMessages({ includedUsage: 50 });
// Free default product
const free = products.base({
id: "free",
items: [messagesItem],
isDefault: true,
});
// Pro product with price
const proMessagesItem = items.monthlyMessages({ includedUsage: 200 });
const pro = products.pro({
id: "pro",
items: [proMessagesItem],
});
const customerId = "temp-invoice-free-then-pro";
// Setup customer with default attached
const { autumnV1 } = await initScenario({
customerId,
customerId: "test",
setup: [
s.customer({ withDefault: true }),
s.products({ list: [free, pro] }),
s.customer({ paymentMethod: "success" }),
// s.products({ list: [free, pro] }),
],
actions: [s.attachPaymentMethod({ type: "fail" })],
actions: [],
});
// Attach pro with invoice mode
const attachResult = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
// invoice: true,
});
console.log("attachResult", attachResult);
return;
expect(attachResult.checkout_url).toBeDefined();
// Complete invoice checkout
await completeInvoiceCheckout({
url: attachResult.checkout_url!,
});
// Verify pro is now active
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectProductActive({
customer: customerAfter,
productId: pro.id,
});
expectCustomerFeatureCorrect({
customer: customerAfter,
featureId: TestFeature.Messages,
balance: 200,
await autumnV1.attach({
customer_id: "test",
product_id: "pro_seed-cancel-test",
options: [
{
feature_id: "messages",
quantity: 100,
},
],
});
});

View File

@@ -16,9 +16,13 @@
import { expect, test } from "bun:test";
import { type ApiCustomerV3, CusExpand } from "@autumn/shared";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import {
expectCustomerProducts,
expectProductActive,
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { expectSubCount } from "@tests/merged/mergeUtils/expectSubCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
import ctx from "@tests/utils/testInitUtils/createTestContext";
@@ -69,13 +73,19 @@ test.concurrent(`${chalk.yellowBright("attach: paid add-on with new_billing_subs
],
});
// After first add-on attach: 2 subs
// After first add-on attach: 2 subs, both products active
await expectSubCount({ ctx, customerId, count: 2 });
const customer1 = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectProductAttached({ customer: customer1, productId: addOn.id });
expect(customer1.invoices.length).toBe(2);
expect(customer1.invoices[0].total).toBe(10);
await expectCustomerProducts({
customer: customer1,
active: [pro.id, addOn.id],
});
expectCustomerInvoiceCorrect({
customer: customer1,
count: 2,
latestTotal: 20, // recurringAddOn uses type: "pro" → $20/month
});
// Attach same add-on again → 3 subs
await autumnV1.attach({
@@ -89,8 +99,11 @@ test.concurrent(`${chalk.yellowBright("attach: paid add-on with new_billing_subs
const customer2 = await autumnV1.customers.get<ApiCustomerV3>(customerId);
const addOnProduct = customer2.products.find((p) => p.id === addOn.id);
expect(addOnProduct?.quantity).toBe(2);
expect(customer2.invoices?.length).toBe(3);
expect(customer2.invoices?.[0].total).toBe(10);
expectCustomerInvoiceCorrect({
customer: customer2,
count: 3,
latestTotal: 20,
});
}, 120000);
// ═══════════════════════════════════════════════════════════════════════════════
@@ -143,7 +156,7 @@ test.concurrent(`${chalk.yellowBright("attach: entities with new_billing_subscri
await expectSubCount({ ctx, customerId, count: 2 });
const entity1 = await autumnV1.entities.get(customerId, entities[0].id);
expectProductAttached({ customer: entity1, productId: premium.id });
await expectProductActive({ customer: entity1, productId: premium.id });
// Attach premium to entity 2 → 3 subs
await autumnV1.attach({
@@ -156,30 +169,25 @@ test.concurrent(`${chalk.yellowBright("attach: entities with new_billing_subscri
await expectSubCount({ ctx, customerId, count: 3 });
const entity2 = await autumnV1.entities.get(customerId, entities[1].id);
expectProductAttached({ customer: entity2, productId: premium.id });
await expectProductActive({ customer: entity2, productId: premium.id });
// Verify final state
// Verify final state: customer pro active, both entity premiums active
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId, {
expand: [CusExpand.Invoices],
});
const customerPro = customer.products.find((p) => p.id === pro.id);
expect(customerPro).toBeDefined();
expect(customerPro?.status).toBe("active");
await expectProductActive({ customer, productId: pro.id });
const entity1Final = await autumnV1.entities.get(customerId, entities[0].id);
const e1Premium = entity1Final.products?.find(
(p: { id?: string }) => p.id === premium.id,
);
expect(e1Premium).toBeDefined();
expect(e1Premium!.status).toBe("active");
await expectProductActive({
customer: entity1Final,
productId: premium.id,
});
const entity2Final = await autumnV1.entities.get(customerId, entities[1].id);
const e2Premium = entity2Final.products?.find(
(p: { id?: string }) => p.id === premium.id,
);
expect(e2Premium).toBeDefined();
expect(e2Premium!.status).toBe("active");
await expectProductActive({
customer: entity2Final,
productId: premium.id,
});
}, 120000);
// ═══════════════════════════════════════════════════════════════════════════════
@@ -229,11 +237,15 @@ test.concurrent(`${chalk.yellowBright("attach: customer upgrade doesn't affect e
// Verify initial: customer pro + entity premium = 2 subs
await expectSubCount({ ctx, customerId, count: 2 });
const customerBefore = await autumnV1.customers.get(customerId);
expectProductAttached({ customer: customerBefore, productId: pro.id });
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectProductActive({ customer: customerBefore, productId: pro.id });
const entityBefore = await autumnV1.entities.get(customerId, entities[0].id);
expectProductAttached({ customer: entityBefore, productId: premium.id });
await expectProductActive({
customer: entityBefore,
productId: premium.id,
});
// Upgrade customer from pro → premium
await autumnV1.attach({
@@ -241,29 +253,25 @@ test.concurrent(`${chalk.yellowBright("attach: customer upgrade doesn't affect e
product_id: premium.id,
});
// Customer should have premium active, pro gone
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
expectProductAttached({ customer: customerAfter, productId: premium.id });
// Pro should be gone from customer-level products
const proProduct = customerAfter.products.find(
(p) => p.id === pro.id && !p.entity_id,
);
expect(proProduct).toBeUndefined();
await expectCustomerProducts({
customer: customerAfter,
active: [premium.id],
notPresent: [pro.id],
});
// Still 2 subs (customer premium + entity premium on separate sub)
await expectSubCount({ ctx, customerId, count: 2 });
// Entity should still have premium on its separate sub
// Entity should still have premium active on its separate sub
const entityAfter = await autumnV1.entities.get(customerId, entities[0].id);
const entityProducts = entityAfter.products!;
expect(entityProducts.length).toBe(1);
const entityPremium = entityProducts.find(
(p: { id?: string }) => p.id === premium.id,
);
expect(entityPremium).toBeDefined();
expect(entityPremium!.status).toBe("active");
await expectProductActive({
customer: entityAfter,
productId: premium.id,
});
expect(entityAfter.products!.length).toBe(1);
const invoices = customerAfter.invoices;
expect(invoices).toBeDefined();
expect(invoices!.length).toBeGreaterThanOrEqual(1);
}, 120000);
expect(customerAfter.invoices).toBeDefined();
expect(customerAfter.invoices!.length).toBeGreaterThanOrEqual(1);
});

View File

@@ -17,6 +17,8 @@ import {
AttachBranch,
CusProductStatus,
} from "@autumn/shared";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectProductTrialing } from "@tests/integration/billing/utils/expectCustomerProductTrialing";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached";
import { items } from "@tests/utils/fixtures/items";
@@ -111,11 +113,12 @@ test.concurrent(`${chalk.yellowBright("legacy-trial 1: upgrade during trial (pro
});
// Premium period_end ≈ curUnix + 7 days
const product = customer.products.find((p: any) => p.id === premium.id)!;
expect(product.current_period_end).toBeDefined();
expect(
Math.abs(product.current_period_end! - addDays(curUnix, 7).getTime()),
).toBeLessThanOrEqual(1000 * 60 * 30); // 30 min tolerance
await expectProductTrialing({
customer: customer as any,
productId: premium.id,
trialEndsAt: addDays(curUnix, 7).getTime(),
toleranceMs: 1000 * 60 * 30, // 30 min tolerance
});
await expectSubToBeCorrect({
db: ctx.db,
@@ -185,16 +188,10 @@ test.concurrent(`${chalk.yellowBright("legacy-trial 2: upgrade after trial ends
const curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId: testClockId!,
advanceTo: addDays(new Date(), 8).getTime(),
advanceTo: addDays(new Date(), 12).getTime(),
waitForSeconds: 30,
});
// Preview attach → should be Upgrade branch (trial ended, product is active)
const attachPreview = await autumnV1.attachPreview({
customer_id: customerId,
product_id: premium.id,
});
expect(attachPreview?.branch).toBe(AttachBranch.Upgrade);
// Get checkout total for comparison
const checkoutRes = await autumnV1.checkout({
customer_id: customerId,
@@ -217,14 +214,21 @@ test.concurrent(`${chalk.yellowBright("legacy-trial 2: upgrade after trial ends
});
// Premium period_end ≈ curUnix + 7 days
const product = customer.products.find((p: any) => p.id === premium.id)!;
expect(product.current_period_end).toBeDefined();
expect(
Math.abs(product.current_period_end! - addDays(curUnix, 7).getTime()),
).toBeLessThanOrEqual(1000 * 60 * 30); // 30 min tolerance
await expectProductTrialing({
customer: customer as any,
productId: premium.id,
trialEndsAt: addDays(curUnix, 7).getTime(),
toleranceMs: 1000 * 60 * 30, // 30 min tolerance
});
await expectCustomerInvoiceCorrect({
customer: customer as any,
count: 4,
latestTotal: 0,
});
// Invoice total should match checkout preview
expect(customer.invoices[0].total).toBe(
expect(customer.invoices?.[1]?.total).toBe(
new Decimal(checkoutRes.total).toDP(2).toNumber(),
);
@@ -235,4 +239,4 @@ test.concurrent(`${chalk.yellowBright("legacy-trial 2: upgrade after trial ends
env: ctx.env,
shouldBeTrialing: true,
});
}, 120000);
});

View File

@@ -14,8 +14,9 @@
import { expect, test } from "bun:test";
import type { ApiCustomerV3, CusProductStatus } from "@autumn/shared";
import { calculateExpectedInvoiceAmount } from "@tests/integration/billing/utils/calculateExpectedInvoiceAmount";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached";
import { items } from "@tests/utils/fixtures/items";
import { products } from "@tests/utils/fixtures/products";
@@ -57,7 +58,7 @@ test.concurrent(`${chalk.yellowBright("legacy-upgrade-merged 1: upgrade entity i
],
actions: [
s.attach({ productId: pro.id, entityIndex: 0 }),
s.attach({ productId: pro.id, entityIndex: 1, timeout: 3000 }),
s.attach({ productId: pro.id, entityIndex: 1, timeout: 4000 }),
s.track({
featureId: TestFeature.Words,
value: entity1Val,
@@ -70,7 +71,7 @@ test.concurrent(`${chalk.yellowBright("legacy-upgrade-merged 1: upgrade entity i
entityIndex: 1,
timeout: 3000,
}),
s.advanceTestClock({ weeks: 2 }),
// s.advanceTestClock({ weeks: 2 }),
],
});
@@ -81,6 +82,24 @@ test.concurrent(`${chalk.yellowBright("legacy-upgrade-merged 1: upgrade entity i
entity_id: "ent-1",
});
const entity1Overage = calculateExpectedInvoiceAmount({
items: pro.items,
usage: [{ featureId: TestFeature.Words, value: entity1Val }],
options: { onlyArrear: true },
});
const entity1Base =
getBasePrice({ product: premium }) - getBasePrice({ product: pro });
const customerAfterAttach =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerInvoiceCorrect({
customer: customerAfterAttach,
count: 3,
latestTotal: entity1Base + entity1Overage,
});
// Advance to next invoice to check usage billing
await advanceToNextInvoice({
stripeCli: ctx.stripeCli,
@@ -89,15 +108,10 @@ test.concurrent(`${chalk.yellowBright("legacy-upgrade-merged 1: upgrade entity i
});
// Entity 2's usage on pro should show up on the invoice
const expectedUsageTotal = await getExpectedInvoiceTotal({
customerId,
productId: pro.id,
const expectedUsageTotal = calculateExpectedInvoiceAmount({
items: pro.items,
usage: [{ featureId: TestFeature.Words, value: entity2Val }],
onlyIncludeUsage: true,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
options: { includeFixed: false, onlyArrear: true },
});
const basePrice =
@@ -106,7 +120,7 @@ test.concurrent(`${chalk.yellowBright("legacy-upgrade-merged 1: upgrade entity i
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
const invoice = customer.invoices![0];
expect(invoice.total).toBe(basePrice + expectedUsageTotal);
}, 120000);
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: Upgrade cancels scheduled downgrade (single entity)

View File

@@ -1,196 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
type AppEnv,
CusProductStatus,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.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 { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils";
import { constructArrearItem } 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";
const premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const premiumAnnual = constructProduct({
id: "premiumAnnual",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
isAnnual: true,
});
const pro = constructProduct({
id: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const ops = [
{
entityId: "1",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
];
const testCase = "mergedUpgrade1";
describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro, premium, premiumAnnual],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
testClockId = res.testClockId!;
await autumn.entities.create(customerId, entities);
});
for (let index = 0; index < ops.length; index++) {
const op = ops[index];
test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => {
try {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
entityId: op.entityId,
});
} catch (error) {
console.log(
`Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
);
throw error;
}
});
}
const entity1Val = 100000;
const entity2Val = 300000;
test("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async () => {
await timeout(3000);
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: entity1Val,
entity_id: "1",
});
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: entity2Val,
entity_id: "2",
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(Date.now(), 2).getTime(),
waitForSeconds: 30,
});
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
entityId: "1",
});
});
test("should advance to next invoice and have correct invoice", async () => {
await advanceToNextInvoice({
stripeCli,
testClockId,
withPause: true,
});
const expectedTotal = await getExpectedInvoiceTotal({
org,
env,
customerId,
productId: pro.id,
stripeCli,
db,
onlyIncludeUsage: true,
usage: [
{
featureId: TestFeature.Words,
value: entity2Val,
},
],
});
const customer = await autumn.customers.get(customerId);
const invoice = customer.invoices[0];
const basePrice =
getBasePrice({ product: pro }) + getBasePrice({ product: premium });
expect(invoice.total).toBe(basePrice + expectedTotal);
});
});

View File

@@ -1,143 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
type AppEnv,
CusProductStatus,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } 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";
// UNCOMMENT FROM HERE
const pro = constructProduct({
id: "pro",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "pro",
});
const free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "free",
isDefault: false,
});
const premium = constructProduct({
id: "premium",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "premium",
});
const growth = constructProduct({
id: "growth",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "growth",
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "1",
product: pro,
results: [
{ product: premium, status: CusProductStatus.Active },
{ product: pro, status: CusProductStatus.Scheduled },
],
},
{
entityId: "1",
product: growth,
results: [{ product: growth, status: CusProductStatus.Active }],
},
];
const testCase = "mergedUpgrade2";
describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a scheduled downgrade")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro, free, premium, growth],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
await autumn.entities.create(customerId, entities);
});
for (const op of ops) {
test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
entities,
entityId: op.entityId,
});
for (const result of op.results) {
const entity = await autumn.entities.get(customerId, op.entityId);
expectProductAttached({
customer: entity,
product: result.product,
status: result.status,
});
}
});
}
});

View File

@@ -1,152 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
type AppEnv,
CusProductStatus,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } 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";
// UNCOMMENT FROM HERE
const pro = constructProduct({
id: "pro",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "pro",
});
const free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "free",
isDefault: false,
});
const premium = constructProduct({
id: "premium",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "premium",
});
const growth = constructProduct({
id: "growth",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "growth",
});
const ops = [
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
{
entityId: "1",
product: pro,
results: [
{ product: premium, status: CusProductStatus.Active },
{ product: pro, status: CusProductStatus.Scheduled },
],
},
{
entityId: "2",
product: pro,
results: [
{ product: premium, status: CusProductStatus.Active },
{ product: pro, status: CusProductStatus.Scheduled },
],
},
{
entityId: "2",
product: growth,
results: [{ product: growth, status: CusProductStatus.Active }],
// waitForInvoice: 5000,
},
];
const testCase = "mergedUpgrade3";
describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a scheduled downgrade")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro, free, premium, growth],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
await autumn.entities.create(customerId, entities);
});
for (const op of ops) {
test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
entities,
entityId: op.entityId,
});
for (const result of op.results) {
const entity = await autumn.entities.get(customerId, op.entityId);
expectProductAttached({
customer: entity,
product: result.product,
status: result.status,
});
}
});
}
});

View File

@@ -1,146 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
type AppEnv,
CusProductStatus,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } 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";
// OPERATIONS:
// Pro, Pro
// Free
// Premium
const pro = constructProduct({
id: "pro",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "pro",
});
const free = constructProduct({
id: "free",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "free",
isDefault: false,
});
const premium = constructProduct({
id: "premium",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "premium",
});
const growth = constructProduct({
id: "growth",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "growth",
});
const ops = [
{
entityId: "1",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
{
entityId: "2",
product: pro,
results: [{ product: pro, status: CusProductStatus.Active }],
},
{
entityId: "1",
product: free,
results: [
{ product: pro, status: CusProductStatus.Active },
{ product: free, status: CusProductStatus.Scheduled },
],
},
{
entityId: "1",
product: premium,
results: [{ product: premium, status: CusProductStatus.Active }],
},
];
const testCase = "mergedUpgrade4";
describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro, free, premium, growth],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
await autumn.entities.create(customerId, entities);
});
for (const op of ops) {
test(`should attach ${op.product.id} to entity ${op.entityId}`, async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: op.product,
stripeCli,
db,
org,
env,
entities,
entityId: op.entityId,
});
for (const result of op.results) {
const entity = await autumn.entities.get(customerId, op.entityId);
expectProductAttached({
customer: entity,
product: result.product,
status: result.status,
});
}
});
}
});

View File

@@ -26,7 +26,7 @@ export const completeCheckoutForm = async (
_isLocal?: boolean,
) => {
const browser = await puppeteer.launch({
headless: false,
headless: true,
executablePath: process.env.TESTS_CHROMIUM_PATH,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});