Files
cfw-autumn/server/tests/integration/billing/attach/params/transition-configs/reset-after-trial-end/reset-after-trial-end-basic.test.ts
2026-02-17 21:22:31 +00:00

489 lines
17 KiB
TypeScript

/**
* reset_after_trial_end Basic Tests (Attach V2)
*
* Tests for the reset_after_trial_end transition config parameter.
*
* Key behaviors:
* - reset_after_trial_end: true → billing cycle starts AFTER trial ends (next_reset_at = trial_end + 30 days)
* - reset_after_trial_end: false (default) → reset happens at trial end (next_reset_at = trial_end)
* - Config is per-feature, applied at attach time only
* - If no trial, the config has no effect (billing cycle starts from now)
*/
import { test } from "bun:test";
import { type ApiCustomerV3, ms } from "@autumn/shared";
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect";
import { expectProductTrialing } from "@tests/integration/billing/utils/expectCustomerProductTrialing";
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";
import { addMonths } from "date-fns";
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 1: reset_after_trial_end: true - billing cycle starts after trial
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has 7-day trial + monthly messages
* - Attach with reset_after_trial_end: true for messages
*
* Expected Result:
* - next_reset_at = advancedTo + 7 days + 30 days (billing cycle starts AFTER trial)
* - NOT advancedTo + 7 days (trial end)
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 1: true - billing cycle starts after trial")}`, async () => {
const customerId = "reset-trial-end-true";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const proTrial = products.proWithTrial({
id: "pro-trial-reset",
items: [messagesItem],
trialDays: 7,
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proTrial] }),
],
actions: [],
});
// Attach with reset_after_trial_end: true
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proTrial.id,
redirect_mode: "if_required",
transition_rules: {
reset_after_trial_end: [TestFeature.Messages],
},
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active and trialing
await expectProductActive({ customer, productId: proTrial.id });
await expectProductTrialing({
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(7),
});
// Verify next_reset_at is trial end + 1 month (billing cycle starts AFTER trial)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: addMonths(advancedTo + ms.days(7), 1).getTime(),
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 2: No transition_rules - reset at trial end (default behavior)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has 7-day trial + monthly messages
* - Attach WITHOUT transition_rules (default behavior)
*
* Expected Result:
* - next_reset_at = advancedTo + 7 days (trial end)
* - Default: feature NOT in reset_after_trial_end array
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 2: no transition_rules - reset at trial end")}`, async () => {
const customerId = "reset-trial-end-false";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const proTrial = products.proWithTrial({
id: "pro-trial-no-reset",
items: [messagesItem],
trialDays: 7,
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proTrial] }),
],
actions: [],
});
// Attach with reset_after_trial_end: false (omit from transition_rules)
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proTrial.id,
redirect_mode: "if_required",
// No transition_rules = default behavior (reset at trial end)
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active and trialing
await expectProductActive({ customer, productId: proTrial.id });
await expectProductTrialing({
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(7),
});
// Verify next_reset_at at trial end (default behavior)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: advancedTo + ms.days(7), // Trial end
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 3: Empty options array - reset at trial end (default behavior)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has 7-day trial + monthly messages
* - Attach WITHOUT any options array
*
* Expected Result:
* - Default behavior (reset at trial end)
* - next_reset_at = advancedTo + 7 days (trial end)
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 3: no options - reset at trial end")}`, async () => {
const customerId = "reset-trial-end-default";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const proTrial = products.proWithTrial({
id: "pro-trial-default",
items: [messagesItem],
trialDays: 7,
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proTrial] }),
],
actions: [],
});
// Attach WITHOUT reset_after_trial_end config
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proTrial.id,
redirect_mode: "if_required",
// No options array - default behavior
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active and trialing
await expectProductActive({ customer, productId: proTrial.id });
await expectProductTrialing({
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(7),
});
// Verify next_reset_at at trial end (default = false)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: advancedTo + ms.days(7), // Default: trial end
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 4: reset_after_trial_end: true but no trial - no effect
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has NO trial + monthly messages
* - Attach with reset_after_trial_end: true for messages
*
* Expected Result:
* - Since there's no trial, the config has no effect
* - next_reset_at = advancedTo + 30 days (normal billing cycle)
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 4: true but no trial - no effect")}`, async () => {
const customerId = "reset-trial-end-no-trial";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const pro = products.pro({
id: "pro-no-trial",
items: [messagesItem],
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [pro] }),
],
actions: [],
});
// Attach with reset_after_trial_end: true (but product has no trial)
await autumnV1.billing.attach({
customer_id: customerId,
product_id: pro.id,
redirect_mode: "if_required",
transition_rules: {
reset_after_trial_end: [TestFeature.Messages],
},
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active (no trial)
await expectProductActive({ customer, productId: pro.id });
// Verify next_reset_at at billing cycle (no trial, config has no effect)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: addMonths(advancedTo, 1).getTime(), // Billing cycle (no trial)
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 5: reset_after_trial_end: per-feature config
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has 7-day trial + monthly messages + monthly words
* - Attach with reset_after_trial_end: [messages] (only messages, not words)
*
* Expected Result:
* - messages next_reset_at = advancedTo + 7 days + 30 days (trial end + cycle)
* - words next_reset_at = advancedTo + 7 days (trial end) - not in array, uses default
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 5: per-feature config")}`, async () => {
const customerId = "reset-trial-end-per-feature";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const wordsItem = items.monthlyWords({ includedUsage: 1000 });
const proTrial = products.proWithTrial({
id: "pro-trial-multi",
items: [messagesItem, wordsItem],
trialDays: 7,
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proTrial] }),
],
actions: [],
});
// Attach with reset_after_trial_end only for messages (not words)
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proTrial.id,
redirect_mode: "if_required",
transition_rules: {
reset_after_trial_end: [TestFeature.Messages], // Only messages, not words
},
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active and trialing
await expectProductActive({ customer, productId: proTrial.id });
await expectProductTrialing({
customer,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(7),
});
// Verify messages resets at trial end + 1 month (billing cycle after trial)
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: addMonths(advancedTo + ms.days(7), 1).getTime(),
});
// Verify words resets at trial end
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Words,
includedUsage: 1000,
balance: 1000,
usage: 0,
resetsAt: advancedTo + ms.days(7), // Trial end
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 6: reset_after_trial_end on upgrade with trial
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Customer has free product
* - Upgrade to proWithTrial (7-day trial)
* - Pass reset_after_trial_end: true
*
* Expected Result:
* - Config is applied at upgrade time
* - next_reset_at = advancedTo + 7 days + 30 days (trial end + billing cycle)
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 6: upgrade with trial + config")}`, async () => {
const customerId = "reset-trial-end-upgrade";
const freeMessagesItem = items.monthlyMessages({ includedUsage: 100 });
const free = products.base({
id: "free-upgrade-test",
items: [freeMessagesItem],
});
const proMessagesItem = items.monthlyMessages({ includedUsage: 500 });
const proTrial = products.proWithTrial({
id: "pro-trial-upgrade",
items: [proMessagesItem],
trialDays: 7,
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [free, proTrial] }),
],
actions: [s.billing.attach({ productId: free.id })],
});
// Verify initial state on free product
const customerBefore =
await autumnV1.customers.get<ApiCustomerV3>(customerId);
await expectProductActive({ customer: customerBefore, productId: free.id });
// Upgrade to pro with trial and reset_after_trial_end: true
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proTrial.id,
redirect_mode: "if_required",
transition_rules: {
reset_after_trial_end: [TestFeature.Messages],
},
});
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify upgraded to pro with trial
await expectProductActive({
customer: customerAfter,
productId: proTrial.id,
});
await expectProductTrialing({
customer: customerAfter,
productId: proTrial.id,
trialEndsAt: advancedTo + ms.days(7),
});
// Verify next_reset_at is trial end + 1 month (config applied at upgrade)
expectCustomerFeatureCorrect({
customer: customerAfter,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: addMonths(advancedTo + ms.days(7), 1).getTime(),
});
});
// ═══════════════════════════════════════════════════════════════════════════════
// TEST 7: reset_after_trial_end: true with long trial (60 days > 1 month cycle)
// ═══════════════════════════════════════════════════════════════════════════════
/**
* Scenario:
* - Product has 60-day trial (longer than monthly billing cycle)
* - Attach with reset_after_trial_end: true
*
* Expected Result:
* - next_reset_at = trial_end + 1 month (60 days + ~30 days)
* - The billing cycle starts AFTER the long trial ends
*/
test.concurrent(`${chalk.yellowBright("reset-after-trial-end 7: true with long trial (60 days)")}`, async () => {
const customerId = "reset-trial-end-long-trial";
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
const proLongTrial = products.proWithTrial({
id: "pro-long-trial",
items: [messagesItem],
trialDays: 60, // 60-day trial (longer than 1 month cycle)
cardRequired: true,
});
const { autumnV1, advancedTo } = await initScenario({
customerId,
setup: [
s.customer({ paymentMethod: "success" }),
s.products({ list: [proLongTrial] }),
],
actions: [],
});
// Attach with reset_after_trial_end: true
await autumnV1.billing.attach({
customer_id: customerId,
product_id: proLongTrial.id,
redirect_mode: "if_required",
transition_rules: {
reset_after_trial_end: [TestFeature.Messages],
},
});
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
// Verify product is active and trialing
await expectProductActive({ customer, productId: proLongTrial.id });
await expectProductTrialing({
customer,
productId: proLongTrial.id,
trialEndsAt: advancedTo + ms.days(60),
toleranceMs: ms.hours(3),
});
// Verify next_reset_at is trial end (60 days) + 1 month
expectCustomerFeatureCorrect({
customer,
featureId: TestFeature.Messages,
includedUsage: 500,
balance: 500,
usage: 0,
resetsAt: addMonths(advancedTo + ms.days(60), 1).getTime(),
toleranceMs: ms.hours(3),
});
});