diff --git a/server/tests/_guides/general-test-guide.md b/server/tests/_guides/general-test-guide.md index ec7bccdde..ee02ad4ce 100644 --- a/server/tests/_guides/general-test-guide.md +++ b/server/tests/_guides/general-test-guide.md @@ -37,6 +37,44 @@ const autumn = new AutumnInt({ ## Common Test Patterns +### Product IDs - Use Variable References, Not Hardcoded Strings + +When using `initScenario`, products are automatically prefixed with the `customerId`. When referencing product IDs in API calls or assertions, **always use the product variable's `.id` property** instead of hardcoding the prefixed string: + +```typescript +const pro = products.pro({ id: "pro", items: [messagesItem] }); +const premium = constructProduct({ id: "premium", items: [...], type: "premium" }); + +const { autumnV1, ctx, entities } = await initScenario({ + customerId, + options: [ + s.products({ list: [pro, premium] }), + s.attach({ productId: "pro", entityIndex: 0 }), // s.attach uses unprefixed ID + ], +}); + +// ✅ GOOD - Use product variable's .id (includes prefix automatically) +await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, // Returns prefixed ID like "my-test_pro" + entity_id: entities[0].id, +}); + +await expectProductActive({ + customer: customerData, + productId: premium.id, // Use variable reference +}); + +// ❌ BAD - Don't hardcode prefixed product IDs +await autumnV1.attach({ + customer_id: customerId, + product_id: `${customerId}_pro`, // Avoid hardcoding + entity_id: entities[0].id, +}); +``` + +**Note:** The `s.attach()` and `s.cancel()` helpers in `initScenario` take the **unprefixed** product ID (e.g., `"pro"`), but direct API calls require the **full prefixed ID** which you get from `pro.id`. + ### Wait for Async Processing ```typescript await new Promise((resolve) => setTimeout(resolve, 2000)); diff --git a/server/tests/billing/update-subscription/custom-plan/free-to-paid.test.ts b/server/tests/billing/update-subscription/custom-plan/free-to-paid.test.ts deleted file mode 100644 index 1910442ce..000000000 --- a/server/tests/billing/update-subscription/custom-plan/free-to-paid.test.ts +++ /dev/null @@ -1,418 +0,0 @@ -import { expect, test } from "bun:test"; -import { expectCustomerFeatureCorrect } from "@tests/billing/utils/expectCustomerFeatureCorrect"; -import { expectCustomerInvoiceCorrect } from "@tests/billing/utils/expectCustomerInvoiceCorrect"; -import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; -import { TestFeature } from "@tests/setup/v2Features.js"; -import { items } from "@tests/utils/fixtures/items.js"; -import { products } from "@tests/utils/fixtures/products.js"; -import { initTestScenario } from "@tests/utils/testInitUtils/initTestScenario.js"; -import chalk from "chalk"; - -// 1. Adding a monthly base price to free product -test.concurrent(`${chalk.yellowBright("free-to-paid: add monthly base price")}`, async () => { - const messagesItem = items.monthlyMessages({ includedUsage: 300 }); - const free = products.base({ items: [messagesItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-add-base", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - await autumnV1.track( - { - customer_id: customerId, - feature_id: TestFeature.Messages, - value: 100, - }, - { timeout: 2000 }, - ); - - const priceItem = items.monthlyPrice(); - - // Preview should show $20 charge - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [messagesItem, priceItem], - }); - - expect(preview.total).toEqual(20); - - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: free.id, - items: [messagesItem, priceItem], - }); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Messages, - includedUsage: messagesItem.included_usage, - balance: messagesItem.included_usage - 100, - usage: 100, - }); - - expectCustomerInvoiceCorrect({ - customer, - count: 1, - latestTotal: 20, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); - -// 2. Adding monthly base price + consumable to free product -test.concurrent(`${chalk.yellowBright("free-to-paid: add monthly base + consumable")}`, async () => { - const messagesItem = items.monthlyMessages({ includedUsage: 100 }); - const free = products.base({ items: [messagesItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-add-base-cons", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - // Track some usage before update - const messagesUsage = 30; - await autumnV1.track( - { - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messagesUsage, - }, - { timeout: 2000 }, - ); - - const priceItem = items.monthlyPrice(); - const consumableItem = items.consumableMessages({ includedUsage: 50 }); - - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [consumableItem, priceItem], - }); - - expect(preview.total).toEqual(20); - - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: free.id, - items: [consumableItem, priceItem], - }); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Messages, - includedUsage: consumableItem.included_usage, - balance: consumableItem.included_usage - messagesUsage, - usage: messagesUsage, - }); - - expectCustomerInvoiceCorrect({ - customer, - count: 1, - latestTotal: 20, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); - -// 3. Adding annual base price + monthly consumable to free product -test.concurrent(`${chalk.yellowBright("free-to-paid: add annual base + monthly consumable")}`, async () => { - const messagesItem = items.monthlyMessages({ includedUsage: 100 }); - const free = products.base({ items: [messagesItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-add-annual-cons", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - const priceItem = items.annualPrice(); - const consumableItem = items.consumableMessages({ includedUsage: 50 }); - - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [consumableItem, priceItem], - }); - - expect(preview.total).toEqual(200); - - await autumnV1.subscriptions.update( - { - customer_id: customerId, - product_id: free.id, - items: [consumableItem, priceItem], - }, - { timeout: 2000 }, - ); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Messages, - includedUsage: consumableItem.included_usage, - balance: consumableItem.included_usage, - usage: 0, - }); - - expectCustomerInvoiceCorrect({ - customer, - count: 1, - latestTotal: 200, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); - -// 4. Updating free feature item to consumable -test.concurrent(`${chalk.yellowBright("free-to-paid: update free item to consumable")}`, async () => { - const messagesItem = items.monthlyMessages({ includedUsage: 100 }); - const free = products.base({ items: [messagesItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-update-to-cons", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - // Track some usage first - const messagesUsage = 50; - await autumnV1.track( - { - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messagesUsage, - }, - { timeout: 2000 }, - ); - - // Update to consumable (pay-per-use after included usage) - const consumableItem = items.consumableMessages({ includedUsage: 100 }); - - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [consumableItem], - }); - - // No immediate charge - consumable bills in arrears - expect(preview.total).toEqual(0); - - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: free.id, - items: [consumableItem], - }); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Messages, - includedUsage: consumableItem.included_usage, - balance: consumableItem.included_usage - messagesUsage, - usage: messagesUsage, - }); - - // No invoice - consumable bills in arrears, no immediate charge - expectCustomerInvoiceCorrect({ - customer, - count: 0, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); - -// 5. Updating free feature item to prepaid -test.concurrent(`${chalk.yellowBright("free-to-paid: update free item to prepaid")}`, async () => { - const messagesItem = items.monthlyMessages({ includedUsage: 100 }); - const free = products.base({ items: [messagesItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-update-to-prepaid", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - // Track some usage first - const messagesUsage = 30; - await autumnV1.track( - { - customer_id: customerId, - feature_id: TestFeature.Messages, - value: messagesUsage, - }, - { timeout: 2000 }, - ); - - // Update to prepaid (purchase units upfront) - const prepaidItem = items.prepaidMessages(); - - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [prepaidItem], - }); - - // No immediate charge for switching to prepaid model - expect(preview.total).toEqual(0); - - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: free.id, - items: [prepaidItem], - options: [ - { - feature_id: TestFeature.Messages, - quantity: 100, - }, - ], - }); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Messages, - includedUsage: 100, - balance: 100 - messagesUsage, - usage: messagesUsage, - }); - - // Prepaid charges upfront - $10 for 100 units - expectCustomerInvoiceCorrect({ - customer, - count: 1, - latestTotal: 10, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); - -// 6. Updating free users to allocated users -test.concurrent(`${chalk.yellowBright("free-to-paid: update free users to allocated")}`, async () => { - const usersItem = items.monthlyUsers({ includedUsage: 5 }); - const free = products.base({ items: [usersItem] }); - - const { customerId, autumnV1, ctx } = await initTestScenario({ - customerId: "f2p-free-to-allocated", - products: [free], - attachProducts: [free.id], - customerOptions: { - withTestClock: true, - attachPm: "success", - }, - }); - - // Use some users (continuous use feature) - const usersUsed = 3; - await autumnV1.track( - { - customer_id: customerId, - feature_id: TestFeature.Users, - value: usersUsed, - }, - { timeout: 2000 }, - ); - - // Verify initial state - const initialCustomer = await autumnV1.customers.get(customerId); - expect(initialCustomer.features[TestFeature.Users].balance).toEqual( - usersItem.included_usage - usersUsed, - ); - - // Update to allocated users ($10/seat prorated billing) - const allocatedUsersItem = items.allocatedUsers({ includedUsage: 2 }); - - const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: free.id, - items: [allocatedUsersItem], - }); - - // Should charge for (usersUsed - includedUsage) extra seats = (3 - 2) = 1 seat @ $10 - expect(preview.total).toEqual(10); - - await autumnV1.subscriptions.update({ - customer_id: customerId, - product_id: free.id, - items: [allocatedUsersItem], - }); - - const customer = await autumnV1.customers.get(customerId); - - expectCustomerFeatureCorrect({ - customer, - featureId: TestFeature.Users, - includedUsage: allocatedUsersItem.included_usage, - balance: allocatedUsersItem.included_usage - usersUsed, - usage: usersUsed, - }); - - // Invoice for the extra seat - expectCustomerInvoiceCorrect({ - customer, - count: 1, - latestTotal: 10, - }); - - await expectSubToBeCorrect({ - db: ctx.db, - customerId, - org: ctx.org, - env: ctx.env, - }); -}); diff --git a/server/tests/billing/update-subscription/custom-plan/update-from-free.test.ts b/server/tests/billing/update-subscription/custom-plan/update-from-free.test.ts new file mode 100644 index 000000000..d89face2b --- /dev/null +++ b/server/tests/billing/update-subscription/custom-plan/update-from-free.test.ts @@ -0,0 +1,1207 @@ +import { expect, test } from "bun:test"; +import { ProductItemInterval } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/billing/utils/expectCustomerInvoiceCorrect"; +import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect"; +import { TestFeature } from "@tests/setup/v2Features.js"; +import { items } from "@tests/utils/fixtures/items.js"; +import { products } from "@tests/utils/fixtures/products.js"; +import { advanceTestClock } from "@tests/utils/stripeUtils"; +import { initTestScenario } from "@tests/utils/testInitUtils/initTestScenario.js"; +import chalk from "chalk"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// FREE-TO-FREE TESTS +// ═══════════════════════════════════════════════════════════════════════════════ + +// 1. Adding a boolean feature to existing free product (usage should stay) +test.concurrent(`${chalk.yellowBright("free-to-free: add boolean feature")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-add-bool", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage before update + const messagesUsage = 40; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Add boolean dashboard feature + const dashboardItem = items.dashboard(); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, dashboardItem], + }); + + // No charge for free-to-free + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, dashboardItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Messages usage should stay the same + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: messagesItem.included_usage, + balance: messagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // Dashboard should be accessible (boolean feature) + expect(customer.features[TestFeature.Dashboard]).toBeDefined(); + + // No invoice for free-to-free + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 2. Adding unlimited feature to existing free product (usage should stay) +test.concurrent(`${chalk.yellowBright("free-to-free: add unlimited feature")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-add-unlimited", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage before update + const messagesUsage = 50; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Replace messages with unlimited + const unlimitedMessagesItem = items.unlimitedMessages(); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [unlimitedMessagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [unlimitedMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Messages should now be unlimited + expect(customer.features[TestFeature.Messages].unlimited).toBe(true); + + // No invoice for free-to-free + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 3. Adding additional included feature (usage should stay for existing) +test.concurrent(`${chalk.yellowBright("free-to-free: add included feature")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-add-included", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage before update + const messagesUsage = 35; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Add words feature + const wordsItem = items.monthlyWords({ includedUsage: 200 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, wordsItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, wordsItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Messages usage should stay the same + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: messagesItem.included_usage, + balance: messagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // Words should have full balance + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Words, + includedUsage: wordsItem.included_usage, + balance: wordsItem.included_usage, + usage: 0, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 4. Removing a feature (usage for remaining should stay) +test.concurrent(`${chalk.yellowBright("free-to-free: remove feature")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const wordsItem = items.monthlyWords({ includedUsage: 200 }); + const free = products.base({ items: [messagesItem, wordsItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-remove-feat", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track usage on both features + const messagesUsage = 25; + const wordsUsage = 75; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Words, + value: wordsUsage, + }, + { timeout: 2000 }, + ); + + // Remove words feature, keep only messages + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Messages usage should stay the same + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: messagesItem.included_usage, + balance: messagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // Words should no longer exist + expect(customer.features[TestFeature.Words]).toBeUndefined(); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 5. Update included usage on existing feature (increase) +test.concurrent(`${chalk.yellowBright("free-to-free: increase included usage")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-inc-included", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 60; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Increase included usage from 100 to 200 + const updatedMessagesItem = items.monthlyMessages({ includedUsage: 200 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [updatedMessagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [updatedMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Usage should stay, balance should increase + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: updatedMessagesItem.included_usage, + balance: updatedMessagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 6. Update included usage on existing feature (decrease) +test.concurrent(`${chalk.yellowBright("free-to-free: decrease included usage")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 200 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-dec-included", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 50; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Decrease included usage from 200 to 100 + const updatedMessagesItem = items.monthlyMessages({ includedUsage: 100 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [updatedMessagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [updatedMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Usage should stay, balance should decrease + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: updatedMessagesItem.included_usage, + balance: updatedMessagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 7. Update interval on existing feature (month -> week) +test.concurrent(`${chalk.yellowBright("free-to-free: change interval month to week")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-int-m2w", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 30; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Change interval from monthly to weekly + const weeklyMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Week, + }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [weeklyMessagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [weeklyMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Usage should stay + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100 - messagesUsage, + usage: messagesUsage, + }); + + // Verify interval changed + expect(customer.features[TestFeature.Messages].interval).toEqual("week"); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 8. Update interval on existing feature (month -> year) +test.concurrent(`${chalk.yellowBright("free-to-free: change interval month to year")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2f-int-m2y", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 45; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Change interval from monthly to yearly + const yearlyMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Year, + }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [yearlyMessagesItem], + }); + + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [yearlyMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Usage should stay + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100 - messagesUsage, + usage: messagesUsage, + }); + + // Verify interval changed + expect(customer.features[TestFeature.Messages].interval).toEqual("year"); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// FREE-TO-FREE: RESET CYCLE ANCHOR PRESERVATION TESTS +// ═══════════════════════════════════════════════════════════════════════════════ + +// 9. Reset cycle anchor stays same after advancing clock 5 days +test.concurrent(`${chalk.yellowBright("free-to-free: anchor stays same after 5 days")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx, testClockId } = await initTestScenario({ + customerId: "f2f-anchor-5d", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 20; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Get the original reset time + const customerBefore = await autumnV1.customers.get(customerId); + const originalResetAt = + customerBefore.features[TestFeature.Messages].next_reset_at; + expect(originalResetAt).toBeDefined(); + + // Advance test clock by 5 days + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfDays: 5, + }); + + // Update with slightly more included usage + const updatedMessagesItem = items.monthlyMessages({ includedUsage: 150 }); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [updatedMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Usage should stay the same + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: updatedMessagesItem.included_usage, + balance: updatedMessagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // Reset anchor should stay approximately the same (within 1 hour) + const newResetAt = customer.features[TestFeature.Messages].next_reset_at; + expect(newResetAt).toBeDefined(); + expect(Math.abs(newResetAt! - originalResetAt!)).toBeLessThanOrEqual(3600000); // 1 hour tolerance + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); + +// 10. Reset cycle anchor stays same after advancing clock 15 days (half month) +test.concurrent(`${chalk.yellowBright("free-to-free: anchor stays same after 15 days")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx, testClockId } = await initTestScenario({ + customerId: "f2f-anchor-15d", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 30; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Get the original reset time + const customerBefore = await autumnV1.customers.get(customerId); + const originalResetAt = + customerBefore.features[TestFeature.Messages].next_reset_at; + expect(originalResetAt).toBeDefined(); + + // Advance test clock by 15 days + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfDays: 15, + }); + + // Add a new feature while updating + const wordsItem = items.monthlyWords({ includedUsage: 200 }); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, wordsItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Messages usage should stay the same + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: messagesItem.included_usage, + balance: messagesItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // Reset anchor for messages should stay approximately the same + const newResetAt = customer.features[TestFeature.Messages].next_reset_at; + expect(newResetAt).toBeDefined(); + expect(Math.abs(newResetAt! - originalResetAt!)).toBeLessThanOrEqual(3600000); // 1 hour tolerance + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); + +// 11. Reset cycle anchor stays same after advancing clock 2 weeks (weekly feature) +test.concurrent(`${chalk.yellowBright("free-to-free: weekly anchor stays same after 2 weeks")}`, async () => { + const weeklyMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 50, + interval: ProductItemInterval.Week, + }); + const free = products.base({ items: [weeklyMessagesItem] }); + + const { customerId, autumnV1, ctx, testClockId } = await initTestScenario({ + customerId: "f2f-anchor-2w", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + }, + }); + + // Track some usage + const messagesUsage = 15; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Get the original reset time + const customerBefore = await autumnV1.customers.get(customerId); + const originalResetAt = + customerBefore.features[TestFeature.Messages].next_reset_at; + expect(originalResetAt).toBeDefined(); + + // Advance test clock by 2 weeks (note: this will trigger resets) + // We're testing that after update, the anchor day remains the same + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfWeeks: 2, + }); + + // Update with more included usage + const updatedWeeklyMessagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Week, + }); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [updatedWeeklyMessagesItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + // After 2 weeks, balance would have reset, but anchor day should be preserved + // The next_reset_at should be aligned to the same day of week as original + const newResetAt = customer.features[TestFeature.Messages].next_reset_at; + expect(newResetAt).toBeDefined(); + + // Calculate day of week from both timestamps (should be same day) + const originalDay = new Date(originalResetAt!).getDay(); + const newDay = new Date(newResetAt!).getDay(); + expect(newDay).toEqual(originalDay); + + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// FREE-TO-PAID TESTS +// ═══════════════════════════════════════════════════════════════════════════════ + +// 1. Adding a monthly base price to free product +test.concurrent(`${chalk.yellowBright("free-to-paid: add monthly base price")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 300 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-add-base", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 100, + }, + { timeout: 2000 }, + ); + + const priceItem = items.monthlyPrice(); + + // Preview should show $20 charge + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, priceItem], + }); + + expect(preview.total).toEqual(20); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [messagesItem, priceItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: messagesItem.included_usage, + balance: messagesItem.included_usage - 100, + usage: 100, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 2. Adding monthly base price + consumable to free product +test.concurrent(`${chalk.yellowBright("free-to-paid: add monthly base + consumable")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-add-base-cons", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + // Track some usage before update + const messagesUsage = 30; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + const priceItem = items.monthlyPrice(); + const consumableItem = items.consumableMessages({ includedUsage: 50 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [consumableItem, priceItem], + }); + + expect(preview.total).toEqual(20); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [consumableItem, priceItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: consumableItem.included_usage, + balance: consumableItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 3. Adding annual base price + monthly consumable to free product +test.concurrent(`${chalk.yellowBright("free-to-paid: add annual base + monthly consumable")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-add-annual-cons", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + const priceItem = items.annualPrice(); + const consumableItem = items.consumableMessages({ includedUsage: 50 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [consumableItem, priceItem], + }); + + expect(preview.total).toEqual(200); + + await autumnV1.subscriptions.update( + { + customer_id: customerId, + product_id: free.id, + items: [consumableItem, priceItem], + }, + { timeout: 2000 }, + ); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: consumableItem.included_usage, + balance: consumableItem.included_usage, + usage: 0, + }); + + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 200, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 4. Updating free feature item to consumable +test.concurrent(`${chalk.yellowBright("free-to-paid: update free item to consumable")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-update-to-cons", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + // Track some usage first + const messagesUsage = 50; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Update to consumable (pay-per-use after included usage) + const consumableItem = items.consumableMessages({ includedUsage: 100 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [consumableItem], + }); + + // No immediate charge - consumable bills in arrears + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [consumableItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: consumableItem.included_usage, + balance: consumableItem.included_usage - messagesUsage, + usage: messagesUsage, + }); + + // No invoice - consumable bills in arrears, no immediate charge + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 5. Updating free feature item to prepaid +test.concurrent(`${chalk.yellowBright("free-to-paid: update free item to prepaid")}`, async () => { + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ items: [messagesItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-update-to-prepaid", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + // Track some usage first + const messagesUsage = 30; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messagesUsage, + }, + { timeout: 2000 }, + ); + + // Update to prepaid (purchase units upfront) + const prepaidItem = items.prepaidMessages(); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [prepaidItem], + }); + + // No immediate charge for switching to prepaid model + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [prepaidItem], + options: [ + { + feature_id: TestFeature.Messages, + quantity: 100, + }, + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100 - messagesUsage, + usage: messagesUsage, + }); + + // Prepaid charges upfront - $10 for 100 units + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 10, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); + +// 6. Updating free users to allocated users +test.concurrent(`${chalk.yellowBright("free-to-paid: update free users to allocated")}`, async () => { + const usersItem = items.monthlyUsers({ includedUsage: 5 }); + const free = products.base({ items: [usersItem] }); + + const { customerId, autumnV1, ctx } = await initTestScenario({ + customerId: "f2p-free-to-allocated", + products: [free], + attachProducts: [free.id], + customerOptions: { + withTestClock: true, + attachPm: "success", + }, + }); + + // Use some users (continuous use feature) + const usersUsed = 3; + await autumnV1.track( + { + customer_id: customerId, + feature_id: TestFeature.Users, + value: usersUsed, + }, + { timeout: 2000 }, + ); + + // Verify initial state + const initialCustomer = await autumnV1.customers.get(customerId); + expect(initialCustomer.features[TestFeature.Users].balance).toEqual( + usersItem.included_usage - usersUsed, + ); + + // Update to allocated users ($10/seat prorated billing) + const allocatedUsersItem = items.allocatedUsers({ includedUsage: 2 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + product_id: free.id, + items: [allocatedUsersItem], + }); + + // Should charge for (usersUsed - includedUsage) extra seats = (3 - 2) = 1 seat @ $10 + expect(preview.total).toEqual(10); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: free.id, + items: [allocatedUsersItem], + }); + + const customer = await autumnV1.customers.get(customerId); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Users, + includedUsage: allocatedUsersItem.included_usage, + balance: allocatedUsersItem.included_usage - usersUsed, + usage: usersUsed, + }); + + // Invoice for the extra seat + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 10, + }); + + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + }); +}); diff --git a/server/tests/billing/update-subscription/multi-product/multi-entity-free-to-paid.test.ts b/server/tests/billing/update-subscription/multi-product/multi-entity-from-free.test.ts similarity index 80% rename from server/tests/billing/update-subscription/multi-product/multi-entity-free-to-paid.test.ts rename to server/tests/billing/update-subscription/multi-product/multi-entity-from-free.test.ts index 7fddef2b4..df2289a2a 100644 --- a/server/tests/billing/update-subscription/multi-product/multi-entity-free-to-paid.test.ts +++ b/server/tests/billing/update-subscription/multi-product/multi-entity-from-free.test.ts @@ -9,7 +9,95 @@ import { products } from "@tests/utils/fixtures/products.js"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; import chalk from "chalk"; -// 1. Entity 1 has paid sub, entity 2 upgrades free to paid +// 1. Entity 1 pro, Entity 2 free - Entity 2 updates free items (stays free) +test.concurrent(`${chalk.yellowBright("multi-entity-from-free: update free items")}`, async () => { + const customerId = "multi-ent-update-free"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + + const pro = products.pro({ + id: "pro", + items: [messagesItem], + }); + + const free = products.base({ + id: "free", + items: [messagesItem], + }); + + const { autumnV1, ctx, entities } = await initScenario({ + customerId, + options: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, free] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + s.attach({ productId: "pro", entityIndex: 0 }), + s.attach({ productId: "free", entityIndex: 1 }), + ], + }); + + // Verify entity 2 starts with 100 included usage + const entity2Before = await autumnV1.entities.get(customerId, entities[1].id); + await expectProductActive({ customer: entity2Before, productId: free.id }); + expectCustomerFeatureCorrect({ + customer: entity2Before, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Entity 2 updates free product to have more included usage (still free) + const updatedMessagesItem = items.monthlyMessages({ includedUsage: 200 }); + + const preview = await autumnV1.subscriptions.previewUpdate({ + customer_id: customerId, + entity_id: entities[1].id, + product_id: free.id, + items: [updatedMessagesItem], + }); + + // Should be $0 since it's still a free product + expect(preview.total).toEqual(0); + + await autumnV1.subscriptions.update({ + customer_id: customerId, + entity_id: entities[1].id, + product_id: free.id, + items: [updatedMessagesItem], + }); + + // Verify entity 2 has updated included usage + const entity2After = await autumnV1.entities.get(customerId, entities[1].id); + await expectProductActive({ customer: entity2After, productId: free.id }); + expectCustomerFeatureCorrect({ + customer: entity2After, + featureId: TestFeature.Messages, + includedUsage: 200, + balance: 200, + usage: 0, + }); + + // Verify only 1 invoice (from entity1's pro attachment) + const customer = await autumnV1.customers.get(customerId); + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, // Only entity 1's pro + }); + + // Should still have 1 subscription (entity 1's pro) + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + entityId: entities[0].id, + subCount: 1, + }); +}); + +// 2. Entity 1 has paid sub, entity 2 upgrades free to paid test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: entity 2 upgrades free to paid")}`, async () => { const customerId = "multi-ent-free-to-paid"; @@ -87,7 +175,7 @@ test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: entity 2 upgra }); }); -// 2. Free to paid with base price + consumable + prepaid +// 3. Free to paid with base price + consumable + prepaid test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: base + consumable + prepaid")}`, async () => { const customerId = "multi-ent-f2p-combo"; @@ -182,7 +270,7 @@ test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: base + consuma }); }); -// 3. Free to paid with annual price +// 4. Free to paid with annual price test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: annual price")}`, async () => { const customerId = "multi-ent-f2p-annual"; @@ -260,7 +348,7 @@ test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: annual price") }); }); -// 4. Free to paid with annual price MID-CYCLE +// 5. Free to paid with annual price MID-CYCLE test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: annual mid-cycle")}`, async () => { const customerId = "multi-ent-f2p-annual-mid"; @@ -340,7 +428,7 @@ test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: annual mid-cyc }); }); -// 5. Monthly price mid-cycle (existing test) +// 6. Monthly price mid-cycle test.concurrent(`${chalk.yellowBright("multi-entity-free-to-paid: monthly mid-cycle")}`, async () => { const customerId = "multi-ent-f2p-midcycle"; diff --git a/server/tests/billing/update-subscription/multi-product/schedules-from-paid.test.ts b/server/tests/billing/update-subscription/multi-product/schedules-from-paid.test.ts index b90c3c981..ed1e6d7f8 100644 --- a/server/tests/billing/update-subscription/multi-product/schedules-from-paid.test.ts +++ b/server/tests/billing/update-subscription/multi-product/schedules-from-paid.test.ts @@ -50,7 +50,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: cancel entity 1, update en ); await expectProductCanceled({ customer: entity1AfterCancel, - productId: `${customerId}_pro`, + productId: pro.id, }); // Entity 2 updates pro's items (change price) @@ -59,7 +59,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: cancel entity 1, update en await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entities[1].id, - product_id: `${customerId}_pro`, + product_id: pro.id, items: [messagesItem, newPriceItem], }); @@ -67,7 +67,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: cancel entity 1, update en const entity2Data = await autumnV1.entities.get(customerId, entities[1].id); await expectProductActive({ customer: entity2Data, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectSubToBeCorrect({ @@ -96,11 +96,11 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: cancel entity 1, update en await expectProductNotPresent({ customer: entity1AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectProductActive({ customer: entity2AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectSubToBeCorrect({ @@ -144,7 +144,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade entity 1, update // Entity 1 downgrades from Premium to Pro (scheduled) await autumnV1.attach({ customer_id: customerId, - product_id: `${customerId}_pro`, + product_id: pro.id, entity_id: entities[0].id, }); @@ -164,7 +164,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade entity 1, update await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entities[1].id, - product_id: `${customerId}_premium`, + product_id: premium.id, items: [consumableItem, newPriceItem], }); @@ -172,7 +172,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade entity 1, update const entity2Data = await autumnV1.entities.get(customerId, entities[1].id); await expectProductActive({ customer: entity2Data, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({ @@ -202,15 +202,15 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade entity 1, update await expectProductNotPresent({ customer: entity1AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectProductActive({ customer: entity1AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectProductActive({ customer: entity2AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({ @@ -266,14 +266,14 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: annual + monthly downgrade // Entity 1 downgrades from Premium Annual to Pro (scheduled for end of annual period) await autumnV1.attach({ customer_id: customerId, - product_id: `${customerId}_pro`, + product_id: pro.id, entity_id: entities[0].id, }); // Entity 2 downgrades from Premium Monthly to Pro (scheduled for end of month) await autumnV1.attach({ customer_id: customerId, - product_id: `${customerId}_pro`, + product_id: pro.id, entity_id: entities[1].id, }); @@ -284,7 +284,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: annual + monthly downgrade await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entities[2].id, - product_id: `${customerId}_premium`, + product_id: premium.id, items: [newConsumable, newPriceItem], }); @@ -292,7 +292,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: annual + monthly downgrade const entity3Data = await autumnV1.entities.get(customerId, entities[2].id); await expectProductActive({ customer: entity3Data, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({ @@ -328,23 +328,23 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: annual + monthly downgrade // Entity 1 should still have premium-annual (annual cycle not over) await expectProductActive({ customer: entity1AfterCycle, - productId: `${customerId}_premium-annual`, + productId: premiumAnnual.id, }); // Entity 2 should be on pro now await expectProductNotPresent({ customer: entity2AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectProductActive({ customer: entity2AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); // Entity 3 should still have premium await expectProductActive({ customer: entity3AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({ @@ -382,18 +382,18 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: update to free item remove const entity2Before = await autumnV1.entities.get(customerId, entities[1].id); await expectProductActive({ customer: entity1Before, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectProductActive({ customer: entity2Before, - productId: `${customerId}_pro`, + productId: pro.id, }); // Entity 2 updates Pro's items to be free (no price items, only feature) await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entities[1].id, - product_id: `${customerId}_pro`, + product_id: pro.id, items: [messagesItem], // Only messages, no price - makes it free }); @@ -401,7 +401,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: update to free item remove const entity2Data = await autumnV1.entities.get(customerId, entities[1].id); await expectProductActive({ customer: entity2Data, - productId: `${customerId}_pro`, + productId: pro.id, }); // Should only have 1 subscription (entity 1's pro) @@ -432,11 +432,11 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: update to free item remove await expectProductActive({ customer: entity1AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectProductActive({ customer: entity2AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectSubToBeCorrect({ @@ -482,7 +482,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade + update to free // Entity 1 downgrades from Premium to Pro (scheduled) await autumnV1.attach({ customer_id: customerId, - product_id: `${customerId}_pro`, + product_id: pro.id, entity_id: entities[0].id, }); @@ -500,7 +500,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade + update to free await autumnV1.subscriptions.update({ customer_id: customerId, entity_id: entities[1].id, - product_id: `${customerId}_premium`, + product_id: premium.id, items: [consumableItem], // Only consumable, no base price - makes it free }); @@ -508,7 +508,7 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade + update to free const entity2Data = await autumnV1.entities.get(customerId, entities[1].id); await expectProductActive({ customer: entity2Data, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({ @@ -539,15 +539,15 @@ test.concurrent(`${chalk.yellowBright("schedules-p2p: downgrade + update to free await expectProductNotPresent({ customer: entity1AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectProductActive({ customer: entity1AfterCycle, - productId: `${customerId}_pro`, + productId: pro.id, }); await expectProductActive({ customer: entity2AfterCycle, - productId: `${customerId}_premium`, + productId: premium.id, }); await expectSubToBeCorrect({