fix: cancel while downgrading
This commit is contained in:
@@ -24,5 +24,6 @@ BUN_PARALLEL_COMPACT \
|
||||
'server/tests/attach/response' \
|
||||
'server/tests/interval/upgrade' \
|
||||
'server/tests/interval/multiSub' \
|
||||
'server/tests/billing/cancel' \
|
||||
--max=6
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
isFreeProduct,
|
||||
} from "@/internal/products/productUtils.js";
|
||||
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
|
||||
import { addSubIdToCache } from "../../../cusCache/subCacheUtils.js";
|
||||
import {
|
||||
attachParamsToCurCusProduct,
|
||||
getCustomerSchedule,
|
||||
@@ -139,6 +140,13 @@ export const handleScheduleFunction2 = async ({
|
||||
}
|
||||
} else {
|
||||
logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
|
||||
|
||||
// Add sub ID to upstash so renew isn't being handled...
|
||||
await addSubIdToCache({
|
||||
subId: curSub.id,
|
||||
scenario: AttachScenario.Renew,
|
||||
});
|
||||
|
||||
schedule = await subToNewSchedule({
|
||||
ctx,
|
||||
sub: curSub,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import {
|
||||
AttachBranch,
|
||||
CusProductStatus,
|
||||
cusProductToPrices,
|
||||
cusProductToProduct,
|
||||
type EntitlementWithFeature,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type Price,
|
||||
ProrationBehavior,
|
||||
RecaseError,
|
||||
} from "@autumn/shared";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
|
||||
@@ -17,6 +15,7 @@ import { handleRenewProduct } from "../attach/attachFunctions/handleRenewProduct
|
||||
import { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js";
|
||||
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
|
||||
import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js";
|
||||
import { CusProductService } from "../cusProducts/CusProductService.js";
|
||||
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import {
|
||||
activateDefaultProduct,
|
||||
@@ -96,20 +95,39 @@ export const handleCancelProduct = async ({
|
||||
const isFree = isFreeProduct(product.prices || []);
|
||||
|
||||
if (isMain) {
|
||||
if (cusProduct.canceled && !expireImmediately) {
|
||||
throw new RecaseError({
|
||||
message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
|
||||
// Delete scheduled product
|
||||
const { curScheduledProduct } = getExistingCusProducts({
|
||||
product: product,
|
||||
cusProducts: fullCus.customer_products,
|
||||
internalEntityId: cusProduct.internal_entity_id,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Current scheduled product: ${curScheduledProduct?.product.name}`,
|
||||
);
|
||||
|
||||
// Delete scheduled product.
|
||||
if (curScheduledProduct) {
|
||||
await CusProductService.delete({
|
||||
db: ctx.db,
|
||||
cusProductId: curScheduledProduct?.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
curScheduledProduct &&
|
||||
!isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
|
||||
});
|
||||
}
|
||||
// if (cusProduct.canceled && !expireImmediately) {
|
||||
// throw new RecaseError({
|
||||
// message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (
|
||||
// curScheduledProduct &&
|
||||
// !isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
|
||||
// ) {
|
||||
// throw new RecaseError({
|
||||
// message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
// 2. If expire at cycle end, just cancel subscriptions
|
||||
|
||||
@@ -114,6 +114,7 @@ export type InsertCusProductParams = {
|
||||
isCustom?: boolean;
|
||||
disableFreeTrial?: boolean;
|
||||
features: Feature[];
|
||||
fromCancel?: boolean;
|
||||
|
||||
entityId?: string;
|
||||
internalEntityId?: string;
|
||||
|
||||
@@ -5,34 +5,43 @@ 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 {
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "../../src/utils/scriptUtils/constructItem.js";
|
||||
import { constructFeatureItem } from "../../src/utils/scriptUtils/constructItem.js";
|
||||
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Credits,
|
||||
includedUsage: 250,
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 50,
|
||||
}),
|
||||
],
|
||||
});
|
||||
const proEntity = constructProduct({
|
||||
type: "pro",
|
||||
id: "pro-entity",
|
||||
isDefault: false,
|
||||
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
|
||||
items: [
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Credits,
|
||||
includedUsage: 0,
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 10,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -52,7 +61,7 @@ describe(`${chalk.yellowBright("temp: Testing entity prorated")}`, () => {
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro, proEntity],
|
||||
products: [premium, pro, free],
|
||||
prefix: customerId,
|
||||
});
|
||||
|
||||
@@ -68,18 +77,11 @@ describe(`${chalk.yellowBright("temp: Testing entity prorated")}`, () => {
|
||||
test("should create a subscription with prepaid and prorated", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
product_id: premium.id,
|
||||
});
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: proEntity.id,
|
||||
entity_id: "1",
|
||||
options: [
|
||||
{
|
||||
feature_id: TestFeature.Credits,
|
||||
quantity: 300,
|
||||
},
|
||||
],
|
||||
product_id: pro.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,26 @@ export const sharedProProduct = constructProduct({
|
||||
}),
|
||||
],
|
||||
});
|
||||
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({
|
||||
|
||||
146
server/tests/attach/migrations/migration5.test.ts
Normal file
146
server/tests/attach/migrations/migration5.test.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { OnDecrease, OnIncrease } from "@autumn/shared";
|
||||
import { defaultApiVersion } from "@tests/constants.js";
|
||||
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 {
|
||||
constructArrearProratedItem,
|
||||
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";
|
||||
import { expectSubToBeCorrect } from "../../merged/mergeUtils/expectSubCorrect.js";
|
||||
import { timeout } from "../../utils/genUtils.js";
|
||||
import { replaceItems } from "../../utils/testProductUtils/testProductUtils.js";
|
||||
|
||||
const prepaidSeats = constructPrepaidItem({
|
||||
featureId: TestFeature.Users,
|
||||
billingUnits: 1,
|
||||
price: 40,
|
||||
includedUsage: 0,
|
||||
config: {
|
||||
on_increase: OnIncrease.ProrateNextCycle,
|
||||
on_decrease: OnDecrease.ProrateNextCycle,
|
||||
},
|
||||
});
|
||||
|
||||
const msgItem = constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
entityFeatureId: TestFeature.Users,
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [prepaidSeats, msgItem],
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
const testCase = "migrations5";
|
||||
const entities = [
|
||||
{
|
||||
id: "1",
|
||||
name: "Entity 1",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Entity 2",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "Entity 3",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
name: "Entity 4",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
];
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for prepaid seats -> pay_per_use seats`)}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro product", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
options: [
|
||||
{
|
||||
feature_id: TestFeature.Users,
|
||||
quantity: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await autumn.entities.create(customerId, entities);
|
||||
});
|
||||
|
||||
test("should update product to new version", async () => {
|
||||
await autumn.products.update(pro.id, {
|
||||
items: replaceItems({
|
||||
items: pro.items,
|
||||
featureId: TestFeature.Users,
|
||||
newItem: constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 40,
|
||||
includedUsage: 1,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
await autumn.migrate({
|
||||
from_product_id: pro.id,
|
||||
to_product_id: pro.id,
|
||||
from_version: 1,
|
||||
to_version: 2,
|
||||
});
|
||||
|
||||
await timeout(3000);
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: pro.id,
|
||||
// version: 2,
|
||||
// });
|
||||
|
||||
// 1. Should have 3 seats
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
const seats = customer.features[TestFeature.Users].balance;
|
||||
expect(seats).toBe(-3); // 3 in overage
|
||||
|
||||
const messages = customer.features[TestFeature.Messages].balance;
|
||||
expect(messages).toBe(400);
|
||||
|
||||
await expectSubToBeCorrect({
|
||||
db: ctx.db,
|
||||
customerId,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
});
|
||||
});
|
||||
});
|
||||
123
server/tests/billing/cancel/cancel6.test.ts
Normal file
123
server/tests/billing/cancel/cancel6.test.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, CusProductStatus } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import { ProductStatus } from "autumn-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";
|
||||
import { expectSubToBeCorrect } from "../../merged/mergeUtils/expectSubCorrect";
|
||||
import { expectProductAttached } from "../../utils/expectUtils/expectProductAttached";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 50,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("cancel6: Downgrade from premium to pro, then cancel premium end of cycle")}`, () => {
|
||||
const customerId = "cancel6";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [premium, pro],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach premium and pro, then cancel premium end of cycle", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.cancel({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
cancel_immediately: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product and subscriptions after cancellation", async () => {
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
const premiumProduct = customer.products.find((p) => p.id === premium.id);
|
||||
const proProduct = customer.products.find((p) => p.id === pro.id);
|
||||
|
||||
expect(premiumProduct).toBeDefined();
|
||||
expect(premiumProduct?.canceled_at).toBeDefined();
|
||||
expect(premiumProduct?.status).toBe(ProductStatus.Active);
|
||||
expect(proProduct).toBeUndefined();
|
||||
|
||||
await expectSubToBeCorrect({
|
||||
db: ctx.db,
|
||||
customerId,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
shouldBeCanceled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro again and have correct product and subscriptions", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: premium,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: pro,
|
||||
status: CusProductStatus.Scheduled,
|
||||
});
|
||||
|
||||
await expectSubToBeCorrect({
|
||||
db: ctx.db,
|
||||
customerId,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
shouldBeCanceled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
89
server/tests/billing/cancel/cancel7.test.ts
Normal file
89
server/tests/billing/cancel/cancel7.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
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 { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 50,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("cancel7: Downgrade from premium to pro, then cancel premium immediately")}`, () => {
|
||||
const customerId = "cancel7";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [premium, pro],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach premium and pro, then cancel premium immediately", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.cancel({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
cancel_immediately: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product and subscriptions after cancellation", async () => {
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
const premiumProduct = customer.products.find((p) => p.id === premium.id);
|
||||
const proProduct = customer.products.find((p) => p.id === pro.id);
|
||||
|
||||
expect(premiumProduct).toBeUndefined();
|
||||
expect(proProduct).toBeUndefined();
|
||||
|
||||
const stripeSubs = await ctx.stripeCli.subscriptions.list({
|
||||
customer: customer.stripe_id!,
|
||||
});
|
||||
|
||||
expect(stripeSubs.data).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
131
server/tests/billing/cancel/cancel8.test.ts
Normal file
131
server/tests/billing/cancel/cancel8.test.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, CusProductStatus } 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.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { expectSubToBeCorrect } from "../../merged/mergeUtils/expectSubCorrect";
|
||||
import { expectProductAttached } from "../../utils/expectUtils/expectProductAttached";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 50,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
|
||||
items: [],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("cancel8: Downgrade from premium to pro, then cancel premium immediately (with default product)")}`, () => {
|
||||
const customerId = "cancel8";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [premium, pro, free],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach premium and pro, then cancel premium immediately", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.cancel({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
cancel_immediately: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product and subscriptions after cancellation", async () => {
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
const proProduct = customer.products.find((p) => p.id === pro.id);
|
||||
|
||||
expect(proProduct).toBeUndefined();
|
||||
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: premium,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: free,
|
||||
status: CusProductStatus.Scheduled,
|
||||
});
|
||||
|
||||
await expectSubToBeCorrect({
|
||||
db: ctx.db,
|
||||
customerId,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
shouldBeCanceled: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach pro again and have correct product and subscriptions", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: pro,
|
||||
status: CusProductStatus.Scheduled,
|
||||
});
|
||||
|
||||
await expectSubToBeCorrect({
|
||||
db: ctx.db,
|
||||
customerId,
|
||||
org: ctx.org,
|
||||
env: ctx.env,
|
||||
shouldBeCanceled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
107
server/tests/billing/cancel/cancel9.test.ts
Normal file
107
server/tests/billing/cancel/cancel9.test.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { ApiVersion, CusProductStatus } 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.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { expectSubCount } from "../../merged/mergeUtils/expectSubCorrect";
|
||||
import {
|
||||
expectProductAttached,
|
||||
expectProductGroupCount,
|
||||
} from "../../utils/expectUtils/expectProductAttached";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
const premium = constructProduct({
|
||||
type: "premium",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 100,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 50,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
isDefault: true,
|
||||
|
||||
items: [],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("cancel9: Downgrade from premium to pro, then cancel premium immediately (with default product)")}`, () => {
|
||||
const customerId = "cancel9";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [premium, pro, free],
|
||||
prefix: customerId,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach premium and pro, then cancel premium immediately (with default product)", async () => {
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
});
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.cancel({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
cancel_immediately: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should have correct product and subscriptions after cancellation", async () => {
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
|
||||
expectProductAttached({
|
||||
customer: customer,
|
||||
product: free,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
|
||||
expectProductGroupCount({
|
||||
customer: customer,
|
||||
group: premium.group!,
|
||||
count: 1,
|
||||
});
|
||||
|
||||
await expectSubCount({
|
||||
ctx,
|
||||
customerId,
|
||||
count: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js";
|
||||
import type { TestContext } from "../../utils/testInitUtils/createTestContext.js";
|
||||
import { cusProductToSubIds } from "../mergeUtils.test.js";
|
||||
|
||||
const compareActualItems = async ({
|
||||
@@ -492,3 +493,31 @@ export const expectSubToBeCorrect = async ({
|
||||
// } else {
|
||||
// }
|
||||
};
|
||||
|
||||
export const expectSubCount = async ({
|
||||
ctx,
|
||||
customerId,
|
||||
count,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
count: number;
|
||||
}) => {
|
||||
const stripeCli = ctx.stripeCli;
|
||||
const customer = await CusService.get({
|
||||
db: ctx.db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
|
||||
if (!customer?.processor?.id) {
|
||||
throw new Error(`Customer ${customerId} has no processor`);
|
||||
}
|
||||
|
||||
const subs = await stripeCli.subscriptions.list({
|
||||
customer: customer?.processor?.id,
|
||||
});
|
||||
|
||||
expect(subs.data.length).toBe(count);
|
||||
};
|
||||
|
||||
@@ -67,6 +67,27 @@ export const expectProductAttached = ({
|
||||
}
|
||||
};
|
||||
|
||||
export const expectProductGroupCount = ({
|
||||
customer,
|
||||
group,
|
||||
count,
|
||||
}: {
|
||||
customer: Customer;
|
||||
group: string;
|
||||
count: number;
|
||||
}) => {
|
||||
const productCount = customer.products.reduce((acc: number, product: any) => {
|
||||
if (product.group === group) {
|
||||
return acc + 1;
|
||||
} else return acc;
|
||||
}, 0);
|
||||
|
||||
expect(
|
||||
productCount,
|
||||
`customer should have ${count} products in group ${group}`,
|
||||
).to.equal(count);
|
||||
};
|
||||
|
||||
export const expectScheduledApiSub = async ({
|
||||
customerId,
|
||||
entityId,
|
||||
|
||||
Reference in New Issue
Block a user