420 lines
14 KiB
TypeScript
420 lines
14 KiB
TypeScript
/**
|
|
* Cancel Error Tests
|
|
*
|
|
* Tests for error cases when using cancel functionality in update subscription.
|
|
*/
|
|
|
|
import { expect, test } from "bun:test";
|
|
import { type ApiCustomerV3, ErrCode, FreeTrialDuration } from "@autumn/shared";
|
|
import {
|
|
expectProductCanceling,
|
|
expectProductNotPresent,
|
|
expectProductScheduled,
|
|
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
|
|
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils";
|
|
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: Cannot cancel a scheduled product
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Premium ($50/mo)
|
|
* - User downgrades to Pro ($20/mo) → Premium is canceling, Pro is scheduled
|
|
* - User tries to cancel Pro (the scheduled product)
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - cannot cancel a scheduled product
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cannot cancel scheduled product")}`, async () => {
|
|
const customerId = "err-cancel-scheduled";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const premiumPriceItem = items.monthlyPrice({ price: 50 });
|
|
const premium = products.base({
|
|
id: "premium",
|
|
items: [messagesItem, premiumPriceItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro, premium] }),
|
|
],
|
|
actions: [
|
|
s.attach({ productId: premium.id }),
|
|
s.attach({ productId: pro.id }), // Downgrade: premium canceling, pro scheduled
|
|
],
|
|
});
|
|
|
|
// Verify pro is scheduled
|
|
const customerAfterDowngrade =
|
|
await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
|
await expectProductCanceling({
|
|
customer: customerAfterDowngrade,
|
|
productId: premium.id,
|
|
});
|
|
await expectProductScheduled({
|
|
customer: customerAfterDowngrade,
|
|
productId: pro.id,
|
|
});
|
|
|
|
// Try to cancel the scheduled product - should fail
|
|
await expectAutumnError({
|
|
errCode: ErrCode.InvalidRequest,
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 2: Cancel immediately twice - second cancel is a no-op
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Pro ($20/mo)
|
|
* - User cancels Pro immediately
|
|
* - User tries to cancel Pro again
|
|
*
|
|
* Expected Result:
|
|
* - First cancel removes the product
|
|
* - Second cancel should fail because product doesn't exist
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cancel non-existent product")}`, async () => {
|
|
const customerId = "err-cancel-twice";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [s.attach({ productId: pro.id })],
|
|
});
|
|
|
|
// Cancel pro immediately
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
});
|
|
|
|
// Verify pro is gone
|
|
const customerAfterCancel =
|
|
await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
|
await expectProductNotPresent({
|
|
customer: customerAfterCancel,
|
|
productId: pro.id,
|
|
});
|
|
expect(customerAfterCancel.products.length).toBe(0);
|
|
|
|
// Try to cancel again - should fail because product doesn't exist
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 3: Cannot combine cancel immediately with options
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Pro ($20/mo) with prepaid messages
|
|
* - User tries to cancel immediately while also updating options (quantity)
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - cannot combine cancel with options
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cancel immediately with options")}`, async () => {
|
|
const customerId = "err-cancel-with-options";
|
|
|
|
const messagesItem = items.prepaidMessages({ includedUsage: 0 });
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [
|
|
s.attach({
|
|
productId: pro.id,
|
|
options: [{ feature_id: "messages", quantity: 100 }],
|
|
}),
|
|
],
|
|
});
|
|
|
|
// Try to cancel immediately while also updating options - should fail
|
|
await expectAutumnError({
|
|
errCode: ErrCode.InvalidRequest,
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
options: [{ feature_id: "messages", quantity: 200 }],
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 4: Cannot combine cancel immediately with version
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Pro ($20/mo)
|
|
* - User tries to cancel immediately while also specifying a version
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - cannot combine cancel with version
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cancel immediately with version")}`, async () => {
|
|
const customerId = "err-cancel-with-version";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [s.attach({ productId: pro.id })],
|
|
});
|
|
|
|
// Try to cancel immediately while also specifying version - should fail
|
|
await expectAutumnError({
|
|
errCode: ErrCode.InvalidRequest,
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
version: 2,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 5: Cannot combine cancel immediately with items
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Pro ($20/mo)
|
|
* - User tries to cancel immediately while also specifying custom items
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - cannot combine cancel with items
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cancel immediately with items")}`, async () => {
|
|
const customerId = "err-cancel-with-items";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const pro = products.pro({
|
|
id: "pro",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [pro] }),
|
|
],
|
|
actions: [s.attach({ productId: pro.id })],
|
|
});
|
|
|
|
// Try to cancel immediately while also specifying custom items - should fail
|
|
await expectAutumnError({
|
|
errCode: ErrCode.InvalidRequest,
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: pro.id,
|
|
cancel_action: "cancel_immediately",
|
|
items: [messagesItem],
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 6: Cannot cancel free product with end_of_cycle
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User has a free product (no subscription)
|
|
* - User tries to cancel with end_of_cycle
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - free products can only be canceled immediately
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cannot cancel free product with end_of_cycle")}`, async () => {
|
|
const customerId = "err-cancel-free-eoc";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const free = products.base({
|
|
id: "free",
|
|
items: [messagesItem],
|
|
isDefault: true,
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [s.customer({}), s.products({ list: [free] })],
|
|
actions: [s.attach({ productId: free.id })],
|
|
});
|
|
|
|
// Try to cancel free product with end_of_cycle - should fail
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: free.id,
|
|
cancel_action: "cancel_end_of_cycle",
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 7: Cannot cancel one-time product with end_of_cycle
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User has a one-time product (no recurring subscription)
|
|
* - User tries to cancel with end_of_cycle
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - one-time products can only be canceled immediately
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cannot cancel one-time product with end_of_cycle")}`, async () => {
|
|
const customerId = "err-cancel-onetime-eoc";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const oneTime = products.oneOff({
|
|
id: "onetime",
|
|
items: [messagesItem],
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [oneTime] }),
|
|
],
|
|
actions: [s.attach({ productId: oneTime.id })],
|
|
});
|
|
|
|
// Try to cancel one-time product with end_of_cycle - should fail
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: oneTime.id,
|
|
cancel_action: "cancel_end_of_cycle",
|
|
});
|
|
},
|
|
});
|
|
});
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
// TEST 8: Cannot pass free_trial when cancel is end_of_cycle
|
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* Scenario:
|
|
* - User is on Pro with trial
|
|
* - User tries to cancel at end of cycle while also passing free_trial
|
|
*
|
|
* Expected Result:
|
|
* - Should return an error - cannot combine cancel: 'end_of_cycle' with free_trial
|
|
*/
|
|
test.concurrent(`${chalk.yellowBright("error: cannot pass free_trial when cancel is end_of_cycle")}`, async () => {
|
|
const customerId = "err-cancel-eoc-with-trial";
|
|
|
|
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
|
|
|
const proTrial = products.proWithTrial({
|
|
id: "pro-trial",
|
|
items: [messagesItem],
|
|
trialDays: 7,
|
|
});
|
|
|
|
const { autumnV1 } = await initScenario({
|
|
customerId,
|
|
setup: [
|
|
s.customer({ paymentMethod: "success" }),
|
|
s.products({ list: [proTrial] }),
|
|
],
|
|
actions: [s.attach({ productId: proTrial.id })],
|
|
});
|
|
|
|
// Try to cancel end_of_cycle while also passing free_trial - should fail
|
|
await expectAutumnError({
|
|
func: async () => {
|
|
await autumnV1.subscriptions.update({
|
|
customer_id: customerId,
|
|
product_id: proTrial.id,
|
|
cancel_action: "cancel_end_of_cycle",
|
|
free_trial: {
|
|
length: 14,
|
|
duration: FreeTrialDuration.Day,
|
|
card_required: true,
|
|
unique_fingerprint: false,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|