fix: cancel while downgrading
This commit is contained in:
@@ -24,5 +24,6 @@ BUN_PARALLEL_COMPACT \
|
|||||||
'server/tests/attach/response' \
|
'server/tests/attach/response' \
|
||||||
'server/tests/interval/upgrade' \
|
'server/tests/interval/upgrade' \
|
||||||
'server/tests/interval/multiSub' \
|
'server/tests/interval/multiSub' \
|
||||||
|
'server/tests/billing/cancel' \
|
||||||
--max=6
|
--max=6
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
isFreeProduct,
|
isFreeProduct,
|
||||||
} from "@/internal/products/productUtils.js";
|
} from "@/internal/products/productUtils.js";
|
||||||
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
|
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
|
||||||
|
import { addSubIdToCache } from "../../../cusCache/subCacheUtils.js";
|
||||||
import {
|
import {
|
||||||
attachParamsToCurCusProduct,
|
attachParamsToCurCusProduct,
|
||||||
getCustomerSchedule,
|
getCustomerSchedule,
|
||||||
@@ -139,6 +140,13 @@ export const handleScheduleFunction2 = async ({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
|
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({
|
schedule = await subToNewSchedule({
|
||||||
ctx,
|
ctx,
|
||||||
sub: curSub,
|
sub: curSub,
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
AttachBranch,
|
AttachBranch,
|
||||||
CusProductStatus,
|
CusProductStatus,
|
||||||
cusProductToPrices,
|
|
||||||
cusProductToProduct,
|
cusProductToProduct,
|
||||||
type EntitlementWithFeature,
|
type EntitlementWithFeature,
|
||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
type Price,
|
type Price,
|
||||||
ProrationBehavior,
|
ProrationBehavior,
|
||||||
RecaseError,
|
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||||
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.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 { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js";
|
||||||
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
|
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
|
||||||
import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js";
|
import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js";
|
||||||
|
import { CusProductService } from "../cusProducts/CusProductService.js";
|
||||||
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||||
import {
|
import {
|
||||||
activateDefaultProduct,
|
activateDefaultProduct,
|
||||||
@@ -96,20 +95,39 @@ export const handleCancelProduct = async ({
|
|||||||
const isFree = isFreeProduct(product.prices || []);
|
const isFree = isFreeProduct(product.prices || []);
|
||||||
|
|
||||||
if (isMain) {
|
if (isMain) {
|
||||||
if (cusProduct.canceled && !expireImmediately) {
|
// Delete scheduled product
|
||||||
throw new RecaseError({
|
const { curScheduledProduct } = getExistingCusProducts({
|
||||||
message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
|
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 (
|
// if (cusProduct.canceled && !expireImmediately) {
|
||||||
curScheduledProduct &&
|
// throw new RecaseError({
|
||||||
!isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
|
// message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
|
||||||
) {
|
// });
|
||||||
throw new RecaseError({
|
// }
|
||||||
message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
|
|
||||||
});
|
// 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
|
// 2. If expire at cycle end, just cancel subscriptions
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export type InsertCusProductParams = {
|
|||||||
isCustom?: boolean;
|
isCustom?: boolean;
|
||||||
disableFreeTrial?: boolean;
|
disableFreeTrial?: boolean;
|
||||||
features: Feature[];
|
features: Feature[];
|
||||||
|
fromCancel?: boolean;
|
||||||
|
|
||||||
entityId?: string;
|
entityId?: string;
|
||||||
internalEntityId?: string;
|
internalEntityId?: string;
|
||||||
|
|||||||
@@ -5,34 +5,43 @@ import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
|||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
import {
|
import { constructFeatureItem } from "../../src/utils/scriptUtils/constructItem.js";
|
||||||
constructFeatureItem,
|
|
||||||
constructPrepaidItem,
|
|
||||||
} from "../../src/utils/scriptUtils/constructItem.js";
|
|
||||||
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
|
import { initCustomerV3 } from "../../src/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||||
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
|
import { initProductsV0 } from "../../src/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||||
|
|
||||||
// UNCOMMENT FROM HERE
|
// UNCOMMENT FROM HERE
|
||||||
|
const premium = constructProduct({
|
||||||
|
type: "premium",
|
||||||
|
isDefault: false,
|
||||||
|
|
||||||
|
items: [
|
||||||
|
constructFeatureItem({
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: 100,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const pro = constructProduct({
|
const pro = constructProduct({
|
||||||
type: "pro",
|
type: "pro",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
constructFeatureItem({
|
constructFeatureItem({
|
||||||
featureId: TestFeature.Credits,
|
featureId: TestFeature.Messages,
|
||||||
includedUsage: 250,
|
includedUsage: 50,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
const proEntity = constructProduct({
|
|
||||||
type: "pro",
|
const free = constructProduct({
|
||||||
id: "pro-entity",
|
type: "free",
|
||||||
isDefault: false,
|
isDefault: true,
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
constructPrepaidItem({
|
constructFeatureItem({
|
||||||
featureId: TestFeature.Credits,
|
featureId: TestFeature.Messages,
|
||||||
includedUsage: 0,
|
includedUsage: 10,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -52,7 +61,7 @@ describe(`${chalk.yellowBright("temp: Testing entity prorated")}`, () => {
|
|||||||
|
|
||||||
await initProductsV0({
|
await initProductsV0({
|
||||||
ctx,
|
ctx,
|
||||||
products: [pro, proEntity],
|
products: [premium, pro, free],
|
||||||
prefix: customerId,
|
prefix: customerId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,18 +77,11 @@ describe(`${chalk.yellowBright("temp: Testing entity prorated")}`, () => {
|
|||||||
test("should create a subscription with prepaid and prorated", async () => {
|
test("should create a subscription with prepaid and prorated", async () => {
|
||||||
await autumn.attach({
|
await autumn.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
product_id: pro.id,
|
product_id: premium.id,
|
||||||
});
|
});
|
||||||
await autumn.attach({
|
await autumn.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
product_id: proEntity.id,
|
product_id: pro.id,
|
||||||
entity_id: "1",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
feature_id: TestFeature.Credits,
|
|
||||||
quantity: 300,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 () => {
|
export const initBasicSharedProducts = async () => {
|
||||||
await createSharedProducts({
|
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";
|
} from "@/internal/products/prices/priceUtils.js";
|
||||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||||
import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js";
|
import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js";
|
||||||
|
import type { TestContext } from "../../utils/testInitUtils/createTestContext.js";
|
||||||
import { cusProductToSubIds } from "../mergeUtils.test.js";
|
import { cusProductToSubIds } from "../mergeUtils.test.js";
|
||||||
|
|
||||||
const compareActualItems = async ({
|
const compareActualItems = async ({
|
||||||
@@ -492,3 +493,31 @@ export const expectSubToBeCorrect = async ({
|
|||||||
// } else {
|
// } 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 ({
|
export const expectScheduledApiSub = async ({
|
||||||
customerId,
|
customerId,
|
||||||
entityId,
|
entityId,
|
||||||
|
|||||||
Reference in New Issue
Block a user