chore: migrated legacy tests to legacy/attach folder

This commit is contained in:
John Yeo
2026-02-22 22:48:16 +00:00
parent 98be3dd54d
commit b6a37c6762
132 changed files with 5677 additions and 9168 deletions

View File

@@ -264,7 +264,12 @@ export class AutumnInt {
{
skipWebhooks,
idempotencyKey,
}: { skipWebhooks?: boolean; idempotencyKey?: string } = {},
timeout,
}: {
skipWebhooks?: boolean;
idempotencyKey?: string;
timeout?: number;
} = {},
): Promise<any> {
const headers: Record<string, string> = {};
if (skipWebhooks !== undefined) {
@@ -280,6 +285,13 @@ export class AutumnInt {
Object.keys(headers).length > 0 ? headers : undefined,
);
const concurrency = Number(process.env.TEST_FILE_CONCURRENCY || "0");
const defaultTimeout = concurrency > 1 ? 5000 : 4000;
const finalTimeout = timeout ?? defaultTimeout;
if (finalTimeout) {
await new Promise((resolve) => setTimeout(resolve, finalTimeout));
}
return data;
}

View File

@@ -0,0 +1,39 @@
import type { TestGroup } from "./types";
export const core: TestGroup = {
name: "core",
description:
"Critical flows that must pass: basic attach, balance operations, CRUD, unit tests",
tier: "core",
paths: [
// ── Balance: Check ──
"integration/balances/check/check-basic.test.ts",
"balances/check/credit-systems",
"balances/check/send-event",
// ── Balance: Track ──
"balances/track/basic",
"balances/track/concurrency",
"balances/track/credit-systems",
"balances/track/entity-balances",
"balances/track/entity-products",
"balances/track/negative",
"balances/track/paid-allocated",
"integration/balances/track/track-misc.test.ts",
// ── Balance: Update ──
"integration/balances/update/balance/update-balance-basic.test.ts",
// ── Legacy Attach ──
"integration/billing/legacy/attach/attach-new-billing-subscription.test.ts",
"integration/billing/legacy/attach/attach-misc.test.ts",
"integration/billing/legacy/attach/downgrade/legacy-downgrade-merged-schedule.test.ts",
"integration/billing/legacy/attach/group/legacy-group-merged.test.ts",
"integration/billing/legacy/attach/invoice/legacy-attach-invoice-mode.test.ts",
"integration/billing/legacy/attach/new/legacy-new-merged.test.ts",
"integration/billing/legacy/attach/separate/legacy-separate.test.ts",
"integration/billing/legacy/attach/trial/legacy-trial.test.ts",
"integration/billing/legacy/attach/update-quantity/legacy-update-quantity.test.ts",
"integration/billing/legacy/attach/upgrade/legacy-upgrade.test.ts",
],
};

View File

@@ -0,0 +1,9 @@
import type { TestGroup } from "../types";
export const advanced: TestGroup = {
name: "advanced",
description:
"Coupons, referrals, custom intervals, usage limits, multi-feature",
tier: "domain",
paths: [],
};

View File

@@ -0,0 +1,8 @@
import type { TestGroup } from "../types";
export const balances: TestGroup = {
name: "balances",
description: "All balance check, track, set-usage, and cron tests",
tier: "domain",
paths: [],
};

View File

@@ -0,0 +1,9 @@
import type { TestGroup } from "../types";
export const billing: TestGroup = {
name: "billing",
description:
"All attach, upgrade, downgrade, checkout, invoice, subscription, and legacy billing tests",
tier: "domain",
paths: [],
};

View File

@@ -0,0 +1,8 @@
import type { TestGroup } from "../types";
export const crud: TestGroup = {
name: "crud",
description: "Customer, plan, entity, and feature CRUD operations",
tier: "domain",
paths: [],
};

View File

@@ -0,0 +1,9 @@
import type { TestGroup } from "../types";
export const misc: TestGroup = {
name: "misc",
description:
"Cron jobs, external PSPs, scenarios, rate limits, archived tests",
tier: "domain",
paths: [],
};

View File

@@ -0,0 +1,9 @@
import type { TestGroup } from "../types";
export const webhooks: TestGroup = {
name: "webhooks",
description: "Stripe and Autumn webhook handlers",
tier: "domain",
maxConcurrency: 3,
paths: [],
};

View File

@@ -0,0 +1,96 @@
import { readdir } from "node:fs/promises";
import { join, relative } from "node:path";
import { core } from "./core";
import { advanced } from "./domains/advanced";
import { balances } from "./domains/balances";
import { billing } from "./domains/billing";
import { crud } from "./domains/crud";
import { misc } from "./domains/misc";
import { webhooks } from "./domains/webhooks";
import { suites } from "./suites";
import type { TestGroup, TestSuite } from "./types";
export type { TestGroup, TestSuite, TestTier } from "./types";
const allGroups: TestGroup[] = [
core,
balances,
billing,
crud,
webhooks,
advanced,
misc,
];
export const getAllGroups = (): TestGroup[] => allGroups;
export const getGroup = ({ name }: { name: string }): TestGroup | undefined =>
allGroups.find((g) => g.name === name);
export const getAllSuites = (): TestSuite[] => suites;
export const getSuite = ({ name }: { name: string }): TestSuite | undefined =>
suites.find((s) => s.name === name);
/** Resolve a suite to its constituent test groups. */
export const resolveSuite = ({
name,
}: {
name: string;
}): TestGroup[] | undefined => {
const suite = getSuite({ name });
if (!suite) return undefined;
return suite.groups
.map((groupName) => getGroup({ name: groupName }))
.filter((g): g is TestGroup => g !== undefined);
};
/** Resolve a name to test paths -- checks groups first, then suites. */
export const resolveTestPaths = ({
name,
}: {
name: string;
}): string[] | undefined => {
const group = getGroup({ name });
if (group) return group.paths;
const suiteGroups = resolveSuite({ name });
if (suiteGroups) {
const paths = suiteGroups.flatMap((g) => g.paths);
return [...new Set(paths)];
}
return undefined;
};
/** Recursively discover all *.test.ts files under a directory. */
export const discoverAllTestFiles = async ({
testsDir,
}: {
testsDir: string;
}): Promise<string[]> => {
const results: string[] = [];
const walk = async ({ dir }: { dir: string }) => {
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
if (
entry.name.startsWith(".") ||
entry.name.startsWith("_") ||
entry.name === "node_modules"
) {
continue;
}
await walk({ dir: fullPath });
} else if (entry.name.endsWith(".test.ts")) {
results.push(relative(testsDir, fullPath));
}
}
};
await walk({ dir: testsDir });
return results.sort();
};

View File

@@ -0,0 +1,14 @@
import type { TestSuite } from "./types";
export const suites: TestSuite[] = [
{
name: "pre-merge",
description: "Run before merging any PR",
groups: ["core"],
},
{
name: "all-domain",
description: "All domain-level test groups",
groups: ["balances", "billing", "crud", "webhooks", "advanced", "misc"],
},
];

View File

@@ -0,0 +1,18 @@
export type TestTier = "core" | "domain";
export type TestGroup = {
name: string;
description: string;
tier: TestTier;
/** Directory paths relative to server/tests/. Resolved recursively for all *.test.ts files. */
paths: string[];
/** Override the default concurrency for this group. */
maxConcurrency?: number;
};
export type TestSuite = {
name: string;
description: string;
/** Group names to include when running this suite. */
groups: string[];
};

View File

@@ -8,7 +8,6 @@ import {
ProductItemInterval,
} from "@autumn/shared";
import { resetAndGetCusEnt } from "@tests/balances/track/rollovers/rolloverTestUtils.js";
import { addPrefixToProducts } from "@tests/attach/utils.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
@@ -60,11 +59,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`,
stripeCli = ctx.stripeCli;
addPrefixToProducts({
products: [free],
prefix: testCase,
});
await initProductsV0({
ctx,
products: [free],

View File

@@ -1,137 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } 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 { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { expectFeaturesCorrect } from "../../utils/expectUtils/expectFeaturesCorrect.js";
import { replaceItems } from "../utils.js";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
export const addOn = constructRawProduct({
id: "add_on_1",
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 200,
}),
],
isAddOn: true,
});
const testCase = "addOn1";
describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating free add on`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, addOn],
prefix: testCase,
});
});
test("should should attach pro product, then add on product", async () => {
await attachAndExpectCorrect({
autumn,
db: ctx.db,
org: ctx.org,
env: ctx.env,
stripeCli: ctx.stripeCli,
customerId,
product: pro,
});
});
test("should should attach add on product", async () => {
const preview = await autumn.attachPreview({
customer_id: customerId,
product_id: addOn.id,
});
await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
});
const customer = await autumn.customers.get(customerId);
expect(customer.products.length).toBe(2);
expectProductAttached({
customer,
product: addOn,
});
expectProductAttached({
customer,
product: pro,
});
});
const customItems = replaceItems({
items: addOn.items,
featureId: TestFeature.Messages,
newItem: constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 400,
}),
});
test("should attach new free add on product", async () => {
const preview = await autumn.attachPreview({
customer_id: customerId,
product_id: addOn.id,
is_custom: true,
items: customItems,
});
await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
is_custom: true,
items: customItems,
});
const customer = await autumn.customers.get(customerId);
expect(customer.products.length).toBe(2);
expectProductAttached({
customer,
product: addOn,
});
expectFeaturesCorrect({
customer,
product: {
...addOn,
items: customItems,
},
otherProducts: [pro, addOn],
});
});
});

View File

@@ -1,92 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.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,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
export const pro = constructProduct({
type: "pro",
items: [],
});
export const addOn = constructRawProduct({
id: "addOn",
isAddOn: true,
items: [
constructFeatureItem({
featureId: TestFeature.Credits,
}),
],
});
const testCase = "addOn2";
describe(`${chalk.yellowBright(`${testCase}: Testing attach free add on twice (should be treated as one off?)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, addOn],
prefix: testCase,
});
});
test("should attach pro product and free add on", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
skipSubCheck: true,
});
await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: addOn,
});
expectFeaturesCorrect({
customer,
product: addOn,
});
});
});

View File

@@ -1,150 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, ProductItemInterval } from "@autumn/shared";
import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
// Pro product (matches global products.pro)
const proProd = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
// Monthly add-on product (matches global products.monthlyAddOnMetered1)
// - Prepaid monthly add-on
// - 0 base allowance, customer specifies quantity
const monthlyAddOn = constructRawProduct({
id: "monthly-add-on-metered-1",
isAddOn: true,
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
price: 9,
billingUnits: 250,
includedUsage: 0,
}),
],
});
const testCase = "basic2";
const customerId = testCase;
describe(`${chalk.yellowBright("basic2: Testing attach monthly add on")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
beforeAll(async () => {
// Create products FIRST before customer creation
await initProductsV0({
ctx,
products: [proProd, monthlyAddOn],
prefix: testCase,
customerId,
});
// Then create customer with payment method
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
});
test("should attach pro", async () => {
await autumnV1.attach({
customer_id: customerId,
product_id: proProd.id,
});
const res = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({
sent: proProd,
cusRes: res,
});
});
const monthlyQuantity = 500;
test("should attach monthly add on", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: monthlyAddOn.id,
forceCheckout: false,
options: [
{
feature_id: TestFeature.Messages,
quantity: monthlyQuantity,
},
],
});
});
test("should have correct product & entitlements", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
// Pro gives 10 Messages
const proMetered1 = 10;
const monthlyMetered1Balance = cusRes.entitlements.find(
(e: ApiCustomerV1["entitlements"][number]) =>
e.feature_id === TestFeature.Messages && e.interval === "month",
);
expect(monthlyMetered1Balance?.balance).toBe(proMetered1 + monthlyQuantity);
expect(cusRes.add_ons).toHaveLength(1);
const monthlyAddOnId = cusRes.add_ons.find(
(a: any) => a.id === monthlyAddOn.id,
);
expect(monthlyAddOnId).toBeDefined();
expect(cusRes.invoices.length).toBe(2);
});
test("should have correct /check result for metered1", async () => {
const res: any = await AutumnCli.entitled(customerId, TestFeature.Messages);
const metered1Balance = res!.balances.find(
(b: any) => b.feature_id === TestFeature.Messages,
);
// Pro gives 10, monthly add-on gives monthlyQuantity
const proMetered1Amt = 10;
const monthlyAddOnMetered1Amt = monthlyQuantity;
expect(metered1Balance!.balance).toBe(
proMetered1Amt + monthlyAddOnMetered1Amt,
);
});
});

View File

@@ -1,101 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
CusProductStatus,
type Customer,
ProductItemInterval,
} from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { advanceToNextInvoice } from "@tests/utils/testAttachUtils/testAttachUtils";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils";
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";
// Pro product (matches global products.pro)
const proProd = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
const testCase = "basic6";
const customerId = testCase;
describe(`${chalk.yellowBright("basic6: Testing subscription past_due")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
let stripeCli: Stripe;
let testClockId: string;
let customer: Customer;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
// Create products FIRST before customer creation
await initProductsV0({
ctx,
products: [proProd],
prefix: testCase,
customerId,
});
// Then create customer with payment method
const result = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
testClockId = result.testClockId;
customer = result.customer;
});
test("should attach pro product and switch to failed payment method", async () => {
const res = await autumnV1.attach({
customer_id: customerId,
product_id: proProd.id,
});
await attachFailedPaymentMethod({
stripeCli,
customer,
});
});
test("should advance to next cycle", async () => {
await advanceToNextInvoice({
stripeCli,
testClockId,
});
});
test("should have pro product in past due status", async () => {
const cusRes: any = await AutumnCli.getCustomer(customerId);
const proProduct = cusRes.products.find((p: any) => p.id === proProd.id);
expect(proProduct).toBeDefined();
expect(proProduct.status).toBe(CusProductStatus.PastDue);
});
});

View File

@@ -1,123 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
CusProductStatus,
FreeTrialDuration,
ProductItemInterval,
} from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
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";
// Pro product with trial (matches global products.proWithTrial)
const proWithTrial = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
const testCase = "basic8";
const customerId = testCase;
const customerId2 = `${testCase}2`;
describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
const randFingerprint = Math.random().toString(36).substring(2, 15);
beforeAll(async () => {
// Create products FIRST before customer creation
await initProductsV0({
ctx,
products: [proWithTrial],
prefix: testCase,
customerIds: [customerId, customerId2],
});
// Create first customer with fingerprint
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: randFingerprint },
attachPm: "success",
withTestClock: true,
});
// Create second customer with same fingerprint
await initCustomerV3({
ctx,
customerId: customerId2,
customerData: { fingerprint: randFingerprint },
attachPm: "success",
withTestClock: true,
});
});
test("should attach pro with trial and have correct product & invoice", async () => {
await autumnV1.attach({
customer_id: customerId,
product_id: proWithTrial.id,
});
const customer = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({
sent: proWithTrial,
cusRes: customer,
status: CusProductStatus.Trialing,
});
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
expect(invoices[0].total).toBe(0);
});
test("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async () => {
await autumnV1.attach({
customer_id: customerId2,
product_id: proWithTrial.id,
});
const customer = await AutumnCli.getCustomer(customerId2);
console.log(JSON.stringify(customer, null, 2));
await expectCustomerV0Correct({
sent: proWithTrial,
cusRes: customer,
status: CusProductStatus.Active,
});
const invoices = customer.invoices;
expect(invoices.length).toBe(1);
expect(invoices[0].total).toBe(20);
});
});

View File

@@ -1,74 +0,0 @@
import { ProductItemInterval } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";
/**
* Shared default product for basic test group
* Used by multiple tests (basic1, basic3) to avoid conflicts
* ID is NOT prefixed - shared across all tests in this group
*/
export const sharedDefaultFree = constructProduct({
id: "shared-default-free",
type: "free",
isDefault: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 5,
interval: ProductItemInterval.Month,
}),
],
});
export const sharedProProduct = constructProduct({
id: "shared-pro-product",
isDefault: false,
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
export const sharedPremiumProduct = constructProduct({
id: "shared-premium-product",
isDefault: false,
type: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
export const initBasicSharedProducts = async () => {
await createSharedProducts({
ctx,
products: [sharedDefaultFree, sharedProProduct],
});
};
// Auto-init on import (backwards compat)
await initBasicSharedProducts();

View File

@@ -1,128 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
type CheckResponseV0,
type Entitlement,
ProductItemInterval,
} from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "@tests/utils/genUtils.js";
import { completeStripeCheckoutFormV2 as completeCheckoutForm } from "@tests/utils/browserPool/completeStripeCheckoutFormV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.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";
// Pro product with boolean, metered, and unlimited features
const proProd = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
// Unlimited feature (maps to global products.pro.infinite1)
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
const testCase = "checkout1";
const customerId = testCase;
describe(`${chalk.yellowBright("checkout1: Testing attach basic product through checkout")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
beforeAll(async () => {
await initProductsV0({
ctx,
products: [proProd],
prefix: testCase,
customerId,
});
// Then create customer
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: true,
});
});
test("should attach pro through checkout", async () => {
const { checkout_url } = await autumnV1.attach({
customer_id: customerId,
product_id: proProd.id,
});
await completeCheckoutForm({ url: checkout_url });
await timeout(12000);
});
test("should have correct product & entitlements", async () => {
const res = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({
sent: proProd,
cusRes: res,
});
expect(res.invoices.length).toBeGreaterThan(0);
});
test("should have correct result when calling /check", async () => {
// Convert ProductV2 to V1 to get reference entitlements (what we SENT)
const proProdV1 = convertProductV2ToV1({
productV2: proProd,
orgId: ctx.org.id,
features: ctx.features,
});
const proEntitlements = proProdV1.entitlements;
// Iterate through reference product's entitlements and verify check responses
for (const entitlement of Object.values(proEntitlements) as Entitlement[]) {
const allowance = entitlement.allowance;
const res = (await AutumnCli.entitled(
customerId,
entitlement.feature_id!,
)) as CheckResponseV0;
const entBalance = res.balances.find(
(b) => b.feature_id === entitlement.feature_id,
);
expect(
res.allowed,
`Allowed for ${entitlement.feature_id} is not true`,
).toBe(true);
expect(
entBalance,
`Entitlement ${entitlement.feature_id} balance not found`,
).toBeDefined();
if (entitlement.allowance) {
expect(
entBalance?.balance,
`Entitlement ${entitlement.feature_id} balance does not match expected balance.`,
).toBe(allowance);
}
}
});
});

View File

@@ -1,159 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
type CheckResponseV0,
type LimitedItem,
ProductItemInterval,
} from "@autumn/shared";
import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "@tests/utils/genUtils.js";
import { completeStripeCheckoutFormV2 as completeCheckoutForm } from "@tests/utils/browserPool/completeStripeCheckoutFormV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
// Pro product (matches global products.pro)
const proProd = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
// One-time add-on product
const oneTimeItem = constructPrepaidItem({
featureId: TestFeature.Messages,
price: 9,
billingUnits: 250,
isOneOff: true,
}) as LimitedItem;
const oneTime = constructRawProduct({
id: "one_off",
items: [oneTimeItem],
isAddOn: true,
});
const testCase = "checkout2";
const customerId = testCase;
describe(`${chalk.yellowBright("checkout2: Testing attach one time add ons (through checkout)")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
beforeAll(async () => {
// Create products FIRST before customer creation
await initProductsV0({
ctx,
products: [proProd, oneTime],
prefix: testCase,
customerId,
});
// Then create customer with payment method
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
});
test("should attach pro", async () => {
await autumnV1.attach({
customer_id: customerId,
product_id: proProd.id,
});
const res = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({
sent: proProd,
cusRes: res,
});
});
const oneTimeQuantity = 500;
const oneTimeBillingUnits = oneTimeItem.billing_units;
const oneTimePurchaseCount = 2;
test("should attach one time add on twice, force checkout", async () => {
for (let i = 0; i < 2; i++) {
const res = await autumnV1.attach({
customer_id: customerId,
product_id: oneTime.id,
force_checkout: true,
});
await completeCheckoutForm({
url: res.checkout_url,
overrideQuantity: oneTimeQuantity / (oneTimeBillingUnits ?? 1),
});
await timeout(15000);
}
});
test("should have correct product & entitlements", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
// Find the add-on balance for Messages with lifetime interval (one-time purchase)
const addOnBalance = cusRes.entitlements.find(
(e: ApiCustomerV1["entitlements"][number]) =>
e.feature_id === TestFeature.Messages && e.interval === "lifetime",
);
const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
expect(addOnBalance?.balance).toBe(expectedAmt);
expect(cusRes.add_ons).toHaveLength(1);
expect(cusRes.add_ons[0].id).toBe(oneTime.id);
expect(cusRes.invoices.length).toBe(1 + oneTimePurchaseCount);
});
test("should have correct /check result for metered1", async () => {
const res = (await AutumnCli.entitled(
customerId,
TestFeature.Messages,
)) as CheckResponseV0;
expect(res.allowed).toBe(true);
// Pro product gives 10 Messages per month
const proMetered1Amt = 10;
const addOnBalance = res.balances.find(
(b: CheckResponseV0["balances"][number]) =>
b.feature_id === TestFeature.Messages,
);
expect(addOnBalance?.balance).toBe(
proMetered1Amt + oneTimeQuantity * oneTimePurchaseCount,
);
});
});

View File

@@ -1,86 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { completeStripeCheckoutFormV2 as completeCheckoutForm } from "@tests/utils/browserPool/completeStripeCheckoutFormV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.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";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
export const oneOff = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Users,
includedUsage: 5,
}),
],
type: "one_off",
isAddOn: true,
});
const testCase = "checkout3";
describe(`${chalk.yellowBright(`${testCase}: Testing multi attach checkout, pro + one off`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, oneOff],
prefix: testCase,
});
});
test("should attach pro and one off product", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_ids: [pro.id, oneOff.id],
});
await completeCheckoutForm({ url: res.checkout_url });
await timeout(10000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
});
expectProductAttached({
customer,
product: oneOff,
});
expectFeaturesCorrect({
customer,
product: pro,
});
expectFeaturesCorrect({
customer,
product: oneOff,
});
});
});

View File

@@ -1,86 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { RewardType } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { completeStripeCheckoutFormV2 as completeStripeCheckoutForm } from "@tests/utils/browserPool/completeStripeCheckoutFormV2";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { createReward } from "@tests/utils/productUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import {
constructCoupon,
constructProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
const reward = constructCoupon({
id: "checkout4",
promoCode: "checkout4code",
discountType: RewardType.PercentageDiscount,
discountValue: 50,
});
const testCase = "checkout4";
describe(`${chalk.yellowBright(`${testCase}: Testing attach coupon`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
await createReward({
orgId: ctx.org.id,
env: ctx.env,
db: ctx.db,
autumn,
reward,
productId: pro.id,
});
});
test("should attach pro and one off product", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
reward: reward.id,
});
await completeStripeCheckoutForm({ url: res.checkout_url });
await timeout(10000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
});
expect(customer.invoices.length).toBe(1);
const totalPrice = getBasePrice({ product: pro });
expect(customer.invoices[0].total).toBe(totalPrice * 0.5);
});
});

View File

@@ -1,84 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { completeInvoiceCheckoutV2 as completeInvoiceCheckout } from "@tests/utils/browserPool/completeInvoiceCheckoutV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
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";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
const testCase = "checkout5";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout, no product till paid`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
test("should attach pro product", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
invoice: true,
});
const customer = await autumn.customers.get(customerId);
const invoice = customer.invoices?.[0];
expect(invoice).toBeDefined();
expect(invoice.total).toBe(getBasePrice({ product: pro }));
expect(invoice.status).toBe("open");
const product = customer.products.find((p) => p.id === pro.id);
expect(product).toBeUndefined();
await completeInvoiceCheckout({
url: res.checkout_url,
});
const customer2 = await autumn.customers.get(customerId);
const invoice2 = customer2.invoices?.[0];
expect(customer2.invoices.length).toBe(1);
expect(invoice2).toBeDefined();
expect(invoice2.status).toBe("paid");
expectProductAttached({
customer: customer2,
product: pro,
});
expectFeaturesCorrect({
customer: customer2,
product: pro,
});
});
});

View File

@@ -1,120 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { completeInvoiceCheckoutV2 as completeInvoiceCheckout } from "@tests/utils/browserPool/completeInvoiceCheckoutV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
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";
const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
const premium = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 250,
}),
],
type: "premium",
});
const testCase = "checkout6";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout via checkout endpoint`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
});
test("should attach pro product via invoice checkout", async () => {
const res = await autumn.checkout({
customer_id: customerId,
product_id: pro.id,
invoice: true,
});
expect(res.url).toBeDefined();
await completeInvoiceCheckout({
url: res.url!,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
});
expectFeaturesCorrect({
customer,
product: pro,
});
});
// test("should have no URL returned if try to attach premium (with invoice true)", async () => {
// const res = await autumn.attach({
// customer_id: customerId,
// product_id: premium.id,
// invoice: true,
// });
// expect(res.url).toBeUndefined();
// });
test("should attach premium product via invoice enable immediately", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: premium.id,
invoice: true,
enable_product_immediately: true,
finalize_invoice: false,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: premium,
});
expectFeaturesCorrect({
customer,
product: premium,
});
const invoices = customer.invoices;
expect(invoices.length).toBe(2);
expect(invoices[0].status).toBe("draft");
expect(invoices[0].total).toBe(
getBasePrice({ product: premium }) - getBasePrice({ product: pro }),
); // proration...
});
});

View File

@@ -1,113 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { completeInvoiceCheckoutV2 as completeInvoiceCheckout } from "@tests/utils/browserPool/completeInvoiceCheckoutV2";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
type: "pro",
});
export const addOn = constructRawProduct({
id: "addOn",
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
billingUnits: 100,
price: 10,
isOneOff: true,
}),
],
});
const testCase = "checkout7";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout with one off product`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, addOn],
prefix: testCase,
});
});
test("should attach pro product, then add on product via invoice checkout", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
const options = [
{
quantity: 200,
feature_id: TestFeature.Messages,
},
];
const res2 = await autumn.checkout({
customer_id: customerId,
product_id: addOn.id,
invoice: true,
options,
});
expect(res2.url).toBeDefined();
await completeInvoiceCheckout({
url: res2.url!,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: addOn,
});
expectFeaturesCorrect({
customer,
product: addOn,
otherProducts: [pro],
options,
});
});
// it("should have no URL returned if try to attach add on (with invoice true)", async function () {
// const res = await autumn.checkout({
// customer_id: customerId,
// product_id: addOn.id,
// invoice: true,
// });
// expect(res.url).to.not.exist;
// });
});

View File

@@ -1,95 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { completeStripeCheckoutFormV2 as completeStripeCheckoutForm } from "@tests/utils/browserPool/completeStripeCheckoutFormV2";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "@tests/utils/genUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } 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";
// Monthly with one-time prepaid product (matches global products.monthlyWithOneTime)
// Has both monthly and one-time prepaid items
const monthlyWithOneTime = constructProduct({
type: "pro",
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
price: 5,
billingUnits: 100,
includedUsage: 0,
isOneOff: true,
}),
constructPrepaidItem({
featureId: TestFeature.Words,
price: 10,
billingUnits: 100,
includedUsage: 0,
isOneOff: true,
}),
],
});
const testCase = "checkout8";
const customerId = testCase;
describe(`${chalk.yellowBright("checkout8: attach monthly with one time prepaid, and quantity = 0")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
const options = [
{
feature_id: TestFeature.Messages,
quantity: 0,
},
{
feature_id: TestFeature.Words,
quantity: 4,
},
];
beforeAll(async () => {
// Create products FIRST before customer creation
await initProductsV0({
ctx,
products: [monthlyWithOneTime],
prefix: testCase,
customerId,
});
// Then create customer
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
});
test("should attach monthly with one time", async () => {
const res = await autumnV1.attach({
customer_id: customerId,
product_id: monthlyWithOneTime.id,
options,
});
await completeStripeCheckoutForm({ url: res.checkout_url });
await timeout(12000);
});
test("should have correct main product and entitlements", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({
sent: monthlyWithOneTime,
cusRes,
optionsList: options,
});
});
});

View File

@@ -1,104 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.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 { 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 testCase = "downgrade1";
const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> pro`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
});
// let nextCycle = Date.now();
let preview = null;
test("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: pro,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
test("should have pro attached on next cycle", async () => {
await expectNextCycleCorrect({
preview: preview!,
autumn,
stripeCli,
customerId,
testClockId,
product: pro,
db,
org,
env,
});
});
});

View File

@@ -1,112 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.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 "@/internal/products/product-items/productItemUtils.js";
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 testCase = "downgrade2";
const free = constructProduct({
items: [
constructFeatureItem({
feature_id: TestFeature.Words,
included_usage: 100,
}),
],
type: "free",
isDefault: false,
});
const premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> free`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [free, premium],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
});
// let nextCycle = Date.now();
let preview = null;
test("should downgrade to free", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: free,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
test("should have pro attached on next cycle", async () => {
await expectNextCycleCorrect({
preview: preview!,
autumn,
stripeCli,
customerId,
testClockId,
product: free,
db,
org,
env,
});
});
});

View File

@@ -1,150 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
type AppEnv,
type Customer,
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 { expectDowngradeCorrect } from "@tests/utils/expectUtils/expectScheduleUtils.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 "@/internal/products/product-items/productItemUtils.js";
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 testCase = "downgrade3";
const free = constructProduct({
items: [
constructFeatureItem({
feature_id: TestFeature.Words,
included_usage: 100,
}),
],
type: "free",
isDefault: false,
});
const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -> free -> pro -> premium`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let customer: Customer;
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1, customer: customer_ } =
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [free, pro, premium],
prefix: testCase,
});
testClockId = testClockId1!;
customer = customer_!;
});
test("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
});
// let nextCycle = Date.now();
let preview = null;
test("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: pro,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
test("should downgrade to free", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: free,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
test("should change downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: pro,
stripeCli,
db,
org,
env,
});
});
test("should renew premium", async () => {
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: premium,
});
});
});

View File

@@ -1,135 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
type AppEnv,
BillingInterval,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.js";
import { advanceMonths } from "@tests/utils/stripeUtils.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 { timeout } from "@/utils/genUtils.js";
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 testCase = "downgrade4";
const proQuarter = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
interval: BillingInterval.Quarter,
});
const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: pro-quarter -> premium -> pro`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [proQuarter, pro, premium],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro quarterly product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: proQuarter,
stripeCli,
db,
org,
env,
});
});
test("should downgrade to premium", async () => {
await expectDowngradeCorrect({
autumn,
customerId,
curProduct: proQuarter,
newProduct: premium,
stripeCli,
db,
org,
env,
});
});
let preview = null;
test("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: proQuarter,
newProduct: pro,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
test("should have correct invoice after cycle", async () => {
await advanceMonths({ stripeCli, testClockId, numberOfMonths: 3 });
await timeout(10000);
await expectNextCycleCorrect({
preview: preview!,
autumn,
stripeCli,
customerId,
testClockId,
product: pro,
db,
org,
env,
});
});
return;
});

View File

@@ -1,153 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { CusProductStatus, ProductItemInterval } from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
// Inline product definitions for downgrade5 test
const proProduct = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
const premiumProduct = constructProduct({
type: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
],
});
const testCase = "downgrade5";
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
const customerId = testCase;
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
// Initialize products for this test
await initProductsV0({
ctx,
products: [proProduct, premiumProduct],
prefix: testCase,
customerId,
});
const { testClockId: testClockId_ } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId_;
});
test("should attach premium", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: premiumProduct.id,
});
});
test("should attach pro", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: proProduct.id,
});
});
test("should have correct product and entitlements for scheduled pro", async () => {
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: premiumProduct,
cusRes: res,
});
const { products: resProducts } = res;
const resPro = resProducts.find(
(p: any) =>
p.id === proProduct.id && p.status === CusProductStatus.Scheduled,
);
expect(resPro).toBeDefined();
});
test("should attach premium and remove scheduled pro", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: premiumProduct.id,
});
const res = await AutumnCli.getCustomer(customerId);
const resPro = res.products.find(
(p: any) =>
p.id === proProduct.id && p.status === CusProductStatus.Scheduled,
);
expect(resPro).toBeUndefined();
expectCustomerV0Correct({
sent: premiumProduct,
cusRes: res,
});
});
// Advance time 1 month
test("should attach pro, advance stripe clock and have pro is attached", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: proProduct.id,
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(), 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 15,
});
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: proProduct,
cusRes: res,
});
});
});

View File

@@ -1,78 +0,0 @@
// import { BillingInterval, ProductItemInterval } from "@autumn/shared";
// import { TestFeature } from "@tests/setup/v2Features.js";
// import ctx from "@tests/utils/testInitUtils/createTestContext.js";
// import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
// import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
// import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";
// /**
// * Shared products for downgrade test group
// * Matches global products.free, products.pro, products.premium
// */
// export const sharedFreeProduct = constructProduct({
// id: "shared-downgrade-free",
// type: "free",
// isDefault: true,
// excludeBase: true,
// items: [
// constructFeatureItem({
// featureId: TestFeature.Messages,
// includedUsage: 5,
// interval: ProductItemInterval.Month,
// }),
// ],
// });
// export const sharedProProduct = constructProduct({
// id: "shared-downgrade-pro",
// type: "pro",
// excludeBase: true,
// items: [
// constructFeatureItem({
// featureId: TestFeature.Dashboard,
// isBoolean: true,
// }),
// constructFeatureItem({
// featureId: TestFeature.Messages,
// includedUsage: 10,
// interval: ProductItemInterval.Month,
// }),
// constructFeatureItem({
// featureId: TestFeature.Admin,
// unlimited: true,
// }),
// constructPriceItem({
// price: 2000,
// interval: BillingInterval.Month,
// }),
// ],
// });
// export const sharedPremiumProduct = constructProduct({
// id: "shared-downgrade-premium",
// type: "premium",
// excludeBase: true,
// items: [
// constructFeatureItem({
// featureId: TestFeature.Messages,
// includedUsage: 100,
// interval: ProductItemInterval.Month,
// }),
// constructPriceItem({
// price: 5000,
// interval: BillingInterval.Month,
// }),
// ],
// });
// export const initDowngradeSharedProducts = async () => {
// await createSharedProducts({
// ctx,
// products: [sharedFreeProduct, sharedProProduct, sharedPremiumProduct],
// });
// };
// // Auto-init on import (backwards compat)
// await initDowngradeSharedProducts();

View File

@@ -1,76 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } 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 { AutumnInt } from "@/external/autumn/autumnCli.js";
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 testCase = "aentity1";
const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via checkout`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
const newEntities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
];
test("should attach pro product to entity 1", async () => {
await autumn.entities.create(customerId, newEntities);
const entityId = newEntities[0].id;
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId,
});
const customer = await autumn.customers.get(customerId);
// console.log("customer products:", customer.products);
expectProductAttached({
customer,
product: pro,
entityId,
});
});
});

View File

@@ -1,138 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { defaultApiVersion } from "@tests/constants.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectInvoiceAfterUsage } from "@tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
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";
import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils";
const testCase = "aentity2";
export const proAnnual = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "pro",
isAnnual: true,
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to entity via checkout`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
let testClockId: string;
let curUnix = Date.now();
beforeAll(async () => {
const result = await initCustomerV3({
ctx,
customerId,
withTestClock: true,
});
testClockId = result.testClockId!;
await initProductsV0({
ctx,
products: [proAnnual],
prefix: testCase,
});
});
const newEntities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
];
let entityId = newEntities[0].id;
test("should attach pro annual product to entity 2", async () => {
await autumn.entities.create(customerId, newEntities);
entityId = newEntities[0].id;
await attachAndExpectCorrect({
autumn,
customerId,
product: proAnnual,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId,
});
});
const usage = 1250130;
test("should track usage", async () => {
await autumn.track({
customer_id: customerId,
entity_id: entityId,
feature_id: TestFeature.Words,
value: usage,
});
const entity = await autumn.entities.get(customerId, entityId);
expectFeaturesCorrect({
customer: entity,
product: proAnnual,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
await timeout(2000);
const nonCachedEntity = await autumn.entities.get(customerId, entityId, {
skip_cache: "true",
});
expectFeaturesCorrect({
customer: nonCachedEntity,
product: proAnnual,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
test("should have correct invoice after cycle", async () => {
curUnix = await advanceToNextInvoice({
stripeCli: ctx.stripeCli,
testClockId,
withPause: true,
});
await expectInvoiceAfterUsage({
autumn,
customerId,
entityId,
featureId: TestFeature.Words,
product: proAnnual,
usage,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
numInvoices: 2,
});
});
});

View File

@@ -1,120 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { defaultApiVersion } from "@tests/constants.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectInvoiceAfterUsage } from "@tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
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 testCase = "aentity3";
export const proAnnual = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Attach pro annual to entity and cancel`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
let testClockId: string;
let curUnix = Date.now();
beforeAll(async () => {
const result = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
testClockId = result.testClockId!;
await initProductsV0({
ctx,
products: [proAnnual],
prefix: testCase,
});
});
const newEntities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
];
let entityId = newEntities[0].id;
test("should attach pro annual to entity", async () => {
await autumn.entities.create(customerId, newEntities);
entityId = newEntities[0].id;
await attachAndExpectCorrect({
autumn,
customerId,
product: proAnnual,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId,
});
});
const nextUsage = 1032100;
test("should cancel and have correct final invoice", async () => {
await autumn.track({
customer_id: customerId,
entity_id: entityId,
feature_id: TestFeature.Words,
value: nextUsage,
});
await autumn.cancel({
customer_id: customerId,
product_id: proAnnual.id,
entity_id: entityId,
});
await timeout(5000);
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addHours(
addMonths(curUnix, 1),
hoursToFinalizeInvoice,
).getTime(),
});
await expectInvoiceAfterUsage({
autumn,
customerId,
entityId,
featureId: TestFeature.Words,
product: proAnnual,
usage: nextUsage,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
numInvoices: 2,
expectExpired: true,
});
});
});

View File

@@ -1,190 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { defaultApiVersion } from "@tests/constants.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
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";
import { timeout } from "../../utils/genUtils.js";
const testCase = "aentity4";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro diff entities and testing track / check`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
const newEntities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
const entity1 = newEntities[0];
const entity2 = newEntities[1];
test("should attach pro product to entity 1", async () => {
await autumn.entities.create(customerId, newEntities);
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId: entity1.id,
numSubs: 1,
});
});
test("should attach pro product to entity 2", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId: entity2.id,
numSubs: 2,
});
// wait for webhooks to clear cache
await timeout(4000);
});
const entity1Usage = Math.random() * 1000000;
test("should track usage on entity 1", async () => {
await autumn.track({
customer_id: customerId,
entity_id: entity1.id,
feature_id: TestFeature.Words,
value: entity1Usage,
});
const entity1Res = await autumn.entities.get(customerId, entity1.id);
const entity2Res = await autumn.entities.get(customerId, entity2.id);
expectFeaturesCorrect({
customer: entity1Res,
product: pro,
usage: [
{
featureId: TestFeature.Words,
value: entity1Usage,
},
],
});
expectFeaturesCorrect({
customer: entity2Res,
product: pro,
});
await timeout(2000);
const entity1ResUncached = await autumn.entities.get(
customerId,
entity1.id,
{
skip_cache: "true",
},
);
const entity2ResUncached = await autumn.entities.get(
customerId,
entity2.id,
{
skip_cache: "true",
},
);
expectFeaturesCorrect({
customer: entity1ResUncached,
product: pro,
usage: [
{
featureId: TestFeature.Words,
value: entity1Usage,
},
],
});
expectFeaturesCorrect({
customer: entity2ResUncached,
product: pro,
});
});
const entity2Usage = Math.random() * 1000000;
test("should track usage on entity 2", async () => {
await autumn.track({
customer_id: customerId,
entity_id: entity2.id,
feature_id: TestFeature.Words,
value: entity2Usage,
});
const entity1Res = await autumn.entities.get(customerId, entity1.id);
const entity2Res = await autumn.entities.get(customerId, entity2.id);
expectFeaturesCorrect({
customer: entity1Res,
product: pro,
usage: [
{
featureId: TestFeature.Words,
value: entity1Usage,
},
],
});
expectFeaturesCorrect({
customer: entity2Res,
product: pro,
usage: [
{
featureId: TestFeature.Words,
value: entity2Usage,
},
],
});
});
});

View File

@@ -1,161 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { CusProductStatus } from "@autumn/shared";
import { defaultApiVersion } from "@tests/constants.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
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";
import {
expectProductAttached,
expectScheduledApiSub,
} from "../../utils/expectUtils/expectProductAttached";
const testCase = "aentity5";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "pro",
});
export const premium = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1500,
}),
],
type: "premium",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing downgrade entity product`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
let testClockId: string;
beforeAll(async () => {
const result = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
testClockId = result.testClockId!;
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
});
const newEntities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
const entity1 = newEntities[0];
const entity2 = newEntities[1];
test("should attach premium product to entity 1", async () => {
await autumn.entities.create(customerId, newEntities);
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId: entity1.id,
numSubs: 1,
});
});
test("should attach premium product to entity 2", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
entityId: entity2.id,
numSubs: 2,
});
});
test("should attach pro product to entity 1", async () => {
await autumn.attach({
customer_id: customerId,
entity_id: entity1.id,
product_id: pro.id,
});
const entity = await autumn.entities.get(customerId, entity1.id);
expectProductAttached({
customer: entity,
product: pro,
status: CusProductStatus.Scheduled,
});
await expectScheduledApiSub({
customerId,
entityId: entity1.id,
productId: pro.id,
});
// const entity = await autumn.entities.get(customerId, entity1.id);
// const proProd = entity.products.find((p: any) => p.id === pro.id);
// expect(proProd).toBeDefined();
// expect(proProd.status).toBe(CusProductStatus.Scheduled);
});
test("should advance test clock and have pro attached to entity 1", async () => {
await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(), 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const entity = await autumn.entities.get(customerId, entity1.id);
const proProd = entity.products?.find((p: any) => p.id === pro.id);
expect(proProd).toBeDefined();
expect(proProd?.status).toBe(CusProductStatus.Active);
expect(entity.products?.length).toBe(1);
const entity2Res = await autumn.entities.get(customerId, entity2.id);
const premiumProd = entity2Res.products?.find(
(p: any) => p.id === premium.id,
);
expect(premiumProd).toBeDefined();
expect(premiumProd?.status).toBe(CusProductStatus.Active);
expect(entity2Res.products?.length).toBe(1);
});
});

View File

@@ -1,90 +0,0 @@
// import { beforeAll, describe, expect } from "bun:test";
// import {
// ApiVersion,
// CusProductStatus,
// FreeTrialDuration,
// } from "@autumn/shared";
// import { TestFeature } from "@tests/setup/v2Features.js";
// import ctx from "@tests/utils/testInitUtils/createTestContext.js";
// import chalk from "chalk";
// 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";
// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
// const pro = constructProduct({
// type: "pro",
// freeTrial: {
// length: 7,
// duration: FreeTrialDuration.Day,
// unique_fingerprint: true,
// card_required: false,
// },
// items: [
// constructFeatureItem({
// featureId: TestFeature.Messages,
// includedUsage: 300,
// entityFeatureId: TestFeature.Users,
// }),
// ],
// });
// const testCase = "entity6";
// describe(`${chalk.yellowBright("entity6: Testing two entities with a free trial")}`, () => {
// const customerId = testCase;
// const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
// beforeAll(async () => {
// await initCustomerV3({
// ctx,
// customerId,
// withTestClock: true,
// attachPm: "success",
// });
// await autumnV1.entities.create(customerId, [
// {
// id: "entity1",
// name: "Entity 1",
// feature_id: TestFeature.Users,
// },
// {
// id: "entity2",
// name: "Entity 2",
// feature_id: TestFeature.Users,
// },
// ]);
// await initProductsV0({
// ctx,
// products: [pro],
// prefix: testCase,
// });
// await autumnV1.attach({
// customer_id: customerId,
// product_id: pro.id,
// entity_id: "entity1",
// });
// const entity1 = await autumnV1.entities.get(customerId, "entity1");
// await autumnV1.attach({
// customer_id: customerId,
// product_id: pro.id,
// entity_id: "entity2",
// });
// const entity2 = await autumnV1.entities.get(customerId, "entity2");
// expect(entity1.products.length).toBe(1);
// expect(entity2.products.length).toBe(1);
// expect(entity1.products[0].id).toBe(pro.id);
// expect(entity2.products[0].id).toBe(pro.id);
// expect(entity1.products[0].status).toBe(CusProductStatus.Trialing);
// expect(entity2.products[0].status).toBe(CusProductStatus.Trialing);
// });
// });

View File

@@ -1,157 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
type ApiCusProductV3,
type ApiCustomerV3,
CreateFreeTrialSchema,
CusProductStatus,
FreeTrialDuration,
LegacyVersion,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addDays } from "date-fns";
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";
const testCase = "free1";
const trial1 = CreateFreeTrialSchema.parse({
length: 7,
duration: FreeTrialDuration.Day,
});
export const free = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
isDefault: false,
freeTrial: trial1,
type: "free",
id: "enterprise_trial",
});
export const addOn = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 1000,
}),
],
isDefault: false,
type: "free",
isAddOn: true,
id: "add_on",
});
describe(`${chalk.yellowBright(`${testCase}: Testing free product with trial and attaching add on`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({
version: LegacyVersion.v1_4,
orgConfig: { multiple_trials: true },
});
let testClockId: string;
beforeAll(async () => {
const result = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
withTestClock: true,
});
testClockId = result.testClockId!;
await initProductsV0({
ctx,
products: [free, addOn],
prefix: testCase,
});
});
const approximateDiff = 1000 * 60 * 80; // 60 minutes
test("should attach free product with trial", async () => {
const attachPreview = await autumn.attachPreview({
customer_id: customerId,
product_id: free.id,
});
const attach = await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
const customer = await autumn.customers.get(customerId);
const freeProduct = customer.products.find(
(p) => p.id === free.id,
) as ApiCusProductV3;
expect(freeProduct).toBeDefined();
expect(freeProduct?.status).toBe(CusProductStatus.Trialing);
expect(freeProduct?.current_period_end).toBeGreaterThanOrEqual(
addDays(Date.now(), trial1.length).getTime() - approximateDiff,
);
expect(freeProduct?.current_period_end).toBeLessThanOrEqual(
addDays(Date.now(), trial1.length).getTime() + approximateDiff,
);
});
const trial2 = CreateFreeTrialSchema.parse({
length: 14,
duration: FreeTrialDuration.Day,
});
test("should update free product's trial end date", async () => {
const attachPreview = await autumn.attachPreview({
customer_id: customerId,
product_id: free.id,
free_trial: trial2,
is_custom: true,
});
const attach = await autumn.attach({
customer_id: customerId,
product_id: free.id,
free_trial: trial2,
is_custom: true,
});
const customer = await autumn.customers.get(customerId);
const freeProduct = customer.products.find(
(p) => p.id === free.id,
) as ApiCusProductV3;
expect(freeProduct?.status).toBe(CusProductStatus.Trialing);
expect(freeProduct?.current_period_end).toBeGreaterThanOrEqual(
addDays(Date.now(), trial2.length).getTime() - approximateDiff,
);
expect(freeProduct?.current_period_end).toBeLessThanOrEqual(
addDays(Date.now(), trial2.length).getTime() + approximateDiff,
);
});
test("should attach add on product", async () => {
const attachPreview = await autumn.attachPreview({
customer_id: customerId,
product_id: addOn.id,
});
const attach = await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
});
const customer = (await autumn.customers.get(customerId)) as ApiCustomerV3;
const addOnProduct = customer.products.find((p) => p.id === addOn.id);
const freeProduct = customer.products.find((p) => p.id === free.id);
expect(addOnProduct).toBeDefined();
expect(addOnProduct?.status).toBe("active");
expect(freeProduct?.status).toBe("trialing");
});
});

View File

@@ -1,133 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
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";
/*
FLOW:
1. Attach pro group 1 & pro group 2 at once -> should have both products as main
2. Upgrade pro group 1 -> premium group 1
3. Upgrade pro group 2 -> premium group 2
*/
const testCase = "multiProduct1";
// Group 1 products (use Messages feature)
const proGroup1 = constructProduct({
id: "proGroup1",
group: `${testCase}-g1`,
type: "pro",
items: [
constructArrearItem({
includedUsage: 10,
featureId: TestFeature.Messages,
price: 100, // $1.00 per unit (100 cents per billing unit of 1)
billingUnits: 1,
}),
],
});
const premiumGroup1 = constructProduct({
id: "premiumGroup1",
group: `${testCase}-g1`,
type: "premium",
items: [
constructArrearItem({
includedUsage: 100,
featureId: TestFeature.Messages,
price: 200, // $2.00 per unit (200 cents per billing unit of 1)
billingUnits: 1,
}),
],
});
// Group 2 products (use Words feature)
const proGroup2 = constructProduct({
id: "proGroup2",
group: `${testCase}-g2`,
type: "pro",
items: [
constructArrearItem({
includedUsage: 10,
featureId: TestFeature.Words,
price: 60, // $0.60 per unit (60 cents per billing unit of 1)
billingUnits: 1,
}),
],
});
const premiumGroup2 = constructProduct({
id: "premiumGroup2",
group: `${testCase}-g2`,
type: "premium",
items: [
constructArrearItem({
includedUsage: 10,
featureId: TestFeature.Words,
price: 90, // $0.90 per unit (90 cents per billing unit of 1)
billingUnits: 1,
}),
],
});
describe(
chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`),
() => {
const customerId = testCase;
beforeAll(async () => {
await initProductsV0({
ctx,
products: [proGroup1, proGroup2, premiumGroup1, premiumGroup2],
// prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
});
test("should attach pro group 1 and pro group 2", async () => {
await AutumnCli.attach({
customerId: customerId,
productIds: [proGroup1.id, proGroup2.id],
});
const cusRes = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({ sent: proGroup1, cusRes });
await expectCustomerV0Correct({ sent: proGroup2, cusRes });
});
test("should upgrade to premium group 1", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: premiumGroup1.id,
});
// 1. Compare main product
const cusRes = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({ sent: premiumGroup1, cusRes });
});
test("should upgrade to premium group 2", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: premiumGroup2.id,
});
// 1. Compare main product
const cusRes = await AutumnCli.getCustomer(customerId);
await expectCustomerV0Correct({ sent: premiumGroup2, cusRes });
});
},
);

View File

@@ -1,108 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.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 testCase = "others1";
export const free = constructProduct({
items: [],
type: "free",
isDefault: false,
});
export const pro = constructProduct({
items: [],
type: "pro",
trial: true,
});
export const premium = constructProduct({
items: [],
type: "premium",
trial: true,
});
describe(`${chalk.yellowBright(`${testCase}: Testing trials: pro with trial -> premium with trial -> free`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
beforeAll(async () => {
await initProductsV0({
ctx,
products: [free, pro, premium],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
test("should attach pro product (with trial)", async () => {
await attachAndExpectCorrect({
autumn,
stripeCli: ctx.stripeCli,
customerId,
product: pro,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should attach premium product (with trial)", async () => {
await attachAndExpectCorrect({
autumn,
stripeCli: ctx.stripeCli,
customerId,
product: premium,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should attach free product at the end of the trial", async () => {
const { preview } = await expectDowngradeCorrect({
autumn,
stripeCli: ctx.stripeCli,
customerId,
curProduct: premium,
newProduct: free,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
expectNextCycleCorrect({
autumn,
preview,
stripeCli: ctx.stripeCli,
customerId,
testClockId,
product: free,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
});

View File

@@ -1,94 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, ErrCode } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { generateId } from "@/utils/genUtils";
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";
const pro = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 500,
}),
],
});
const testCase = "others10";
describe(`${chalk.yellowBright(`${testCase}/idempotency: idempotency key already exists`)}`, () => {
const customerId = testCase;
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
let results: PromiseSettledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>[];
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
attachPm: "success",
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
const idempotencyKey = generateId("it");
results = await Promise.allSettled([
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
idempotencyKey,
},
),
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
idempotencyKey,
},
),
]);
});
test("should reject duplicate idempotency key with 409", async () => {
// Exactly one request should succeed
const fulfilled = results.filter((r) => r.status === "fulfilled");
const rejected = results.filter((r) => r.status === "rejected");
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
// The successful request should have attached the product
const successResult = fulfilled[0] as PromiseFulfilledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>;
expect(successResult.value.success).toBe(true);
expect(successResult.value.customer_id).toBe(customerId);
expect(successResult.value.product_ids).toContain(pro.id);
// The rejected request should have the duplicate idempotency key error
const rejectedResult = rejected[0] as PromiseRejectedResult;
expect(rejectedResult.reason).toBeInstanceOf(AutumnError);
expect((rejectedResult.reason as AutumnError).code).toBe(
ErrCode.DuplicateIdempotencyKey,
);
});
});

View File

@@ -1,69 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
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 { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { CusService } from "@/internal/customers/CusService.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 testCase = "others3";
export const pro = constructProduct({
type: "pro",
items: [],
});
describe(`${chalk.yellowBright(`${testCase}: Testing attach payment failure`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
// Payment failure
test("should handle payment failure", async () => {
const customer = await CusService.get({
db: ctx.db,
idOrInternalId: customerId,
orgId: ctx.org.id,
env: ctx.env,
});
await attachFailedPaymentMethod({
stripeCli: ctx.stripeCli,
customer: customer!,
});
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
// console.log(res);
expect(res.checkout_url).toBeDefined();
});
});

View File

@@ -1,122 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectAttachCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { timeout } from "../../utils/genUtils";
export const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const testCase = "others6";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with customer ID and entity ID null`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const email = `${customerId}@test.com`;
beforeAll(async () => {
const customer = await CusService.getByEmail({
db: ctx.db,
orgId: ctx.org.id,
env: ctx.env,
email,
});
if (customer.length > 0) {
await autumn.customers.delete(customer[0].internal_id);
}
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
let internalCustomerId = "";
let internalEntityId = "";
const entityId = "1";
test("should attach create customer with no ID", async () => {
const customer = await autumn.customers.create({
id: null,
email: `${customerId}@test.com`,
name: customerId,
withAutumnId: true,
});
expect(customer.autumn_id).toBeDefined();
internalCustomerId = customer.autumn_id;
const data = await autumn.entities.create(internalCustomerId, {
id: null,
feature_id: TestFeature.Users,
});
internalEntityId = data.autumn_id;
expect(internalEntityId).toBeDefined();
});
test("should be able to attach pro product, invoice only", async () => {
await autumn.attach({
customer_id: internalCustomerId,
entity_id: internalEntityId,
product_id: pro.id,
invoice: true,
enable_product_immediately: true,
});
const customer = await autumn.customers.get(internalCustomerId);
expectAttachCorrect({
customer,
product: pro,
});
expect(customer.invoices!.length).toBe(1);
expect(customer.invoices![0].status).toBe("draft");
});
test("should create customer with ID, and attach pro product", async () => {
const customer = await autumn.customers.create({
id: customerId,
email: `${customerId}@test.com`,
});
expect(customer.autumn_id).toBe(internalCustomerId);
const entity = await autumn.entities.create(customer.id, {
id: entityId,
feature_id: TestFeature.Users,
});
internalEntityId = entity.autumn_id;
await timeout(2000);
const customer2 = await autumn.customers.get(customerId);
expectAttachCorrect({
customer: customer2,
product: pro,
});
const entity2 = await autumn.entities.get(customerId, entityId);
expectAttachCorrect({
customer: entity2,
product: pro,
});
});
});

View File

@@ -1,61 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectAttachCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.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 { 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";
export const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
trial: true,
});
const testCase = "others7";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with free_trial=False`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
test("should attach pro product with free_trial=False", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
free_trial: false,
});
const customer = await autumn.customers.get(customerId);
expectAttachCorrect({
customer,
product: pro,
});
expect(customer.invoices.length).toBe(1);
expect(customer.invoices[0].total).toBe(getBasePrice({ product: pro }));
});
});

View File

@@ -1,86 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.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,
constructPrepaidItem,
} 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";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
}),
constructPrepaidItem({
isOneOff: true,
featureId: TestFeature.Users,
billingUnits: 1,
price: 100,
}),
],
isAnnual: true,
type: "pro",
});
const testCase = "others8";
describe(`${chalk.yellowBright(`${testCase}: Testing annual pro with one off prepaid`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
test("should attach annual pro product with one off prepaid", async () => {
const options = [
{
feature_id: TestFeature.Users,
quantity: 1,
},
];
const preview = await autumn.attachPreview({
customer_id: customerId,
product_id: pro.id,
options,
});
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
options,
});
console.log(preview);
const customer = await autumn.customers.get(customerId);
const invoice = customer.invoices[0];
// expect(preview.total).toBe(invoice.total);
expect(invoice.total).toBe(
getBasePrice({ product: pro }) + options[0].quantity * 100,
);
});
});

View File

@@ -1,74 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.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";
export const free = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
}),
],
isAnnual: false,
type: "free",
isDefault: false,
});
// Pro trial
// Pro
const testCase = "others9";
describe(`${chalk.yellowBright(`${testCase}: Testing attach free product again`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [free],
prefix: testCase,
});
});
test("should attach free product, then try again and hit error", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: free,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
skipSubCheck: true,
});
await expectAutumnError({
func: async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
},
});
});
});

View File

@@ -1,62 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "attach-response1";
const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 response for attach, scenario: new`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
});
test("should return v0.2 responses for attach", async () => {
const attachResponse = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(attachResponse.checkout_url).toBeDefined();
expect(Object.keys(attachResponse)).toEqual(["checkout_url"]);
});
test("should return correct v1.2 responses for attach", async () => {
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
const attachResponse = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(attachResponse).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
checkout_url: expect.any(String),
});
expect(attachResponse.code).toBeDefined();
expect(attachResponse.message).toBeDefined();
expect(attachResponse.message).toBeDefined();
});
});

View File

@@ -1,98 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "../../../src/utils/scriptUtils/testUtils/initCustomerV3";
const testCase = "attach-response2";
const pro = constructProduct({
type: "pro",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
});
const premium = constructProduct({
type: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for attach, scenario: upgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
});
test("should return v0.2 responses for attach", async () => {
const attachResponse = await autumn.attach({
customer_id: customerId,
product_id: premium.id,
});
expect(Object.keys(attachResponse)).toEqual(["success", "message"]);
});
test("should return correct v1.2 responses for attach", async () => {
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
await autumnV1.cancel({
customer_id: customerId,
product_id: premium.id,
cancel_immediately: true,
});
await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
const attachResponse = await autumnV1.attach({
customer_id: customerId,
product_id: premium.id,
});
expect(attachResponse).toMatchObject({
customer_id: customerId,
product_ids: [premium.id],
code: expect.any(String),
message: expect.any(String),
});
// expect(attachResponse.code).toBeDefined();
// expect(attachResponse.message).toBeDefined();
// expect(attachResponse.message).toBeDefined();
});
});

View File

@@ -1,90 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "../../../src/utils/scriptUtils/testUtils/initCustomerV3";
const testCase = "attach-response3";
const pro = constructProduct({
type: "pro",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
});
const premium = constructProduct({
type: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for attach, scenario: downgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
});
});
test("should return v0.2 responses for attach", async () => {
const attachResponse = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(Object.keys(attachResponse)).toEqual(["success", "message"]);
});
test("should return correct v1.2 responses for attach", async () => {
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: premium.id,
cancel_action: "uncancel",
});
const attachResponse = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(attachResponse).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
code: expect.any(String),
message: expect.any(String),
});
});
});

View File

@@ -1,75 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "../../../src/utils/scriptUtils/testUtils/initCustomerV3";
const testCase = "attach-response4";
const pro = constructProduct({
type: "pro",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for attach, scenario: new (card on file)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
});
test("should return v0.2 responses for attach", async () => {
const attachResponse = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(Object.keys(attachResponse)).toEqual(["success", "message"]);
});
test("should return correct v1.2 responses for attach", async () => {
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
await autumnV1.cancel({
customer_id: customerId,
product_id: pro.id,
cancel_immediately: true,
});
const attachResponse = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(attachResponse).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
code: expect.any(String),
message: expect.any(String),
});
});
});

View File

@@ -1,77 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "../../../src/utils/scriptUtils/testUtils/initCustomerV3";
const testCase = "attach-response5";
const oneOff = constructProduct({
type: "one_off",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 1000,
interval: null,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing v0.2 / v1.2 response for attach, scenario: one off (card on file)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V0_2 });
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [oneOff],
prefix: testCase,
});
});
test("should return v0.2 responses for attach", async () => {
const attachResponse = await autumn.attach({
customer_id: customerId,
product_id: oneOff.id,
});
expect(attachResponse).toMatchObject({
// customer_id: customerId,
// product_ids: [oneOff.id],
// code: expect.any(String),
success: true,
message: expect.any(String),
});
});
test("should return correct v1.2 responses for attach", async () => {
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
const attachResponse = await autumnV1.attach({
customer_id: customerId,
product_id: oneOff.id,
});
expect(attachResponse).toMatchObject({
success: true,
customer_id: customerId,
product_ids: [oneOff.id],
code: expect.any(String),
message: expect.any(String),
});
});
});

View File

@@ -1,130 +0,0 @@
import { expect } from "bun:test";
import {
type AppEnv,
AttachBranch,
type Organization,
type ProductItem,
type ProductV2,
} from "@autumn/shared";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import {
expectSubItemsCorrect,
getSubsFromCusId,
} from "@tests/utils/expectUtils/expectSubUtils.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
const runUpdateEntsTest = async ({
autumn,
stripeCli,
customerId,
customProduct,
newVersion,
db,
org,
env,
customItems,
usage,
}: {
autumn: AutumnInt;
stripeCli: Stripe;
customerId: string;
customProduct: ProductV2;
newVersion?: number;
db: DrizzleCli;
org: Organization;
env: AppEnv;
customItems?: ProductItem[];
usage?: {
featureId: string;
value: number;
}[];
}) => {
// 1. Get subs before
const { subs: subsBefore } = await getSubsFromCusId({
stripeCli,
customerId,
productId: customProduct.id,
db,
org,
env,
});
const preview = await autumn.attachPreview({
customer_id: customerId,
product_id: customProduct.id,
version: newVersion,
is_custom: customItems ? true : undefined,
items: customItems,
});
if (newVersion) {
expect(preview.branch).toBe(AttachBranch.NewVersion);
} else {
expect(preview.branch).toBe(AttachBranch.SameCustomEnts);
expect(preview.due_today).toBeUndefined();
}
await autumn.attach({
customer_id: customerId,
product_id: customProduct.id,
version: newVersion,
is_custom: customItems ? true : undefined,
items: customItems,
});
// 1. Ensure no new invoices created
const { subs: subsAfter, cusProduct } = await getSubsFromCusId({
stripeCli,
customerId,
productId: customProduct.id,
db,
org,
env,
});
const invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
const invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
const subIdsBefore = subsBefore.map((sub) => sub.id);
const subIdsAfter = subsAfter.map((sub) => sub.id);
// let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end);
// let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end);
expect(invoicesAfter).toStrictEqual(invoicesBefore);
expect(subIdsAfter).toStrictEqual(subIdsBefore);
// expect(periodEndsAfter).toStrictEqual(periodEndsBefore);
if (customItems) {
expect(cusProduct.is_custom).toBe(true);
}
const customer = await autumn.customers.get(customerId);
expectFeaturesCorrect({
customer,
product: customProduct,
usage,
});
// 2. Expect product attached
await expectSubItemsCorrect({
stripeCli,
customerId,
product: customProduct,
db,
org,
env,
});
await expectSubToBeCorrect({
customerId,
db,
org,
env,
});
};
export default runUpdateEntsTest;

View File

@@ -1,155 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } 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 ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
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";
import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils.js";
import { replaceItems } from "../utils.js";
import runUpdateEntsTest from "./expectUpdateEnts.js";
const testCase = "updateEnts1";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 10000,
}),
],
type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
const numUsers = 0;
beforeAll(async () => {
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const newItem = constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 20000,
});
const customItems = replaceItems({
items: pro.items,
featureId: TestFeature.Words,
newItem,
});
const usage = 50000;
const overage = 50000 - (newItem.included_usage as number);
test("should update overage item to have new included usage", async () => {
const customProduct = {
...pro,
items: customItems,
};
await autumn.track({
customer_id: customerId,
value: usage,
feature_id: TestFeature.Words,
});
await timeout(5000);
await runUpdateEntsTest({
autumn,
stripeCli: ctx.stripeCli,
customerId,
customProduct,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customItems,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
test("should have correct invoice next cycle", async () => {
const invoiceTotal = await getExpectedInvoiceTotal({
org: ctx.org,
env: ctx.env,
customerId,
productId: pro.id,
stripeCli: ctx.stripeCli,
db: ctx.db,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
await advanceToNextInvoice({
stripeCli: ctx.stripeCli,
testClockId,
withPause: true,
});
// let curUnix = Date.now();
// curUnix = await advanceTestClock({
// stripeCli: ctx.stripeCli,
// testClockId,
// advanceTo: addMonths(curUnix, 1).getTime(),
// });
// await advanceTestClock({
// stripeCli: ctx.stripeCli,
// testClockId,
// advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
// waitForSeconds: 10,
// });
const customer = await autumn.customers.get(customerId);
const invoice = customer.invoices![0];
expect(invoice.total).toBe(invoiceTotal);
});
});

View File

@@ -1,170 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.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 ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths, addWeeks } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
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";
import { replaceItems } from "../utils.js";
import runUpdateEntsTest from "./expectUpdateEnts.js";
const testCase = "updateEnts2";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 10000,
}),
],
type: "pro",
isAnnual: true,
});
/**
* updateEnts2:
* Testing updating entitlements for annual plans
* 1. Start with pro annual plan (usage-based)
* 2. Update included usage amount
* 3. Verify features and usage are updated correctly
* 4. Verify invoice total is correct in next billing cycle
*
* Verifies that updating entitlements works correctly for annual plans
* and that usage/billing is calculated properly
*/
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage) for annual plan`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
const numUsers = 0;
beforeAll(async () => {
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro annual product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const newItem = constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 5000,
});
const customItems = replaceItems({
items: pro.items,
featureId: TestFeature.Words,
newItem,
});
const usage = 1200500;
test("should attach custom pro product", async () => {
await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 2).getTime(),
waitForSeconds: 30,
});
const customProduct = {
...pro,
items: customItems,
};
await autumn.track({
customer_id: customerId,
value: usage,
feature_id: TestFeature.Words,
});
await timeout(5000);
await runUpdateEntsTest({
autumn,
stripeCli: ctx.stripeCli,
customerId,
customProduct,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customItems,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
test("should have correct invoice usage next cycle", async () => {
const invoiceTotal = await getExpectedInvoiceTotal({
org: ctx.org,
env: ctx.env,
customerId,
productId: pro.id,
stripeCli: ctx.stripeCli,
db: ctx.db,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
onlyIncludeMonthly: true,
});
let curUnix = Date.now();
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addMonths(curUnix, 1).getTime(),
});
await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
waitForSeconds: 10,
});
const customer = await autumn.customers.get(customerId);
const invoice = customer.invoices![0];
expect(invoice.total).toBe(invoiceTotal);
});
});

View File

@@ -1,186 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js";
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";
import { replaceItems } from "../utils.js";
import runUpdateEntsTest from "./expectUpdateEnts.js";
const testCase = "updateEnts3";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 10000,
}),
],
type: "pro",
isAnnual: true,
});
/**
* updateEnts2:
* Testing updating entitlements for annual plans
* 1. Start with pro annual plan (usage-based)
* 2. Update included usage amount
* 3. Verify features and usage are updated correctly
* 4. Verify invoice total is correct in next billing cycle
*
* Verifies that updating entitlements works correctly for annual plans
* and that usage/billing is calculated properly
*/
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing feature items)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
beforeAll(async () => {
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro annual product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const newFeatureItem = constructFeatureItem({
feature_id: TestFeature.Messages,
included_usage: 500,
});
const usage = 1200500;
const customItems = [...pro.items, newFeatureItem];
test("should attach custom pro product with new feature item", async () => {
const customProduct = {
...pro,
items: customItems,
};
await autumn.track({
customer_id: customerId,
value: usage,
feature_id: TestFeature.Words,
});
await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 2).getTime(),
waitForSeconds: 10,
});
await runUpdateEntsTest({
autumn,
stripeCli: ctx.stripeCli,
customerId,
customProduct,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customItems,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
test("should attach custom pro product with updated feature item", async () => {
const customItems2 = replaceItems({
items: customItems,
featureId: TestFeature.Messages,
newItem: constructFeatureItem({
feature_id: TestFeature.Messages,
included_usage: 1000,
}),
});
const customProduct = {
...pro,
items: customItems2,
};
await runUpdateEntsTest({
autumn,
stripeCli: ctx.stripeCli,
customerId,
customProduct,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customItems: customItems2,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
test("should attach custom pro product with removed feature item", async () => {
const customItems2 = customItems.filter(
(item) => item.feature_id !== TestFeature.Messages,
);
const customProduct = {
...pro,
items: customItems2,
};
await runUpdateEntsTest({
autumn,
stripeCli: ctx.stripeCli,
customerId,
customProduct,
db: ctx.db,
org: ctx.org,
env: ctx.env,
customItems: customItems2,
usage: [
{
featureId: TestFeature.Words,
value: usage,
},
],
});
});
});

View File

@@ -1,85 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { AttachBranch, BillingInterval, LegacyVersion } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { nullish } from "@/utils/genUtils.js";
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 testCase = "updateEnts4";
export const pro = constructProduct({
items: [
constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 10000,
}),
],
type: "pro",
isAnnual: true,
});
describe(`${chalk.yellowBright(`${testCase}: Checking price changes don't result in update ents func`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
const curUnix = new Date().getTime();
beforeAll(async () => {
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro annual product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("branch should not be same custom ents if base price updated", async () => {
let customItems = pro.items.filter((item) => !nullish(item.feature_id));
customItems = [
...customItems,
constructPriceItem({
price: 10,
interval: BillingInterval.Year,
}),
];
const preview = await autumn.attachPreview({
customer_id: customerId,
product_id: pro.id,
is_custom: true,
items: customItems,
});
expect(preview.branch).toBe(AttachBranch.SameCustom);
});
});

View File

@@ -1,134 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.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 { 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";
// UNCOMMENT FROM HERE
const pro = constructProduct({
id: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const premium = constructProduct({
id: "premium",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
const growth = constructProduct({
id: "growth",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "growth",
});
describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
const customerId = "upgrade1";
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let curUnix: number;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium, growth],
prefix: customerId,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
test("should attach premium product", async () => {
const wordsUsage = 100000;
await timeout(4000);
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: wordsUsage,
});
// curUnix = await advanceTestClock({
// stripeCli,
// testClockId,
// advanceTo: addWeeks(new Date(), 2).getTime(),
// waitForSeconds: 10,
// });
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
await timeout(2000);
});
test("should attach growth product", async () => {
const wordsUsage = 200000;
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: wordsUsage,
});
curUnix = await advanceTestClock({
stripeCli,
testClockId,
numberOfWeeks: 1,
waitForSeconds: 30,
});
await attachAndExpectCorrect({
autumn,
customerId,
product: growth,
stripeCli,
db,
org,
env,
});
});
});

View File

@@ -1,140 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
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";
// Shared products for attach tests
const testCase = "upgrade2";
export const pro = constructProduct({
id: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
export const proAnnual = constructProduct({
id: "pro_annual",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
isAnnual: true,
});
export const premiumAnnual = constructProduct({
id: "premium_annual",
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
isAnnual: true,
});
/**
* upgrade2:
* 1. Start with pro monthly plan (usage-based)
* 2. Upgrade to pro annual plan (usage-based)
* 3. Upgrade to premium annual plan (usage-based)
*
* Verifies subscription items and anchors are correct after each upgrade
*/
describe(`${chalk.yellowBright("upgrade2: Testing usage upgrades with monthly -> annual")}`, () => {
const customerId = "upgrade2";
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let stripeCli: Stripe;
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let curUnix = new Date().getTime();
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, proAnnual, premiumAnnual],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
test("should attach pro annual product", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: 100000,
});
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 2).getTime(),
});
await attachAndExpectCorrect({
autumn,
customerId,
product: proAnnual,
stripeCli,
db,
org,
env,
});
});
test("should attach premium annual product", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: 5000000,
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
waitForSeconds: 10,
});
await attachAndExpectCorrect({
autumn,
customerId,
product: premiumAnnual,
stripeCli,
db,
org,
env,
});
});
});

View File

@@ -1,187 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.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";
const testCase = "upgrade3";
const pro = constructProduct({
items: [
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 12,
}),
],
type: "pro",
});
const premium = constructProduct({
items: [
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 20,
}),
],
type: "premium",
});
const proAnnual = constructProduct({
items: [
constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 12,
}),
],
type: "pro",
isAnnual: true,
});
/**
* upgrade3:
* Testing upgrades for arrear prorated
* 1. Start with pro monthly plan (usage-based)
* 2. Upgrade to pro annual plan (usage-based)
* 3. Upgrade to premium annual plan (usage-based)
*
* Verifies subscription items and anchors are correct after each upgrade
* with arrear prorated billing
*/
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with arrear prorated`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
let numUsers = 0;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium, proAnnual],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro product (arrear prorated)", async () => {
// 1. Create multiple entities
const entities = await autumn.entities.create(customerId, [
{
id: "entity1",
name: "entity1",
feature_id: TestFeature.Users,
},
{
id: "entity2",
name: "entity2",
feature_id: TestFeature.Users,
},
]);
numUsers = 2;
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
usage: [
{
featureId: TestFeature.Users,
value: 2,
},
],
});
});
test("should create entity, then upgrade to premium product (arrear prorated)", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
});
// TODO: Check price paid for entity3
await autumn.entities.create(customerId, [
{
id: "entity3",
name: "entity3",
feature_id: TestFeature.Users,
},
]);
numUsers += 1;
await timeout(3000);
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
usage: [
{
featureId: TestFeature.Users,
value: numUsers,
},
],
});
});
test("should upgrade to pro-annual product (arrear prorated)", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
});
await attachAndExpectCorrect({
autumn,
customerId,
product: proAnnual,
stripeCli,
db,
org,
env,
usage: [
{
featureId: TestFeature.Users,
value: numUsers,
},
],
});
});
});

View File

@@ -1,168 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } 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 testCase = "upgrade4";
export const pro = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Users,
price: 12,
billingUnits: 1,
}),
],
type: "pro",
});
export const premium = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Users,
price: 20,
billingUnits: 1,
}),
],
type: "premium",
});
export const proAnnual = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Users,
price: 12,
billingUnits: 1,
}),
],
type: "pro",
isAnnual: true,
});
/**
* upgrade3:
* Testing upgrades for arrear prorated
* 1. Start with pro monthly plan (usage-based)
* 2. Upgrade to pro annual plan (usage-based)
* 3. Upgrade to premium annual plan (usage-based)
*
* Verifies subscription items and anchors are correct after each upgrade
* with arrear prorated billing
*/
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid continuous use`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
const numUsers = 0;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium, proAnnual],
prefix: testCase,
});
testClockId = testClockId1!;
});
const proOpts = [
{
feature_id: TestFeature.Users,
quantity: 4,
},
];
test("should attach pro product (arrear prorated)", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
options: proOpts,
});
});
const premiumOpts = [
{
feature_id: TestFeature.Users,
quantity: 6,
},
];
test("should create entity, then upgrade to premium product (arrear prorated)", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
});
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
options: premiumOpts,
});
});
const proAnnualOpts = [
{
feature_id: TestFeature.Users,
quantity: 3,
},
];
test("should upgrade to pro-annual product (arrear prorated)", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
});
await attachAndExpectCorrect({
autumn,
customerId,
product: proAnnual,
stripeCli,
db,
org,
env,
options: proAnnualOpts,
});
});
});

View File

@@ -1,122 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } 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 testCase = "upgrade5";
export const pro = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
price: 12,
billingUnits: 100,
}),
],
type: "pro",
});
export const premium = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
price: 8,
billingUnits: 100,
}),
],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
const numUsers = 0;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
testClockId = testClockId1!;
});
const proOpts = [
{
feature_id: TestFeature.Messages,
quantity: 300,
},
];
test("should attach pro product (prepaid single use)", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
options: proOpts,
});
});
const premiumOpts = [
{
feature_id: TestFeature.Messages,
quantity: 600,
},
];
test("should upgrade to premium product (prepaid single use)", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addWeeks(curUnix, 1).getTime(),
waitForSeconds: 20,
});
return;
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
options: premiumOpts,
});
});
});

View File

@@ -1,80 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.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 { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "upgrade7";
export const pro = constructProduct({
items: [],
type: "pro",
});
export const premium = constructProduct({
items: [],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing upgrade via cancel + attach`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
test("should cancel than attach premium product", async () => {
await autumn.cancel({
customer_id: customerId,
product_id: pro.id,
cancel_immediately: true,
});
await autumn.attach({
customer_id: customerId,
product_id: premium.id,
force_checkout: true,
});
});
});

View File

@@ -1,128 +0,0 @@
import {
BillingInterval,
FreeTrialDuration,
ProductItemInterval,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";
/**
* Shared products for upgradeOld test group
* Matches global products.pro, products.proWithTrial, products.premium, products.premiumWithTrial
*/
export const sharedProProduct = constructProduct({
id: "shared-upgradeold-pro",
type: "pro",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
constructPriceItem({
price: 2000,
interval: BillingInterval.Month,
}),
],
});
export const sharedProWithTrialProduct = constructProduct({
id: "shared-upgradeold-pro-trial",
type: "pro",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
constructPriceItem({
price: 2000,
interval: BillingInterval.Month,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
export const sharedPremiumProduct = constructProduct({
id: "shared-upgradeold-premium",
type: "premium",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 5000,
interval: BillingInterval.Month,
}),
],
});
export const sharedPremiumWithTrialProduct = constructProduct({
id: "shared-upgradeold-premium-trial",
type: "premium",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 5000,
interval: BillingInterval.Month,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
export const initUpgradeOldSharedProducts = async () => {
await createSharedProducts({
ctx,
products: [
sharedProProduct,
sharedProWithTrialProduct,
sharedPremiumProduct,
sharedPremiumWithTrialProduct,
],
});
};
// Auto-init removed to prevent conflicts when running tests in parallel
// Each test file now initializes its own products with unique prefixes
// await initUpgradeOldSharedProducts();

View File

@@ -1,101 +0,0 @@
import { beforeAll, describe, test } from "bun:test";
import {
BillingInterval,
FreeTrialDuration,
ProductItemInterval,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.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";
const testCase = "upgradeOld2";
const proProduct = constructProduct({
type: "pro",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
constructPriceItem({
price: 2000,
interval: BillingInterval.Month,
}),
],
});
const premiumWithTrialProduct = constructProduct({
type: "premium",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 5000,
interval: BillingInterval.Month,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
describe(`${chalk.yellowBright(
"upgradeOld2: Testing upgrade (paid to trial)",
)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
beforeAll(async () => {
await initProductsV0({
ctx,
products: [proProduct, premiumWithTrialProduct],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
});
test("should attach pro", async () => {
await autumn.attach({
customer_id: customerId,
product_id: proProduct.id,
});
});
test("should attach premium with trial and have trial", async () => {
await autumn.attach({
customer_id: customerId,
product_id: premiumWithTrialProduct.id,
});
});
});

View File

@@ -1,139 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
BillingInterval,
CusProductStatus,
FreeTrialDuration,
ProductItemInterval,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addDays } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "upgradeOld3";
const proWithTrialProduct = constructProduct({
type: "pro",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
constructPriceItem({
price: 2000,
interval: BillingInterval.Month,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
const premiumWithTrialProduct = constructProduct({
type: "premium",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 5000,
interval: BillingInterval.Month,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: true,
card_required: true,
},
});
describe(`${chalk.yellowBright("upgradeOld3: Testing upgrade (trial to trial)")}`, () => {
const customerId = testCase;
let testClockId: string;
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [proWithTrialProduct, premiumWithTrialProduct],
prefix: testCase,
customerId,
});
const { testClockId: testClockId_ } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId_;
});
test("should attach pro with trial", async () => {
await autumn.attach({
customer_id: customerId,
product_id: proWithTrialProduct.id,
});
console.log(` ${chalk.greenBright("Attached pro with trial")}`);
});
test("should attach premium with trial", async () => {
const advanceTo = addDays(new Date(), 3).getTime();
await advanceTestClock({
stripeCli,
testClockId,
advanceTo,
waitForSeconds: 10,
});
await autumn.attach({
customer_id: customerId,
product_id: premiumWithTrialProduct.id,
});
});
test("should check product and ents", async () => {
const res = await autumn.customers.get(customerId);
expectCustomerV0Correct({
sent: premiumWithTrialProduct,
cusRes: res,
status: CusProductStatus.Trialing,
});
const invoices = res.invoices;
expect(invoices![0].total).toBe(0);
});
});

View File

@@ -1,163 +0,0 @@
// TESTING UPGRADES
import { beforeAll, describe, test } from "bun:test";
import {
BillingInterval,
type Customer,
ProductItemInterval,
} from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
attachFailedPaymentMethod,
attachPmToCus,
} from "@/external/stripe/stripeCusUtils.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.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";
const testCase = "upgradeOld4";
const proProduct = constructProduct({
type: "pro",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
constructPriceItem({
price: 2000,
interval: BillingInterval.Month,
}),
],
});
const premiumProduct = constructProduct({
type: "premium",
excludeBase: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 5000,
interval: BillingInterval.Month,
}),
],
});
describe(`${chalk.yellowBright("upgradeOld4: Testing upgrade from pro -> premium")}`, () => {
let customer: Customer;
const customerId = testCase;
let stripeCli: Stripe;
const autumn: AutumnInt = new AutumnInt();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [proProduct, premiumProduct],
prefix: testCase,
customerId,
});
const { customer: customer_ } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
customer = customer_;
});
test("should attach pro (trial)", async () => {
await autumn.attach({
customer_id: customerId,
product_id: proProduct.id,
});
const res = await autumn.customers.get(customerId);
expectCustomerV0Correct({
sent: proProduct,
cusRes: res,
});
});
// 1. Try force checkout...
test("should attach premium and not be able to force checkout", async () => {
expectAutumnError({
func: async () => {
await autumn.attach({
customer_id: customerId,
product_id: premiumProduct.id,
force_checkout: true,
});
},
});
});
test("should attach premium and not be able to upgrade (without payment method)", async () => {
await attachFailedPaymentMethod({
stripeCli: stripeCli,
customer: customer,
});
await expectAutumnError({
func: async () => {
await autumn.attach({
customer_id: customerId,
product_id: premiumProduct.id,
force_checkout: true,
});
},
});
});
return;
// Attach payment method
test("should attach successful payment method", async () => {
await attachPmToCus({
db: ctx.db,
customer: customer,
org: ctx.org,
env: ctx.env,
});
});
test("should attach premium and have correct product and entitlements", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: premiumProduct.id,
});
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: premiumProduct,
cusRes: res,
});
});
});

View File

@@ -1,185 +0,0 @@
import type { BillingInterval, ProductItem, ProductV2 } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
// export const runAttachTest = async ({
// autumn,
// customerId,
// entityId,
// product,
// options,
// stripeCli,
// db,
// org,
// env,
// usage,
// waitForInvoice = 0,
// isCanceled = false,
// skipFeatureCheck = false,
// singleInvoice = false,
// skipSubCheck = false,
// entities,
// }: {
// autumn: AutumnInt;
// customerId: string;
// entityId?: string;
// product: ProductV2;
// options?: FeatureOptions[];
// stripeCli: Stripe;
// db: DrizzleCli;
// org: Organization;
// env: AppEnv;
// usage?: {
// featureId: string;
// value: number;
// }[];
// waitForInvoice?: number;
// isCanceled?: boolean;
// skipFeatureCheck?: boolean;
// singleInvoice?: boolean;
// skipSubCheck?: boolean;
// entities?: CreateEntity[];
// }) => {
// const preview = await autumn.attachPreview({
// customer_id: customerId,
// product_id: product.id,
// entity_id: entityId,
// });
// const total = getAttachTotal({
// preview,
// options,
// });
// await autumn.attach({
// customer_id: customerId,
// product_id: product.id,
// entity_id: entityId,
// options: toSnakeCase(options),
// });
// if (waitForInvoice) {
// await timeout(waitForInvoice);
// }
// const customer = await autumn.customers.get(customerId);
// const productCount = customer.products.reduce((acc: number, p: any) => {
// if (product.group == p.group) {
// return acc + 1;
// } else return acc;
// }, 0);
// expect(
// productCount,
// `customer should only have 1 product (from this group: ${product.group})`
// ).to.equal(1);
// expectProductAttached({
// customer,
// product,
// entityId,
// });
// let intervals = Array.from(
// new Set(product.items.map((item) => item.interval))
// ).filter(notNullish);
// const multiInterval = intervals.length > 1;
// const freeProduct = isFreeProductV2({ product });
// if (!freeProduct) {
// let multiInvoice = !singleInvoice && multiInterval;
// expectInvoicesCorrect({
// customer,
// first: multiInvoice ? undefined : { productId: product.id, total },
// second: multiInvoice ? { productId: product.id, total } : undefined,
// });
// }
// if (!skipFeatureCheck) {
// expectFeaturesCorrect({
// customer,
// product,
// usage,
// options,
// entities,
// });
// }
// const branch = preview.branch;
// if (branch == AttachBranch.OneOff || freeProduct) {
// return;
// }
// if (skipSubCheck) return;
// await expectSubItemsCorrect({
// stripeCli,
// customerId,
// product,
// db,
// org,
// env,
// isCanceled,
// });
// const stripeSubs = await stripeCli.subscriptions.list({
// customer: customer.stripe_id!,
// });
// if (multiInterval) {
// expect(stripeSubs.data.length).to.equal(2, "should have 2 subscriptions");
// } else {
// expect(stripeSubs.data.length).to.equal(
// 1,
// "should only have 1 subscription"
// );
// }
// };
export const addPrefixToProducts = ({
products,
prefix,
}: {
products: ProductV2[];
prefix: string;
}) => {
for (const product of products) {
product.id = `${prefix}_${product.id}`;
product.name = `${product.name} ${prefix}`;
product.group = prefix;
}
return products;
};
export const replaceItems = ({
featureId,
interval,
newItem,
items,
}: {
featureId?: string;
interval?: BillingInterval;
newItem: ProductItem;
items: ProductItem[];
}) => {
const newItems = structuredClone(items);
let index;
if (featureId) {
index = newItems.findIndex((item) => item.feature_id === featureId);
}
if (interval) {
index = newItems.findIndex(
(item) => item.interval === (interval as any) && nullish(item.feature_id),
);
}
if (index === -1) {
throw new Error("Item not found");
}
newItems[index!] = newItem;
return newItems;
};

View File

@@ -1,211 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
AppEnv,
CusProductStatus,
customers,
ProcessorType,
} from "@autumn/shared";
import { replaceItems } from "@tests/attach/utils.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { eq } from "drizzle-orm";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { RCMappingService } from "@/external/revenueCat/misc/RCMappingService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { encryptData } from "@/utils/encryptUtils.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";
import { expectFeaturesCorrect } from "../../utils/expectUtils/expectFeaturesCorrect.js";
import { timeout } from "../../utils/genUtils.js";
import {
expectWebhookSuccess,
RevenueCatWebhookClient,
} from "./utils/revenue-cat-webhook-client.js";
const testCase = "rcMigration1";
const RC_WEBHOOK_SECRET = "test_rc_webhook_secret_migration";
// RevenueCat product ID
const RC_PRO_MONTHLY_ID = "com.app.migration_pro_monthly";
// Autumn product definitions
const messagesFeature = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
});
const proMonthlyV1 = constructProduct({
id: `${testCase}-pro-monthly`,
type: "pro",
items: [messagesFeature],
isDefault: false,
});
const proMonthlyV2 = {
...proMonthlyV1,
version: 2,
items: replaceItems({
items: proMonthlyV1.items,
featureId: TestFeature.Messages,
newItem: constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 2000,
}),
}),
};
describe(
chalk.yellowBright("rcMigration1: RevenueCat customer migration"),
() => {
const customerId = `${testCase}-customer`;
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
let internalCustomerId: string | null = null;
let rcClient: RevenueCatWebhookClient;
beforeAll(async () => {
// 1. Configure org with RevenueCat processor config
if (
ctx.org.processor_configs?.revenuecat?.sandbox_webhook_secret !==
RC_WEBHOOK_SECRET
) {
await OrgService.update({
db: ctx.db,
orgId: ctx.org.id,
updates: {
processor_configs: {
...ctx.org.processor_configs,
revenuecat: {
api_key: encryptData("mock_rc_api_key_live"),
sandbox_api_key: encryptData("mock_rc_api_key_sandbox"),
project_id: "mock_project_live",
sandbox_project_id: "mock_project_sandbox",
webhook_secret: RC_WEBHOOK_SECRET,
sandbox_webhook_secret: RC_WEBHOOK_SECRET,
},
},
},
});
}
// 2. Create product and mappings
await initProductsV0({
ctx,
products: [proMonthlyV1],
prefix: testCase,
customerId,
});
await RCMappingService.upsert({
db: ctx.db,
data: {
org_id: ctx.org.id,
env: AppEnv.Sandbox,
autumn_product_id: proMonthlyV1.id,
revenuecat_product_ids: [RC_PRO_MONTHLY_ID],
},
});
// 3. Create customer
await initCustomerV3({
ctx,
customerId,
withTestClock: false,
});
const dbCustomer = await ctx.db.query.customers.findFirst({
where: eq(customers.id, customerId),
});
expect(dbCustomer).toBeDefined();
internalCustomerId = dbCustomer!.internal_id;
// Initialize RevenueCat webhook client
rcClient = new RevenueCatWebhookClient({
orgId: ctx.org.id,
env: ctx.env,
webhookSecret: RC_WEBHOOK_SECRET,
});
});
test("should create customer with pro monthly v1 product via initial purchase", async () => {
const { response, data } = await rcClient.initialPurchase({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "migration_tx_12345",
});
expectWebhookSuccess({ response, data });
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthlyV1.id);
// Verify cus_product has RevenueCat processor
const cusProducts = await CusProductService.list({
db: ctx.db,
internalCustomerId: internalCustomerId!,
inStatuses: [CusProductStatus.Active],
});
expect(cusProducts).toHaveLength(1);
expect(cusProducts[0].processor?.type).toBe(ProcessorType.RevenueCat);
});
test("should create v2 of the product with updated features", async () => {
// Create v2 with increased usage
const newItems = replaceItems({
items: proMonthlyV1.items,
featureId: TestFeature.Messages,
newItem: constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 2000, // Increased from 1000
}),
});
await autumnV1.products.update(proMonthlyV1.id, {
items: newItems,
});
});
test("should migrate customer from v1 to v2", async () => {
await autumnV1.track({
customer_id: customerId,
value: 500,
feature_id: TestFeature.Messages,
});
await timeout(2000);
// Run migration via API
await autumnV1.migrate({
from_product_id: proMonthlyV1.id,
to_product_id: proMonthlyV1.id,
from_version: 1,
to_version: 2,
});
await timeout(5000);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthlyV1.id);
expect(customer.products[0].version).toBe(2);
expectFeaturesCorrect({
customer,
product: proMonthlyV2,
usage: [
{
featureId: TestFeature.Messages,
value: 500,
},
],
});
});
},
);

View File

@@ -1,395 +0,0 @@
import { beforeAll, describe, expect, test } from "bun:test";
import {
ApiVersion,
AppEnv,
CusProductStatus,
customers,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { eq } from "drizzle-orm";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { RCMappingService } from "@/external/revenueCat/misc/RCMappingService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { encryptData } from "@/utils/encryptUtils.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";
import {
expectWebhookSuccess,
RevenueCatWebhookClient,
} from "./utils/revenue-cat-webhook-client.js";
const testCase = "rc1";
const RC_WEBHOOK_SECRET = "test_rc_webhook_secret_12345";
// RevenueCat product IDs (what RC sends in webhooks)
const RC_PRO_MONTHLY_ID = "com.app.pro_monthly";
const RC_PRO_YEARLY_ID = "com.app.pro_yearly";
const RC_ADD_ON_ID = "com.app.add_on_pack";
// Autumn product definitions
const messagesFeature = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
});
const proMonthly = constructProduct({
id: `${testCase}-pro-monthly`,
type: "pro",
items: [messagesFeature],
isDefault: false,
});
const proYearly = constructProduct({
id: `${testCase}-pro-yearly`,
type: "pro",
isAnnual: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
}),
],
isDefault: false,
});
const addOnPack = constructProduct({
id: `${testCase}-add-on`,
type: "one_off",
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 100,
}),
],
isAddOn: true,
isDefault: false,
});
describe(chalk.yellowBright("rc1: RevenueCat webhook integration"), () => {
const customerId = `${testCase}-customer`;
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
let proMonthlyCusProductId: string | null = null;
let internalCustomerId: string | null = null;
let rcClient: RevenueCatWebhookClient;
const fetchLatestActiveCusProductId = async () => {
if (!internalCustomerId) {
throw new Error("internalCustomerId not set");
}
const cusProducts = await CusProductService.list({
db: ctx.db,
internalCustomerId,
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Scheduled,
],
});
const activeSorted = cusProducts
.filter(
(cp) =>
cp.status === CusProductStatus.Active ||
cp.status === CusProductStatus.PastDue,
)
.sort((a, b) => (a.created_at ?? 0) - (b.created_at ?? 0));
expect(
activeSorted.length > 0,
"Expected at least one active cus_product for customer",
).toBe(true);
// Return the latest active cus_product id for the customer
return activeSorted[activeSorted.length - 1]!.id;
};
const fetchLatestCusProductIdAnyStatus = async () => {
if (!internalCustomerId) {
throw new Error("internalCustomerId not set");
}
const cusProducts = await CusProductService.list({
db: ctx.db,
internalCustomerId,
inStatuses: undefined,
});
if (cusProducts.length === 0) {
return null;
}
const sorted = [...cusProducts].sort(
(a, b) => (a.created_at ?? 0) - (b.created_at ?? 0),
);
return sorted[sorted.length - 1]!.id;
};
const getBaselineCusProductId = () => {
if (!proMonthlyCusProductId) {
throw new Error(
"Baseline CusProduct ID was not set from initial purchase",
);
}
return proMonthlyCusProductId;
};
const updateBaselineCusProductId = (cusProductId: string) => {
proMonthlyCusProductId = cusProductId;
};
beforeAll(async () => {
// 1. Configure org with RevenueCat processor config
if (
ctx.org.processor_configs?.revenuecat?.sandbox_webhook_secret !==
RC_WEBHOOK_SECRET
) {
await OrgService.update({
db: ctx.db,
orgId: ctx.org.id,
updates: {
processor_configs: {
...ctx.org.processor_configs,
revenuecat: {
api_key: encryptData("mock_rc_api_key_live"),
sandbox_api_key: encryptData("mock_rc_api_key_sandbox"),
project_id: "mock_project_live",
sandbox_project_id: "mock_project_sandbox",
webhook_secret: RC_WEBHOOK_SECRET,
sandbox_webhook_secret: RC_WEBHOOK_SECRET,
},
},
},
});
}
// Initialize RevenueCat webhook client
rcClient = new RevenueCatWebhookClient({
orgId: ctx.org.id,
env: ctx.env,
webhookSecret: RC_WEBHOOK_SECRET,
});
// 2-4. Create products, mappings, and customer concurrently
await Promise.all([
initProductsV0({
ctx,
products: [proMonthly, proYearly, addOnPack],
prefix: testCase,
}),
RCMappingService.upsert({
db: ctx.db,
data: {
org_id: ctx.org.id,
env: AppEnv.Sandbox,
autumn_product_id: proMonthly.id,
revenuecat_product_ids: [RC_PRO_MONTHLY_ID],
},
}),
RCMappingService.upsert({
db: ctx.db,
data: {
org_id: ctx.org.id,
env: AppEnv.Sandbox,
autumn_product_id: addOnPack.id,
revenuecat_product_ids: [RC_ADD_ON_ID],
},
}),
RCMappingService.upsert({
db: ctx.db,
data: {
org_id: ctx.org.id,
env: AppEnv.Sandbox,
autumn_product_id: proYearly.id,
revenuecat_product_ids: [RC_PRO_YEARLY_ID],
},
}),
initCustomerV3({
ctx,
customerId,
withTestClock: false,
}),
]);
const dbCustomer = await ctx.db.query.customers.findFirst({
where: eq(customers.id, customerId),
});
expect(dbCustomer).toBeDefined();
internalCustomerId = dbCustomer!.internal_id;
});
test("should create customer with pro monthly product", async () => {
const result = await rcClient.initialPurchase({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthly.id);
proMonthlyCusProductId = await fetchLatestActiveCusProductId();
});
test("should upgrade customer to pro yearly product upon renewal", async () => {
const result = await rcClient.renewal({
productId: RC_PRO_YEARLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
});
expectWebhookSuccess(result);
await fetchLatestActiveCusProductId();
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proYearly.id);
});
test("should downgrade customer to pro monthly product upon initial purchase", async () => {
const result = await rcClient.initialPurchase({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthly.id);
const currentCusProductId = await fetchLatestActiveCusProductId();
console.log("currentCusProductId", currentCusProductId);
expect(currentCusProductId).not.toBe(getBaselineCusProductId());
updateBaselineCusProductId(currentCusProductId);
});
test("should go to cancelling state upon cancellation", async () => {
const result = await rcClient.cancellation({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
expirationAtMs: Date.now() + 1000 * 60 * 60 * 24 * 30,
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthly.id);
const canceledAt = customer.products[0].canceled_at ?? 0;
expect(typeof canceledAt).toBe("number");
expect(Math.abs(Date.now() - canceledAt)).toBeLessThanOrEqual(3000);
const currentCusProductId = await fetchLatestActiveCusProductId();
expect(currentCusProductId).toBe(getBaselineCusProductId());
});
test("should uncancel customer after cancellation event", async () => {
const result = await rcClient.uncancellation({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthly.id);
expect(customer.products[0].canceled_at).toBeNull();
const currentCusProductId = await fetchLatestActiveCusProductId();
expect(currentCusProductId).toBe(getBaselineCusProductId());
});
test("should mark product as past due upon billing issue", async () => {
const result = await rcClient.billingIssue({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(proMonthly.id);
expect(String(customer.products[0].status)).toBe("past_due");
const currentCusProductId = await fetchLatestActiveCusProductId();
expect(currentCusProductId).toBe(getBaselineCusProductId());
});
test("should go to expired state upon expiration", async () => {
const result = await rcClient.expiration({
productId: RC_PRO_MONTHLY_ID,
appUserId: customerId,
originalTransactionId: "1234567890",
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(0);
const latestCusProductId = await fetchLatestCusProductIdAnyStatus();
// After expiration, there may no longer be a cus_product row at all. In that
// case, we just assert there are no cus_products for this customer anymore.
if (latestCusProductId === null) {
const allCusProducts = await CusProductService.list({
db: ctx.db,
internalCustomerId: internalCustomerId!,
inStatuses: undefined,
});
expect(allCusProducts.length).toBe(0);
} else {
expect(latestCusProductId).toBe(getBaselineCusProductId());
}
});
test("should attach add-on product after expiration via non-renewing purchase", async () => {
const result = await rcClient.nonRenewingPurchase({
productId: RC_ADD_ON_ID,
appUserId: customerId,
originalTransactionId: "add_on_tx_12345",
});
expectWebhookSuccess(result);
const customer = await autumnV1.customers.get(customerId);
expect(customer).toBeDefined();
expect(customer.id).toBe(customerId);
expect(customer.products).toHaveLength(1);
expect(customer.products[0].id).toBe(addOnPack.id);
const addOnCusProducts = await CusProductService.getByProductId({
db: ctx.db,
productId: addOnPack.id,
orgId: ctx.org.id,
env: ctx.env,
limit: 1,
});
expect(
addOnCusProducts.length > 0,
`CusProduct for add-on product ${addOnPack.id} should exist`,
).toBe(true);
const addOnCusProductId = addOnCusProducts[0]!.id;
expect(typeof addOnCusProductId).toBe("string");
});
});

View File

@@ -0,0 +1,244 @@
/**
* Legacy Add-on Attach Tests
*
* Migrated from:
* - server/tests/attach/addOn/addOn1.test.ts (attach pro then free add-on)
* - server/tests/attach/addOn/addOn2.test.ts (attach pro then free add-on with credits)
* - server/tests/attach/basic/basic2.test.ts (attach pro then monthly prepaid add-on with quantity)
*
* Tests V1 attach (s.attach) behavior for:
* - Attaching a free add-on after a base product
* - Verifying both products appear as active
* - Verifying feature balances after add-on attachment
* - Attaching prepaid add-ons with quantity options
*/
import { expect, test } from "bun:test";
import type { ApiCustomerV1, ApiCustomerV3 } from "@autumn/shared";
import { AutumnCli } from "@tests/cli/AutumnCli";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
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";
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 1: Attach pro then free add-on with messages
// (from addOn1)
//
// Scenario:
// - Pro product ($20/month) with Messages feature (100 included)
// - Free add-on with Messages feature (200 included)
// - Attach Pro, then attach add-on
//
// Expected:
// - Customer has both Pro and add-on active
// - 1 invoice for $20 (Pro subscription only, add-on is free)
// - Messages balance = 100 (Pro) + 200 (add-on) = 300
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("legacy-addon 1: attach pro then free add-on with messages")}`, async () => {
const customerId = "legacy-addon-1";
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
const pro = products.pro({ id: "pro", items: [messagesItem] });
const addOnMessagesItem = items.monthlyMessages({ includedUsage: 200 });
const addOn = products.base({
id: "addon",
items: [addOnMessagesItem],
isAddOn: true,
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro, addOn] }),
],
actions: [
s.attach({ productId: pro.id }),
s.attach({ productId: addOn.id }),
],
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [pro.id, addOn.id],
});
await expectCustomerInvoiceCorrect({
customer,
count: 1,
latestTotal: 20,
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
balance: 300,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: Attach pro then free add-on twice
// (from addOn2)
//
// Scenario:
// - Pro product ($20/month) with no feature items
// - Free add-on with Credits feature (100 included)
// - Attach Pro, then attach add-on, then attach add-on again
//
// Expected:
// - Customer has both Pro and add-on active after each attachment
// - Credits feature balance remains 100 after second attach
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("legacy-addon 2: attach pro then free add-on twice")}`, async () => {
const customerId = "legacy-addon-2";
const pro = products.pro({ id: "pro", items: [] });
const creditsItem = items.monthlyCredits();
const addOn = products.base({
id: "addon",
items: [creditsItem],
isAddOn: true,
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro, addOn] }),
],
actions: [
s.attach({ productId: pro.id }),
s.attach({ productId: addOn.id }),
],
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer,
active: [pro.id, addOn.id],
});
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Credits,
balance: 100,
usage: 0,
});
// Attach the same free add-on a second time
await autumnV1.attach({
customer_id: customerId,
product_id: `${addOn.id}_${customerId}`,
});
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer: customerAfter,
active: [pro.id, addOn.id],
});
expectCustomerFeatureCorrect({
customer: customerAfter,
featureId: TestFeature.Credits,
balance: 100,
usage: 0,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: Attach pro then monthly prepaid add-on with quantity
// (from basic2)
//
// Scenario:
// - Pro product ($20/month) with Messages feature (10 included)
// - Monthly prepaid add-on for Messages ($9/250 units, 0 included)
// - Attach Pro, then attach add-on with quantity 500 (2 packs)
//
// Expected:
// - Customer has both Pro and add-on active
// - 2 invoices (Pro $20, add-on $18)
// - Messages balance = 10 (Pro) + 500 (add-on) = 510
// - /check returns correct combined balance (using V0 entitled endpoint)
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("legacy-addon 3: attach pro then monthly prepaid add-on with quantity")}`, async () => {
const customerId = "legacy-addon-3";
const monthlyQuantity = 500;
const messagesItem = items.monthlyMessages({ includedUsage: 10 });
const pro = products.pro({ id: "pro", items: [messagesItem] });
const prepaidMessagesItem = items.prepaidMessages({
price: 9,
billingUnits: 250,
includedUsage: 0,
});
const addOn = products.base({
id: "monthly-add-on",
items: [prepaidMessagesItem],
isAddOn: true,
});
await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro, addOn] }),
],
actions: [s.attach({ productId: pro.id })],
});
// Attach the prepaid add-on with quantity options (using AutumnCli like original)
await AutumnCli.attach({
customerId,
productId: addOn.id,
forceCheckout: false,
options: [
{
feature_id: TestFeature.Messages,
quantity: monthlyQuantity,
},
],
});
// Use AutumnCli.getCustomer for V1 response format (entitlements, add_ons)
const cusRes = await AutumnCli.getCustomer(customerId) as ApiCustomerV1;
// Pro gives 10 Messages, add-on gives 500
const expectedBalance = 10 + monthlyQuantity;
const monthlyMessagesBalance = cusRes.entitlements.find(
(e) => e.feature_id === TestFeature.Messages && e.interval === "month",
);
expect(monthlyMessagesBalance?.balance).toBe(expectedBalance);
expect(cusRes.add_ons).toHaveLength(1);
const monthlyAddOnFound = cusRes.add_ons.find((a) => a.id === addOn.id);
expect(monthlyAddOnFound).toBeDefined();
expect(cusRes.invoices.length).toBe(2);
// Verify /entitled returns correct balance (V0 endpoint with balances array)
const entitledRes = await AutumnCli.entitled(customerId, TestFeature.Messages) as {
allowed: boolean;
balances: { feature_id: string; balance: number }[];
};
const messagesBalance = entitledRes.balances.find(
(b) => b.feature_id === TestFeature.Messages,
);
expect(messagesBalance?.balance).toBe(expectedBalance);
});

View File

@@ -1,10 +1,16 @@
import { expect, test } from "bun:test";
import type { ApiCustomerV3 } from "@autumn/shared";
import { expectCustomerProducts } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { TestFeature } from "@tests/setup/v2Features";
import { completeInvoiceCheckoutV2 as completeInvoiceCheckout } from "@tests/utils/browserPool/completeInvoiceCheckoutV2";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import { items } from "@tests/utils/fixtures/items.js";
import { products } from "@tests/utils/fixtures/products.js";
import { completeInvoiceCheckoutV2 as completeInvoiceCheckout } from "@tests/utils/browserPool/completeInvoiceCheckoutV2";
import ctx from "@tests/utils/testInitUtils/createTestContext";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
import { CusService } from "@/internal/customers/CusService";
import { removeAllPaymentMethods } from "@/external/stripe/customers/paymentMethods/operations/removeAllPaymentMethods.js";
import { attachPaymentMethod } from "@/utils/scriptUtils/initCustomer.js";
@@ -107,3 +113,159 @@ test.concurrent(`${chalk.yellowBright("attach: pro then upgrade to premium with
product: premium,
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: Create customer and entity with null IDs (autumn_id generation)
// (from others6)
//
// Scenario:
// - Create customer with id=null → gets autumn_id
// - Create entity with id=null → gets autumn_id
// - Attach pro product with invoice option
// - Then assign external ID to the same customer
//
// Expected:
// - Customer and entity get autumn_id when created with null
// - Can attach product using autumn_id
// - Can later assign external ID to same customer
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-edge-case 3: null customer_id and entity_id (autumn_id generation)")}`, async () => {
const customerId = "edge-case-null-ids";
const email = `${customerId}@test.com`;
const wordsItem = items.consumableWords();
const pro = products.pro({
id: "pro",
items: [wordsItem],
});
// Clean up any existing customer with this email
const existingCustomers = await CusService.getByEmail({
db: ctx.db,
orgId: ctx.org.id,
env: ctx.env,
email,
});
const { autumnV1 } = await initScenario({
setup: [s.products({ list: [pro], prefix: customerId })],
actions: [],
});
if (existingCustomers.length > 0) {
await autumnV1.customers.delete(existingCustomers[0].internal_id);
}
// Create customer with id=null → should get autumn_id
const customer = await autumnV1.customers.create({
id: null,
email,
name: customerId,
withAutumnId: true,
});
expect(customer.autumn_id).toBeDefined();
const internalCustomerId = customer.autumn_id;
// Create entity with id=null → should get autumn_id
const entity = await autumnV1.entities.create(internalCustomerId, {
id: null,
feature_id: TestFeature.Users,
});
expect(entity.autumn_id).toBeDefined();
const internalEntityId = entity.autumn_id;
// Attach pro product using autumn_ids, with invoice option
await autumnV1.attach({
customer_id: internalCustomerId,
entity_id: internalEntityId,
product_id: pro.id,
invoice: true,
enable_product_immediately: true,
});
const customerAfterAttach = await autumnV1.customers.get<ApiCustomerV3>(internalCustomerId);
await expectCustomerProducts({
customer: customerAfterAttach,
active: [pro.id],
});
expect(customerAfterAttach.invoices?.length).toBe(1);
expect(customerAfterAttach.invoices?.[0].status).toBe("draft");
// Now assign external ID to the same customer
const customerWithId = await autumnV1.customers.create({
id: customerId,
email,
});
// Should be the same customer (same autumn_id)
expect(customerWithId.autumn_id).toBe(internalCustomerId);
// Can now fetch by external ID
const customerById = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer: customerById,
active: [pro.id],
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 4: Duplicate attach error (attach same product twice)
// (from others9)
//
// Scenario:
// - Free product with Words feature
// - Attach free product → success
// - Attach same free product again → error
//
// Expected:
// - First attach succeeds
// - Second attach throws AutumnError
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-edge-case 4: duplicate attach error")}`, async () => {
const customerId = "edge-case-duplicate-attach";
const wordsItem = items.monthlyWords({ includedUsage: 100 });
const free = products.base({
id: "free",
items: [wordsItem],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [free] }),
],
actions: [],
});
// First attach should succeed
await autumnV1.attach({
customer_id: customerId,
product_id: free.id,
});
const customerAfterFirstAttach = await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectCustomerProducts({
customer: customerAfterFirstAttach,
active: [free.id],
});
// Second attach should fail
await expectAutumnError({
func: async () => {
await autumnV1.attach({
customer_id: customerId,
product_id: free.id,
});
},
});
});

View File

@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import type { AppEnv } from "@autumn/shared";
import { type AppEnv, ErrCode } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
@@ -8,8 +8,9 @@ import { products } from "@tests/utils/fixtures/products.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
import chalk from "chalk";
import AutumnError from "@/external/autumn/autumnCli";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { timeout } from "@/utils/genUtils.js";
import { generateId, timeout } from "@/utils/genUtils.js";
// ============================================================================
// Test 1: Auto-create customer and entity via attach
@@ -160,3 +161,72 @@ test.skip(`${chalk.yellowBright("attach-misc: attach race condition - concurrent
const successes = responses.filter((r) => r.status === "fulfilled");
expect(successes.length).toEqual(1);
});
// ============================================================================
// Test 3: Idempotency key - duplicate requests rejected
// (from others10)
// ============================================================================
test.concurrent(`${chalk.yellowBright("attach-misc: idempotency key duplicate rejected")}`, async () => {
const customerId = "attach-misc-idempotency";
const creditsItem = items.monthlyMessages({ includedUsage: 500 });
const pro = products.pro({
id: "pro",
items: [creditsItem],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro] }),
],
actions: [],
});
const idempotencyKey = generateId("it");
// Send two concurrent attach requests with the same idempotency key
const results = await Promise.allSettled([
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
idempotencyKey,
},
),
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
idempotencyKey,
},
),
]);
// Exactly one request should succeed
const fulfilled = results.filter((r) => r.status === "fulfilled");
const rejected = results.filter((r) => r.status === "rejected");
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
// The successful request should have attached the product
const successResult = fulfilled[0] as PromiseFulfilledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>;
expect(successResult.value.success).toBe(true);
expect(successResult.value.customer_id).toBe(customerId);
expect(successResult.value.product_ids).toContain(pro.id);
// The rejected request should have the duplicate idempotency key error
const rejectedResult = rejected[0] as PromiseRejectedResult;
expect(rejectedResult.reason).toBeInstanceOf(AutumnError);
expect((rejectedResult.reason as AutumnError).code).toBe(
ErrCode.DuplicateIdempotencyKey,
);
});

View File

@@ -0,0 +1,313 @@
/**
* Legacy Attach Response Format Tests
*
* Migrated from:
* - server/tests/attach/response/attach-response1.test.ts (new, no card → checkout_url)
* - server/tests/attach/response/attach-response2.test.ts (upgrade response)
* - server/tests/attach/response/attach-response3.test.ts (downgrade response)
* - server/tests/attach/response/attach-response4.test.ts (new, card on file)
* - server/tests/attach/response/attach-response5.test.ts (one-off, card on file)
*
* Tests that attach responses have the correct shape for v0.2 and v1.2 API versions
* across different attach scenarios (new, upgrade, downgrade, one-off).
*/
/** biome-ignore-all lint/suspicious/noExplicitAny: test file */
import { expect, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
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";
import { AutumnInt } from "@/external/autumn/autumnCli";
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 1: New attach with no card on file → returns checkout_url
// (from attach-response1)
//
// Scenario:
// - Pro product ($20/month) with consumable Words (1000 included)
// - Customer with NO payment method
// - v0.2: attach returns only { checkout_url }
// - v1.2: attach returns { customer_id, product_ids, checkout_url, code, message }
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-response 1: new attach, no card → checkout_url")}`, async () => {
const customerId = "attach-response-1";
const pro = products.pro({
id: "pro",
items: [items.consumableWords({ includedUsage: 1000 })],
});
await initScenario({
customerId,
setup: [s.customer({}), s.products({ list: [pro] })],
actions: [],
});
const autumnV0 = new AutumnInt({ version: ApiVersion.V0_2 });
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
// v0.2 response: only checkout_url
const v0Response = await autumnV0.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(v0Response.checkout_url).toBeDefined();
expect(Object.keys(v0Response)).toEqual(["checkout_url"]);
// v1.2 response: richer object
const v1Response = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(v1Response).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
checkout_url: expect.any(String),
});
expect(v1Response.code).toBeDefined();
expect(v1Response.message).toBeDefined();
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: Upgrade response
// (from attach-response2)
//
// Scenario:
// - Pro ($20/month) and Premium ($50/month) with Words (1000 included)
// - Customer with payment method, attach Pro first
// - Upgrade to Premium
// - v0.2: returns { success, message }
// - v1.2: cancel Premium, re-attach Pro, then upgrade to Premium →
// returns { customer_id, product_ids, code, message }
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-response 2: upgrade response")}`, async () => {
const customerId1 = "attach-response-2-1";
const customerId2 = "attach-response-2-2";
const wordsItem = items.monthlyWords({ includedUsage: 1000 });
const pro = products.pro({ id: "pro", items: [wordsItem] });
const premium = products.premium({ id: "premium", items: [wordsItem] });
const { autumnV1 } = await initScenario({
customerId: customerId1,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro, premium] }),
s.otherCustomers([{ id: customerId2, paymentMethod: "success" }]),
],
actions: [s.attach({ productId: pro.id })],
});
const autumnV0 = new AutumnInt({ version: ApiVersion.V0_2 });
// v0.2 upgrade response
const v0Response = await autumnV0.attach({
customer_id: customerId1,
product_id: premium.id,
});
expect(Object.keys(v0Response)).toEqual(["success", "message"]);
await autumnV1.attach({
customer_id: customerId2,
product_id: pro.id,
});
// v1.2 upgrade response
const v1Response = await autumnV1.attach({
customer_id: customerId2,
product_id: premium.id,
});
expect(v1Response).toMatchObject({
customer_id: customerId2,
product_ids: [premium.id],
code: expect.any(String),
message: expect.any(String),
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: Downgrade response
// (from attach-response3)
//
// Scenario:
// - Pro ($20/month) and Premium ($50/month) with Words (1000 included)
// - Customer with payment method, attach Premium first
// - Downgrade to Pro
// - v0.2: returns { success, message }
// - v1.2: uncancel Premium, then downgrade to Pro →
// returns { customer_id, product_ids, code, message }
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-response 3: downgrade response")}`, async () => {
const customerId = "attach-response-3";
const wordsItem = items.monthlyWords({ includedUsage: 1000 });
const pro = products.pro({ id: "pro", items: [wordsItem] });
const premium = products.premium({ id: "premium", items: [wordsItem] });
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro, premium] }),
],
actions: [s.attach({ productId: premium.id })],
});
const autumnV0 = new AutumnInt({ version: ApiVersion.V0_2 });
// v0.2 downgrade response
const v0Response = await autumnV0.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(Object.keys(v0Response)).toEqual(["success", "message"]);
// Reset: uncancel Premium for v1.2 test
await autumnV1.subscriptions.update({
customer_id: customerId,
product_id: premium.id,
cancel_action: "uncancel",
});
// v1.2 downgrade response
const v1Response = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(v1Response).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
code: expect.any(String),
message: expect.any(String),
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 4: New attach with card on file → immediate success
// (from attach-response4)
//
// Scenario:
// - Pro ($20/month) with Words (1000 included)
// - Customer with payment method
// - v0.2: attach returns { success, message }
// - v1.2: cancel Pro, re-attach →
// returns { customer_id, product_ids, code, message }
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-response 4: new attach, card on file")}`, async () => {
const customerId = "attach-response-4";
const pro = products.pro({
id: "pro",
items: [items.monthlyWords({ includedUsage: 1000 })],
});
const { autumnV1 } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [pro] }),
],
actions: [],
});
const autumnV0 = new AutumnInt({ version: ApiVersion.V0_2 });
// v0.2 response
const v0Response = await autumnV0.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(Object.keys(v0Response)).toEqual(["success", "message"]);
// Reset: cancel Pro for v1.2 test
await autumnV1.cancel({
customer_id: customerId,
product_id: pro.id,
cancel_immediately: true,
});
// v1.2 response
const v1Response = await autumnV1.attach({
customer_id: customerId,
product_id: pro.id,
});
expect(v1Response).toMatchObject({
customer_id: customerId,
product_ids: [pro.id],
code: expect.any(String),
message: expect.any(String),
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 5: One-off attach with card on file
// (from attach-response5)
//
// Scenario:
// - One-off product ($10) with lifetime Words (1000 included)
// - Customer with payment method
// - v0.2: attach returns { success, message }
// - v1.2: attach again (one-off can be attached multiple times) →
// returns { success, customer_id, product_ids, code, message }
// ═══════════════════════════════════════════════════════════════════════════════
test.concurrent(`${chalk.yellowBright("attach-response 5: one-off attach, card on file")}`, async () => {
const customerId = "attach-response-5";
const oneOff = products.oneOff({
id: "one-off",
items: [items.lifetimeMessages({ includedUsage: 1000 })],
});
await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success", testClock: true }),
s.products({ list: [oneOff] }),
],
actions: [],
});
const autumnV0 = new AutumnInt({ version: ApiVersion.V0_2 });
const autumnV1 = new AutumnInt({ version: ApiVersion.V1_2 });
// v0.2 response
const v0Response = await autumnV0.attach({
customer_id: customerId,
product_id: oneOff.id,
});
expect(v0Response).toMatchObject({
success: true,
message: expect.any(String),
});
// v1.2 response (attach again — one-off can accumulate)
const v1Response = await autumnV1.attach({
customer_id: customerId,
product_id: oneOff.id,
});
expect(v1Response).toMatchObject({
success: true,
customer_id: customerId,
product_ids: [oneOff.id],
code: expect.any(String),
message: expect.any(String),
});
});

Some files were not shown because too many files have changed in this diff Show More