diff --git a/server/tests/advanced/check/check1.ts b/server/tests/advanced/check/check1.test.ts similarity index 67% rename from server/tests/advanced/check/check1.ts rename to server/tests/advanced/check/check1.test.ts index 9124c6750..36db97432 100644 --- a/server/tests/advanced/check/check1.ts +++ b/server/tests/advanced/check/check1.test.ts @@ -1,17 +1,15 @@ import { type Customer, LegacyVersion, type LimitedItem } from "@autumn/shared"; -import { expect } from "chai"; +import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { Decimal } from "decimal.js"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; const creditCost = 0.2; const freeProduct = constructProduct({ @@ -35,40 +33,29 @@ describe(`${chalk.yellowBright("check1: Checking credit systems")}`, () => { const customerId = testCase; let testClockId: string; let customer: Customer; - let stripeCli: Stripe; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); - before(async function () { - await setupBefore(this); - stripeCli = this.stripeCli; - + beforeAll(async () => { const { customer: customer_, testClockId: testClockId_ } = - await initCustomer({ + await initCustomerV3({ + ctx, customerId, - org: this.org, - env: this.env, - db: this.db, - autumn: this.autumnJs, + customerData: {}, attachPm: "success", + withTestClock: true, }); - addPrefixToProducts({ + await initProductsV0({ + ctx, products: [freeProduct, pro], prefix: testCase, }); - await createProducts({ - products: [freeProduct, pro], - orgId: this.org.id, - env: this.env, - autumn: this.autumnJs, - db: this.db, - }); customer = customer_; testClockId = testClockId_; }); - it("should attach free product and check action1 allowed", async () => { + test("should attach free product and check action1 allowed", async () => { await autumn.attach({ customer_id: customerId, product_id: freeProduct.id, @@ -84,11 +71,11 @@ describe(`${chalk.yellowBright("check1: Checking credit systems")}`, () => { feature_id: TestFeature.Credits, }); - expect(actionCheck.allowed).to.be.true; - expect(creditsCheck.allowed).to.be.false; + expect(actionCheck.allowed).toBe(true); + expect(creditsCheck.allowed).toBe(false); }); - it("should attach pro product and check allowed", async () => { + test("should attach pro product and check allowed", async () => { await autumn.attach({ customer_id: customerId, product_id: pro.id, @@ -104,11 +91,11 @@ describe(`${chalk.yellowBright("check1: Checking credit systems")}`, () => { feature_id: TestFeature.Action1, }); - expect(actionCheck.allowed).to.be.true; - expect(creditsCheck.allowed).to.be.true; + expect(actionCheck.allowed).toBe(true); + expect(creditsCheck.allowed).toBe(true); }); - it("should use up credits and have correct check response", async () => { + test("should use up credits and have correct check response", async () => { const usage = 50; const creditUsage = new Decimal(creditCost).mul(usage).toNumber(); @@ -129,6 +116,6 @@ describe(`${chalk.yellowBright("check1: Checking credit systems")}`, () => { feature_id: TestFeature.Credits, }); - expect(creditsCheck.balance).to.be.equal(creditBalance); + expect(creditsCheck.balance).toBe(creditBalance); }); }); diff --git a/server/tests/advanced/coupons/coupon1.backup.ts b/server/tests/advanced/coupons/coupon1.backup.ts new file mode 100644 index 000000000..2e97e1924 --- /dev/null +++ b/server/tests/advanced/coupons/coupon1.backup.ts @@ -0,0 +1,234 @@ +import { + type AppEnv, + type Customer, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { rewards } from "tests/global.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { + advanceTestClock, + completeCheckoutForm, + getDiscount, +} from "tests/utils/stripeUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const testCase = "coupon1"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const simulateOneCycle = async ({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix, +}: { + customerId: string; + db: DrizzleCli; + org: Organization; + env: AppEnv; + stripeCli: Stripe; + autumn: AutumnInt; + testClockId: string; + couponAmount: number; + curUnix: number; +}) => { + const usage = Math.random() * 100000 + 10000; + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + // Expected invoice total + const expectedTotal = await getExpectedInvoiceTotal({ + usage: [{ featureId: TestFeature.Words, value: usage }], + customerId, + productId: pro.id, + db, + org, + env, + stripeCli, + }); + + couponAmount -= expectedTotal; + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(curUnix, 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).to.equal(0); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(cusDiscount).to.exist; + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + + expect(cusDiscount.coupon?.amount_off).to.equal( + Math.round(couponAmount * 100), + `Expected stripe cus to have coupon amount ${couponAmount * 100}`, + ); + + return { + couponAmount, + curUnix, + }; +}; + +describe( + chalk.yellow( + `${testCase} - Testing invoice credits reward, apply to all product`, + ), + () => { + const customerId = "coupon1"; + let stripeCli: Stripe; + let customer: Customer; + let testClockId: string; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let couponAmount = rewards.rolloverAll.discount_config.discount_value; + let curUnix = new Date().getTime(); + + before(async function () { + await setupBefore(this); + db = this.db; + org = this.org; + env = this.env; + stripeCli = this.stripeCli; + + const res = await initCustomer({ + customerId, + org, + env, + db, + autumn: this.autumnJs, + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + products: [pro], + orgId: org.id, + env, + db, + autumn, + }); + + testClockId = res.testClockId; + customer = res.customer; + }); + + // CYCLE 0 + it("should attach pro", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + await completeCheckoutForm( + res.checkout_url, + undefined, + rewards.rolloverAll.id, + ); + + await timeout(10000); + + couponAmount -= getBasePrice({ product: pro }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ customer, product: pro }); + + expect(customer.invoices![0].total).to.equal(0); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(cusDiscount).to.exist; + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal( + rewards.rolloverAll.id, + ); + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); + }); + + it("should run one cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix: new Date().getTime(), + }); + + couponAmount = res.couponAmount; + curUnix = res.curUnix; + }); + + // CYCLE 1 + it("should run another cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix, + }); + }); + }, +); diff --git a/server/tests/advanced/coupons/coupon1.test.ts b/server/tests/advanced/coupons/coupon1.test.ts new file mode 100644 index 000000000..98c6c5095 --- /dev/null +++ b/server/tests/advanced/coupons/coupon1.test.ts @@ -0,0 +1,229 @@ +import { + type AppEnv, + type Customer, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import { rewards } from "tests/global.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { + advanceTestClock, + completeCheckoutForm, + getDiscount, +} from "tests/utils/stripeUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; + +const testCase = "coupon1"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const simulateOneCycle = async ({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix, +}: { + customerId: string; + db: DrizzleCli; + org: Organization; + env: AppEnv; + stripeCli: Stripe; + autumn: AutumnInt; + testClockId: string; + couponAmount: number; + curUnix: number; +}) => { + const usage = Math.random() * 100000 + 10000; + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + // Expected invoice total + const expectedTotal = await getExpectedInvoiceTotal({ + usage: [{ featureId: TestFeature.Words, value: usage }], + customerId, + productId: pro.id, + db, + org, + env, + stripeCli, + }); + + couponAmount -= expectedTotal; + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(curUnix, 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).toBe(0); + + const cusDiscount = await getDiscount({ + stripeCli: stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(cusDiscount).toBeDefined(); + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe( + rewards.rolloverAll.id, + ); + + expect(cusDiscount.coupon?.amount_off).toBe( + Math.round(couponAmount * 100), + ); + + return { + couponAmount, + curUnix, + }; +}; + +describe( + chalk.yellow( + `${testCase} - Testing invoice credits reward, apply to all product`, + ), + () => { + const customerId = "coupon1"; + let stripeCli: Stripe; + let customer: Customer; + let testClockId: string; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let couponAmount = rewards.rolloverAll.discount_config.discount_value; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + stripeCli = ctx.stripeCli; + + const res = await initCustomerV3({ + ctx, + customerId, + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + products: [pro], + orgId: org.id, + env, + db, + autumn, + }); + + testClockId = res.testClockId; + customer = res.customer; + }); + + // CYCLE 0 + test("should attach pro", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + await completeCheckoutForm( + res.checkout_url, + undefined, + rewards.rolloverAll.id, + ); + + await timeout(10000); + + couponAmount -= getBasePrice({ product: pro }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ customer, product: pro }); + + expect(customer.invoices![0].total).toBe(0); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(cusDiscount).toBeDefined(); + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe( + rewards.rolloverAll.id, + ); + expect(cusDiscount.coupon?.amount_off).toBe(couponAmount * 100); + }); + + test("should run one cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix: new Date().getTime(), + }); + + couponAmount = res.couponAmount; + curUnix = res.curUnix; + }); + + // CYCLE 1 + test("should run another cycle and have correct invoice + coupon amount", async () => { + const res = await simulateOneCycle({ + customerId, + db, + org, + env, + stripeCli, + autumn, + testClockId, + couponAmount, + curUnix, + }); + }); + }, +); diff --git a/server/tests/advanced/coupons/coupon2.backup.ts b/server/tests/advanced/coupons/coupon2.backup.ts new file mode 100644 index 000000000..cadd28dd8 --- /dev/null +++ b/server/tests/advanced/coupons/coupon2.backup.ts @@ -0,0 +1,197 @@ +import { + type AppEnv, + CouponDurationType, + type CreateReward, + LegacyVersion, + type Organization, + RewardType, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { completeCheckoutForm, getDiscount } from "tests/utils/stripeUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const testCase = "coupon2"; + +// Create reward input +const reward: CreateReward = { + id: "usage", + name: "usage", + promo_codes: [{ code: "usage" }], + type: RewardType.InvoiceCredits, + discount_config: { + discount_value: 10000, + duration_type: CouponDurationType.Forever, + duration_value: 1, + should_rollover: true, + apply_to_all: false, + price_ids: [], + }, +}; + +describe( + chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`), + () => { + const customerId = testCase; + let stripeCli: Stripe; + let testClockId: string; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let org: Organization; + let env: AppEnv; + let db: DrizzleCli; + + let couponAmount = reward.discount_config?.discount_value ?? 0; + + before(async function () { + await setupBefore(this); + + org = this.org; + env = this.env; + db = this.db; + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + }); + + testClockId = testClockId1; + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + orgId: this.org.id, + env: this.env, + db: this.db, + autumn, + products: [pro], + }); + + await createReward({ + orgId: org.id, + env, + db, + autumn, + reward, + productId: pro.id, + onlyUsage: true, + }); + }); + + // CYCLE 0 + it("should attach pro with promo code", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + await completeCheckoutForm(res.checkout_url, undefined, reward.id); + + await timeout(10000); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + it("should have fixed price invoice and correct remaining coupon amount", async () => { + const customer = await autumn.customers.get(customerId); + const fixedPrice = getBasePrice({ product: pro }); + expect(customer.invoices![0].total).to.equal(fixedPrice); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id); + expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100); + }); + + // CYCLE 1 + it("should track usage and have correct invoice amount", async () => { + const usage = new Decimal(Math.random() * 1250120 + 10000) + .toDecimalPlaces(2) + .toNumber(); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + const usageTotal = await getExpectedInvoiceTotal({ + org, + env, + db, + customerId, + productId: pro.id, + usage: [{ featureId: TestFeature.Words, value: usage }], + stripeCli, + onlyIncludeUsage: true, + }); + + const basePrice = getBasePrice({ product: pro }); + + couponAmount = couponAmount - usageTotal; + + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 20, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).to.equal(basePrice); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id); + + expect(cusDiscount.coupon?.amount_off).to.equal( + Math.round(couponAmount * 100), + ); + }); + }, +); diff --git a/server/tests/advanced/coupons/coupon2.test.ts b/server/tests/advanced/coupons/coupon2.test.ts new file mode 100644 index 000000000..5293c35c7 --- /dev/null +++ b/server/tests/advanced/coupons/coupon2.test.ts @@ -0,0 +1,192 @@ +import { + type AppEnv, + CouponDurationType, + type CreateReward, + LegacyVersion, + type Organization, + RewardType, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { completeCheckoutForm, getDiscount } from "tests/utils/stripeUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const testCase = "coupon2"; + +// Create reward input +const reward: CreateReward = { + id: "usage", + name: "usage", + promo_codes: [{ code: "usage" }], + type: RewardType.InvoiceCredits, + discount_config: { + discount_value: 10000, + duration_type: CouponDurationType.Forever, + duration_value: 1, + should_rollover: true, + apply_to_all: false, + price_ids: [], + }, +}; + +describe( + chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`), + () => { + const customerId = testCase; + let stripeCli: Stripe; + let testClockId: string; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let org: Organization; + let env: AppEnv; + let db: DrizzleCli; + + let couponAmount = reward.discount_config?.discount_value ?? 0; + + beforeAll(async () => { + org = ctx.org; + env = ctx.env; + db = ctx.db; + stripeCli = ctx.stripeCli; + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + }); + + testClockId = testClockId1; + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + orgId: ctx.org.id, + env: ctx.env, + db: ctx.db, + autumn, + products: [pro], + }); + + await createReward({ + orgId: org.id, + env, + db, + autumn, + reward, + productId: pro.id, + onlyUsage: true, + }); + }); + + // CYCLE 0 + test("should attach pro with promo code", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + await completeCheckoutForm(res.checkout_url, undefined, reward.id); + + await timeout(10000); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + test("should have fixed price invoice and correct remaining coupon amount", async () => { + const customer = await autumn.customers.get(customerId); + const fixedPrice = getBasePrice({ product: pro }); + expect(customer.invoices![0].total).toBe(fixedPrice); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(reward.id); + expect(cusDiscount.coupon?.amount_off).toBe(couponAmount * 100); + }); + + // CYCLE 1 + test("should track usage and have correct invoice amount", async () => { + const usage = new Decimal(Math.random() * 1250120 + 10000) + .toDecimalPlaces(2) + .toNumber(); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Words, + value: usage, + }); + + const usageTotal = await getExpectedInvoiceTotal({ + org, + env, + db, + customerId, + productId: pro.id, + usage: [{ featureId: TestFeature.Words, value: usage }], + stripeCli, + onlyIncludeUsage: true, + }); + + const basePrice = getBasePrice({ product: pro }); + + couponAmount = couponAmount - usageTotal; + + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 20, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices![0].total).toBe(basePrice); + + const cusDiscount = await getDiscount({ + stripeCli, + stripeId: customer.stripe_id!, + }); + + expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(reward.id); + + expect(cusDiscount.coupon?.amount_off).toBe( + Math.round(couponAmount * 100), + ); + }); + }, +); diff --git a/server/tests/advanced/coupons/coupon3.backup.ts b/server/tests/advanced/coupons/coupon3.backup.ts new file mode 100644 index 000000000..ce9c002c5 --- /dev/null +++ b/server/tests/advanced/coupons/coupon3.backup.ts @@ -0,0 +1,176 @@ +import { + type AppEnv, + CouponDurationType, + type CreateReward, + LegacyVersion, + type Organization, + RewardType, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const oneOff = constructProduct({ + type: "one_off", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], +}); + +// Create reward input +const rewardId = "attach_coupon"; +const promoCode = "attach_coupon_code"; +const reward: CreateReward = { + id: rewardId, + name: "attach_coupon", + promo_codes: [{ code: promoCode }], + type: RewardType.FixedDiscount, + discount_config: { + discount_value: 5, + duration_type: CouponDurationType.OneOff, + duration_value: 1, + should_rollover: true, + apply_to_all: true, + price_ids: [], + }, +}; + +const testCase = "coupon3"; +describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => { + const customerId = testCase; + let stripeCli: Stripe; + let testClockId: string; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let org: Organization; + let env: AppEnv; + let db: DrizzleCli; + + const couponAmount = reward.discount_config!.discount_value; + + before(async function () { + await setupBefore(this); + + org = this.org; + env = this.env; + db = this.db; + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + testClockId = testClockId1; + + addPrefixToProducts({ + products: [pro, oneOff], + prefix: testCase, + }); + + await createProducts({ + orgId: this.org.id, + env: this.env, + db: this.db, + autumn, + products: [pro, oneOff], + }); + + await createReward({ + orgId: org.id, + env, + db, + autumn, + reward, + productId: pro.id, + }); + }); + + // CYCLE 0 + it("should attach pro with reward ID", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + reward: rewardId, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: pro, + }); + + const invoice = customer.invoices![0]; + const basePrice = getBasePrice({ product: pro }); + expect(invoice.total).to.equal(basePrice - couponAmount); + }); + + it("should attach one off with reward ID", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: oneOff.id, + reward: rewardId, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: oneOff, + }); + + const invoice = customer.invoices![0]; + const basePrice = getBasePrice({ product: oneOff }); + expect(invoice.total).to.equal(basePrice - couponAmount); + expect(invoice.product_ids).to.include(oneOff.id); + }); + + it("should attach one off with promo code", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: oneOff.id, + reward: promoCode, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: oneOff, + }); + + expect(customer.invoices!.length).to.equal(3); + const basePrice = getBasePrice({ product: oneOff }); + for (let i = 0; i < 2; i++) { + const invoice = customer.invoices![i]; + expect(invoice.total).to.equal(basePrice - couponAmount); + expect(invoice.product_ids).to.include(oneOff.id); + } + }); +}); diff --git a/server/tests/advanced/coupons/coupon3.test.ts b/server/tests/advanced/coupons/coupon3.test.ts new file mode 100644 index 000000000..0781c7250 --- /dev/null +++ b/server/tests/advanced/coupons/coupon3.test.ts @@ -0,0 +1,171 @@ +import { + type AppEnv, + CouponDurationType, + type CreateReward, + LegacyVersion, + type Organization, + RewardType, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { createProducts, createReward } from "tests/utils/productUtils.js"; +import { + addPrefixToProducts, + getBasePrice, +} from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; + +const pro = constructProduct({ + type: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], +}); + +const oneOff = constructProduct({ + type: "one_off", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 500, + }), + ], +}); + +// Create reward input +const rewardId = "attach_coupon"; +const promoCode = "attach_coupon_code"; +const reward: CreateReward = { + id: rewardId, + name: "attach_coupon", + promo_codes: [{ code: promoCode }], + type: RewardType.FixedDiscount, + discount_config: { + discount_value: 5, + duration_type: CouponDurationType.OneOff, + duration_value: 1, + should_rollover: true, + apply_to_all: true, + price_ids: [], + }, +}; + +const testCase = "coupon3"; +describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => { + const customerId = testCase; + let stripeCli: Stripe; + let testClockId: string; + + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let org: Organization; + let env: AppEnv; + let db: DrizzleCli; + + const couponAmount = reward.discount_config!.discount_value; + + beforeAll(async () => { + org = ctx.org; + env = ctx.env; + db = ctx.db; + stripeCli = ctx.stripeCli; + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + }); + + testClockId = testClockId1; + + addPrefixToProducts({ + products: [pro, oneOff], + prefix: testCase, + }); + + await createProducts({ + orgId: ctx.org.id, + env: ctx.env, + db: ctx.db, + autumn, + products: [pro, oneOff], + }); + + await createReward({ + orgId: org.id, + env, + db, + autumn, + reward, + productId: pro.id, + }); + }); + + // CYCLE 0 + test("should attach pro with reward ID", async () => { + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + reward: rewardId, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: pro, + }); + + const invoice = customer.invoices![0]; + const basePrice = getBasePrice({ product: pro }); + expect(invoice.total).toBe(basePrice - couponAmount); + }); + + test("should attach one off with reward ID", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: oneOff.id, + reward: rewardId, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: oneOff, + }); + + const invoice = customer.invoices![0]; + const basePrice = getBasePrice({ product: oneOff }); + expect(invoice.total).toBe(basePrice - couponAmount); + expect(invoice.product_ids).toContain(oneOff.id); + }); + + test("should attach one off with promo code", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: oneOff.id, + reward: promoCode, + }); + + const customer = await autumn.customers.get(customerId); + expectAttachCorrect({ + customer, + product: oneOff, + }); + + expect(customer.invoices!.length).toBe(3); + const basePrice = getBasePrice({ product: oneOff }); + for (let i = 0; i < 2; i++) { + const invoice = customer.invoices![i]; + expect(invoice.total).toBe(basePrice - couponAmount); + expect(invoice.product_ids).toContain(oneOff.id); + } + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals13.backup.ts b/server/tests/advanced/referrals/paid/referrals13.backup.ts new file mode 100644 index 000000000..14dcc6704 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals13.backup.ts @@ -0,0 +1,255 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; + +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals13"; + +describe(`${chalk.yellowBright( + "referrals13: Testing referrals - referrer on Pro, gets discount on next cycle - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-13"; + const redeemer = "referral13-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + db = this.db; + org = this.org; + env = this.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with Pro product already attached + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + // Attach Pro product to main customer first + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.pro.id, + }); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: redeemerPM, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + it("should advance clock 10 days before redeeming", async () => { + // Advance 10 days after Pro is attached + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 10, + stripeCli, + }), + ), + ); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.exists(referralCode.code); + }); + + it("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + assert.fail("Should not be able to redeem again"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + it("should have referrer already on Pro, and redeemer gets free product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.redeemer_applied, true); + + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemer)).products; + + // Main customer (referrer) should have the pro product (already attached) + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); + + // Redeemer should only have the free product (no pro product given in referrer-only program) + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.free.id); + + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomerId), + product: products.pro, + status: CusProductStatus.Active, + }); + + // Verify redeemer only has free product + expectProductV1Attached({ + customer: await autumn.customers.get(redeemer), + product: products.free, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and verify referrer gets discount on next Pro cycle", async () => { + // Advance 31 days from current time to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that main customer's Pro invoice has discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices, CusExpand.Rewards], + }, + ); + + const proInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + + const expectedTotal = products.pro.prices[0].config.amount; + + const actualTotal = proInvoice?.total; + + if (proInvoice) { + // Should have a discount applied - invoice total should be less than full Pro price ($10) + assert.isBelow( + actualTotal!, + expectedTotal, // $10 in cents + "Pro invoice should have discount applied, making it less than full price", + ); + + // For referrer-only reward, the discount should make it significantly cheaper or free + assert.isAtMost( + actualTotal!, + expectedTotal / 2, // $5 or less in cents - assuming at least 50% discount + "Referrer should get substantial discount on Pro product", + ); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - keeps Pro with discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward in referrer-only program) + { name: "Free", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + assert.exists( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals13.test.ts b/server/tests/advanced/referrals/paid/referrals13.test.ts new file mode 100644 index 000000000..ee79f0c01 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals13.test.ts @@ -0,0 +1,236 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals13"; + +describe(`${chalk.yellowBright( + "referrals13: Testing referrals - referrer on Pro, gets discount on next cycle - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-13"; + const redeemer = "referral13-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with Pro product already attached + const res = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + // Attach Pro product to main customer first + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.pro.id, + }); + + const redeemerRes = await initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: redeemerPM as "success", + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + test("should advance clock 10 days before redeeming", async () => { + // Advance 10 days after Pro is attached + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 10, + stripeCli, + }), + ), + ); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + expect(referralCode.code).toBeDefined(); + }); + + test("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + throw new Error("Should not be able to redeem again"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + test("should have referrer already on Pro, and redeemer gets free product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + expect(redemptionResult.redeemer_applied).toBe(true); + + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemer)).products; + + // Main customer (referrer) should have the pro product (already attached) + expect(mainProds.length).toBe(1); + expect(mainProds[0].id).toBe(products.pro.id); + + // Redeemer should only have the free product (no pro product given in referrer-only program) + expect(redeemerProds.length).toBe(1); + expect(redeemerProds[0].id).toBe(products.free.id); + + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomerId), + product: products.pro, + status: CusProductStatus.Active, + }); + + // Verify redeemer only has free product + expectProductV1Attached({ + customer: await autumn.customers.get(redeemer), + product: products.free, + status: CusProductStatus.Active, + }); + }); + + test("should advance test clock and verify referrer gets discount on next Pro cycle", async () => { + // Advance 31 days from current time to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that main customer's Pro invoice has discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices, CusExpand.Rewards], + }, + ); + + const proInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + + const expectedTotal = products.pro.prices[0].config.amount; + + const actualTotal = proInvoice?.total; + + if (proInvoice) { + // Should have a discount applied - invoice total should be less than full Pro price ($10) + expect(actualTotal!).toBeLessThan(expectedTotal); + + // For referrer-only reward, the discount should make it significantly cheaper or free + expect(actualTotal!).toBeLessThanOrEqual(expectedTotal / 2); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - keeps Pro with discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward in referrer-only program) + { name: "Free", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + expect(matchingProduct).toBeDefined(); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals14.backup.ts b/server/tests/advanced/referrals/paid/referrals14.backup.ts new file mode 100644 index 000000000..6b12ddf22 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals14.backup.ts @@ -0,0 +1,264 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals14"; + +describe(`${chalk.yellowBright( + "referrals14: Testing referrals - referrer on Premium (higher tier), gets pro_amount discount - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-14"; + const redeemer = "referral14-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + db = this.db; + org = this.org; + env = this.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), + autumn.customers.delete(redeemer, { deleteInStripe: true }), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with Premium product already attached + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + // Attach Premium product to main customer first (higher tier than Pro) + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.premium.id, + }); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: redeemerPM, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + + // Advance 10 days after Premium is attached, then redeem the code + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 5, + stripeCli, + }), + ), + ); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.equal(referralCode2.code, referralCode.code); + }); + + it("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + assert.fail("Should not be able to redeem again"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + it("should have referrer already on Premium, and redeemer gets free product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.redeemer_applied, true); + + const mainCus = await autumn.customers.get(mainCustomerId); + const redeemerCus = await autumn.customers.get(redeemer); + const mainProds = mainCus.products; + const redeemerProds = redeemerCus.products; + + // Main customer (referrer) should have the premium product (already attached) + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.premium.id); + + // Redeemer should only have the free product (no pro product given in referrer-only program) + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.free.id); + + expectProductV1Attached({ + customer: mainCus, + product: products.premium, + status: CusProductStatus.Active, + }); + + // Verify redeemer only has free product + expectProductV1Attached({ + customer: redeemerCus, + product: products.free, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and verify referrer gets pro_amount discount on Premium cycle", async () => { + // Advance 21 more days (total 31 days from start) to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that main customer's Premium invoice has pro_amount discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices], + }, + ); + + const premiumInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.premium.id), + ); + if (premiumInvoice) { + // Premium costs $50, Pro costs $10 - so referrer should get $10 discount on Premium + // Expected: Premium ($50) - Pro amount ($10) = $40 + console.log(products.premium.prices); + const premiumPrice = products.premium.prices[0].config.amount; // $50 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = premiumPrice - proAmount; // $40 + + // The invoice total should be exactly Premium price minus pro_amount + assert.equal( + premiumInvoice.total, + expectedTotal, + `Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${premiumInvoice.total}`, + ); + + // Verify that the discount was applied (total is less than full Premium price) + assert.isBelow( + premiumInvoice.total, + premiumPrice, + "Referrer on Premium should get pro_amount discount, making it less than full Premium price", + ); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - keeps Premium with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Premium", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward in referrer-only program) + { name: "Free", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + assert.exists( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals14.test.ts b/server/tests/advanced/referrals/paid/referrals14.test.ts new file mode 100644 index 000000000..a03ff9327 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals14.test.ts @@ -0,0 +1,245 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals14"; + +describe(`${chalk.yellowBright( + "referrals14: Testing referrals - referrer on Premium (higher tier), gets pro_amount discount - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-14"; + const redeemer = "referral14-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), + autumn.customers.delete(redeemer, { deleteInStripe: true }), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with Premium product already attached + const res = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + // Attach Premium product to main customer first (higher tier than Pro) + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.premium.id, + }); + + const redeemerRes = await initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: redeemerPM as "success", + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + + // Advance 10 days after Premium is attached, then redeem the code + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 5, + stripeCli, + }), + ), + ); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + expect(referralCode.code).toBeDefined(); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + expect(referralCode2.code).toBe(referralCode.code); + }); + + test("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + throw new Error("Should not be able to redeem again"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + test("should have referrer already on Premium, and redeemer gets free product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + expect(redemptionResult.redeemer_applied).toBe(true); + + const mainCus = await autumn.customers.get(mainCustomerId); + const redeemerCus = await autumn.customers.get(redeemer); + const mainProds = mainCus.products; + const redeemerProds = redeemerCus.products; + + // Main customer (referrer) should have the premium product (already attached) + expect(mainProds.length).toBe(1); + expect(mainProds[0].id).toBe(products.premium.id); + + // Redeemer should only have the free product (no pro product given in referrer-only program) + expect(redeemerProds.length).toBe(1); + expect(redeemerProds[0].id).toBe(products.free.id); + + expectProductV1Attached({ + customer: mainCus, + product: products.premium, + status: CusProductStatus.Active, + }); + + // Verify redeemer only has free product + expectProductV1Attached({ + customer: redeemerCus, + product: products.free, + status: CusProductStatus.Active, + }); + }); + + test("should advance test clock and verify referrer gets pro_amount discount on Premium cycle", async () => { + // Advance 21 more days (total 31 days from start) to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that main customer's Premium invoice has pro_amount discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices], + }, + ); + + const premiumInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.premium.id), + ); + if (premiumInvoice) { + // Premium costs $50, Pro costs $10 - so referrer should get $10 discount on Premium + // Expected: Premium ($50) - Pro amount ($10) = $40 + const premiumPrice = products.premium.prices[0].config.amount; // $50 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = premiumPrice - proAmount; // $40 + + // The invoice total should be exactly Premium price minus pro_amount + expect(premiumInvoice.total).toBe(expectedTotal); + + // Verify that the discount was applied (total is less than full Premium price) + expect(premiumInvoice.total).toBeLessThan(premiumPrice); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - keeps Premium with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Premium", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward in referrer-only program) + { name: "Free", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + expect(matchingProduct).toBeDefined(); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals15.backup.ts b/server/tests/advanced/referrals/paid/referrals15.backup.ts new file mode 100644 index 000000000..1f8c15c82 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals15.backup.ts @@ -0,0 +1,296 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals15"; + +describe(`${chalk.yellowBright( + "referrals15: Testing referrals - referrer starts with no product, gets pro_amount discount - immediate, both - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-15"; + const redeemer = "referral15-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + db = this.db; + org = this.org; + env = this.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), + autumn.customers.delete(redeemer, { deleteInStripe: true }), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with NO paid product (just free tier) + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: redeemerPM, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + it("should advance clock 10 days before redeeming", async () => { + // Advance 10 days after setup + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 10, + stripeCli, + }), + ), + ); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + assert.equal(referralCode2.code, referralCode.code); + }); + + it("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + assert.fail("Should not be able to redeem again"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + it("should have both referrer and redeemer get pro product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.redeemer_applied, true); + + const mainCus = await autumn.customers.get(mainCustomerId); + const redeemerCus = await autumn.customers.get(redeemer); + const mainProds = mainCus.products; + const redeemerProds = redeemerCus.products; + + // Main customer (referrer) should now have the pro product + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); + + // Redeemer should also have the pro product (both get reward) + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.pro.id); + + expectProductV1Attached({ + customer: mainCus, + product: products.pro, + status: CusProductStatus.Active, + }); + + expectProductV1Attached({ + customer: redeemerCus, + product: products.pro, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and verify both customers get pro_amount discount on Pro cycle", async () => { + // Advance 31 days from current time to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that both customers' Pro invoices have pro_amount discount applied + const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([ + autumn.customers.get(mainCustomerId, { + expand: [CusExpand.Invoices], + }), + autumn.customers.get(redeemer, { + expand: [CusExpand.Invoices], + }), + ]); + + // console.log( + // "Main Customer Invoices:\n", + // mainCustomerWithInvoices.invoices + // .map( + // (x) => + // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, + // ) + // .join("\n"), + // ); + + // console.log( + // "Redeemer Invoices:\n", + // redeemerWithInvoices.invoices + // .map( + // (x) => + // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, + // ) + // .join("\n"), + // ); + + // Check main customer (referrer) invoice + const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + if (mainProInvoice) { + // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) + const proPrice = products.pro.prices[0].config.amount; // $10 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = proPrice - proAmount; // $0 + + // console.log("Main customer expected total:", expectedTotal); + // console.log("Main customer Pro invoice total:", mainProInvoice.total); + + assert.equal( + mainProInvoice.total, + expectedTotal, + `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}`, + ); + } + + // Check redeemer invoice + const redeemerProInvoice = redeemerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + if (redeemerProInvoice) { + // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) + const proPrice = products.pro.prices[0].config.amount; // $10 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = proPrice - proAmount; // $0 + + // console.log("Redeemer expected total:", expectedTotal); + // console.log("Redeemer Pro invoice total:", redeemerProInvoice.total); + + assert.equal( + redeemerProInvoice.total, + expectedTotal, + `Redeemer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${redeemerProInvoice.total}`, + ); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - has Pro with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - also has Pro with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + assert.exists( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals15.test.ts b/server/tests/advanced/referrals/paid/referrals15.test.ts new file mode 100644 index 000000000..71ba24132 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals15.test.ts @@ -0,0 +1,252 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals15"; + +describe(`${chalk.yellowBright( + "referrals15: Testing referrals - referrer starts with no product, gets pro_amount discount - immediate, both - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-15"; + const redeemer = "referral15-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: RewardRedemption; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), + autumn.customers.delete(redeemer, { deleteInStripe: true }), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + // Initialize main customer with NO paid product (just free tier) + const res = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + attachPm: "success", + }); + + testClockIds.push(res.testClockId); + + const redeemerRes = await initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: redeemerPM as "success", + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + test("should advance clock 10 days before redeeming", async () => { + // Advance 10 days after setup + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 10, + waitForSeconds: 10, + stripeCli, + }), + ), + ); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + expect(referralCode.code).toBeDefined(); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + expect(referralCode2.code).toBe(referralCode.code); + }); + + test("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + // Try redeem for redeemer again + try { + await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + throw new Error("Should not be able to redeem again"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + test("should have both referrer and redeemer get pro product", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + expect(redemptionResult.redeemer_applied).toBe(true); + + const mainCus = await autumn.customers.get(mainCustomerId); + const redeemerCus = await autumn.customers.get(redeemer); + const mainProds = mainCus.products; + const redeemerProds = redeemerCus.products; + + // Main customer (referrer) should now have the pro product + expect(mainProds.length).toBe(1); + expect(mainProds[0].id).toBe(products.pro.id); + + // Redeemer should also have the pro product (both get reward) + expect(redeemerProds.length).toBe(1); + expect(redeemerProds[0].id).toBe(products.pro.id); + + expectProductV1Attached({ + customer: mainCus, + product: products.pro, + status: CusProductStatus.Active, + }); + + expectProductV1Attached({ + customer: redeemerCus, + product: products.pro, + status: CusProductStatus.Active, + }); + }); + + test("should advance test clock and verify both customers get pro_amount discount on Pro cycle", async () => { + // Advance 31 days from current time to trigger next billing cycle + // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31 + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that both customers' Pro invoices have pro_amount discount applied + const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([ + autumn.customers.get(mainCustomerId, { + expand: [CusExpand.Invoices], + }), + autumn.customers.get(redeemer, { + expand: [CusExpand.Invoices], + }), + ]); + + // Check main customer (referrer) invoice + const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + if (mainProInvoice) { + // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) + const proPrice = products.pro.prices[0].config.amount; // $10 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = proPrice - proAmount; // $0 + + expect(mainProInvoice.total).toBe(expectedTotal); + } + + // Check redeemer invoice + const redeemerProInvoice = redeemerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + if (redeemerProInvoice) { + // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) + const proPrice = products.pro.prices[0].config.amount; // $10 + const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) + const expectedTotal = proPrice - proAmount; // $0 + + expect(redeemerProInvoice.total).toBe(expectedTotal); + } + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - has Pro with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - also has Pro with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + ]; + + dbCustomers.forEach((customer, index) => { + const expectedProductsForCustomer = expectedProducts[index]; + expectedProductsForCustomer.forEach((expectedProduct) => { + const matchingProduct = customer.customer_products.find( + (cp) => + cp.product.name === expectedProduct.name && + cp.status === expectedProduct.status, + ); + const unMatchedProduct = customer.customer_products.find( + (cp) => cp.product.name === expectedProduct.name, + ); + + expect(matchingProduct).toBeDefined(); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals16.backup.ts b/server/tests/advanced/referrals/paid/referrals16.backup.ts new file mode 100644 index 000000000..0b56bcab2 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals16.backup.ts @@ -0,0 +1,351 @@ +// import { +// type AppEnv, +// CusExpand, +// CusProductStatus, +// ErrCode, +// type Organization, +// type ReferralCode, +// type RewardRedemption, +// } from "@autumn/shared"; +// import { assert } from "chai"; +// import chalk from "chalk"; +// import type { Stripe } from "stripe"; +// import { setupBefore } from "tests/before.js"; +// import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +// import { +// advanceTestClock, +// completeCheckoutForm, +// } from "tests/utils/stripeUtils.js"; +// import type { DrizzleCli } from "@/db/initDrizzle.js"; +// import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +// import { CusService } from "@/internal/customers/CusService.js"; +// import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +// import { products, referralPrograms, rewards } from "../../../global.js"; + +// export const group = "referrals16"; + +// describe(`${chalk.yellowBright( +// "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based" +// )}`, () => { +// const mainCustomerId = "main-referral-16"; +// const redeemer = "referral16-r1"; +// const redeemerPM = "success"; +// const autumn: AutumnInt = new AutumnInt(); +// let stripeCli: Stripe; +// const testClockIds: string[] = []; +// let referralCode: ReferralCode; + +// let redemption: RewardRedemption; +// let db: DrizzleCli; +// let org: Organization; +// let env: AppEnv; + +// before(async function () { +// await setupBefore(this); +// stripeCli = this.stripeCli; +// db = this.db; +// org = this.org; +// env = this.env; + +// try { +// await Promise.all([ +// autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), +// autumn.customers.delete(redeemer, { deleteInStripe: true }), +// RewardRedemptionService._resetCustomerRedemptions({ +// db, +// internalCustomerId: [mainCustomerId, redeemer], +// }), +// ]); +// } catch {} + +// // Initialize main customer with NO paid product (just free tier) +// const res = await initCustomer({ +// autumn: this.autumnJs, +// customerId: mainCustomerId, +// db, +// org, +// env, +// attachPm: "success", +// }); + +// testClockIds.push(res.testClockId); + +// const redeemerRes = await initCustomer({ +// autumn: this.autumnJs, +// customerId: redeemer, +// db: this.db, +// org: this.org, +// env: this.env, +// attachPm: redeemerPM, +// withTestClock: true, +// }); + +// testClockIds.push(redeemerRes.testClockId); +// }); + +// it("should advance clock 10 days before redeeming", async () => { +// // Advance 10 days after setup +// await Promise.all( +// testClockIds.map((x) => +// advanceTestClock({ +// testClockId: x, +// numberOfDays: 10, +// waitForSeconds: 10, +// stripeCli, +// }) +// ) +// ); +// }); + +// it("should create code once", async () => { +// referralCode = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.id, +// }); + +// assert.exists(referralCode.code); + +// // Get referral code again +// const referralCode2 = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.id, +// }); + +// assert.equal(referralCode2.code, referralCode.code); +// }); + +// it("should create redemption for redeemer and fail if redeemed again", async () => { +// redemption = await autumn.referrals.redeem({ +// customerId: redeemer, +// code: referralCode.code, +// }); + +// // Try redeem for redeemer again +// try { +// await autumn.referrals.redeem({ +// customerId: redeemer, +// code: referralCode.code, +// }); +// assert.fail("Should not be able to redeem again"); +// } catch (error) { +// assert.instanceOf(error, AutumnError); +// assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); +// } +// }); + +// it("should have referrer and redeemer still on free tier (reward not triggered yet)", async () => { +// const redemptionResult = await autumn.redemptions.get(redemption.id); +// assert.equal(redemptionResult.triggered, false); // Checkout trigger not fired yet + +// const mainCus = await autumn.customers.get(mainCustomerId); +// const redeemerCus = await autumn.customers.get(redeemer); +// const mainProds = mainCus.products; +// const redeemerProds = redeemerCus.products; + +// // Both customers should still only have free product +// assert.equal(mainProds.length, 1); +// assert.equal(mainProds[0].id, products.free.id); + +// assert.equal(redeemerProds.length, 1); +// assert.equal(redeemerProds[0].id, products.free.id); + +// expectProductV1Attached({ +// customer: mainCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); + +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); +// }); + +// it("should trigger reward when redeemer checks out with Premium", async () => { +// // Redeemer purchases Premium product (triggers checkout reward) +// const checkoutRes = await autumn.attach({ +// customer_id: redeemer, +// product_id: products.premium.id, +// force_checkout: true, +// }); + +// await completeCheckoutForm(checkoutRes.checkout_url); + +// // Wait a bit for webhook processing +// await new Promise((resolve) => setTimeout(resolve, 10000)); + +// // Now both customers should have the reward applied +// const redemptionResult = await autumn.redemptions.get(redemption.id); + +// assert.equal(redemptionResult.applied, true); + +// const mainCus = await autumn.customers.get(mainCustomerId); +// const redeemerCus = await autumn.customers.get(redeemer); +// const mainProds = mainCus.products; +// const redeemerProds = redeemerCus.products; + +// // Main customer (referrer) should now have the pro product +// assert.equal(mainProds.length, 1); +// assert.equal(mainProds[0].id, products.pro.id); + +// // Redeemer should have both Premium (purchased) and the Pro price discount +// assert.equal(redeemerProds.length, 1); +// const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve( +// redeemerProds.find((x) => x.id === products.premium.id) +// ?.subscription_ids?.[0]!, +// { +// expand: ["discounts"], +// } +// ); + +// const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => { +// if (typeof x === "string") { +// return x; +// } else if (typeof x === "object") { +// return x.coupon.id; +// } else return null; +// })!; + +// assert.equal( +// redeemerStripeDiscounts.discounts.length, +// 1, +// `Redeemer Stripe Discounts: ${JSON.stringify(redeemerStripeDiscounts.discounts, null, 4)}` +// ); +// assert.equal( +// typeof parsedDiscountID === "object" +// ? parsedDiscountID.coupon.id +// : parsedDiscountID, +// rewards.paidProductWithConfig.id, +// `Parsed Discount ID: ${parsedDiscountID}` +// ); + +// assert.exists( +// redeemerProds.find((x) => x.id === products.premium.id), +// `Redeemer must have Premium product` +// ); + +// expectProductV1Attached({ +// customer: mainCus, +// product: products.pro, +// status: CusProductStatus.Active, +// }); + +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.premium, +// status: CusProductStatus.Active, +// }); +// }); + +// it("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => { +// // Advance 31 days from current time to trigger next billing cycle +// await Promise.all( +// testClockIds.map((x) => +// advanceTestClock({ +// testClockId: x, +// numberOfDays: 31, +// waitForSeconds: 25, +// stripeCli, +// }) +// ) +// ); + +// // Test that both customers' invoices have pro_amount discount applied +// const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([ +// autumn.customers.get(mainCustomerId, { +// expand: [CusExpand.Invoices], +// }), +// autumn.customers.get(redeemer, { +// expand: [CusExpand.Invoices], +// }), +// ]); + +// // Check main customer (referrer) Pro invoice +// const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) => +// x.product_ids.includes(products.pro.id) +// ); +// if (mainProInvoice) { +// // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) +// const proPrice = products.pro.prices[0].config.amount; // $10 +// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) +// const expectedTotal = proPrice - proAmount; // $0 + +// // console.log("Main customer expected total:", expectedTotal); +// // console.log("Main customer Pro invoice total:", mainProInvoice.total); + +// assert.equal( +// mainProInvoice.total, +// expectedTotal, +// `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}` +// ); +// } + +// // Check redeemer Premium invoice (should have $10 off) +// const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) => +// x.product_ids.includes(products.premium.id) +// ); +// if (redeemerPremiumInvoice) { +// // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40) +// const premiumPrice = products.premium.prices[0].config.amount; // $50 +// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) +// const expectedTotal = premiumPrice - proAmount; // $40 + +// assert.equal( +// redeemerPremiumInvoice.total, +// expectedTotal, +// `Redeemer Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${redeemerPremiumInvoice.total}` +// ); +// } + +// const dbCustomers = await Promise.all( +// [mainCustomerId, redeemer].map((x) => +// CusService.getFull({ +// db, +// idOrInternalId: x, +// orgId: org.id, +// env, +// inStatuses: [ +// CusProductStatus.Active, +// CusProductStatus.PastDue, +// CusProductStatus.Expired, +// ], +// }) +// ) +// ); + +// const expectedProducts = [ +// [ +// // Main referrer - has Pro with pro_amount discount applied +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// ], +// [ +// // Redeemer - has both Premium (purchased) and Pro (reward) with discounts +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// { name: "Premium", status: CusProductStatus.Active }, +// ], +// ]; + +// dbCustomers.forEach((customer, index) => { +// const expectedProductsForCustomer = expectedProducts[index]; +// expectedProductsForCustomer.forEach((expectedProduct) => { +// const matchingProduct = customer.customer_products.find( +// (cp) => +// cp.product.name === expectedProduct.name && +// cp.status === expectedProduct.status +// ); +// const unMatchedProduct = customer.customer_products.find( +// (cp) => cp.product.name === expectedProduct.name +// ); + +// assert.exists( +// matchingProduct, +// `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}` +// ); +// }); +// }); +// }); +// }); diff --git a/server/tests/advanced/referrals/paid/referrals16.test.ts b/server/tests/advanced/referrals/paid/referrals16.test.ts new file mode 100644 index 000000000..f9e739cb5 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals16.test.ts @@ -0,0 +1,326 @@ +// NOTE: This test is commented out in the original file (referrals16.ts) +// Keeping it commented out in the migrated version as well + +// import { +// type AppEnv, +// CusExpand, +// CusProductStatus, +// ErrCode, +// type Organization, +// type ReferralCode, +// type RewardRedemption, +// } from "@autumn/shared"; +// import { beforeAll, describe, expect, test } from "bun:test"; +// import chalk from "chalk"; +// import type { Stripe } from "stripe"; +// import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +// import { +// advanceTestClock, +// completeCheckoutForm, +// } from "tests/utils/stripeUtils.js"; +// import ctx from "tests/utils/testInitUtils/createTestContext.js"; +// import type { DrizzleCli } from "@/db/initDrizzle.js"; +// import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +// import { CusService } from "@/internal/customers/CusService.js"; +// import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +// import { products, referralPrograms, rewards } from "../../../global.js"; + +// export const group = "referrals16"; + +// describe(`${chalk.yellowBright( +// "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based" +// )}`, () => { +// const mainCustomerId = "main-referral-16"; +// const redeemer = "referral16-r1"; +// const redeemerPM = "success"; +// const autumn: AutumnInt = new AutumnInt(); +// let stripeCli: Stripe; +// const testClockIds: string[] = []; +// let referralCode: ReferralCode; + +// let redemption: RewardRedemption; +// let db: DrizzleCli; +// let org: Organization; +// let env: AppEnv; + +// beforeAll(async () => { +// stripeCli = ctx.stripeCli; +// db = ctx.db; +// org = ctx.org; +// env = ctx.env; + +// try { +// await Promise.all([ +// autumn.customers.delete(mainCustomerId, { deleteInStripe: true }), +// autumn.customers.delete(redeemer, { deleteInStripe: true }), +// RewardRedemptionService._resetCustomerRedemptions({ +// db, +// internalCustomerId: [mainCustomerId, redeemer], +// }), +// ]); +// } catch {} + +// // Initialize main customer with NO paid product (just free tier) +// const res = await initCustomerV3({ +// ctx, +// customerId: mainCustomerId, +// attachPm: "success", +// }); + +// testClockIds.push(res.testClockId); + +// const redeemerRes = await initCustomerV3({ +// ctx, +// customerId: redeemer, +// attachPm: redeemerPM as "success", +// withTestClock: true, +// }); + +// testClockIds.push(redeemerRes.testClockId); +// }); + +// test("should advance clock 10 days before redeeming", async () => { +// // Advance 10 days after setup +// await Promise.all( +// testClockIds.map((x) => +// advanceTestClock({ +// testClockId: x, +// numberOfDays: 10, +// waitForSeconds: 10, +// stripeCli, +// }) +// ) +// ); +// }); + +// test("should create code once", async () => { +// referralCode = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.id, +// }); + +// expect(referralCode.code).toBeDefined(); + +// // Get referral code again +// const referralCode2 = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.id, +// }); + +// expect(referralCode2.code).toBe(referralCode.code); +// }); + +// test("should create redemption for redeemer and fail if redeemed again", async () => { +// redemption = await autumn.referrals.redeem({ +// customerId: redeemer, +// code: referralCode.code, +// }); + +// // Try redeem for redeemer again +// try { +// await autumn.referrals.redeem({ +// customerId: redeemer, +// code: referralCode.code, +// }); +// throw new Error("Should not be able to redeem again"); +// } catch (error) { +// expect(error).toBeInstanceOf(AutumnError); +// expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); +// } +// }); + +// test("should have referrer and redeemer still on free tier (reward not triggered yet)", async () => { +// const redemptionResult = await autumn.redemptions.get(redemption.id); +// expect(redemptionResult.triggered).toBe(false); // Checkout trigger not fired yet + +// const mainCus = await autumn.customers.get(mainCustomerId); +// const redeemerCus = await autumn.customers.get(redeemer); +// const mainProds = mainCus.products; +// const redeemerProds = redeemerCus.products; + +// // Both customers should still only have free product +// expect(mainProds.length).toBe(1); +// expect(mainProds[0].id).toBe(products.free.id); + +// expect(redeemerProds.length).toBe(1); +// expect(redeemerProds[0].id).toBe(products.free.id); + +// expectProductV1Attached({ +// customer: mainCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); + +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); +// }); + +// test("should trigger reward when redeemer checks out with Premium", async () => { +// // Redeemer purchases Premium product (triggers checkout reward) +// const checkoutRes = await autumn.attach({ +// customer_id: redeemer, +// product_id: products.premium.id, +// force_checkout: true, +// }); + +// await completeCheckoutForm(checkoutRes.checkout_url); + +// // Wait a bit for webhook processing +// await new Promise((resolve) => setTimeout(resolve, 10000)); + +// // Now both customers should have the reward applied +// const redemptionResult = await autumn.redemptions.get(redemption.id); + +// expect(redemptionResult.applied).toBe(true); + +// const mainCus = await autumn.customers.get(mainCustomerId); +// const redeemerCus = await autumn.customers.get(redeemer); +// const mainProds = mainCus.products; +// const redeemerProds = redeemerCus.products; + +// // Main customer (referrer) should now have the pro product +// expect(mainProds.length).toBe(1); +// expect(mainProds[0].id).toBe(products.pro.id); + +// // Redeemer should have both Premium (purchased) and the Pro price discount +// expect(redeemerProds.length).toBe(1); +// const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve( +// redeemerProds.find((x) => x.id === products.premium.id) +// ?.subscription_ids?.[0]!, +// { +// expand: ["discounts"], +// } +// ); + +// const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => { +// if (typeof x === "string") { +// return x; +// } else if (typeof x === "object") { +// return x.coupon.id; +// } else return null; +// })!; + +// expect(redeemerStripeDiscounts.discounts.length).toBe(1); +// expect( +// typeof parsedDiscountID === "object" +// ? parsedDiscountID.coupon.id +// : parsedDiscountID +// ).toBe(rewards.paidProductWithConfig.id); + +// expect( +// redeemerProds.find((x) => x.id === products.premium.id) +// ).toBeDefined(); + +// expectProductV1Attached({ +// customer: mainCus, +// product: products.pro, +// status: CusProductStatus.Active, +// }); + +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.premium, +// status: CusProductStatus.Active, +// }); +// }); + +// test("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => { +// // Advance 31 days from current time to trigger next billing cycle +// await Promise.all( +// testClockIds.map((x) => +// advanceTestClock({ +// testClockId: x, +// numberOfDays: 31, +// waitForSeconds: 25, +// stripeCli, +// }) +// ) +// ); + +// // Test that both customers' invoices have pro_amount discount applied +// const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([ +// autumn.customers.get(mainCustomerId, { +// expand: [CusExpand.Invoices], +// }), +// autumn.customers.get(redeemer, { +// expand: [CusExpand.Invoices], +// }), +// ]); + +// // Check main customer (referrer) Pro invoice +// const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) => +// x.product_ids.includes(products.pro.id) +// ); +// if (mainProInvoice) { +// // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0) +// const proPrice = products.pro.prices[0].config.amount; // $10 +// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) +// const expectedTotal = proPrice - proAmount; // $0 + +// expect(mainProInvoice.total).toBe(expectedTotal); +// } + +// // Check redeemer Premium invoice (should have $10 off) +// const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) => +// x.product_ids.includes(products.premium.id) +// ); +// if (redeemerPremiumInvoice) { +// // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40) +// const premiumPrice = products.premium.prices[0].config.amount; // $50 +// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount) +// const expectedTotal = premiumPrice - proAmount; // $40 + +// expect(redeemerPremiumInvoice.total).toBe(expectedTotal); +// } + +// const dbCustomers = await Promise.all( +// [mainCustomerId, redeemer].map((x) => +// CusService.getFull({ +// db, +// idOrInternalId: x, +// orgId: org.id, +// env, +// inStatuses: [ +// CusProductStatus.Active, +// CusProductStatus.PastDue, +// CusProductStatus.Expired, +// ], +// }) +// ) +// ); + +// const expectedProducts = [ +// [ +// // Main referrer - has Pro with pro_amount discount applied +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// ], +// [ +// // Redeemer - has both Premium (purchased) and Pro (reward) with discounts +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// { name: "Premium", status: CusProductStatus.Active }, +// ], +// ]; + +// dbCustomers.forEach((customer, index) => { +// const expectedProductsForCustomer = expectedProducts[index]; +// expectedProductsForCustomer.forEach((expectedProduct) => { +// const matchingProduct = customer.customer_products.find( +// (cp) => +// cp.product.name === expectedProduct.name && +// cp.status === expectedProduct.status +// ); +// const unMatchedProduct = customer.customer_products.find( +// (cp) => cp.product.name === expectedProduct.name +// ); + +// expect(matchingProduct).toBeDefined(); +// }); +// }); +// }); +// }); diff --git a/server/tests/advanced/referrals/referrals1.backup.ts b/server/tests/advanced/referrals/referrals1.backup.ts new file mode 100644 index 000000000..2ed5f38ad --- /dev/null +++ b/server/tests/advanced/referrals/referrals1.backup.ts @@ -0,0 +1,292 @@ +import { + type AppEnv, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import { addDays } from "date-fns"; +import type { Stripe } from "stripe"; +import { addPrefixToProducts } from "tests/attach/utils.js"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../global.js"; + +const pro = constructProduct({ + id: "pro", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +// UNCOMMENT FROM HERE +describe(`${chalk.yellowBright( + "referrals1: Testing referrals (on checkout)", +)}`, () => { + const mainCustomerId = "main-referral-1"; + const alternateCustomerId = "alternate-referral-1"; + const redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: any; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + db = this.db; + org = this.org; + env = this.env; + + addPrefixToProducts({ + products: [pro], + prefix: mainCustomerId, + }); + + await createProducts({ + autumn: this.autumnJs, + products: [pro], + db, + orgId: org.id, + env, + customerId: mainCustomerId, + }); + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + fingerprint: "main-referral-1", + db, + org, + env, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockId = res.testClockId; + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: pro.id, + }); + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }), + ); + } + + batchCreate.push( + initCustomer({ + autumn: this.autumnJs, + customerId: alternateCustomerId, + fingerprint: "main-referral-1", + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }), + ); + await Promise.all(batchCreate); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.onCheckout.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.onCheckout.id, + }); + + assert.equal(referralCode2.code, referralCode.code); + }); + + it("should fail if same customer tries to redeem code again", async () => { + try { + await autumn.referrals.redeem({ + customerId: mainCustomerId, + code: referralCode.code, + }); + assert.fail("Own customer should not be able to redeem code"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode); + } + + try { + await autumn.referrals.redeem({ + customerId: alternateCustomerId, + code: referralCode.code, + }); + assert.fail( + "Own customer (same fingerprint) should not be able to redeem code", + ); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode); + } + }); + + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + redemptions.push(redemption); + } + + // Try redeem for redeemer1 again + try { + const redemption1 = await autumn.referrals.redeem({ + customerId: redeemers[0], + code: referralCode.code, + }); + assert.fail("Should not be able to redeem again"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + // return; + + it("should be triggered (and applied) when redeemers check out", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + + await autumn.attach({ + customer_id: redeemer, + product_id: products.pro.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[i].id); + + // Check if redemption is triggered + const count = i + 1; + + if (count > referralPrograms.onCheckout.max_redemptions) { + assert.equal(redemption.triggered, false); + assert.equal(redemption.applied, false); + } else { + assert.equal(redemption.triggered, true); + assert.equal(redemption.applied, i === 0); + } + + // Check stripe customer + const stripeCus = (await stripeCli.customers.retrieve( + mainCustomer.processor?.id, + )) as Stripe.Customer; + + assert.notEqual(stripeCus.discount, null); + } + }); + + let curTime = new Date(); + it("customer should have discount for first purchase", async () => { + curTime = addDays(addDays(curTime, 7), 4); + await advanceTestClock({ + testClockId, + advanceTo: curTime.getTime(), + stripeCli, + }); + + // 1. Get invoice + const { invoices } = await autumn.customers.get(mainCustomerId); + assert.equal(invoices.length, 2); + assert.equal(invoices[0].total, 0); + }); + + // it("customer should have discount for second purchase", async function () { + // // 2. Check that customer has another discount + // let stripeCus = (await stripeCli.customers.retrieve( + // mainCustomer.processor?.id, + // )) as Stripe.Customer; + + // assert.notEqual(stripeCus.discount, null); + + // // 2. Advance test clock to 1 month from start (trigger discount.deleted event) + // curTime = addHours(addMonths(new Date(), 1), 2); + // await advanceTestClock({ + // testClockId, + // advanceTo: curTime.getTime(), + // stripeCli, + // }); + + // // 3. Advance test clock to 1 month + 12 days from start (trigger new invoice) + // curTime = addDays(curTime, 12); + // await advanceTestClock({ + // testClockId, + // advanceTo: curTime.getTime(), + // stripeCli, + // }); + + // // // 3. Get invoice again + // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId); + + // assert.equal(invoices2.length, 3); + // assert.equal(invoices2[0].total, 0); + // }); +}); + +// const { testClockId: testClockId1, customer } = +// await initCustomerWithTestClock({ +// customerId: mainCustomerId, +// db: this.db, +// org: this.org, +// env: this.env, +// fingerprint: "main-referral-1", +// }); +// testClockId = testClockId1; +// mainCustomer = customer; + +// await autumn.attach({ +// customer_id: mainCustomerId, +// product_id: products.proWithTrial.id, +// }); + +// initCustomer({ +// customer_data: { +// id: alternateCustomerId, +// name: "Alternate Referral 1", +// email: "alternate-referral-1@example.com", +// fingerprint: "main-referral-1", +// }, +// db: this.db, +// org: this.org, +// env: this.env, +// }) diff --git a/server/tests/advanced/referrals/referrals1.test.ts b/server/tests/advanced/referrals/referrals1.test.ts new file mode 100644 index 000000000..8dee36af0 --- /dev/null +++ b/server/tests/advanced/referrals/referrals1.test.ts @@ -0,0 +1,220 @@ +import { + type AppEnv, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addDays } from "date-fns"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { 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 { products, referralPrograms } from "../../global.js"; + +const pro = constructProduct({ + id: "pro", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +describe(`${chalk.yellowBright( + "referrals1: Testing referrals (on checkout)", +)}`, () => { + const mainCustomerId = "main-referral-1"; + const alternateCustomerId = "alternate-referral-1"; + const redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"]; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: any; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + db = ctx.db; + org = ctx.org; + env = ctx.env; + + addPrefixToProducts({ + products: [pro], + prefix: mainCustomerId, + }); + + await createProducts({ + autumn: new AutumnInt({ secretKey: ctx.orgSecretKey }), + products: [pro], + db, + orgId: org.id, + env, + customerId: mainCustomerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + customerData: { fingerprint: "main-referral-1" }, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockId = res.testClockId; + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: pro.id, + }); + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: "success", + }), + ); + } + + batchCreate.push( + initCustomerV3({ + ctx, + customerId: alternateCustomerId, + customerData: { fingerprint: "main-referral-1" }, + attachPm: "success", + }), + ); + await Promise.all(batchCreate); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.onCheckout.id, + }); + + expect(referralCode.code).toBeDefined(); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.onCheckout.id, + }); + + expect(referralCode2.code).toBe(referralCode.code); + }); + + test("should fail if same customer tries to redeem code again", async () => { + try { + await autumn.referrals.redeem({ + customerId: mainCustomerId, + code: referralCode.code, + }); + throw new Error("Own customer should not be able to redeem code"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode); + } + + try { + await autumn.referrals.redeem({ + customerId: alternateCustomerId, + code: referralCode.code, + }); + throw new Error( + "Own customer (same fingerprint) should not be able to redeem code", + ); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode); + } + }); + + test("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + redemptions.push(redemption); + } + + // Try redeem for redeemer1 again + try { + const redemption1 = await autumn.referrals.redeem({ + customerId: redeemers[0], + code: referralCode.code, + }); + throw new Error("Should not be able to redeem again"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + test("should be triggered (and applied) when redeemers check out", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + + await autumn.attach({ + customer_id: redeemer, + product_id: products.pro.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[i].id); + + // Check if redemption is triggered + const count = i + 1; + + if (count > referralPrograms.onCheckout.max_redemptions) { + expect(redemption.triggered).toBe(false); + expect(redemption.applied).toBe(false); + } else { + expect(redemption.triggered).toBe(true); + expect(redemption.applied).toBe(i === 0); + } + + // Check stripe customer + const stripeCus = (await stripeCli.customers.retrieve( + mainCustomer.processor?.id, + )) as Stripe.Customer; + + expect(stripeCus.discount).not.toBe(null); + } + }); + + let curTime = new Date(); + test("customer should have discount for first purchase", async () => { + curTime = addDays(addDays(curTime, 7), 4); + await advanceTestClock({ + testClockId, + advanceTo: curTime.getTime(), + stripeCli, + }); + + // 1. Get invoice + const { invoices } = await autumn.customers.get(mainCustomerId); + expect(invoices.length).toBe(2); + expect(invoices[0].total).toBe(0); + }); +}); diff --git a/server/tests/advanced/referrals/referrals2.backup.ts b/server/tests/advanced/referrals/referrals2.backup.ts new file mode 100644 index 000000000..1aa238e1c --- /dev/null +++ b/server/tests/advanced/referrals/referrals2.backup.ts @@ -0,0 +1,174 @@ +import { + type AppEnv, + type Customer, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import { addDays } from "date-fns"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "tests/utils/init.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../global.js"; + +// UNCOMMENT FROM HERE +describe(`${chalk.yellowBright( + "referrals2: Testing referrals (immediate redemption)", +)}`, () => { + const mainCustomerId = "main-referral-2"; + const redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + let org: Organization; + let env: AppEnv; + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + org = this.org; + env = this.env; + + const { testClockId: testClockId1, customer } = await initCustomerV2({ + customerId: mainCustomerId, + db: this.db, + org: this.org, + env: this.env, + autumn, + }); + testClockId = testClockId1; + mainCustomer = customer; + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomer({ + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: true, + }), + ); + } + + await Promise.all(batchCreate); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.immediate.id, + }); + + assert.exists(referralCode.code); + }); + + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + const count = i + 1; + try { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + redemptions.push(redemption); + + if (count > referralPrograms.immediate.max_redemptions) { + assert.equal(redemption.triggered, false); + assert.equal(redemption.applied, false); + } else { + assert.fail("Should not be able to redeem again"); + } + } catch (error) { + if (count > referralPrograms.immediate.max_redemptions) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.ReferralCodeMaxRedemptionsReached); + } + } + } + + // Check stripe customer + const legacyStripe = createStripeCli({ + org: org, + env: env, + legacyVersion: true, + }); + + const stripeCus = (await legacyStripe.customers.retrieve( + mainCustomer.processor?.id, + { + expand: ["discount"], + }, + )) as Stripe.Customer; + + assert.notEqual(stripeCus.discount, null); + }); + + let curTime = new Date(); + it("customer should have discount for first purchase", async () => { + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + await timeout(3000); + + curTime = addDays(addDays(curTime, 7), 4); + await advanceTestClock({ + testClockId, + advanceTo: curTime.getTime(), + stripeCli, + waitForSeconds: 30, + }); + + // 1. Get invoice + const { invoices } = await autumn.customers.get(mainCustomerId); + + assert.equal(invoices!.length, 2); + assert.equal(invoices![0].total, 0); + }); + + // it("customer should have discount for second purchase", async function () { + // // 2. Check that customer has another discount + // let stripeCus = (await stripeCli.customers.retrieve( + // mainCustomer.processor?.id, + // )) as Stripe.Customer; + + // assert.notEqual(stripeCus.discount, null); + + // // 2. Advance test clock to 1 month from start (trigger discount.deleted event) + // curTime = addHours(addMonths(new Date(), 1), 2); + // await advanceTestClock({ + // testClockId, + // advanceTo: curTime.getTime(), + // stripeCli, + // }); + + // // 3. Advance test clock to 1 month + 7 days from start (trigger new invoice) + // curTime = addDays(curTime, 8); + // await advanceTestClock({ + // testClockId, + // advanceTo: curTime.getTime(), + // stripeCli, + // }); + + // // // 3. Get invoice again + // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId); + + // assert.equal(invoices2!.length, 3); + // assert.equal(invoices2![0].total, 0); + // }); +}); diff --git a/server/tests/advanced/referrals/referrals2.test.ts b/server/tests/advanced/referrals/referrals2.test.ts new file mode 100644 index 000000000..412e5b8eb --- /dev/null +++ b/server/tests/advanced/referrals/referrals2.test.ts @@ -0,0 +1,136 @@ +import { + type AppEnv, + type Customer, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addDays } from "date-fns"; +import type { Stripe } from "stripe"; +import { timeout } from "tests/utils/genUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { products, referralPrograms } from "../../global.js"; + +describe(`${chalk.yellowBright( + "referrals2: Testing referrals (immediate redemption)", +)}`, () => { + const mainCustomerId = "main-referral-2"; + const redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + org = ctx.org; + env = ctx.env; + + const { testClockId: testClockId1, customer } = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + }); + testClockId = testClockId1; + mainCustomer = customer; + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: "success", + }), + ); + } + + await Promise.all(batchCreate); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.immediate.id, + }); + + expect(referralCode.code).toBeDefined(); + }); + + test("should create redemption for each redeemer and fail if redeemed again", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + const count = i + 1; + try { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + redemptions.push(redemption); + + if (count > referralPrograms.immediate.max_redemptions) { + expect(redemption.triggered).toBe(false); + expect(redemption.applied).toBe(false); + } else { + throw new Error("Should not be able to redeem again"); + } + } catch (error) { + if (count > referralPrograms.immediate.max_redemptions) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.ReferralCodeMaxRedemptionsReached); + } + } + } + + // Check stripe customer + const legacyStripe = createStripeCli({ + org: org, + env: env, + legacyVersion: true, + }); + + const stripeCus = (await legacyStripe.customers.retrieve( + mainCustomer.processor?.id, + { + expand: ["discount"], + }, + )) as Stripe.Customer; + + expect(stripeCus.discount).not.toBe(null); + }); + + let curTime = new Date(); + test("customer should have discount for first purchase", async () => { + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + await timeout(3000); + + curTime = addDays(addDays(curTime, 7), 4); + await advanceTestClock({ + testClockId, + advanceTo: curTime.getTime(), + stripeCli, + waitForSeconds: 30, + }); + + // 1. Get invoice + const { invoices } = await autumn.customers.get(mainCustomerId); + + expect(invoices!.length).toBe(2); + expect(invoices![0].total).toBe(0); + }); +}); diff --git a/server/tests/advanced/referrals/referrals3.backup.ts b/server/tests/advanced/referrals/referrals3.backup.ts new file mode 100644 index 000000000..500f5294c --- /dev/null +++ b/server/tests/advanced/referrals/referrals3.backup.ts @@ -0,0 +1,141 @@ +import { + type Customer, + ErrCode, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { compareProductEntitlements } from "tests/utils/compare.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "tests/utils/init.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { features, products, referralPrograms } from "../../global.js"; + +// UNCOMMENT FROM HERE +describe(`${chalk.yellowBright( + "referrals3: Testing free product referrals", +)}`, () => { + const mainCustomerId = "main-referral-3"; + const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + + before(async function () { + await setupBefore(this); + autumn = this.autumn; + stripeCli = this.stripeCli; + + const { testClockId: testClockId1, customer } = + await initCustomerWithTestClock({ + customerId: mainCustomerId, + db: this.db, + org: this.org, + env: this.env, + fingerprint: "main-referral-3", + }); + testClockId = testClockId1; + mainCustomer = customer; + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomer({ + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: true, + }), + ); + } + + await Promise.all(batchCreate); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.freeProduct.id, + }); + + assert.exists(referralCode.code); + }); + + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + redemptions.push(redemption); + + // assert.equal(redemption.triggered, false); + // assert.equal(redemption.applied, false); + } + + // Try redeem for redeemer1 again + try { + const redemption1 = await autumn.referrals.redeem({ + customerId: redeemers[0], + code: referralCode.code, + }); + assert.fail("Should not be able to redeem again"); + } catch (error) { + assert.instanceOf(error, AutumnError); + assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + it("should be triggered (and applied) when redeemers check out", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + + await autumn.attach({ + customer_id: redeemer, + product_id: products.pro.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[i].id); + + // Check if redemption is triggered + const count = i + 1; + + if (count > referralPrograms.freeProduct.max_redemptions) { + assert.equal(redemption.triggered, false); + assert.equal(redemption.applied, false); + } else { + // 1. Check that main customer has free add on + compareProductEntitlements({ + customerId: mainCustomerId, + product: products.freeAddOn, + features, + quantity: count, + }); + + compareProductEntitlements({ + customerId: redeemer, + product: products.freeAddOn, + features, + }); + } + } + }); +}); diff --git a/server/tests/advanced/referrals/referrals3.test.ts b/server/tests/advanced/referrals/referrals3.test.ts new file mode 100644 index 000000000..cc1607016 --- /dev/null +++ b/server/tests/advanced/referrals/referrals3.test.ts @@ -0,0 +1,130 @@ +import { + type Customer, + ErrCode, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { compareProductEntitlements } from "tests/utils/compare.js"; +import { timeout } from "tests/utils/genUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { features, products, referralPrograms } from "../../global.js"; + +describe(`${chalk.yellowBright( + "referrals3: Testing free product referrals", +)}`, () => { + const mainCustomerId = "main-referral-3"; + const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let testClockId: string; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + + beforeAll(async () => { + autumn = new AutumnInt({ secretKey: ctx.orgSecretKey }); + stripeCli = ctx.stripeCli; + + const { testClockId: testClockId1, customer } = await initCustomerV3({ + ctx, + customerId: mainCustomerId, + customerData: { fingerprint: "main-referral-3" }, + }); + testClockId = testClockId1; + mainCustomer = customer; + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomerV3({ + ctx, + customerId: redeemer, + attachPm: "success", + }), + ); + } + + await Promise.all(batchCreate); + }); + + test("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.freeProduct.id, + }); + + expect(referralCode.code).toBeDefined(); + }); + + test("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + redemptions.push(redemption); + } + + // Try redeem for redeemer1 again + try { + const redemption1 = await autumn.referrals.redeem({ + customerId: redeemers[0], + code: referralCode.code, + }); + throw new Error("Should not be able to redeem again"); + } catch (error) { + expect(error).toBeInstanceOf(AutumnError); + expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode); + } + }); + + test("should be triggered (and applied) when redeemers check out", async () => { + for (let i = 0; i < redeemers.length; i++) { + const redeemer = redeemers[i]; + + await autumn.attach({ + customer_id: redeemer, + product_id: products.pro.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[i].id); + + // Check if redemption is triggered + const count = i + 1; + + if (count > referralPrograms.freeProduct.max_redemptions) { + expect(redemption.triggered).toBe(false); + expect(redemption.applied).toBe(false); + } else { + // 1. Check that main customer has free add on + compareProductEntitlements({ + customerId: mainCustomerId, + product: products.freeAddOn, + features, + quantity: count, + }); + + compareProductEntitlements({ + customerId: redeemer, + product: products.freeAddOn, + features, + }); + } + } + }); +}); diff --git a/server/tests/advanced/referrals/referrals4.backup.ts b/server/tests/advanced/referrals/referrals4.backup.ts new file mode 100644 index 000000000..2de7fc1a4 --- /dev/null +++ b/server/tests/advanced/referrals/referrals4.backup.ts @@ -0,0 +1,125 @@ +import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import { addDays, addHours } from "date-fns"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { compareProductEntitlements } from "tests/utils/compare.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "tests/utils/init.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { features, products, referralPrograms } from "../../global.js"; + +// UNCOMMENT FROM HERE +describe(`${chalk.yellowBright( + "referrals4: Testing free product referrals with trial", +)}`, () => { + const mainCustomerId = "main-referral-4"; + // let redeemers = ["referral4-r1", "referral4-r2"]; + const redeemerId = "referral4-r1"; + + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + let redeemer: Customer; + + let testClockId: string; + before(async function () { + await setupBefore(this); + autumn = this.autumn; + stripeCli = this.stripeCli; + + await initCustomer({ + customerId: mainCustomerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: true, + }); + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + const { testClockId: testClockId1, customer } = + await initCustomerWithTestClock({ + customerId: redeemerId, + db: this.db, + org: this.org, + env: this.env, + }); + + testClockId = testClockId1; + redeemer = customer; + }); + + it("should create referral code", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.freeProduct.id, + }); + + assert.exists(referralCode.code); + }); + + it("should create redemption for each redeemer and fail if redeemed again", async () => { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemerId, + code: referralCode.code, + }); + + redemptions.push(redemption); + }); + + it("should not be triggered because of trial", async () => { + await autumn.attach({ + customer_id: redeemerId, + product_id: products.proWithTrial.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[0].id); + + assert.equal(redemption.triggered, false); + }); + + it("should be triggered after trial ends", async () => { + const advanceTo = addHours( + addDays(new Date(), 7), + hoursToFinalizeInvoice, + ).getTime(); + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo, + waitForSeconds: 30, + }); + + const redemption = await autumn.redemptions.get(redemptions[0].id); + + assert.equal(redemption.triggered, true); + + compareProductEntitlements({ + customerId: mainCustomerId, + product: products.freeAddOn, + features, + quantity: 1, + }); + + compareProductEntitlements({ + customerId: redeemerId, + product: products.freeAddOn, + features, + quantity: 1, + }); + }); +}); diff --git a/server/tests/advanced/referrals/referrals4.test.ts b/server/tests/advanced/referrals/referrals4.test.ts new file mode 100644 index 000000000..e9c4c3047 --- /dev/null +++ b/server/tests/advanced/referrals/referrals4.test.ts @@ -0,0 +1,117 @@ +import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addDays, addHours } from "date-fns"; +import type { Stripe } from "stripe"; +import { compareProductEntitlements } from "tests/utils/compare.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { features, products, referralPrograms } from "../../global.js"; + +describe(`${chalk.yellowBright( + "referrals4: Testing free product referrals with trial", +)}`, () => { + const mainCustomerId = "main-referral-4"; + const redeemerId = "referral4-r1"; + + let autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + let referralCode: ReferralCode; + + const redemptions: RewardRedemption[] = []; + let mainCustomer: Customer; + let redeemer: Customer; + + let testClockId: string; + + beforeAll(async () => { + autumn = new AutumnInt({ secretKey: ctx.orgSecretKey }); + stripeCli = ctx.stripeCli; + + await initCustomerV3({ + ctx, + customerId: mainCustomerId, + attachPm: "success", + }); + + await autumn.attach({ + customer_id: mainCustomerId, + product_id: products.proWithTrial.id, + }); + + const { testClockId: testClockId1, customer } = await initCustomerV3({ + ctx, + customerId: redeemerId, + }); + + testClockId = testClockId1; + redeemer = customer; + }); + + test("should create referral code", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.freeProduct.id, + }); + + expect(referralCode.code).toBeDefined(); + }); + + test("should create redemption for each redeemer and fail if redeemed again", async () => { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemerId, + code: referralCode.code, + }); + + redemptions.push(redemption); + }); + + test("should not be triggered because of trial", async () => { + await autumn.attach({ + customer_id: redeemerId, + product_id: products.proWithTrial.id, + }); + + await timeout(3000); + + // Get redemption object + const redemption = await autumn.redemptions.get(redemptions[0].id); + + expect(redemption.triggered).toBe(false); + }); + + test("should be triggered after trial ends", async () => { + const advanceTo = addHours( + addDays(new Date(), 7), + hoursToFinalizeInvoice, + ).getTime(); + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo, + waitForSeconds: 30, + }); + + const redemption = await autumn.redemptions.get(redemptions[0].id); + + expect(redemption.triggered).toBe(true); + + compareProductEntitlements({ + customerId: mainCustomerId, + product: products.freeAddOn, + features, + quantity: 1, + }); + + compareProductEntitlements({ + customerId: redeemerId, + product: products.freeAddOn, + features, + quantity: 1, + }); + }); +}); diff --git a/server/tests/advanced/usage/sharedProducts.ts b/server/tests/advanced/usage/sharedProducts.ts new file mode 100644 index 000000000..5003e889a --- /dev/null +++ b/server/tests/advanced/usage/sharedProducts.ts @@ -0,0 +1,42 @@ +import { BillingInterval, ProductItemInterval } from "@autumn/shared"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { + constructFeatureItem, + constructArrearItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js"; + +/** + * Shared products for usage test group + * Matches global products.proWithOverage + */ + +export const sharedProWithOverage = constructProduct({ + id: "pro-with-overage", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 2000, // $20/month + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + // Overage pricing for usage beyond included + }), + ], +}); + +await (async () => { + await createSharedProducts({ + ctx, + products: [sharedProWithOverage], + }); +})(); diff --git a/server/tests/advanced/usage/usage1.backup.ts b/server/tests/advanced/usage/usage1.backup.ts new file mode 100644 index 000000000..489c0ea1d --- /dev/null +++ b/server/tests/advanced/usage/usage1.backup.ts @@ -0,0 +1,125 @@ +import type { Customer } from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { calculateMetered1Price } from "@/external/stripe/utils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { features, products } from "../../global.js"; +import { compareMainProduct } from "../../utils/compare.js"; +import { timeout } from "../../utils/genUtils.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; + +const testCase = "usage1"; + +describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { + const NUM_EVENTS = 50; + const customerId = testCase; + let testClockId: string; + let customer: Customer; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + stripeCli = this.stripeCli; + + const { customer: customer_, testClockId: testClockId_ } = + await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + customer = customer_; + testClockId = testClockId_; + }); + + it("should attach usage based product", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithOverage.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + + compareMainProduct({ + sent: products.proWithOverage, + cusRes: res, + }); + }); + + it("usage1: should send metered1 events", async () => { + const batchUpdates = []; + for (let i = 0; i < NUM_EVENTS; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(25000); + }); + + it("should have correct metered1 balance after sending events", async () => { + const res: any = await AutumnCli.entitled(customerId, features.metered1.id); + + expect(res!.allowed).to.be.true; + + const balance = res!.balances.find( + (balance: any) => balance.feature_id === features.metered1.id, + ); + + const proOverageAmt = + products.proWithOverage.entitlements.metered1.allowance; + + expect(res!.allowed, "should be allowed").to.be.true; + + expect(balance?.balance, "should have correct metered1 balance").to.equal( + proOverageAmt! - NUM_EVENTS, + ); + + expect(balance?.usage_allowed, "should have usage_allowed").to.be.true; + }); + + // Check invoice + it("should advance stripe test clock and wait for event", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + }); + + it("should have correct invoice amount", async () => { + const cusRes = await AutumnCli.getCustomer(customerId); + const invoices = cusRes!.invoices; + + // calculate price + const price = calculateMetered1Price({ + product: products.proWithOverage, + numEvents: NUM_EVENTS, + metered1Feature: features.metered1, + }); + + expect(invoices.length).to.equal(2); + + const invoice = invoices[0]; + + const basePrice = v1ProductToBasePrice({ + prices: products.proWithOverage.prices, + }); + + expect(invoice.total).to.equal( + price + basePrice, + "invoice total should be usage price + base price", + ); + }); +}); diff --git a/server/tests/advanced/usage/usage1.test.ts b/server/tests/advanced/usage/usage1.test.ts new file mode 100644 index 000000000..b6a3d7f1a --- /dev/null +++ b/server/tests/advanced/usage/usage1.test.ts @@ -0,0 +1,139 @@ +import type { Customer } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { calculateMetered1Price } from "@/external/stripe/utils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { timeout } from "../../utils/genUtils.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; +import { sharedProWithOverage } from "./sharedProducts.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js"; + +const testCase = "usage1"; + +describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => { + const NUM_EVENTS = 50; + const customerId = testCase; + let testClockId: string; + let customer: Customer; + let stripeCli: Stripe; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + + const { customer: customer_, testClockId: testClockId_ } = + await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + withTestClock: true, + attachPm: "success", + }); + + customer = customer_; + testClockId = testClockId_; + }); + + test("should attach usage based product", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedProWithOverage.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + + expectCustomerV0Correct({ + sent: sharedProWithOverage, + cusRes: res, + ctx, + }); + }); + + test("usage1: should send metered1 events", async () => { + const batchUpdates = []; + for (let i = 0; i < NUM_EVENTS; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: TestFeature.Messages, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(25000); + }); + + test("should have correct metered1 balance after sending events", async () => { + const res: any = await AutumnCli.entitled(customerId, TestFeature.Messages); + + expect(res!.allowed).toBe(true); + + const balance = res!.balances.find( + (balance: any) => balance.feature_id === TestFeature.Messages, + ); + + // Convert V2 product to V1 to access entitlements + const productV1 = convertProductV2ToV1({ + productV2: sharedProWithOverage, + orgId: ctx.org.id, + features: ctx.features, + }); + + const proOverageAmt = + productV1.entitlements.messages.allowance; + + expect(res!.allowed, "should be allowed").toBe(true); + + expect(balance?.balance, "should have correct metered1 balance").toBe( + proOverageAmt! - NUM_EVENTS, + ); + + expect(balance?.usage_allowed, "should have usage_allowed").toBe(true); + }); + + // Check invoice + test("should advance stripe test clock and wait for event", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + }); + + test("should have correct invoice amount", async () => { + const cusRes = await AutumnCli.getCustomer(customerId); + const invoices = cusRes!.invoices; + + // Convert V2 product to V1 for price calculations + const productV1 = convertProductV2ToV1({ + productV2: sharedProWithOverage, + orgId: ctx.org.id, + features: ctx.features, + }); + + // calculate price + const price = calculateMetered1Price({ + product: productV1, + numEvents: NUM_EVENTS, + metered1Feature: ctx.features[TestFeature.Messages], + }); + + expect(invoices.length).toBe(2); + + const invoice = invoices[0]; + + const basePrice = v1ProductToBasePrice({ + prices: productV1.prices, + }); + + expect(invoice.total, "invoice total should be usage price + base price").toBe( + price + basePrice, + ); + }); +}); diff --git a/server/tests/advanced/usage/usage2.backup.ts b/server/tests/advanced/usage/usage2.backup.ts new file mode 100644 index 000000000..3c5152fb7 --- /dev/null +++ b/server/tests/advanced/usage/usage2.backup.ts @@ -0,0 +1,136 @@ +import { expect } from "chai"; +import chalk from "chalk"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceProducts, creditSystems, features } from "../../global.js"; +import { getCreditsUsed } from "../../utils/advancedUsageUtils.js"; +import { compareMainProduct } from "../../utils/compare.js"; +import { timeout } from "../../utils/genUtils.js"; + +// FIRST, REGULAR CHECK GPU STARTER MONTHLY + +const testCase = "usage2"; +describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { + const customerId = testCase; + const PRECISION = 10; + const ASSERT_INVOICE_AMOUNT = true; + const CREDIT_MULTIPLIER = 100000; + + let testClockId = ""; + let totalCreditsUsed = 0; + + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const { testClockId: createdTestClockId } = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + testClockId = createdTestClockId; + + stripeCli = this.stripeCli; + }); + + it("should attach gpu system starter", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemStarter.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: advanceProducts.gpuSystemStarter, + cusRes: res, + }); + }); + + // Use up events + it("should send events and have correct balance (up to 10 DP)", async () => { + const eventCount = 20; + + const batchEvents = []; + for (let i = 0; i < eventCount; i++) { + const randomVal = new Decimal(Math.random().toFixed(PRECISION)) + .mul(CREDIT_MULTIPLIER) + .mul(Math.random() > 0.2 ? 1 : -1) + .toNumber(); + const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id; + + const creditsUsed = getCreditsUsed( + creditSystems.gpuCredits, + gpuId, + randomVal, + ); + + totalCreditsUsed = new Decimal(totalCreditsUsed) + .plus(creditsUsed) + .toNumber(); + + batchEvents.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: gpuId, + properties: { value: randomVal }, + }), + ); + } + + await Promise.all(batchEvents); + + await timeout(10000); + + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + creditSystems.gpuCredits.id, + true, + ); + + const creditAllowance = + advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + + expect(allowed).to.be.true; + expect(balanceObj!.balance).to.equal( + new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), + ); + // console.log(" - Total credits used: ", totalCreditsUsed); + // console.log(" - Balance: ", balanceObj!.balance); + }); + + // Check invoice.created event + it("should have correct invoice amount / updated meter balance", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: ASSERT_INVOICE_AMOUNT, + }); + // const res = await AutumnCli.getCustomer(customerId); + // const invoices = res!.invoices; + // if (ASSERT_INVOICE_AMOUNT) { + // await checkUsageInvoiceAmount({ + // invoices, + // totalUsage: totalCreditsUsed, + // product: advanceProducts.gpuSystemStarter, + // featureId: creditSystems.gpuCredits.id, + // }); + // } else { + // const { allowed, balanceObj }: any = await AutumnCli.entitled( + // customerId, + // creditSystems.gpuCredits.id, + // true, + // ); + // const allowance = + // advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + // assert.equal(balanceObj.balance, allowance); + // } + }); +}); diff --git a/server/tests/advanced/usage/usage2.test.ts b/server/tests/advanced/usage/usage2.test.ts new file mode 100644 index 000000000..ef6357fcf --- /dev/null +++ b/server/tests/advanced/usage/usage2.test.ts @@ -0,0 +1,116 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { advanceClockForInvoice } from "tests/utils/stripeUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceProducts, creditSystems, features } from "../../global.js"; +import { getCreditsUsed } from "../../utils/advancedUsageUtils.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { timeout } from "../../utils/genUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; + +// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter) +// These products are not yet converted to ProductV2 format in sharedProducts.ts +// The test has been migrated to Bun but still uses ProductV1 from global.ts + +const testCase = "usage2"; +describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => { + const customerId = testCase; + const PRECISION = 10; + const ASSERT_INVOICE_AMOUNT = true; + const CREDIT_MULTIPLIER = 100000; + + let testClockId = ""; + let totalCreditsUsed = 0; + + let stripeCli: Stripe; + + beforeAll(async () => { + const { testClockId: createdTestClockId } = await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + withTestClock: true, + attachPm: "success", + }); + + testClockId = createdTestClockId; + + stripeCli = ctx.stripeCli; + }); + + test("should attach gpu system starter", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemStarter.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ + sent: advanceProducts.gpuSystemStarter, + cusRes: res, + ctx, + }); + }); + + // Use up events + test("should send events and have correct balance (up to 10 DP)", async () => { + const eventCount = 20; + + const batchEvents = []; + for (let i = 0; i < eventCount; i++) { + const randomVal = new Decimal(Math.random().toFixed(PRECISION)) + .mul(CREDIT_MULTIPLIER) + .mul(Math.random() > 0.2 ? 1 : -1) + .toNumber(); + const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id; + + const creditsUsed = getCreditsUsed( + creditSystems.gpuCredits, + gpuId, + randomVal, + ); + + totalCreditsUsed = new Decimal(totalCreditsUsed) + .plus(creditsUsed) + .toNumber(); + + batchEvents.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: gpuId, + properties: { value: randomVal }, + }), + ); + } + + await Promise.all(batchEvents); + + await timeout(10000); + + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + creditSystems.gpuCredits.id, + true, + ); + + const creditAllowance = + advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + + expect(allowed).toBe(true); + expect(balanceObj!.balance).toBe( + new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), + ); + }); + + // Check invoice.created event + test("should have correct invoice amount / updated meter balance", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: ASSERT_INVOICE_AMOUNT, + }); + }); +}); diff --git a/server/tests/advanced/usage/usage3.backup.ts b/server/tests/advanced/usage/usage3.backup.ts new file mode 100644 index 000000000..21d4c76c5 --- /dev/null +++ b/server/tests/advanced/usage/usage3.backup.ts @@ -0,0 +1,140 @@ +import chalk from "chalk"; +import { advanceProducts } from "../../global.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { sendGPUEvents } from "../../utils/advancedUsageUtils.js"; +import { advanceTestClock } from "../../utils/stripeUtils.js"; +import { assert, expect } from "chai"; +import { Decimal } from "decimal.js"; +import { compareMainProduct } from "../../utils/compare.js"; +import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { setupBefore } from "tests/before.js"; +import Stripe from "stripe"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; + +const testCase = "usage3"; +const ASSERT_INVOICE_AMOUNT = true; + +describe(`${chalk.yellowBright( + "usage3: upgrade from GPU starter monthly to GPU pro monthly", +)}`, () => { + const customerId = "usage3"; + let testClockId = ""; + let totalCreditsUsed = 0; + let stripeCli: Stripe; + let curUnix = 0; + + before(async function () { + await setupBefore(this); + let { testClockId: insertedTestClockId } = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + testClockId = insertedTestClockId; + stripeCli = this.stripeCli; + }); + + // 1. Attach GPU starter monthly + it("usage3: should attach GPU starter monthly", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemStarter.id, + }); + }); + + // 2. Send 20 events + it("usage3: should send 20 events", async function () { + let eventCount = 20; + const { creditsUsed } = await sendGPUEvents({ + customerId, + eventCount, + }); + + totalCreditsUsed = creditsUsed; + }); + + // 3. Advance test clock by 15 days and upgrade + it("should advance test clock by 15 days and upgrade to GPU pro monthly", async function () { + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + numberOfDays: 15, + }); + + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemPro.id, + }); + + // MAKE SURE STRIPE SUB ONLY HAS GPU PRO + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: advanceProducts.gpuSystemPro, + cusRes: res, + }); + + let subscriptionId = res.products[0].subscription_ids![0]!; + await checkSubscriptionContainsProducts({ + db: this.db, + org: this.org, + env: this.env, + subscriptionId, + productIds: [advanceProducts.gpuSystemPro.id], + }); + }); + + // 4. Check invoice for 15 days of starter usage + it("should have invoice for 15 days of starter usage", async function () { + const res = await AutumnCli.getCustomer(customerId); + const invoices = res!.invoices; + + let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount; + let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount; + + let { subs } = await getSubsFromCusId({ + db: this.db, + org: this.org, + env: this.env, + customerId, + stripeCli, + productId: advanceProducts.gpuSystemPro.id, + }); + + let sub = subs[0]; + + const { start, end } = subToPeriodStartEnd({ sub }); + let baseDiff = calculateProrationAmount({ + periodStart: start * 1000, + periodEnd: end * 1000, + now: curUnix, + amount: basePrice2 - basePrice1, + allowNegative: true, + }); + + let usagePrice = advanceProducts.gpuSystemStarter.prices[1]; + let overage = + totalCreditsUsed - + advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + + let overagePrice = priceToInvoiceAmount({ + price: usagePrice, + overage, + }); + + let calculatedTotal = new Decimal(baseDiff) + .plus(overagePrice) + .toDecimalPlaces(2) + .toNumber(); + + expect(invoices[0].total).to.equal(calculatedTotal); + }); +}); diff --git a/server/tests/advanced/usage/usage3.test.ts b/server/tests/advanced/usage/usage3.test.ts new file mode 100644 index 000000000..0e660b3a8 --- /dev/null +++ b/server/tests/advanced/usage/usage3.test.ts @@ -0,0 +1,144 @@ +import chalk from "chalk"; +import { advanceProducts } from "../../global.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { sendGPUEvents } from "../../utils/advancedUsageUtils.js"; +import { advanceTestClock } from "../../utils/stripeUtils.js"; +import { expect } from "bun:test"; +import { Decimal } from "decimal.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { beforeAll, describe, test } from "bun:test"; +import Stripe from "stripe"; +import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; + +// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter, gpuSystemPro) +// These products are not yet converted to ProductV2 format in sharedProducts.ts +// The test has been migrated to Bun but still uses ProductV1 from global.ts + +const testCase = "usage3"; +const ASSERT_INVOICE_AMOUNT = true; + +describe(`${chalk.yellowBright( + "usage3: upgrade from GPU starter monthly to GPU pro monthly", +)}`, () => { + const customerId = "usage3"; + let testClockId = ""; + let totalCreditsUsed = 0; + let stripeCli: Stripe; + let curUnix = 0; + + beforeAll(async () => { + let { testClockId: insertedTestClockId } = await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + withTestClock: true, + attachPm: "success", + }); + + testClockId = insertedTestClockId; + stripeCli = ctx.stripeCli; + }); + + // 1. Attach GPU starter monthly + test("usage3: should attach GPU starter monthly", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemStarter.id, + }); + }); + + // 2. Send 20 events + test("usage3: should send 20 events", async () => { + let eventCount = 20; + const { creditsUsed } = await sendGPUEvents({ + customerId, + eventCount, + }); + + totalCreditsUsed = creditsUsed; + }); + + // 3. Advance test clock by 15 days and upgrade + test("should advance test clock by 15 days and upgrade to GPU pro monthly", async () => { + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + numberOfDays: 15, + }); + + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuSystemPro.id, + }); + + // MAKE SURE STRIPE SUB ONLY HAS GPU PRO + + const res = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ + sent: advanceProducts.gpuSystemPro, + cusRes: res, + ctx, + }); + + let subscriptionId = res.products[0].subscription_ids![0]!; + await checkSubscriptionContainsProducts({ + db: ctx.db, + org: ctx.org, + env: ctx.env, + subscriptionId, + productIds: [advanceProducts.gpuSystemPro.id], + }); + }); + + // 4. Check invoice for 15 days of starter usage + test("should have invoice for 15 days of starter usage", async () => { + const res = await AutumnCli.getCustomer(customerId); + const invoices = res!.invoices; + + let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount; + let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount; + + let { subs } = await getSubsFromCusId({ + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + stripeCli, + productId: advanceProducts.gpuSystemPro.id, + }); + + let sub = subs[0]; + + const { start, end } = subToPeriodStartEnd({ sub }); + let baseDiff = calculateProrationAmount({ + periodStart: start * 1000, + periodEnd: end * 1000, + now: curUnix, + amount: basePrice2 - basePrice1, + allowNegative: true, + }); + + let usagePrice = advanceProducts.gpuSystemStarter.prices[1]; + let overage = + totalCreditsUsed - + advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!; + + let overagePrice = priceToInvoiceAmount({ + price: usagePrice, + overage, + }); + + let calculatedTotal = new Decimal(baseDiff) + .plus(overagePrice) + .toDecimalPlaces(2) + .toNumber(); + + expect(invoices[0].total).toBe(calculatedTotal); + }); +}); diff --git a/server/tests/advanced/usage/usage4.backup.ts b/server/tests/advanced/usage/usage4.backup.ts new file mode 100644 index 000000000..181e1cd24 --- /dev/null +++ b/server/tests/advanced/usage/usage4.backup.ts @@ -0,0 +1,172 @@ +import type { Customer } from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceProducts, creditSystems } from "../../global.js"; +import { + checkCreditBalance, + checkUsageInvoiceAmount, + sendGPUEvents, +} from "../../utils/advancedUsageUtils.js"; +import { compareMainProduct } from "../../utils/compare.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; + +// THIRD, TEST GPU PRO ANNUAL + +const testCase = "usage4"; + +describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { + const customerId = testCase; + let totalCreditsUsed = 0; + + let testClockId = ""; + let customer: Customer; + let stripeCli: Stripe; + + before(async function () { + await setupBefore(this); + const res = await initCustomer({ + customerId, + org: this.org, + env: this.env, + db: this.db, + autumn: this.autumnJs, + attachPm: "success", + }); + + testClockId = res.testClockId; + customer = res.customer; + stripeCli = this.stripeCli; + }); + + it("should attach GPU starter annual", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuStarterAnnual.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + compareMainProduct({ + sent: advanceProducts.gpuStarterAnnual, + cusRes: res, + }); + + expect(res!.invoices.length).to.equal(1); + }); + + it("should send 20 events and have correct balance", async () => { + const eventCount = 20; + const { creditsUsed } = await sendGPUEvents({ + customerId, + eventCount, + }); + + totalCreditsUsed = creditsUsed; + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); + + it("should have invoice after a month and correct balance", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + + const res = await AutumnCli.getCustomer(customerId); + const invoices = res!.invoices; + + const invoiceIndex = invoices.findIndex((invoice: any) => + invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), + ); + + await checkUsageInvoiceAmount({ + invoices, + totalUsage: totalCreditsUsed, + product: advanceProducts.gpuStarterAnnual, + featureId: creditSystems.gpuCredits.id, + invoiceIndex, + includeBase: false, + }); + + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed: 0, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); +}); + +// // Advance by 1 year and check if latest invoice is correct +// it.skip("should have correct invoice after 1 year", async function () { +// const stripeCli = createStripeCli({ org: this.org, env: this.env }); + +// // 1. Advance by 11 months +// let numberOfMonths = 11; +// await advanceMonths({ +// stripeCli, +// testClockId, +// numberOfMonths, +// }); + +// // 2. Send 20 events +// let eventCount = 20; +// const { creditsUsed } = await sendGPUEvents({ +// customerId, +// eventCount, +// }); + +// let totalCreditsUsed = creditsUsed; +// console.log(" - Total credits used: ", totalCreditsUsed); + +// // Advance by a month and check for usage +// await advanceClockForInvoice({ +// stripeCli, +// testClockId, +// waitForMeterUpdate: true, +// startingFrom: addMonths(new Date(), numberOfMonths), +// }); + +// const res = await AutumnCli.getCustomer(customerId); +// const invoices = res!.invoices; + +// let usagePrice = await getUsageInArrearPrice({ +// org: this.org, +// env: this.env, +// productId: advanceProducts.gpuStarterAnnual.id, +// }); + +// // Get billing meter event summary +// let eventSummary = await checkBillingMeterEventSummary({ +// stripeCli, +// startTime: addMonths(new Date(), 11), +// stripeMeterId: usagePrice?.config?.stripe_meter_id, +// stripeCustomerId: customer.processor.id, +// }); + +// try { +// assert.exists(eventSummary); +// assert.equal( +// eventSummary?.aggregated_value, +// Math.round(totalCreditsUsed), +// ); +// assert.equal(invoices.length, 13 + 2); +// } catch (error) { +// console.group(); +// console.log(" - Event summary: ", eventSummary); +// console.log(" - Total credits used: ", totalCreditsUsed); +// console.log(" - Last 3 invoices: ", invoices.slice(-3)); +// console.groupEnd(); +// throw error; +// } +// }); diff --git a/server/tests/advanced/usage/usage4.test.ts b/server/tests/advanced/usage/usage4.test.ts new file mode 100644 index 000000000..c950aadce --- /dev/null +++ b/server/tests/advanced/usage/usage4.test.ts @@ -0,0 +1,112 @@ +import type { Customer } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnCli } from "../../cli/AutumnCli.js"; +import { advanceProducts, creditSystems } from "../../global.js"; +import { + checkCreditBalance, + checkUsageInvoiceAmount, + sendGPUEvents, +} from "../../utils/advancedUsageUtils.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { advanceClockForInvoice } from "../../utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; + +// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuStarterAnnual) +// These products are not yet converted to ProductV2 format in sharedProducts.ts +// The test has been migrated to Bun but still uses ProductV1 from global.ts +// However, it does use checkUsageInvoiceAmountV2 for the V2 helper function + +const testCase = "usage4"; + +describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => { + const customerId = testCase; + let totalCreditsUsed = 0; + + let testClockId = ""; + let customer: Customer; + let stripeCli: Stripe; + + beforeAll(async () => { + const res = await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + withTestClock: true, + attachPm: "success", + }); + + testClockId = res.testClockId; + customer = res.customer; + stripeCli = ctx.stripeCli; + }); + + test("should attach GPU starter annual", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: advanceProducts.gpuStarterAnnual.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ + sent: advanceProducts.gpuStarterAnnual, + cusRes: res, + ctx, + }); + + expect(res!.invoices.length).toBe(1); + }); + + test("should send 20 events and have correct balance", async () => { + const eventCount = 20; + const { creditsUsed } = await sendGPUEvents({ + customerId, + eventCount, + }); + + totalCreditsUsed = creditsUsed; + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); + + test("should have invoice after a month and correct balance", async () => { + await advanceClockForInvoice({ + stripeCli, + testClockId, + waitForMeterUpdate: true, + }); + + const res = await AutumnCli.getCustomer(customerId); + const invoices = res!.invoices; + + const invoiceIndex = invoices.findIndex((invoice: any) => + invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), + ); + + // NOTE: Using checkUsageInvoiceAmount (V1) as gpuStarterAnnual is not yet converted to V2 + // When GPU products are migrated to ProductV2, this should use checkUsageInvoiceAmountV2 + await checkUsageInvoiceAmount({ + invoices, + totalUsage: totalCreditsUsed, + product: advanceProducts.gpuStarterAnnual, + featureId: creditSystems.gpuCredits.id, + invoiceIndex, + includeBase: false, + }); + + await checkCreditBalance({ + customerId, + featureId: creditSystems.gpuCredits.id, + totalCreditsUsed: 0, + originalAllowance: + advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!, + }); + }); +}); diff --git a/server/tests/archives/basic10.backup.test.ts b/server/tests/archives/basic10.backup.ts similarity index 100% rename from server/tests/archives/basic10.backup.test.ts rename to server/tests/archives/basic10.backup.ts diff --git a/server/tests/attach/downgrade/downgrade5.test.ts b/server/tests/attach/downgrade/downgrade5.test.ts index a48d5c599..a52518db6 100644 --- a/server/tests/attach/downgrade/downgrade5.test.ts +++ b/server/tests/attach/downgrade/downgrade5.test.ts @@ -4,13 +4,16 @@ import chalk from "chalk"; import { addHours, addMonths } from "date-fns"; import type Stripe from "stripe"; import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { products } from "tests/global.js"; -import { compareMainProduct } from "tests/utils/compare.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { + sharedProProduct, + sharedPremiumProduct, +} from "./sharedProducts.js"; const testCase = "downgrade5"; describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => { @@ -36,30 +39,31 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa test("should attach premium", async () => { await AutumnCli.attach({ customerId: customerId, - productId: products.premium.id, + productId: sharedPremiumProduct.id, }); }); test("should attach pro", async () => { await AutumnCli.attach({ customerId: customerId, - productId: products.pro.id, + productId: sharedProProduct.id, }); }); test("should have correct product and entitlements for scheduled pro", async () => { const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premium, + expectCustomerV0Correct({ + sent: sharedPremiumProduct, cusRes: res, + ctx, }); const { products: resProducts } = res; const resPro = resProducts.find( (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled, + p.id === sharedProProduct.id && p.status === CusProductStatus.Scheduled, ); expect(resPro).toBeDefined(); @@ -68,20 +72,21 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa test("should attach premium and remove scheduled pro", async () => { await AutumnCli.attach({ customerId: customerId, - productId: products.premium.id, + productId: sharedPremiumProduct.id, }); const res = await AutumnCli.getCustomer(customerId); const resPro = res.products.find( (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled, + p.id === sharedProProduct.id && p.status === CusProductStatus.Scheduled, ); expect(resPro).toBeUndefined(); - compareMainProduct({ - sent: products.premium, + expectCustomerV0Correct({ + sent: sharedPremiumProduct, cusRes: res, + ctx, }); }); @@ -89,7 +94,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa test("should attach pro, advance stripe clock and have pro is attached", async () => { await AutumnCli.attach({ customerId: customerId, - productId: products.pro.id, + productId: sharedProProduct.id, }); await advanceTestClock({ @@ -103,9 +108,10 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa }); const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.pro, + expectCustomerV0Correct({ + sent: sharedProProduct, cusRes: res, + ctx, }); }); }); diff --git a/server/tests/attach/downgrade/downgrade6.test.ts b/server/tests/attach/downgrade/downgrade6.test.ts index 2bb182d75..ba80aa380 100644 --- a/server/tests/attach/downgrade/downgrade6.test.ts +++ b/server/tests/attach/downgrade/downgrade6.test.ts @@ -2,11 +2,14 @@ import { beforeAll, describe, test } from "bun:test"; import type { Customer } from "@autumn/shared"; import chalk from "chalk"; import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { products } from "tests/global.js"; -import { compareMainProduct } from "tests/utils/compare.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { + sharedFreeProduct, + sharedPremiumProduct, +} from "./sharedProducts.js"; const testCase = "downgrade6"; describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => { @@ -32,7 +35,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => { test("should attach premium", async () => { await autumn.attach({ customer_id: customerId, - product_id: products.premium.id, + product_id: sharedPremiumProduct.id, }); }); @@ -45,7 +48,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => { // await AutumnCli.expire(cusProduct!.id); await autumn.cancel({ customer_id: customerId, - product_id: products.premium.id, + product_id: sharedPremiumProduct.id, cancel_immediately: true, }); }); @@ -53,9 +56,10 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => { test("should have correct product and entitlements after expiration", async () => { const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.free, + expectCustomerV0Correct({ + sent: sharedFreeProduct, cusRes: res, + ctx, }); }); }); diff --git a/server/tests/attach/downgrade/downgrade7.test.ts b/server/tests/attach/downgrade/downgrade7.test.ts index 5fc263f38..e56ea7a57 100644 --- a/server/tests/attach/downgrade/downgrade7.test.ts +++ b/server/tests/attach/downgrade/downgrade7.test.ts @@ -3,12 +3,15 @@ import type { Customer } from "@autumn/shared"; import chalk from "chalk"; import type Stripe from "stripe"; import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { products } from "tests/global.js"; -import { compareMainProduct } from "tests/utils/compare.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { + sharedProProduct, + sharedPremiumProduct, +} from "./sharedProducts.js"; const testCase = "downgrade7"; describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}`, () => { @@ -38,12 +41,12 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)} test("should attach premium, then attach pro", async () => { await AutumnCli.attach({ customerId: customerId, - productId: products.premium.id, + productId: sharedPremiumProduct.id, }); await AutumnCli.attach({ customerId: customerId, - productId: products.pro.id, + productId: sharedProProduct.id, }); }); @@ -51,13 +54,13 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)} // const cusProduct = await findCusProductById({ // db: this.db, // internalCustomerId: customer.internal_id, - // productId: products.pro.id, + // productId: sharedProProduct.id, // }); // expect(cusProduct).to.exist; await autumn.cancel({ customer_id: customerId, - product_id: products.pro.id, + product_id: sharedProProduct.id, cancel_immediately: true, }); // await AutumnCli.expire(cusProduct!.id); @@ -66,15 +69,16 @@ describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)} test("should have correct product and entitlements (premium)", async () => { // Check that free is attached const res = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: products.premium, + expectCustomerV0Correct({ + sent: sharedPremiumProduct, cusRes: res, + ctx, }); const { subs } = await getSubsFromCusId({ stripeCli, customerId: customerId, - productId: products.premium.id, + productId: sharedPremiumProduct.id, db: ctx.db, org: ctx.org, env: ctx.env, diff --git a/server/tests/attach/downgrade/sharedProducts.ts b/server/tests/attach/downgrade/sharedProducts.ts new file mode 100644 index 000000000..6656cdc4c --- /dev/null +++ b/server/tests/attach/downgrade/sharedProducts.ts @@ -0,0 +1,72 @@ +import { BillingInterval, ProductItemInterval } from "@autumn/shared"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js"; + +/** + * Shared products for downgrade test group + * Matches global products.free, products.pro, products.premium + */ + +export const sharedFreeProduct = constructProduct({ + id: "shared-downgrade-free", + type: "free", + isDefault: true, + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 5, + interval: ProductItemInterval.Month, + }), + ], +}); + +export const sharedProProduct = constructProduct({ + id: "shared-downgrade-pro", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Dashboard, + isBoolean: true, + }), + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructFeatureItem({ + featureId: TestFeature.Admin, + unlimited: true, + }), + constructPriceItem({ + price: 2000, + interval: BillingInterval.Month, + }), + ], +}); + +export const sharedPremiumProduct = constructProduct({ + id: "shared-downgrade-premium", + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 5000, + interval: BillingInterval.Month, + }), + ], +}); + +await (async () => { + await createSharedProducts({ + ctx, + products: [sharedFreeProduct, sharedProProduct, sharedPremiumProduct], + }); +})(); diff --git a/server/tests/attach/migrations/migration1.ts b/server/tests/attach/migrations/migration1.test.ts similarity index 73% rename from server/tests/attach/migrations/migration1.ts rename to server/tests/attach/migrations/migration1.test.ts index b23be8a20..a9c5213b0 100644 --- a/server/tests/attach/migrations/migration1.ts +++ b/server/tests/attach/migrations/migration1.test.ts @@ -1,25 +1,19 @@ -import type { - AppEnv, - LimitedItem, - Organization, - ProductV2, -} from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import type { LimitedItem, ProductV2 } from "@autumn/shared"; import chalk from "chalk"; import { addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; import { defaultApiVersion } from "tests/constants.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { replaceItems } from "../utils.js"; import { runMigrationTest } from "./runMigrationTest.js"; const messagesItem = constructFeatureItem({ @@ -44,55 +38,37 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product` const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [free], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [free], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach free product", async () => { + test("should attach free product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: free, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, skipSubCheck: true, }); }); @@ -100,7 +76,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product` let newFree: ProductV2; const increaseMessagesBy = 100; const reduceWordsBy = 50; - it("should update product to new version", async () => { + test("should update product to new version", async () => { newFree = structuredClone(free); let newItems = replaceItems({ @@ -129,7 +105,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product` }); }); - it("should attach track usage and get correct balance", async () => { + test("should attach track usage and get correct balance", async () => { const wordsUsage = 25; const messagesUsage = 20; await autumn.track({ @@ -146,7 +122,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product` await timeout(2000); await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addWeeks(Date.now(), 1).getTime(), waitForSeconds: 30, @@ -161,20 +137,20 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product` to_version: 2, }); - await timeout(4000); + await new Promise((resolve) => setTimeout(resolve, 4000)); // 1. Get features customer = await autumn.customers.get(customerId); await runMigrationTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, fromProduct: free, toProduct: newFree, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, usage: [ { featureId: TestFeature.Words, diff --git a/server/tests/attach/migrations/migration2.ts b/server/tests/attach/migrations/migration2.test.ts similarity index 63% rename from server/tests/attach/migrations/migration2.ts rename to server/tests/attach/migrations/migration2.test.ts index 88e40bc17..1816397ec 100644 --- a/server/tests/attach/migrations/migration2.ts +++ b/server/tests/attach/migrations/migration2.test.ts @@ -1,18 +1,9 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { - AppEnv, - BillingInterval, - Organization, - ProductItemInterval, - ProductV2, -} from "@autumn/shared"; +import { BillingInterval, ProductItemInterval, ProductV2 } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; -import Stripe from "stripe"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { setupBefore } from "tests/before.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "../utils.js"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; @@ -23,6 +14,8 @@ import { addWeeks } from "date-fns"; import { defaultApiVersion } from "tests/constants.js"; import { runMigrationTest } from "./runMigrationTest.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; let wordsItem = constructArrearItem({ featureId: TestFeature.Words, @@ -37,64 +30,46 @@ export let pro = constructProduct({ const testCase = "migrations2"; describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage product`)}`, () => { - let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion }); + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; - let curUnix = new Date().getTime(); + const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach free product", async function () { + test("should attach free product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: pro, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); let newPro: ProductV2; - let increaseWordsBy = 1500; - it("should update product to new version", async function () { + const increaseWordsBy = 1500; + test("should update product to new version", async () => { newPro = structuredClone(pro); let newItems = replaceItems({ @@ -121,8 +96,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage pro }); }); - it("should attach track usage and get correct balance", async function () { - let wordsUsage = 120000; + test("should attach track usage and get correct balance", async () => { + const wordsUsage = 120000; await autumn.track({ customer_id: customerId, value: wordsUsage, @@ -130,7 +105,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage pro }); await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addWeeks(Date.now(), 1).getTime(), }); @@ -146,13 +121,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage pro await runMigrationTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, fromProduct: pro, toProduct: newPro, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, usage: [ { featureId: TestFeature.Words, diff --git a/server/tests/attach/migrations/migration3.ts b/server/tests/attach/migrations/migration3.test.ts similarity index 67% rename from server/tests/attach/migrations/migration3.ts rename to server/tests/attach/migrations/migration3.test.ts index 74f64348e..53deb2b1f 100644 --- a/server/tests/attach/migrations/migration3.ts +++ b/server/tests/attach/migrations/migration3.test.ts @@ -1,25 +1,19 @@ -import { - type AppEnv, - BillingInterval, - type Organization, - ProductItemInterval, - type ProductV2, -} from "@autumn/shared"; +import { BillingInterval, ProductItemInterval, type ProductV2 } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addDays } from "date-fns"; import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { defaultApiVersion } from "tests/constants.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { replaceItems } from "../utils.js"; import { runMigrationTest } from "./runMigrationTest.js"; const wordsItem = constructArrearItem({ @@ -39,61 +33,43 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach free product", async () => { + test("should attach free product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: pro, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); let newPro: ProductV2; const increaseWordsBy = 1500; - it("should update product to new version", async () => { + test("should update product to new version", async () => { newPro = structuredClone(pro); let newItems = replaceItems({ @@ -121,7 +97,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria }); }); - it("should attach track usage and get correct balance", async () => { + test("should attach track usage and get correct balance", async () => { const wordsUsage = 120000; await autumn.track({ customer_id: customerId, @@ -130,7 +106,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria }); await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addDays(Date.now(), 4).getTime(), }); @@ -139,13 +115,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria await runMigrationTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, fromProduct: pro, toProduct: newPro, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, usage: [ { featureId: TestFeature.Words, diff --git a/server/tests/attach/migrations/migration4.ts b/server/tests/attach/migrations/migration4.test.ts similarity index 63% rename from server/tests/attach/migrations/migration4.ts rename to server/tests/attach/migrations/migration4.test.ts index 9f409dbdb..05526d6a6 100644 --- a/server/tests/attach/migrations/migration4.ts +++ b/server/tests/attach/migrations/migration4.test.ts @@ -1,19 +1,16 @@ -import type { AppEnv, Organization } from "@autumn/shared"; -import { expect } from "chai"; +import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { defaultApiVersion } from "tests/constants.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { addPrefixToProducts } from "../utils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import { runMigrationTest } from "./runMigrationTest.js"; const wordsItem = constructArrearItem({ @@ -44,59 +41,41 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro, proWithTrial], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach pro product", async () => { + test("should attach pro product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: pro, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); - it("should update product to new version", async () => { + test("should update product to new version", async () => { proWithTrial.version = 2; await autumn.products.update(pro.id, { items: proWithTrial.items, @@ -104,7 +83,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi }); }); - it("should attach track usage and get correct balance", async () => { + test("should attach track usage and get correct balance", async () => { const wordsUsage = 120000; await autumn.track({ customer_id: customerId, @@ -116,13 +95,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi const { stripeSubs, cusProduct } = await runMigrationTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, fromProduct: pro, toProduct: proWithTrial, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, usage: [ { featureId: TestFeature.Words, @@ -131,7 +110,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi ], }); - expect(stripeSubs[0].trial_end).to.equal(null); - expect(cusProduct?.free_trial).to.equal(null); + expect(stripeSubs[0].trial_end).toBe(null); + expect(cusProduct?.free_trial).toBe(null); }); }); diff --git a/server/tests/attach/migrations/runMigrationTest.ts b/server/tests/attach/migrations/runMigrationTest.ts index e8873c301..61967a786 100644 --- a/server/tests/attach/migrations/runMigrationTest.ts +++ b/server/tests/attach/migrations/runMigrationTest.ts @@ -7,7 +7,7 @@ import { getSubsFromCusId, } from "tests/utils/expectUtils/expectSubUtils.js"; import Stripe from "stripe"; -import { expect } from "chai"; +import { expect } from "bun:test"; import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; import { expectResetAtCorrect } from "tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js"; import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; @@ -30,9 +30,9 @@ export const expectSubsSame = ({ const periodsBefore = subsBefore.map((sub) => subToPeriodStartEnd({ sub })); const periodsAfter = subsAfter.map((sub) => subToPeriodStartEnd({ sub })); - // expect(invoicesAfter).to.deep.equal(invoicesBefore); - expect(subIdsAfter).to.deep.equal(subIdsBefore); - expect(periodsBefore).to.deep.equal(periodsAfter); + // expect(invoicesAfter).toEqual(invoicesBefore); + expect(subIdsAfter).toEqual(subIdsBefore); + expect(periodsBefore).toEqual(periodsAfter); }; export const runMigrationTest = async ({ diff --git a/server/tests/attach/multiProduct/multiProduct1.ts b/server/tests/attach/multiProduct/multiProduct1.backup.ts similarity index 100% rename from server/tests/attach/multiProduct/multiProduct1.ts rename to server/tests/attach/multiProduct/multiProduct1.backup.ts diff --git a/server/tests/attach/multiProduct/multiProduct1.test.ts b/server/tests/attach/multiProduct/multiProduct1.test.ts new file mode 100644 index 000000000..d443b1c16 --- /dev/null +++ b/server/tests/attach/multiProduct/multiProduct1.test.ts @@ -0,0 +1,72 @@ +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import chalk from "chalk"; +import { beforeAll, describe, expect, test } from "bun:test"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { Customer } from "@autumn/shared"; +import { + sharedProGroup1, + sharedProGroup2, + sharedPremiumGroup1, + sharedPremiumGroup2, +} from "./sharedProducts.js"; + +/* +FLOW: +1. Attach pro group 1 & pro group 2 at once -> should have both products as main +2. Upgrade pro group 1 -> premium group 1 +3. Upgrade pro group 2 -> premium group 2 +*/ + +const testCase = "multiProduct1"; +describe( + chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`), + () => { + const customerId = testCase; + let customer: Customer; + beforeAll(async () => { + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + customer = res.customer; + }); + + test("should attach pro group 1 and pro group 2", async () => { + await AutumnCli.attach({ + customerId: customerId, + productIds: [sharedProGroup1.id, sharedProGroup2.id], + }); + + const cusRes = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ sent: sharedProGroup1, cusRes, ctx }); + expectCustomerV0Correct({ sent: sharedProGroup2, cusRes, ctx }); + }); + + test("should upgrade to premium group 1", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedPremiumGroup1.id, + }); + + // 1. Compare main product + const cusRes = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ sent: sharedPremiumGroup1, cusRes, ctx }); + }); + + test("should upgrade to premium group 2", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedPremiumGroup2.id, + }); + + // 1. Compare main product + const cusRes = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ sent: sharedPremiumGroup2, cusRes, ctx }); + }); + }, +); diff --git a/server/tests/attach/multiProduct/multiProduct2.ts b/server/tests/attach/multiProduct/multiProduct2.backup.ts similarity index 100% rename from server/tests/attach/multiProduct/multiProduct2.ts rename to server/tests/attach/multiProduct/multiProduct2.backup.ts diff --git a/server/tests/attach/multiProduct/multiProduct2.test.ts b/server/tests/attach/multiProduct/multiProduct2.test.ts new file mode 100644 index 000000000..a916ddbf9 --- /dev/null +++ b/server/tests/attach/multiProduct/multiProduct2.test.ts @@ -0,0 +1,159 @@ +import chalk from "chalk"; + +import type { Stripe } from "stripe"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { CusProductStatus, Customer } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { checkProductIsScheduled } from "tests/utils/compare.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { searchCusProducts } from "tests/utils/genUtils.js"; +import { checkScheduleContainsProducts } from "tests/utils/scheduleCheckUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { + sharedPremiumGroup1, + sharedPremiumGroup2, + sharedStarterGroup1, + sharedStarterGroup2, + sharedFreeGroup2, +} from "./sharedProducts.js"; + +/* +FLOW: +1. Attach pro group 1 & premium group 2 +2. Downgrade to starter group 1 +3. Downgrade to starter group 2 +4. Change downgrade to pro group 2 +*/ + +const testCase = "multiProduct2"; +describe(`${chalk.yellowBright( + "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free", +)}`, () => { + const customerId = testCase; + let customer: Customer; + let stripeCli: Stripe; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + customer = res.customer; + }); + + test("should attach premium group 1 and premium group 2", async () => { + await AutumnCli.attach({ + customerId: customerId, + productIds: [ + sharedPremiumGroup1.id, + sharedPremiumGroup2.id, + ], + }); + + const cusRes = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ sent: sharedPremiumGroup1, cusRes, ctx }); + expectCustomerV0Correct({ sent: sharedPremiumGroup2, cusRes, ctx }); + }); + + test("should downgrade to starter group 1 and starter group 2", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedStarterGroup1.id, + }); + + await AutumnCli.attach({ + customerId: customerId, + productId: sharedStarterGroup2.id, + }); + + // Check starter group 1 scheduled and starter group 2 scheduled + const cusRes = await AutumnCli.getCustomer(customerId); + checkProductIsScheduled({ + product: sharedStarterGroup1, + cusRes, + }); + checkProductIsScheduled({ + product: sharedStarterGroup2, + cusRes, + }); + + // Check if scheduled id is the same + const cusProducts = await CusProductService.list({ + db: ctx.db, + internalCustomerId: customer.internal_id, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Scheduled, + ], + }); + + // 1. Pro group 1: + const starter1 = searchCusProducts({ + cusProducts, + productId: sharedStarterGroup1.id, + }); + + const starter2 = searchCusProducts({ + cusProducts, + productId: sharedStarterGroup2.id, + }); + + expect(starter1).toBeDefined(); + expect(starter2).toBeDefined(); + expect(starter1?.scheduled_ids![0]).toBe(starter2?.scheduled_ids![0]); + + const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve( + starter1?.scheduled_ids![0]!, + ); + + // console.log(stripeSchedule); + checkScheduleContainsProducts({ + db: ctx.db, + schedule: stripeSchedule, + productIds: [ + sharedStarterGroup1.id, + sharedStarterGroup2.id, + ], + org: ctx.org, + env: ctx.env, + }); + }); + + test("should downgrade to free", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedFreeGroup2.id, + }); + + const cusRes = await AutumnCli.getCustomer(customerId); + checkProductIsScheduled({ + product: sharedFreeGroup2, + cusRes, + }); + + const cusProducts = await CusProductService.list({ + db: ctx.db, + internalCustomerId: customer.internal_id, + }); + + const starterGroup2 = searchCusProducts({ + cusProducts, + productId: sharedStarterGroup2.id, + }); + + checkScheduleContainsProducts({ + db: ctx.db, + scheduleId: starterGroup2?.scheduled_ids![0], + productIds: [sharedStarterGroup2.id], + org: ctx.org, + env: ctx.env, + }); + }); +}); diff --git a/server/tests/attach/multiProduct/multiProduct3.ts b/server/tests/attach/multiProduct/multiProduct3.backup.ts similarity index 100% rename from server/tests/attach/multiProduct/multiProduct3.ts rename to server/tests/attach/multiProduct/multiProduct3.backup.ts diff --git a/server/tests/attach/multiProduct/sharedProducts.ts b/server/tests/attach/multiProduct/sharedProducts.ts new file mode 100644 index 000000000..1f48d7242 --- /dev/null +++ b/server/tests/attach/multiProduct/sharedProducts.ts @@ -0,0 +1,170 @@ +import { BillingInterval, ProductItemInterval } from "@autumn/shared"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { + constructFeatureItem, + constructArrearItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js"; + +/** + * Shared products for multiProduct test group + * Matches global attachProducts.{proGroup1, premiumGroup1, proGroup2, premiumGroup2, etc.} + */ + +// Group 1 products (use Messages feature) +export const sharedProGroup1 = constructProduct({ + id: "proGroup1", + group: "g1", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 3000, // $30 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + pricePerUnit: 100, // $1.00 per unit + }), + ], +}); + +export const sharedPremiumGroup1 = constructProduct({ + id: "premiumGroup1", + group: "g1", + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 5000, // $50 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + pricePerUnit: 200, // $2.00 per unit + }), + ], +}); + +export const sharedStarterGroup1 = constructProduct({ + id: "starterGroup1", + group: "g1", + type: "starter", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 1000, // $10 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Messages, + pricePerUnit: 50, // $0.50 per unit + }), + ], +}); + +// Group 2 products (use Words feature) +export const sharedProGroup2 = constructProduct({ + id: "proGroup2", + group: "g2", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 4000, // $40 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Words, + pricePerUnit: 60, // $0.60 per unit + }), + ], +}); + +export const sharedPremiumGroup2 = constructProduct({ + id: "premiumGroup2", + group: "g2", + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 6000, // $60 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Words, + pricePerUnit: 90, // $0.90 per unit + }), + ], +}); + +export const sharedStarterGroup2 = constructProduct({ + id: "starterGroup2", + group: "g2", + type: "starter", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 2000, // $20 + interval: BillingInterval.Month, + }), + constructArrearItem({ + featureId: TestFeature.Words, + pricePerUnit: 30, // $0.30 per unit + }), + ], +}); + +export const sharedFreeGroup2 = constructProduct({ + id: "freeGroup2", + group: "g2", + type: "free", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 10, + }), + ], +}); + +await (async () => { + await createSharedProducts({ + ctx, + products: [ + sharedProGroup1, + sharedPremiumGroup1, + sharedStarterGroup1, + sharedProGroup2, + sharedPremiumGroup2, + sharedStarterGroup2, + sharedFreeGroup2, + ], + }); +})(); diff --git a/server/tests/attach/newVersion/newVersion1.ts b/server/tests/attach/newVersion/newVersion1.test.ts similarity index 70% rename from server/tests/attach/newVersion/newVersion1.ts rename to server/tests/attach/newVersion/newVersion1.test.ts index 711e28d67..9298dd902 100644 --- a/server/tests/attach/newVersion/newVersion1.ts +++ b/server/tests/attach/newVersion/newVersion1.test.ts @@ -1,30 +1,27 @@ import { - type AppEnv, BillingInterval, LegacyVersion, - type Organization, type ProductV2, } from "@autumn/shared"; -import { expect } from "chai"; +import { beforeAll, describe, expect, test } from "bun:test"; import chalk from "chalk"; import { addHours, addMonths, addWeeks } from "date-fns"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; -import { createProducts } from "tests/utils/productUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { timeout } from "@/utils/genUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; +import { replaceItems } from "../utils.js"; + export const pro = constructProduct({ items: [constructArrearItem({ featureId: TestFeature.Words })], type: "pro", @@ -36,61 +33,43 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach pro product", async () => { + test("should attach pro product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: pro, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); const usage = 50000; let newPro: ProductV2; - it("should update product to new version", async () => { + test("should update product to new version", async () => { newPro = structuredClone(pro); let newItems = replaceItems({ items: pro.items, @@ -118,9 +97,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` }); }); - it("should attach pro v2", async () => { + test("should attach pro v2", async () => { await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addWeeks(Date.now(), 1).getTime(), }); @@ -135,13 +114,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` await runUpdateEntsTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, customProduct: newPro, newVersion: 2, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, usage: [ { featureId: TestFeature.Words, @@ -151,14 +130,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` }); }); - it("should have correct invoice total on next cycle", async () => { + test("should have correct invoice total on next cycle", async () => { const invoiceTotal = await getExpectedInvoiceTotal({ - org, - env, + org: ctx.org, + env: ctx.env, customerId, productId: pro.id, - stripeCli, - db, + stripeCli: ctx.stripeCli, + db: ctx.db, usage: [ { featureId: TestFeature.Words, @@ -170,14 +149,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` let curUnix = Date.now(); curUnix = await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addMonths(curUnix, 1).getTime(), waitForSeconds: 30, }); await advanceTestClock({ - stripeCli, + stripeCli: ctx.stripeCli, testClockId, advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), waitForSeconds: 10, @@ -185,9 +164,6 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}` const customer = await autumn.customers.get(customerId); const invoice = customer.invoices[0]; - expect(invoice.total).to.equal( - invoiceTotal, - "invoice total after 1 cycle should be correct", - ); + expect(invoice.total).toBe(invoiceTotal); }); }); diff --git a/server/tests/attach/newVersion/newVersion2.ts b/server/tests/attach/newVersion/newVersion2.test.ts similarity index 72% rename from server/tests/attach/newVersion/newVersion2.ts rename to server/tests/attach/newVersion/newVersion2.test.ts index 3ae80e98e..aa3f59c2f 100644 --- a/server/tests/attach/newVersion/newVersion2.ts +++ b/server/tests/attach/newVersion/newVersion2.test.ts @@ -1,24 +1,21 @@ import { - type AppEnv, BillingInterval, LegacyVersion, - type Organization, type ProductV2, } from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; import chalk from "chalk"; -import type Stripe from "stripe"; -import { setupBefore } from "tests/before.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; import { TestFeature } from "tests/setup/v2Features.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js"; -import { addPrefixToProducts, replaceItems } from "../utils.js"; +import { replaceItems } from "../utils.js"; export const pro = constructProduct({ items: [constructArrearItem({ featureId: TestFeature.Words })], @@ -32,61 +29,43 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for tria const customerId = testCase; const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); let testClockId: string; - let db: DrizzleCli, org: Organization, env: AppEnv; - let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { - await setupBefore(this); - const { autumnJs } = this; - db = this.db; - org = this.org; - env = this.env; - - stripeCli = this.stripeCli; - - addPrefixToProducts({ + beforeAll(async () => { + await initProductsV0({ + ctx, products: [pro], prefix: testCase, - }); - - await createProducts({ - db, - orgId: org.id, - env, - autumn, - products: [pro], customerId, }); - const { testClockId: testClockId1 } = await initCustomer({ - autumn: autumnJs, + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, customerId, - db, - org, - env, + customerData: {}, attachPm: "success", + withTestClock: true, }); testClockId = testClockId1!; }); - it("should attach pro product", async () => { + test("should attach pro product", async () => { await attachAndExpectCorrect({ autumn, customerId, product: pro, - stripeCli, - db, - org, - env, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); const usage = 50000; let newPro: ProductV2; - it("should update product to new version", async () => { + test("should update product to new version", async () => { newPro = structuredClone(pro); const newItems = replaceItems({ items: pro.items, @@ -107,16 +86,16 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for tria return; - it("should attach pro v2", async () => { + test("should attach pro v2", async () => { await runUpdateEntsTest({ autumn, - stripeCli, + stripeCli: ctx.stripeCli, customerId, customProduct: newPro, newVersion: 2, - db, - org, - env, + db: ctx.db, + org: ctx.org, + env: ctx.env, }); }); diff --git a/server/tests/attach/others/others1.ts b/server/tests/attach/others/others1.backup.ts similarity index 100% rename from server/tests/attach/others/others1.ts rename to server/tests/attach/others/others1.backup.ts diff --git a/server/tests/attach/others/others1.test.ts b/server/tests/attach/others/others1.test.ts new file mode 100644 index 000000000..568eea815 --- /dev/null +++ b/server/tests/attach/others/others1.test.ts @@ -0,0 +1,109 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { + expectDowngradeCorrect, + expectNextCycleCorrect, +} from "tests/utils/expectUtils/expectScheduleUtils.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +const testCase = "others1"; + +export const free = constructProduct({ + items: [], + type: "free", + isDefault: false, +}); + +export const pro = constructProduct({ + items: [], + type: "pro", + trial: true, +}); + +export const premium = constructProduct({ + items: [], + type: "premium", + trial: true, +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing trials: pro with trial -> premium with trial -> free`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [free, pro, premium], + prefix: testCase, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro product (with trial)", async () => { + await attachAndExpectCorrect({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + product: pro, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should attach premium product (with trial)", async () => { + await attachAndExpectCorrect({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + product: premium, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should attach free product at the end of the trial", async () => { + const { preview } = await expectDowngradeCorrect({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + curProduct: premium, + newProduct: free, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + expectNextCycleCorrect({ + autumn, + preview, + stripeCli: ctx.stripeCli, + customerId, + testClockId, + product: free, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); +}); diff --git a/server/tests/attach/others/others2.ts b/server/tests/attach/others/others2.backup.ts similarity index 100% rename from server/tests/attach/others/others2.ts rename to server/tests/attach/others/others2.backup.ts diff --git a/server/tests/attach/others/others2.test.ts b/server/tests/attach/others/others2.test.ts new file mode 100644 index 000000000..f8ea1d0b1 --- /dev/null +++ b/server/tests/attach/others/others2.test.ts @@ -0,0 +1,124 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +const testCase = "others2"; + +export const oneOff = constructProduct({ + type: "one_off", + items: [ + constructPrepaidItem({ + isOneOff: true, + featureId: TestFeature.Messages, + price: 8, + billingUnits: 250, + }), + ], +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing one-off`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [oneOff], + prefix: testCase, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + const options = [ + { + feature_id: TestFeature.Messages, + quantity: 500, + }, + ]; + + test("should attach one-off product", async () => { + await attachAndExpectCorrect({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + product: oneOff, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options, + }); + }); + + const options2 = [ + { + feature_id: TestFeature.Messages, + quantity: 750, + }, + ]; + test("should be able to attach again", async () => { + await attachAndExpectCorrect({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + product: oneOff, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: options2, + skipFeatureCheck: true, + }); + + const totalBalance = options[0].quantity + options2[0].quantity; + const customer = await autumn.customers.get(customerId); + + const balance = customer.features[TestFeature.Messages].balance; + expect(balance).toBe(totalBalance); + }); + + // Payment failure + test("should handle payment failure", async () => { + const customer = await CusService.get({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); + + await attachFailedPaymentMethod({ + stripeCli: ctx.stripeCli, + customer: customer!, + }); + + const res = await autumn.attach({ + customer_id: customerId, + product_id: oneOff.id, + options, + }); + + expect(res.checkout_url).toBeDefined(); + }); +}); diff --git a/server/tests/attach/others/others3.ts b/server/tests/attach/others/others3.backup.ts similarity index 100% rename from server/tests/attach/others/others3.ts rename to server/tests/attach/others/others3.backup.ts diff --git a/server/tests/attach/others/others3.test.ts b/server/tests/attach/others/others3.test.ts new file mode 100644 index 000000000..ddc246121 --- /dev/null +++ b/server/tests/attach/others/others3.test.ts @@ -0,0 +1,69 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +const testCase = "others3"; + +export const pro = constructProduct({ + type: "pro", + items: [], +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing attach payment failure`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + // Payment failure + test("should handle payment failure", async () => { + const customer = await CusService.get({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); + + await attachFailedPaymentMethod({ + stripeCli: ctx.stripeCli, + customer: customer!, + }); + + const res = await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + // console.log(res); + + expect(res.checkout_url).toBeDefined(); + }); +}); diff --git a/server/tests/attach/others/others4.ts b/server/tests/attach/others/others4.backup.ts similarity index 100% rename from server/tests/attach/others/others4.ts rename to server/tests/attach/others/others4.backup.ts diff --git a/server/tests/attach/others/others5.ts b/server/tests/attach/others/others5.backup.ts similarity index 100% rename from server/tests/attach/others/others5.ts rename to server/tests/attach/others/others5.backup.ts diff --git a/server/tests/attach/others/others5.test.ts b/server/tests/attach/others/others5.test.ts new file mode 100644 index 000000000..0bb2abd84 --- /dev/null +++ b/server/tests/attach/others/others5.test.ts @@ -0,0 +1,242 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { features, products } from "tests/global.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { timeout } from "../../utils/genUtils.js"; + +const checkEntitledOnProduct = async ({ + customerId, + product, + totalAllowance, + finish = false, + usageBased = false, + timeoutMs = 8000, +}: { + customerId: string; + product: any; + totalAllowance?: number; + finish?: boolean; + usageBased?: boolean; + timeoutMs?: number; +}) => { + // 1. Send events + const allowance = totalAllowance || product.entitlements.metered1.allowance; + // const randomNum = Math.floor(Math.random() * (allowance - 1)); + const randomNum = 3; + + const batchUpdates = []; + for (let i = 0; i < randomNum; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(timeoutMs); + let used = randomNum; + + // 2. Check entitled + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + true, + ); + + try { + expect(allowed).toBe(true); + expect(balanceObj!.balance).toBe(allowance - randomNum); + + if (!finish) { + return used; + } + } catch (error) { + console.group(); + console.group(); + console.log("Allowance: ", allowance, "Random num: ", randomNum); + console.log("Expected balance to be: ", allowance - randomNum); + console.log("Entitled res: ", { allowed, balanceObj }); + console.groupEnd(); + console.groupEnd(); + throw error; + } + + // Finish up + const batchUpdates2 = []; + for (let i = 0; i < allowance - randomNum; i++) { + batchUpdates2.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + await Promise.all(batchUpdates2); + await timeout(timeoutMs); + used += allowance - randomNum; + + // 3. Check entitled again + const { allowed: allowed2, balanceObj: balanceObj2 }: any = + await AutumnCli.entitled(customerId, features.metered1.id, true); + try { + if (usageBased) { + expect(allowed2).toBe(true); + } else { + expect(allowed2).toBe(false); + } + expect(balanceObj2!.balance).toBe(0); + return used; + } catch (error) { + console.group(); + console.group(); + console.log("Expected balance to be: ", 0); + console.log("Entitled res: ", { allowed2, balanceObj2 }); + console.groupEnd(); + console.groupEnd(); + throw error; + } +}; + +// TODO: Add test case for unlimited feature + +const testCase = "others5"; +describe(`${chalk.yellowBright( + "others5: Testing /events and /entitled, for pro, one time top up", +)}`, () => { + const customerId = testCase; + + let curAllowance = 0; + const oneTimeBillingUnits = + products.oneTimeAddOnMetered1.prices[0].config.billing_units!; + const oneTimeQuantity = 2 * oneTimeBillingUnits; + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + }); + + // test("should have correct entitlements (free)", async function () { + // await checkEntitledOnProduct({ + // customerId: customerId, + // product: products.free, + // finish: true, + // }); + // }); + + test("should attach pro", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: products.pro.id, + }); + }); + + test("should have correct entitlements (pro)", async () => { + const used = await checkEntitledOnProduct({ + customerId: customerId, + product: products.pro, + finish: false, + }); + + curAllowance = products.pro.entitlements.metered1.allowance! - used; + }); + + test("should attach one time top up", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: products.oneTimeAddOnMetered1.id, + options: [ + { + feature_id: features.metered1.id, + quantity: oneTimeQuantity, + }, + ], + }); + }); + + test("should have correct entitlements (one time top up)", async () => { + // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity; + + await checkEntitledOnProduct({ + customerId: customerId, + product: products.oneTimeAddOnMetered1, + finish: true, + totalAllowance: curAllowance + oneTimeQuantity, + timeoutMs: 15000, + }); + }); +}); + +describe(`${chalk.yellowBright( + "others5: Testing /entitled & /events, for pro with overage", +)}`, () => { + const customerId = testCase; + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + }); + + // PRO WITH OVERAGE + test("should attach pro (with overage)", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: products.proWithOverage.id, + }); + }); + + test("should have correct entitlements (pro with overage)", async () => { + await checkEntitledOnProduct({ + customerId: customerId, + product: products.proWithOverage, + finish: true, + totalAllowance: products.proWithOverage.entitlements.metered1.allowance!, + usageBased: true, + }); + }); + + test("should have correct usage-based balance (balance < 0)", async () => { + const { allowed, balanceObj }: any = await AutumnCli.entitled( + customerId, + features.metered1.id, + true, + ); + + expect(allowed).toBe(true); + expect(balanceObj!.balance).toBe(0); + + // Sent 5 events + const batchUpdates = []; + for (let i = 0; i < 5; i++) { + batchUpdates.push( + AutumnCli.sendEvent({ + customerId: customerId, + eventName: features.metered1.eventName, + }), + ); + } + + await Promise.all(batchUpdates); + await timeout(10000); + + const { allowed: allowed2, balanceObj: balanceObj2 }: any = + await AutumnCli.entitled(customerId, features.metered1.id, true); + + expect(allowed2).toBe(true); + expect(balanceObj2!.balance).toBe(-5); + expect(balanceObj2!.usage_allowed).toBe(true); + }); +}); diff --git a/server/tests/attach/others/others6.ts b/server/tests/attach/others/others6.backup.ts similarity index 100% rename from server/tests/attach/others/others6.ts rename to server/tests/attach/others/others6.backup.ts diff --git a/server/tests/attach/others/others6.test.ts b/server/tests/attach/others/others6.test.ts new file mode 100644 index 000000000..d25a1077b --- /dev/null +++ b/server/tests/attach/others/others6.test.ts @@ -0,0 +1,116 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +export const pro = constructProduct({ + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const testCase = "others6"; + +describe(`${chalk.yellowBright(`${testCase}: Testing attach with customer ID and entity ID null`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const email = `${customerId}@test.com`; + beforeAll(async () => { + const customer = await CusService.getByEmail({ + db: ctx.db, + orgId: ctx.org.id, + env: ctx.env, + email, + }); + + if (customer.length > 0) { + await autumn.customers.delete(customer[0].internal_id); + } + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + }); + + let internalCustomerId = ""; + let internalEntityId = ""; + const entityId = "1"; + test("should attach create customer with no ID", async () => { + const customer = await autumn.customers.create({ + // @ts-expect-error + id: null, + email: `${customerId}@test.com`, + name: customerId, + }); + + expect(customer.autumn_id).toBeDefined(); + + internalCustomerId = customer.autumn_id; + + const data = await autumn.entities.create(internalCustomerId, { + // @ts-expect-error + id: null, + feature_id: TestFeature.Users, + }); + + internalEntityId = data.autumn_id; + + expect(internalEntityId).toBeDefined(); + }); + + test("should be able to attach pro product, invoice only", async () => { + await autumn.attach({ + customer_id: internalCustomerId, + entity_id: internalEntityId, + product_id: pro.id, + invoice: true, + enable_product_immediately: true, + }); + + const customer = await autumn.customers.get(internalCustomerId); + + expectAttachCorrect({ + customer, + product: pro, + }); + + expect(customer.invoices.length).toBe(1); + expect(customer.invoices[0].status).toBe("draft"); + }); + + test("should create customer with ID, and attach pro product", async () => { + const customer = await autumn.customers.create({ + id: customerId, + email: `${customerId}@test.com`, + }); + + expect(customer.autumn_id).toBe(internalCustomerId); + + const entity = await autumn.entities.create(customer.autumn_id, { + id: entityId, + feature_id: TestFeature.Users, + }); + + internalEntityId = entity.autumn_id; + + const customer2 = await autumn.customers.get(customerId); + + expectAttachCorrect({ + customer: customer2, + product: pro, + entityId, + }); + }); +}); diff --git a/server/tests/attach/others/others7.ts b/server/tests/attach/others/others7.backup.ts similarity index 100% rename from server/tests/attach/others/others7.ts rename to server/tests/attach/others/others7.backup.ts diff --git a/server/tests/attach/others/others7.test.ts b/server/tests/attach/others/others7.test.ts new file mode 100644 index 000000000..3fdcce2fc --- /dev/null +++ b/server/tests/attach/others/others7.test.ts @@ -0,0 +1,61 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +export const pro = constructProduct({ + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", + trial: true, +}); + +const testCase = "others7"; + +describe(`${chalk.yellowBright(`${testCase}: Testing attach with free_trial=False`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + }); + + test("should attach pro product with free_trial=False", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + free_trial: false, + }); + + const customer = await autumn.customers.get(customerId); + + expectAttachCorrect({ + customer, + product: pro, + }); + + expect(customer.invoices.length).toBe(1); + expect(customer.invoices[0].total).toBe(getBasePrice({ product: pro })); + }); +}); diff --git a/server/tests/attach/others/others8.ts b/server/tests/attach/others/others8.backup.ts similarity index 100% rename from server/tests/attach/others/others8.ts rename to server/tests/attach/others/others8.backup.ts diff --git a/server/tests/attach/others/others8.test.ts b/server/tests/attach/others/others8.test.ts new file mode 100644 index 000000000..86f4127ed --- /dev/null +++ b/server/tests/attach/others/others8.test.ts @@ -0,0 +1,86 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructFeatureItem, + constructPrepaidItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +export const pro = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + }), + constructPrepaidItem({ + isOneOff: true, + featureId: TestFeature.Users, + billingUnits: 1, + price: 100, + }), + ], + isAnnual: true, + type: "pro", +}); + +const testCase = "others8"; + +describe(`${chalk.yellowBright(`${testCase}: Testing annual pro with one off prepaid`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + }); + + test("should attach annual pro product with one off prepaid", async () => { + const options = [ + { + feature_id: TestFeature.Users, + quantity: 1, + }, + ]; + + const preview = await autumn.attachPreview({ + customer_id: customerId, + product_id: pro.id, + options, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + options, + }); + + console.log(preview); + + const customer = await autumn.customers.get(customerId); + + const invoice = customer.invoices[0]; + // expect(preview.total).toBe(invoice.total); + expect(invoice.total).toBe( + getBasePrice({ product: pro }) + options[0].quantity * 100, + ); + }); +}); diff --git a/server/tests/attach/others/others9.ts b/server/tests/attach/others/others9.backup.ts similarity index 100% rename from server/tests/attach/others/others9.ts rename to server/tests/attach/others/others9.backup.ts diff --git a/server/tests/attach/others/others9.test.ts b/server/tests/attach/others/others9.test.ts new file mode 100644 index 000000000..2cd76137f --- /dev/null +++ b/server/tests/attach/others/others9.test.ts @@ -0,0 +1,74 @@ +import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; + +export const free = constructProduct({ + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + }), + ], + isAnnual: false, + type: "free", + isDefault: false, +}); + +// Pro trial + +// Pro + +const testCase = "others9"; + +describe(`${chalk.yellowBright(`${testCase}: Testing attach free product again`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + beforeAll(async () => { + await initCustomerV3({ + ctx, + customerId, + customerData: { fingerprint: "test" }, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [free], + prefix: testCase, + }); + }); + + test("should attach free product, then try again and hit error", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: free, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + skipSubCheck: true, + }); + + await expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: free.id, + }); + }, + }); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid1.ts b/server/tests/attach/prepaid/prepaid1.backup.ts similarity index 100% rename from server/tests/attach/prepaid/prepaid1.ts rename to server/tests/attach/prepaid/prepaid1.backup.ts diff --git a/server/tests/attach/prepaid/prepaid1.test.ts b/server/tests/attach/prepaid/prepaid1.test.ts new file mode 100644 index 000000000..be7c7aba0 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid1.test.ts @@ -0,0 +1,173 @@ +import { type Customer, LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; +import { 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 { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +const testCase = "prepaid1"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 100, + price: 12.5, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, + }), + ], + excludeBase: true, + type: "pro", +}); + +describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proration downgrade, single use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + let customer: Customer; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + customer = res.customer; + testClockId = res.testClockId!; + }); + + const options = [ + { + feature_id: TestFeature.Messages, + quantity: 300, + }, + ]; + + test("should attach pro product to customer", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + test("should reduce quantity to 200 and have correct sub item quantity + cus product quantity", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 200, + }, + ], + }); + }); + + test("should increase quantity to 400 and have correct sub item quantity + invoice..", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 400, + }, + ], + waitForInvoice: 5000, + }); + }); + + const newQuantity = 200; + test("should decrease quantity to 200, advance clock to next cycle and have correct balance", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: newQuantity, + }, + ], + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 40, + }); + + const autumnCus = await autumn.customers.get(customerId); + expect(autumnCus.features[TestFeature.Messages].balance).toBe( + newQuantity, + ); + + expect(autumnCus.invoices.length).toBe(3); + expect(autumnCus.invoices[0].total).toBe((newQuantity / 100) * 12.5); + + const cusProduct = await getMainCusProduct({ + db: ctx.db, + internalCustomerId: customer.internal_id, + productGroup: testCase, + }); + + expect(cusProduct?.options[0].quantity).toBe(newQuantity / 100); + expect(cusProduct?.options[0].upcoming_quantity).toBeUndefined(); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid2.ts b/server/tests/attach/prepaid/prepaid2.backup.ts similarity index 100% rename from server/tests/attach/prepaid/prepaid2.ts rename to server/tests/attach/prepaid/prepaid2.backup.ts diff --git a/server/tests/attach/prepaid/prepaid2.test.ts b/server/tests/attach/prepaid/prepaid2.test.ts new file mode 100644 index 000000000..af81f156d --- /dev/null +++ b/server/tests/attach/prepaid/prepaid2.test.ts @@ -0,0 +1,125 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { 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 { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +const testCase = "prepaid2"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 100, + price: 12.5, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, + }), + ], + excludeBase: true, + type: "pro", +}); + +describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate immediately, single use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const options = [ + { + feature_id: TestFeature.Messages, + quantity: 300, + }, + ]; + + test("should attach pro product to customer", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + test("should increase advance test clock, increase quantity to 400 and have correct sub item quantity + invoice..", async () => { + const usage = Math.floor(Math.random() * 220); + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: usage, + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 30, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 400, + }, + ], + usage: [ + { + featureId: TestFeature.Messages, + value: usage, + }, + ], + waitForInvoice: 5000, + }); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid3.ts b/server/tests/attach/prepaid/prepaid3.backup.ts similarity index 100% rename from server/tests/attach/prepaid/prepaid3.ts rename to server/tests/attach/prepaid/prepaid3.backup.ts diff --git a/server/tests/attach/prepaid/prepaid3.test.ts b/server/tests/attach/prepaid/prepaid3.test.ts new file mode 100644 index 000000000..5233d1404 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid3.test.ts @@ -0,0 +1,133 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { 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 { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +const testCase = "prepaid3"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 100, + price: 12.5, + config: { + on_increase: OnIncrease.ProrateNextCycle, + on_decrease: OnDecrease.None, + }, + }), + ], + excludeBase: true, + type: "pro", +}); + +describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate next cycle, single use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const options = [ + { + feature_id: TestFeature.Messages, + quantity: 300, + }, + ]; + + test("should attach pro product to customer", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + test("should increase advance test clock, increase quantity to 400", async () => { + const usage = Math.floor(Math.random() * 220); + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: usage, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 400, + }, + ], + usage: [ + { + featureId: TestFeature.Messages, + value: usage, + }, + ], + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices.length).toBe(1); + }); + + test("should advance test clock to end of cycle and have correct invoice", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 10, + }); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid4.ts b/server/tests/attach/prepaid/prepaid4.backup.ts similarity index 100% rename from server/tests/attach/prepaid/prepaid4.ts rename to server/tests/attach/prepaid/prepaid4.backup.ts diff --git a/server/tests/attach/prepaid/prepaid4.test.ts b/server/tests/attach/prepaid/prepaid4.test.ts new file mode 100644 index 000000000..12e685703 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid4.test.ts @@ -0,0 +1,123 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { 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 { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; + +const testCase = "prepaid4"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 100, + price: 12.5, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, + }), + ], + excludeBase: true, + type: "pro", +}); + +describe(`${chalk.yellowBright(`attach/${testCase}: Testing prepaid reset`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const options = [ + { + feature_id: TestFeature.Messages, + quantity: 300, + }, + ]; + + test("should attach pro product to customer", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + // return; + + const usage = 100; + test("should track usage for prepaid and have correct balance", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: usage, + }); + + await timeout(3000); + + const customer = await autumn.customers.get(customerId); + const newBalance = options[0].quantity - usage; + expect(customer.features[TestFeature.Messages].balance).toBe( + newBalance, + ); + }); + + test("should advance clock to next cycle and have correct balance", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.features[TestFeature.Messages].balance).toBe( + options[0].quantity, + ); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid5.ts b/server/tests/attach/prepaid/prepaid5.backup.ts similarity index 100% rename from server/tests/attach/prepaid/prepaid5.ts rename to server/tests/attach/prepaid/prepaid5.backup.ts diff --git a/server/tests/attach/prepaid/prepaid5.test.ts b/server/tests/attach/prepaid/prepaid5.test.ts new file mode 100644 index 000000000..999aafb82 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid5.test.ts @@ -0,0 +1,234 @@ +import { type Customer, LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + 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"; + +const testCase = "prepaid5"; + +export const prepaidAddOn = constructProduct({ + type: "pro", + excludeBase: true, + id: "topup", + items: [ + constructPrepaidItem({ + featureId: TestFeature.Messages, + billingUnits: 100, + price: 12.5, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, + }), + ], + isAddOn: true, +}); + +export const pro = constructProduct({ + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 250, + }), + ], +}); +export const premium = constructProduct({ + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 1000, + }), + ], +}); + +describe(`${chalk.yellowBright(`attach/${testCase}: prepaid add on, with entities`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + let customer: Customer; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, premium, prepaidAddOn], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: false, + }); + + customer = res.customer; + // testClockId = res.testClockId!; + }); + + const entity1Id = "1"; + const entity2Id = "2"; + const entities = [ + { + id: entity1Id, + name: "entity1", + feature_id: TestFeature.Users, + }, + { + id: entity2Id, + name: "entity2", + feature_id: TestFeature.Users, + }, + ]; + + test("should attach pro product to entity1", async () => { + await autumn.entities.create(customerId, entities); + + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity1Id, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity1Id, + product: prepaidAddOn, + otherProducts: [pro], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 100, + }, + ], + numSubs: 2, + }); + }); + + const oldEntity2Quantity = 300; + test("should advance test clock and attach top up to entity2", async () => { + // await advanceTestClock({ + // stripeCli, + // testClockId, + // advanceTo: addWeeks(new Date(), 2).getTime(), + // waitForSeconds: 10, + // }); + + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity2Id, + product: premium, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + numSubs: 3, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity2Id, + product: prepaidAddOn, + otherProducts: [premium], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: oldEntity2Quantity, + }, + ], + numSubs: 4, + }); + }); + + test("should increase prepaid add on quantity for entity1", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity1Id, + product: prepaidAddOn, + otherProducts: [pro], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: 200, + }, + ], + numSubs: 4, + waitForInvoice: 10000, + }); + }); + + const newEntity2Quantity = 200; + test("should decrease prepaid add on quantity for entity2", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + entityId: entity2Id, + product: prepaidAddOn, + otherProducts: [premium], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + options: [ + { + feature_id: TestFeature.Messages, + quantity: newEntity2Quantity, + }, + ], + numSubs: 4, + waitForInvoice: 5000, + }); + + const entity2 = await autumn.entities.get(customerId, entity2Id); + expect(entity2.invoices.length).toBe(2); + const creditProd = entity2.products.find( + (p: any) => p.id == prepaidAddOn.id, + ); + expect(creditProd).toBeDefined(); + const messagesItem = creditProd!.items.find( + (i: any) => i.feature_id == TestFeature.Messages, + ); + + expect(messagesItem).toBeDefined(); + expect(messagesItem.quantity).toBe(oldEntity2Quantity); + expect(messagesItem.next_cycle_quantity).toBe(newEntity2Quantity); + }); + + return; +}); diff --git a/server/tests/attach/prepaid/prepaid6.backup.ts b/server/tests/attach/prepaid/prepaid6.backup.ts new file mode 100644 index 000000000..cf50e6202 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid6.backup.ts @@ -0,0 +1,173 @@ +import { + type AppEnv, + type Customer, + LegacyVersion, + OnDecrease, + OnIncrease, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { addPrefixToProducts } from "../utils.js"; + +const userItem = constructPrepaidItem({ + featureId: TestFeature.Users, + price: 10, + billingUnits: 1, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + excludeBase: true, + type: "pro", +}); + +const testCase = "prepaid6"; +describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proration downgrade, cont use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + const curUnix = new Date().getTime(); + let customer: Customer; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const res = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + customer = res.customer; + testClockId = res.testClockId!; + }); + + const options = [ + { + feature_id: TestFeature.Users, + quantity: 4, + }, + ]; + + const originalQuantity = 4; + it("should attach pro product to customer", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options, + }); + + const customer = await autumn.customers.get(customerId); + expectProductAttached({ + customer, + product: pro, + }); + }); + + const usage = 3; + const newQuantity = 3; + it("should use 3 users, then downgrade to 3 seats", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: usage, + }); + + await timeout(3000); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options: [ + { + feature_id: TestFeature.Users, + quantity: newQuantity, + }, + ], + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + it("should have correct balance (0) next cycle", async () => { + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const autumnCus = await autumn.customers.get(customerId); + + expect(autumnCus.features[TestFeature.Users].balance).to.equal(0); + const product = autumnCus.products.find((p: any) => p.id == pro.id) as any; + const userItem = product.items.find( + (i: any) => i.feature_id == TestFeature.Users, + ); + + expect(userItem?.quantity).to.equal(newQuantity); + expect(userItem?.upcoming_quantity).to.not.exist; + expect(autumnCus.invoices[0].total).to.equal(newQuantity * userItem.price); + }); +}); diff --git a/server/tests/attach/prepaid/prepaid7.backup.ts b/server/tests/attach/prepaid/prepaid7.backup.ts new file mode 100644 index 000000000..651bedf84 --- /dev/null +++ b/server/tests/attach/prepaid/prepaid7.backup.ts @@ -0,0 +1,186 @@ +// import { AutumnInt } from "@/external/autumn/autumnCli.js"; +// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +// import { +// LegacyVersion, +// AppEnv, +// Customer, +// OnDecrease, +// OnIncrease, +// Organization, +// } from "@autumn/shared"; +// import chalk from "chalk"; +// import Stripe from "stripe"; +// import { DrizzleCli } from "@/db/initDrizzle.js"; +// import { setupBefore } from "tests/before.js"; +// import { createProducts } from "tests/utils/productUtils.js"; +// import { addPrefixToProducts } from "../utils.js"; +// import { +// constructFeatureItem, +// constructPrepaidItem, +// } from "@/utils/scriptUtils/constructItem.js"; +// import { TestFeature } from "tests/setup/v2Features.js"; +// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +// import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +// import { expect } from "chai"; + +// const testCase = "prepaid6"; + +// export let pro = constructProduct({ +// type: "pro", +// items: [ +// constructPrepaidItem({ +// featureId: TestFeature.Messages, +// billingUnits: 100, +// price: 12.5, +// config: { +// on_increase: OnIncrease.ProrateImmediately, +// on_decrease: OnDecrease.None, +// }, +// }), +// ], +// }); +// export let premium = constructProduct({ +// type: "premium", +// items: [ +// constructPrepaidItem({ +// featureId: TestFeature.Messages, +// billingUnits: 100, +// price: 12.5, +// config: { +// on_increase: OnIncrease.ProrateImmediately, +// on_decrease: OnDecrease.None, +// }, +// }), +// ], +// }); + +// describe(`${chalk.yellowBright(`attach/${testCase}: prepaid add on, with entities`)}`, () => { +// let customerId = testCase; +// let autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); +// let testClockId: string; +// let db: DrizzleCli, org: Organization, env: AppEnv; +// let stripeCli: Stripe; + +// let curUnix = new Date().getTime(); +// let customer: Customer; + +// before(async function () { +// await setupBefore(this); +// const { autumnJs } = this; +// db = this.db; +// org = this.org; +// env = this.env; + +// stripeCli = this.stripeCli; + +// const res = await initCustomer({ +// autumn: autumnJs, +// customerId, +// db, +// org, +// env, +// attachPm: "success", +// withTestClock: false, +// }); + +// addPrefixToProducts({ +// products: [pro, premium], +// prefix: testCase, +// }); + +// await createProducts({ +// autumn, +// products: [pro, premium], +// db, +// orgId: org.id, +// env, +// }); + +// customer = res.customer; +// // testClockId = res.testClockId!; +// }); + +// it("should attach pro product", async function () { +// await attachAndExpectCorrect({ +// autumn, +// customerId, +// product: pro, +// stripeCli, +// db, +// org, +// env, +// options: [ +// { +// feature_id: TestFeature.Messages, +// quantity: 300, +// }, +// ], +// }); +// }); + +// return; + +// // it("should advance test clock and attach premium", async function () { +// // await advanceTestClock({ +// // stripeCli, +// // testClockId, +// // advanceTo: addWeeks(new Date(), 2).getTime(), +// // waitForSeconds: 10, +// // }); + +// // await attachAndExpectCorrect({ +// // autumn, +// // customerId, +// // entityId: entity2Id, +// // product: premium, +// // stripeCli, +// // db, +// // org, +// // env, +// // numSubs: 3, +// // }); + +// // await attachAndExpectCorrect({ +// // autumn, +// // customerId, +// // entityId: entity2Id, +// // product: prepaidAddOn, +// // otherProducts: [premium], +// // stripeCli, +// // db, +// // org, +// // env, +// // options: [ +// // { +// // feature_id: TestFeature.Messages, +// // quantity: oldEntity2Quantity, +// // }, +// // ], +// // numSubs: 4, +// // }); +// // }); + +// // it("should increase prepaid add on quantity for entity1", async function () { +// // await attachAndExpectCorrect({ +// // autumn, +// // customerId, +// // entityId: entity1Id, +// // product: prepaidAddOn, +// // otherProducts: [pro], +// // stripeCli, +// // db, +// // org, +// // env, +// // options: [ +// // { +// // feature_id: TestFeature.Messages, +// // quantity: 200, +// // }, +// // ], +// // numSubs: 4, +// // waitForInvoice: 10000, +// // }); +// // }); + +// return; +// }); diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts b/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts new file mode 100644 index 000000000..d5fb5f2ef --- /dev/null +++ b/server/tests/attach/updateEnts/expectUpdateEnts.backup.ts @@ -0,0 +1,130 @@ +import { + type AppEnv, + AttachBranch, + type Organization, + type ProductItem, + type ProductV2, +} from "@autumn/shared"; +import { expect } from "chai"; +import type Stripe from "stripe"; +import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; +import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; +import { + expectSubItemsCorrect, + getSubsFromCusId, +} from "tests/utils/expectUtils/expectSubUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; + +const runUpdateEntsTest = async ({ + autumn, + stripeCli, + customerId, + customProduct, + newVersion, + db, + org, + env, + customItems, + usage, +}: { + autumn: AutumnInt; + stripeCli: Stripe; + customerId: string; + customProduct: ProductV2; + newVersion?: number; + db: DrizzleCli; + org: Organization; + env: AppEnv; + customItems?: ProductItem[]; + usage?: { + featureId: string; + value: number; + }[]; +}) => { + // 1. Get subs before + + const { subs: subsBefore } = await getSubsFromCusId({ + stripeCli, + customerId, + productId: customProduct.id, + db, + org, + env, + }); + + const preview = await autumn.attachPreview({ + customer_id: customerId, + product_id: customProduct.id, + version: newVersion, + is_custom: customItems ? true : undefined, + items: customItems, + }); + + if (newVersion) { + expect(preview.branch).to.equal(AttachBranch.NewVersion); + } else { + expect(preview.branch).to.equal(AttachBranch.SameCustomEnts); + expect(preview.due_today).to.be.undefined; + } + + await autumn.attach({ + customer_id: customerId, + product_id: customProduct.id, + version: newVersion, + is_custom: customItems ? true : undefined, + items: customItems, + }); + + // 1. Ensure no new invoices created + const { subs: subsAfter, cusProduct } = await getSubsFromCusId({ + stripeCli, + customerId, + productId: customProduct.id, + db, + org, + env, + }); + + const invoicesBefore = subsBefore.map((sub) => sub.latest_invoice); + const invoicesAfter = subsAfter.map((sub) => sub.latest_invoice); + const subIdsBefore = subsBefore.map((sub) => sub.id); + const subIdsAfter = subsAfter.map((sub) => sub.id); + + // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end); + // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end); + + expect(invoicesAfter).to.deep.equal(invoicesBefore); + expect(subIdsAfter).to.deep.equal(subIdsBefore); + // expect(periodEndsAfter).to.deep.equal(periodEndsBefore); + + if (customItems) { + expect(cusProduct.is_custom).to.be.true; + } + + const customer = await autumn.customers.get(customerId); + expectFeaturesCorrect({ + customer, + product: customProduct, + usage, + }); + + // 2. Expect product attached + await expectSubItemsCorrect({ + stripeCli, + customerId, + product: customProduct, + db, + org, + env, + }); + + await expectSubToBeCorrect({ + customerId, + db, + org, + env, + }); +}; + +export default runUpdateEntsTest; diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.ts b/server/tests/attach/updateEnts/expectUpdateEnts.ts index d5fb5f2ef..2eda2d30b 100644 --- a/server/tests/attach/updateEnts/expectUpdateEnts.ts +++ b/server/tests/attach/updateEnts/expectUpdateEnts.ts @@ -5,7 +5,7 @@ import { type ProductItem, type ProductV2, } from "@autumn/shared"; -import { expect } from "chai"; +import { expect } from "bun:test"; import type Stripe from "stripe"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; @@ -62,10 +62,10 @@ const runUpdateEntsTest = async ({ }); if (newVersion) { - expect(preview.branch).to.equal(AttachBranch.NewVersion); + expect(preview.branch).toBe(AttachBranch.NewVersion); } else { - expect(preview.branch).to.equal(AttachBranch.SameCustomEnts); - expect(preview.due_today).to.be.undefined; + expect(preview.branch).toBe(AttachBranch.SameCustomEnts); + expect(preview.due_today).toBeUndefined(); } await autumn.attach({ @@ -94,12 +94,12 @@ const runUpdateEntsTest = async ({ // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end); // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end); - expect(invoicesAfter).to.deep.equal(invoicesBefore); - expect(subIdsAfter).to.deep.equal(subIdsBefore); - // expect(periodEndsAfter).to.deep.equal(periodEndsBefore); + expect(invoicesAfter).toEqual(invoicesBefore); + expect(subIdsAfter).toEqual(subIdsBefore); + // expect(periodEndsAfter).toEqual(periodEndsBefore); if (customItems) { - expect(cusProduct.is_custom).to.be.true; + expect(cusProduct.is_custom).toBe(true); } const customer = await autumn.customers.get(customerId); diff --git a/server/tests/attach/updateEnts/updateEnts1.ts b/server/tests/attach/updateEnts/updateEnts1.backup.ts similarity index 100% rename from server/tests/attach/updateEnts/updateEnts1.ts rename to server/tests/attach/updateEnts/updateEnts1.backup.ts diff --git a/server/tests/attach/updateEnts/updateEnts1.test.ts b/server/tests/attach/updateEnts/updateEnts1.test.ts new file mode 100644 index 000000000..484125ea5 --- /dev/null +++ b/server/tests/attach/updateEnts/updateEnts1.test.ts @@ -0,0 +1,153 @@ +import { LegacyVersion } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearItem } 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 { replaceItems } from "../utils.js"; +import runUpdateEntsTest from "./expectUpdateEnts.js"; + +const testCase = "updateEnts1"; + +export const pro = constructProduct({ + items: [ + constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 10000, + }), + ], + type: "pro", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage)`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + const numUsers = 0; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro product", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + const newItem = constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 20000, + }); + + const customItems = replaceItems({ + items: pro.items, + featureId: TestFeature.Words, + newItem, + }); + + const usage = 50000; + const overage = 50000 - (newItem.included_usage as number); + + test("should update overage item to have new included usage", async () => { + const customProduct = { + ...pro, + items: customItems, + }; + + await autumn.track({ + customer_id: customerId, + value: usage, + feature_id: TestFeature.Words, + }); + + await timeout(5000); + + await runUpdateEntsTest({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + customProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customItems, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + }); + return; + + test("should have correct invoice next cycle", async () => { + const invoiceTotal = await getExpectedInvoiceTotal({ + org: ctx.org, + env: ctx.env, + customerId, + productId: pro.id, + stripeCli: ctx.stripeCli, + db: ctx.db, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + + let curUnix = Date.now(); + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addMonths(curUnix, 1).getTime(), + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), + waitForSeconds: 10, + }); + + const customer = await autumn.customers.get(customerId); + const invoice = customer.invoices![0]; + expect(invoice.total).toBe(invoiceTotal); + }); +}); diff --git a/server/tests/attach/updateEnts/updateEnts2.ts b/server/tests/attach/updateEnts/updateEnts2.backup.ts similarity index 100% rename from server/tests/attach/updateEnts/updateEnts2.ts rename to server/tests/attach/updateEnts/updateEnts2.backup.ts diff --git a/server/tests/attach/updateEnts/updateEnts2.test.ts b/server/tests/attach/updateEnts/updateEnts2.test.ts new file mode 100644 index 000000000..b16b12604 --- /dev/null +++ b/server/tests/attach/updateEnts/updateEnts2.test.ts @@ -0,0 +1,170 @@ +import { LegacyVersion } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths, addWeeks } from "date-fns"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearItem } 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 { replaceItems } from "../utils.js"; +import runUpdateEntsTest from "./expectUpdateEnts.js"; + +const testCase = "updateEnts2"; + +export const pro = constructProduct({ + items: [ + constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 10000, + }), + ], + type: "pro", + isAnnual: true, +}); + +/** + * updateEnts2: + * Testing updating entitlements for annual plans + * 1. Start with pro annual plan (usage-based) + * 2. Update included usage amount + * 3. Verify features and usage are updated correctly + * 4. Verify invoice total is correct in next billing cycle + * + * Verifies that updating entitlements works correctly for annual plans + * and that usage/billing is calculated properly + */ + +describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage) for annual plan`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + const numUsers = 0; + + beforeAll(async () => { + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro annual product", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + const newItem = constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 5000, + }); + + const customItems = replaceItems({ + items: pro.items, + featureId: TestFeature.Words, + newItem, + }); + + const usage = 1200500; + + test("should attach custom pro product", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 2).getTime(), + waitForSeconds: 30, + }); + + const customProduct = { + ...pro, + items: customItems, + }; + + await autumn.track({ + customer_id: customerId, + value: usage, + feature_id: TestFeature.Words, + }); + + await timeout(5000); + + await runUpdateEntsTest({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + customProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customItems, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + }); + + test("should have correct invoice usage next cycle", async () => { + const invoiceTotal = await getExpectedInvoiceTotal({ + org: ctx.org, + env: ctx.env, + customerId, + productId: pro.id, + stripeCli: ctx.stripeCli, + db: ctx.db, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + onlyIncludeMonthly: true, + }); + + let curUnix = Date.now(); + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addMonths(curUnix, 1).getTime(), + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(), + waitForSeconds: 10, + }); + + const customer = await autumn.customers.get(customerId); + const invoice = customer.invoices![0]; + expect(invoice.total).toBe(invoiceTotal); + }); +}); diff --git a/server/tests/attach/updateEnts/updateEnts3.ts b/server/tests/attach/updateEnts/updateEnts3.backup.ts similarity index 100% rename from server/tests/attach/updateEnts/updateEnts3.ts rename to server/tests/attach/updateEnts/updateEnts3.backup.ts diff --git a/server/tests/attach/updateEnts/updateEnts3.test.ts b/server/tests/attach/updateEnts/updateEnts3.test.ts new file mode 100644 index 000000000..ccc4f981d --- /dev/null +++ b/server/tests/attach/updateEnts/updateEnts3.test.ts @@ -0,0 +1,186 @@ +import { LegacyVersion } from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js"; +import { constructArrearItem } 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 { replaceItems } from "../utils.js"; +import runUpdateEntsTest from "./expectUpdateEnts.js"; + +const testCase = "updateEnts3"; + +export const pro = constructProduct({ + items: [ + constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 10000, + }), + ], + type: "pro", + isAnnual: true, +}); + +/** + * updateEnts2: + * Testing updating entitlements for annual plans + * 1. Start with pro annual plan (usage-based) + * 2. Update included usage amount + * 3. Verify features and usage are updated correctly + * 4. Verify invoice total is correct in next billing cycle + * + * Verifies that updating entitlements works correctly for annual plans + * and that usage/billing is calculated properly + */ + +describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing feature items)`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro annual product", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + const newFeatureItem = constructFeatureItem({ + feature_id: TestFeature.Messages, + included_usage: 500, + }); + + const usage = 1200500; + + const customItems = [...pro.items, newFeatureItem]; + + test("should attach custom pro product with new feature item", async () => { + const customProduct = { + ...pro, + items: customItems, + }; + + await autumn.track({ + customer_id: customerId, + value: usage, + feature_id: TestFeature.Words, + }); + + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 2).getTime(), + waitForSeconds: 10, + }); + + await runUpdateEntsTest({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + customProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customItems, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + }); + + test("should attach custom pro product with updated feature item", async () => { + const customItems2 = replaceItems({ + items: customItems, + featureId: TestFeature.Messages, + newItem: constructFeatureItem({ + feature_id: TestFeature.Messages, + included_usage: 1000, + }), + }); + + const customProduct = { + ...pro, + items: customItems2, + }; + + await runUpdateEntsTest({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + customProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customItems: customItems2, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + }); + + test("should attach custom pro product with removed feature item", async () => { + const customItems2 = customItems.filter( + (item) => item.feature_id != TestFeature.Messages, + ); + + const customProduct = { + ...pro, + items: customItems2, + }; + + await runUpdateEntsTest({ + autumn, + stripeCli: ctx.stripeCli, + customerId, + customProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customItems: customItems2, + usage: [ + { + featureId: TestFeature.Words, + value: usage, + }, + ], + }); + }); +}); diff --git a/server/tests/attach/updateEnts/updateEnts4.ts b/server/tests/attach/updateEnts/updateEnts4.backup.ts similarity index 100% rename from server/tests/attach/updateEnts/updateEnts4.ts rename to server/tests/attach/updateEnts/updateEnts4.backup.ts diff --git a/server/tests/attach/updateEnts/updateEnts4.test.ts b/server/tests/attach/updateEnts/updateEnts4.test.ts new file mode 100644 index 000000000..d5160ce99 --- /dev/null +++ b/server/tests/attach/updateEnts/updateEnts4.test.ts @@ -0,0 +1,89 @@ +import { + AttachBranch, + BillingInterval, + LegacyVersion, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { nullish } from "@/utils/genUtils.js"; +import { constructArrearItem } 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"; + +const testCase = "updateEnts4"; + +export const pro = constructProduct({ + items: [ + constructArrearItem({ + featureId: TestFeature.Words, + includedUsage: 10000, + }), + ], + type: "pro", + isAnnual: true, +}); + +describe(`${chalk.yellowBright(`${testCase}: Checking price changes don't result in update ents func`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro annual product", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("branch should not be same custom ents if base price updated", async () => { + let customItems = pro.items.filter((item) => !nullish(item.feature_id)); + + customItems = [ + ...customItems, + constructPriceItem({ + price: 10, + interval: BillingInterval.Year, + }), + ]; + + const preview = await autumn.attachPreview({ + customer_id: customerId, + product_id: pro.id, + is_custom: true, + items: customItems, + }); + + expect(preview.branch).toBe(AttachBranch.SameCustom); + }); +}); diff --git a/server/tests/attach/updateQuantity/updateQuantity1.backup.ts b/server/tests/attach/updateQuantity/updateQuantity1.backup.ts new file mode 100644 index 000000000..8d769f826 --- /dev/null +++ b/server/tests/attach/updateQuantity/updateQuantity1.backup.ts @@ -0,0 +1,154 @@ +import { + type AppEnv, + AttachErrCode, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { addPrefixToProducts } from "../utils.js"; + +const testCase = "updateQuantity1"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Users, + price: 12, + billingUnits: 1, + }), + ], + type: "pro", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + let curUnix = new Date().getTime(); + const numUsers = 0; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + const proOpts = [ + { + feature_id: TestFeature.Users, + quantity: 2, + }, + ]; + + it("should attach pro product (arrear prorated)", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options: proOpts, + }); + }); + + it("should throw error if try to attach same options", async () => { + await expectAutumnError({ + errCode: AttachErrCode.ProductAlreadyAttached, + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + options: proOpts, + }); + }, + }); + }); + + const updatedOpts = [ + { + feature_id: TestFeature.Users, + quantity: 4, + }, + ]; + + it("should update quantity to 4 users and have usage stay the same", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 2, + }); + await timeout(3000); + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 30, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options: updatedOpts, + usage: [ + { + featureId: TestFeature.Users, + value: 2, + }, + ], + waitForInvoice: 15000, + }); + }); +}); diff --git a/server/tests/attach/updateQuantity/updateQuantity1.test.ts b/server/tests/attach/updateQuantity/updateQuantity1.test.ts new file mode 100644 index 000000000..6e074661e --- /dev/null +++ b/server/tests/attach/updateQuantity/updateQuantity1.test.ts @@ -0,0 +1,150 @@ +import { + type AppEnv, + AttachErrCode, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; + +const testCase = "updateQuantity1"; + +export const pro = constructProduct({ + items: [ + constructPrepaidItem({ + featureId: TestFeature.Users, + price: 12, + billingUnits: 1, + }), + ], + type: "pro", +}); + +describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let stripeCli: Stripe; + + let curUnix = new Date().getTime(); + const numUsers = 0; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + attachPm: "success", + }); + + addPrefixToProducts({ + products: [pro], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [pro], + db, + orgId: org.id, + env, + }); + + testClockId = testClockId1!; + }); + + const proOpts = [ + { + feature_id: TestFeature.Users, + quantity: 2, + }, + ]; + + test("should attach pro product (arrear prorated)", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options: proOpts, + }); + }); + + test("should throw error if try to attach same options", async () => { + await expectAutumnError({ + errCode: AttachErrCode.ProductAlreadyAttached, + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + options: proOpts, + }); + }, + }); + }); + + const updatedOpts = [ + { + feature_id: TestFeature.Users, + quantity: 4, + }, + ]; + + test("should update quantity to 4 users and have usage stay the same", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 2, + }); + await timeout(3000); + + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 30, + }); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli, + db, + org, + env, + options: updatedOpts, + usage: [ + { + featureId: TestFeature.Users, + value: 2, + }, + ], + waitForInvoice: 15000, + }); + }); +}); diff --git a/server/tests/attach/upgradeOld/sharedProducts.ts b/server/tests/attach/upgradeOld/sharedProducts.ts new file mode 100644 index 000000000..f7f589a48 --- /dev/null +++ b/server/tests/attach/upgradeOld/sharedProducts.ts @@ -0,0 +1,120 @@ +import { + BillingInterval, + FreeTrialDuration, + ProductItemInterval, +} from "@autumn/shared"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js"; + +/** + * Shared products for upgradeOld test group + * Matches global products.pro, products.proWithTrial, products.premium, products.premiumWithTrial + */ + +export const sharedProProduct = constructProduct({ + id: "shared-upgradeold-pro", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Dashboard, + isBoolean: true, + }), + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructFeatureItem({ + featureId: TestFeature.Admin, + unlimited: true, + }), + constructPriceItem({ + price: 2000, + interval: BillingInterval.Month, + }), + ], +}); + +export const sharedProWithTrialProduct = constructProduct({ + id: "shared-upgradeold-pro-trial", + type: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Dashboard, + isBoolean: true, + }), + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 10, + interval: ProductItemInterval.Month, + }), + constructFeatureItem({ + featureId: TestFeature.Admin, + unlimited: true, + }), + constructPriceItem({ + price: 2000, + interval: BillingInterval.Month, + }), + ], + freeTrial: { + length: 7, + duration: FreeTrialDuration.Day, + unique_fingerprint: true, + card_required: true, + }, +}); + +export const sharedPremiumProduct = constructProduct({ + id: "shared-upgradeold-premium", + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 5000, + interval: BillingInterval.Month, + }), + ], +}); + +export const sharedPremiumWithTrialProduct = constructProduct({ + id: "shared-upgradeold-premium-trial", + type: "premium", + items: [ + constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + interval: ProductItemInterval.Month, + }), + constructPriceItem({ + price: 5000, + interval: BillingInterval.Month, + }), + ], + freeTrial: { + length: 7, + duration: FreeTrialDuration.Day, + unique_fingerprint: true, + card_required: true, + }, +}); + +await (async () => { + await createSharedProducts({ + ctx, + products: [ + sharedProProduct, + sharedProWithTrialProduct, + sharedPremiumProduct, + sharedPremiumWithTrialProduct, + ], + }); +})(); diff --git a/server/tests/attach/upgradeOld/upgradeOld1.ts b/server/tests/attach/upgradeOld/upgradeOld1.backup.ts similarity index 100% rename from server/tests/attach/upgradeOld/upgradeOld1.ts rename to server/tests/attach/upgradeOld/upgradeOld1.backup.ts diff --git a/server/tests/attach/upgradeOld/upgradeOld1.test.ts b/server/tests/attach/upgradeOld/upgradeOld1.test.ts new file mode 100644 index 000000000..49b6e125a --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld1.test.ts @@ -0,0 +1,73 @@ +import chalk from "chalk"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { Customer } from "@autumn/shared"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { addDays } from "date-fns"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import type Stripe from "stripe"; +import { + sharedProWithTrialProduct, + sharedPremiumProduct, +} from "./sharedProducts.js"; + +describe(`${chalk.yellowBright( + "upgradeOld1: Testing upgrade (trial to paid)", +)}`, () => { + const customerId = "upgradeOld1"; + let testClockId: string; + let customer: Customer; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + customer = customer_; + testClockId = testClockId_; + }); + + test("should attach pro with trial", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedProWithTrialProduct.id, + }); + }); + + test("should attach premium", async () => { + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(new Date(), 3).getTime(), + waitForSeconds: 10, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: sharedPremiumProduct.id, + }); + }); + + test("should check product, ents and invoices", async () => { + const res = await autumn.customers.get(customerId); + expectCustomerV0Correct({ + sent: sharedPremiumProduct, + cusRes: res, + ctx, + }); + + const invoices = await res.invoices; + + expect(invoices[0].total).toBe(5000); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld2.ts b/server/tests/attach/upgradeOld/upgradeOld2.backup.ts similarity index 100% rename from server/tests/attach/upgradeOld/upgradeOld2.ts rename to server/tests/attach/upgradeOld/upgradeOld2.backup.ts diff --git a/server/tests/attach/upgradeOld/upgradeOld2.test.ts b/server/tests/attach/upgradeOld/upgradeOld2.test.ts new file mode 100644 index 000000000..8f50a3263 --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld2.test.ts @@ -0,0 +1,51 @@ +import type Stripe from "stripe"; +import chalk from "chalk"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { Customer } from "@autumn/shared"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { + sharedProProduct, + sharedPremiumWithTrialProduct, +} from "./sharedProducts.js"; + +describe(`${chalk.yellowBright( + "upgradeOld2: Testing upgrade (paid to trial)", +)}`, () => { + const customerId = "upgradeOld2"; + let testClockId: string; + let customer: Customer; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + customer = customer_; + testClockId = testClockId_; + }); + + test("should attach pro", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedProProduct.id, + }); + }); + + test("should attach premium with trial and have trial", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedPremiumWithTrialProduct.id, + }); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld3.ts b/server/tests/attach/upgradeOld/upgradeOld3.backup.ts similarity index 100% rename from server/tests/attach/upgradeOld/upgradeOld3.ts rename to server/tests/attach/upgradeOld/upgradeOld3.backup.ts diff --git a/server/tests/attach/upgradeOld/upgradeOld3.test.ts b/server/tests/attach/upgradeOld/upgradeOld3.test.ts new file mode 100644 index 000000000..c8a6407c1 --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld3.test.ts @@ -0,0 +1,73 @@ +import chalk from "chalk"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { CusProductStatus } from "@autumn/shared"; +import { addDays } from "date-fns"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import type Stripe from "stripe"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { + sharedProWithTrialProduct, + sharedPremiumWithTrialProduct, +} from "./sharedProducts.js"; + +describe(`${chalk.yellowBright("upgradeOld3: Testing upgrade (trial to trial)")}`, () => { + const customerId = "upgradeOld3"; + let testClockId: string; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + beforeAll(async () => { + stripeCli = ctx.stripeCli; + const { customer: customer_, testClockId: testClockId_ } = + await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId_; + }); + + test("should attach pro with trial", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedProWithTrialProduct.id, + }); + + console.log(` ${chalk.greenBright("Attached pro with trial")}`); + }); + + test("should attach premium with trial", async () => { + const advanceTo = addDays(new Date(), 3).getTime(); + + await advanceTestClock({ + stripeCli, + testClockId, + advanceTo, + waitForSeconds: 10, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: sharedPremiumWithTrialProduct.id, + }); + }); + + test("should check product and ents", async () => { + const res = await autumn.customers.get(customerId); + expectCustomerV0Correct({ + sent: sharedPremiumWithTrialProduct, + cusRes: res, + ctx, + status: CusProductStatus.Trialing, + }); + + const invoices = res.invoices; + + expect(invoices![0].total).toBe(0); + }); +}); diff --git a/server/tests/attach/upgradeOld/upgradeOld4.ts b/server/tests/attach/upgradeOld/upgradeOld4.backup.ts similarity index 100% rename from server/tests/attach/upgradeOld/upgradeOld4.ts rename to server/tests/attach/upgradeOld/upgradeOld4.backup.ts diff --git a/server/tests/attach/upgradeOld/upgradeOld4.test.ts b/server/tests/attach/upgradeOld/upgradeOld4.test.ts new file mode 100644 index 000000000..3a555c57e --- /dev/null +++ b/server/tests/attach/upgradeOld/upgradeOld4.test.ts @@ -0,0 +1,111 @@ +// TESTING UPGRADES + +import chalk from "chalk"; +import { beforeAll, describe, expect, test } from "bun:test"; +import { AutumnCli } from "tests/cli/AutumnCli.js"; +import { + attachFailedPaymentMethod, + attachPmToCus, +} from "@/external/stripe/stripeCusUtils.js"; +import { Customer } from "@autumn/shared"; +import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import type Stripe from "stripe"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { + sharedProProduct, + sharedPremiumProduct, +} from "./sharedProducts.js"; + +const testCase = "upgradeOld4"; +describe(`${chalk.yellowBright("upgradeOld4: Testing upgrade from pro -> premium")}`, () => { + let customer: Customer; + const customerId = testCase; + + let stripeCli: Stripe; + const autumn: AutumnInt = new AutumnInt(); + + beforeAll(async () => { + stripeCli = ctx.stripeCli; + + const { customer: customer_ } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + customer = customer_; + }); + + test("should attach pro (trial)", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedProProduct.id, + }); + + const res = await autumn.customers.get(customerId); + expectCustomerV0Correct({ + sent: sharedProProduct, + cusRes: res, + ctx, + }); + }); + + // 1. Try force checkout... + test("should attach premium and not be able to force checkout", async () => { + expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedPremiumProduct.id, + force_checkout: true, + }); + }, + }); + }); + + test("should attach premium and not be able to upgrade (without payment method)", async () => { + await attachFailedPaymentMethod({ + stripeCli: stripeCli, + customer: customer, + }); + + await expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + product_id: sharedPremiumProduct.id, + force_checkout: true, + }); + }, + }); + }); + + // Attach payment method + test("should attach successful payment method", async () => { + await attachPmToCus({ + db: ctx.db, + customer: customer, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should attach premium and have correct product and entitlements", async () => { + await AutumnCli.attach({ + customerId: customerId, + productId: sharedPremiumProduct.id, + }); + + const res = await AutumnCli.getCustomer(customerId); + expectCustomerV0Correct({ + sent: sharedPremiumProduct, + cusRes: res, + ctx, + }); + }); +}); diff --git a/server/tests/contUse/entities/entity1.ts b/server/tests/contUse/entities/entity1.backup.ts similarity index 100% rename from server/tests/contUse/entities/entity1.ts rename to server/tests/contUse/entities/entity1.backup.ts diff --git a/server/tests/contUse/entities/entity1.test.ts b/server/tests/contUse/entities/entity1.test.ts new file mode 100644 index 000000000..92cb82cfc --- /dev/null +++ b/server/tests/contUse/entities/entity1.test.ts @@ -0,0 +1,193 @@ +import { + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "entity1"; + +// Pro is $20 / month, Seat is $50 / user + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete entities`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + test("should create entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += 1; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: 1, + }, + ], + }); + }); + + const entities = [ + { + id: "2", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test2", + feature_id: TestFeature.Users, + }, + ]; + + test("should create 2 entities and have correct invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 30, + }); + + await autumn.entities.create(customerId, entities); + await timeout(3000); + + usage += entities.length; + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + itemQuantity: usage, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices!; + expect(invoices.length).toBe(2); + expect(invoices[0].total).toBe(userItem.price! * entities.length); + }); + + test("should delete 1 entity and have no new invoice", async () => { + await autumn.entities.delete(customerId, entities[0].id); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices!; + expect(invoices.length).toBe(2); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 1, + itemQuantity: usage - 1, + }); + }); + + const newEntities = [ + { + id: "4", + name: "test3", + feature_id: TestFeature.Users, + }, + { + id: "5", + name: "test4", + feature_id: TestFeature.Users, + }, + ]; + + test("should create 2 entities and have correct invoice (only pay for 1)", async () => { + await autumn.entities.create(customerId, newEntities); + await timeout(3000); + usage += 1; + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices!; + + expect(invoices.length).toBe(3); + expect(invoices[0].total).toBe(userItem.price!); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + itemQuantity: usage, + }); + }); +}); diff --git a/server/tests/contUse/entities/entity2.ts b/server/tests/contUse/entities/entity2.backup.ts similarity index 100% rename from server/tests/contUse/entities/entity2.ts rename to server/tests/contUse/entities/entity2.backup.ts diff --git a/server/tests/contUse/entities/entity2.test.ts b/server/tests/contUse/entities/entity2.test.ts new file mode 100644 index 000000000..36914d714 --- /dev/null +++ b/server/tests/contUse/entities/entity2.test.ts @@ -0,0 +1,177 @@ +import { + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { + calcProrationAndExpectInvoice, + expectSubQuantityCorrect, +} from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, +}); + +const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "entity2"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate now`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = Date.now(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + test("should create entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += 1; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + const newEntities = [ + { + id: "2", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test2", + feature_id: TestFeature.Users, + }, + ]; + + test("should create 2 entities and have correct invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 30, + }); + + await autumn.entities.create(customerId, newEntities); + usage += newEntities.length; + + const { stripeSubs } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + itemQuantity: usage, + }); + + await timeout(5000); + + await calcProrationAndExpectInvoice({ + autumn, + stripeSubs, + customerId, + quantity: newEntities.length, + unitPrice: userItem.price!, + curUnix, + numInvoices: 2, + }); + }); + + test("should delete 1 entity and have correct invoice amount", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 30, + }); + + await timeout(5000); + + await autumn.entities.delete(customerId, newEntities[0].id); + usage -= 1; + + const { stripeSubs } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await calcProrationAndExpectInvoice({ + autumn, + stripeSubs, + customerId, + quantity: -1, + unitPrice: userItem.price!, + curUnix, + numInvoices: 3, + }); + }); +}); diff --git a/server/tests/contUse/entities/entity3.ts b/server/tests/contUse/entities/entity3.backup.ts similarity index 100% rename from server/tests/contUse/entities/entity3.ts rename to server/tests/contUse/entities/entity3.backup.ts diff --git a/server/tests/contUse/entities/entity3.test.ts b/server/tests/contUse/entities/entity3.test.ts new file mode 100644 index 000000000..e981ad002 --- /dev/null +++ b/server/tests/contUse/entities/entity3.test.ts @@ -0,0 +1,164 @@ +import { + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths, addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "entity3"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + test("should create three entities, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += firstEntities.length; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + test("should delete 2 entities and have no new invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 30, + }); + + await autumn.entities.delete(customerId, firstEntities[0].id); + await autumn.entities.delete(customerId, firstEntities[1].id); + + const numReplaceables = 2; + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables, + itemQuantity: usage - numReplaceables, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices!; + expect(invoices.length).toBe(1); + }); + + test("should advance clock to next cycle and have correct invoice", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + }); + + usage -= 2; // 2 entities deleted + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + + const basePrice = getBasePrice({ product: pro }); + expect(invoices.length).toBe(2); + expect(invoices[0].total).toBe(basePrice); // 0 entities + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + itemQuantity: usage, + numReplaceables: 0, + }); + }); +}); diff --git a/server/tests/contUse/entities/entity4.ts b/server/tests/contUse/entities/entity4.backup.ts similarity index 100% rename from server/tests/contUse/entities/entity4.ts rename to server/tests/contUse/entities/entity4.backup.ts diff --git a/server/tests/contUse/entities/entity4.test.ts b/server/tests/contUse/entities/entity4.test.ts new file mode 100644 index 000000000..ba74c5fed --- /dev/null +++ b/server/tests/contUse/entities/entity4.test.ts @@ -0,0 +1,221 @@ +// Handling per entity features! + +import { + CusExpand, + LegacyVersion, + type LimitedItem, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { useEntityBalanceAndExpect } from "tests/utils/expectUtils/expectContUse/expectEntityUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { + constructArrearProratedItem, + 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +const perEntityItem = constructFeatureItem({ + featureId: TestFeature.Messages, + entityFeatureId: TestFeature.Users, + includedUsage: 500, +}) as LimitedItem; + +export const pro = constructProduct({ + items: [userItem, perEntityItem], + type: "pro", +}); + +const testCase = "entity4"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + test("should create one entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += firstEntities.length; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + test("should create 3 entities and have correct message balance", async () => { + const newEntities = [ + { + id: "2", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + await autumn.entities.create(customerId, newEntities); + usage += newEntities.length; + + const customer = await autumn.customers.get(customerId, { + expand: [CusExpand.Entities], + }); + + const res = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(res.balance).toBe( + (perEntityItem.included_usage as number) * usage, + ); + + // @ts-expect-error + for (const entity of customer.entities) { + const entRes = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: entity.id, + }); + + expect(entRes.balance).toBe(perEntityItem.included_usage); + } + }); + + return; + + // 1. Use from main balance... + test("should use from top level balance", async () => { + const deduction = 600; + const perEntityIncluded = perEntityItem.included_usage as number; + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: deduction, + }); + await timeout(5000); + + const { balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(balance).toBe(perEntityIncluded * usage - deduction); + }); + + test("should use from entity balance", async () => { + await useEntityBalanceAndExpect({ + autumn, + customerId, + featureId: TestFeature.Messages, + entityId: "2", + }); + + await useEntityBalanceAndExpect({ + autumn, + customerId, + featureId: TestFeature.Messages, + entityId: "3", + }); + }); + + // Delete one entity and create a new one and master balance should be same + const deletedEntityId = "2"; + const newEntity = { + id: "4", + name: "test", + feature_id: TestFeature.Users, + }; + test("should delete one entity and create a new one", async () => { + const { balance: masterBalanceBefore } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + const { balance: entityBalanceBefore } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: deletedEntityId, + }); + + await autumn.entities.delete(customerId, deletedEntityId); + await autumn.entities.create(customerId, [newEntity]); + + const { balance: masterBalanceAfter } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + expect(masterBalanceAfter).toBe(masterBalanceBefore); + + const { balance: entityBalanceAfter } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: newEntity.id, + }); + + expect(entityBalanceAfter).toBe(entityBalanceBefore); + }); +}); diff --git a/server/tests/contUse/entities/entity5.ts b/server/tests/contUse/entities/entity5.backup.ts similarity index 100% rename from server/tests/contUse/entities/entity5.ts rename to server/tests/contUse/entities/entity5.backup.ts diff --git a/server/tests/contUse/entities/entity5.test.ts b/server/tests/contUse/entities/entity5.test.ts new file mode 100644 index 000000000..823890340 --- /dev/null +++ b/server/tests/contUse/entities/entity5.test.ts @@ -0,0 +1,163 @@ +// test payment failures + +import { + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "entity5"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payment fail`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + ]; + + test("should create one entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += firstEntities.length; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + test("should attach failed payment method", async () => { + const fullCus = await CusService.getFull({ + db: ctx.db, + idOrInternalId: customerId, + orgId: ctx.org.id, + env: ctx.env, + }); + + await attachFailedPaymentMethod({ + stripeCli: ctx.stripeCli, + customer: fullCus, + }); + }); + + test("should try to create entities and fail", async () => { + await expectAutumnError({ + errMessage: "(Stripe Error) Your card was declined.", + func: async () => { + await autumn.entities.create(customerId, [ + { + id: "2", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test", + feature_id: TestFeature.Users, + }, + ]); + }, + }); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + }); + + test("should track usage for users and fail", async () => { + await expectAutumnError({ + errMessage: "(Stripe Error) Your card was declined.", + func: async () => { + return await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 2, + }); + }, + }); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + }); +}); diff --git a/server/tests/contUse/roles/role1.ts b/server/tests/contUse/roles/role1.backup.ts similarity index 100% rename from server/tests/contUse/roles/role1.ts rename to server/tests/contUse/roles/role1.backup.ts diff --git a/server/tests/contUse/roles/role1.test.ts b/server/tests/contUse/roles/role1.test.ts new file mode 100644 index 000000000..7ddfcdf68 --- /dev/null +++ b/server/tests/contUse/roles/role1.test.ts @@ -0,0 +1,223 @@ +// Handling per entity features! + +import { + LegacyVersion, + type LimitedItem, + type ProductItem, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { Decimal } from "decimal.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.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"; + +const user = TestFeature.Users; +const admin = TestFeature.Admin; + +const userMessages = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 100, + entityFeatureId: user, +}) as LimitedItem; + +const adminMessages = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 500, + entityFeatureId: admin, +}) as LimitedItem; + +const adminRights = constructFeatureItem({ + featureId: TestFeature.AdminRights, + entityFeatureId: admin, + isBoolean: true, +}) as ProductItem; + +export const pro = constructProduct({ + items: [userMessages, adminMessages, adminRights], + type: "pro", +}); + +const testCase = "role1"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing roles`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + const userId = "user1"; + const adminId = "admin1"; + const firstEntities = [ + { + id: userId, + name: "test", + feature_id: user, + }, + { + id: adminId, + name: "test", + feature_id: admin, + }, + ]; + + test("should create initial entities, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should have correct check result for admin rights", async () => { + const { allowed } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.AdminRights, + entity_id: adminId, + }); + + const entity = await autumn.entities.get(customerId, adminId); + + expect(allowed).toBe(true); + expect(entity.features[TestFeature.AdminRights]).toBeDefined(); + + const { allowed: userAllowed } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.AdminRights, + entity_id: userId, + }); + const userEntity = await autumn.entities.get(customerId, userId); + + expect(userAllowed).toBe(false); + expect(userEntity.features[TestFeature.AdminRights]).toBeUndefined(); + }); + + test("should have correct total balance", async () => { + const { balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + }); + + const totalIncluded = + userMessages.included_usage + adminMessages.included_usage; + + expect(balance).toBe(totalIncluded); + }); + + test("should have correct per entity balance", async () => { + const { balance: userBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: userId, + }); + + const userEntity = await autumn.entities.get(customerId, userId); + + expect(userBalance).toBe(userMessages.included_usage); + expect(userEntity.features[TestFeature.Messages].included_usage).toBe( + userMessages.included_usage, + ); + + const { balance: adminBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: adminId, + }); + + const adminEntity = await autumn.entities.get(customerId, adminId); + + expect(adminBalance).toBe(adminMessages.included_usage); + expect(adminEntity.features[TestFeature.Messages].included_usage).toBe( + adminMessages.included_usage, + ); + }); + + const userUsage = Math.random() * 50; + const expectedUserBalance = new Decimal(userMessages.included_usage) + .minus(userUsage) + .toNumber(); + test("should have correct user usage", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: userUsage, + entity_id: userId, + }); + await timeout(2000); + + const { balance: userBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: userId, + }); + + const { balance: adminBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: adminId, + }); + + expect(adminBalance).toBe(adminMessages.included_usage); + expect(userBalance).toBe(expectedUserBalance); + }); + + const adminUsage = Math.random() * 50; + const expectedAdminBalance = new Decimal(adminMessages.included_usage) + .minus(adminUsage) + .toNumber(); + test("Should have correct admin usage", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: adminUsage, + entity_id: adminId, + }); + await timeout(2000); + + const { balance: adminBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: adminId, + }); + + const { balance: userBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: userId, + }); + + expect(adminBalance).toBe(expectedAdminBalance); + expect(userBalance).toBe(expectedUserBalance); + }); +}); diff --git a/server/tests/contUse/roles/role2.ts b/server/tests/contUse/roles/role2.backup.ts similarity index 100% rename from server/tests/contUse/roles/role2.ts rename to server/tests/contUse/roles/role2.backup.ts diff --git a/server/tests/contUse/roles/role2.test.ts b/server/tests/contUse/roles/role2.test.ts new file mode 100644 index 000000000..cc0bd2cbd --- /dev/null +++ b/server/tests/contUse/roles/role2.test.ts @@ -0,0 +1,167 @@ +import { + type CreateEntity, + LegacyVersion, + type LimitedItem, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +const user = TestFeature.Users; +const admin = TestFeature.Admin; + +const userMessages = constructArrearItem({ + featureId: TestFeature.Messages, + price: 0.5, + entityFeatureId: user, +}) as LimitedItem; + +export const pro = constructProduct({ + items: [userMessages], + type: "pro", +}); + +const testCase = "role2"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing overages for per entity`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const user1 = "user1"; + const user2 = "user2"; + + const firstEntities: CreateEntity[] = [ + { + id: user1, + name: "test", + feature_id: user, + }, + { + id: user2, + name: "test", + feature_id: user, + }, + ]; + + test("should create initial entities, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + entities: firstEntities, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.features[TestFeature.Messages].included_usage).toBe( + userMessages.included_usage * firstEntities.length, + ); + }); + + const user1Usage = 125000; + const user2Usage = 150000; + test("should track correct usage for seat messages", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: user1Usage, + entity_id: user1, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: user2Usage, + entity_id: user2, + }); + + await timeout(4000); + + const includedUsage = userMessages.included_usage; + + const { balance: userBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: user1, + }); + + expect(userBalance).toBe(includedUsage - user1Usage); + + const { balance: user2Balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: user2, + }); + + expect(user2Balance).toBe(includedUsage - user2Usage); + }); + + test("should have correct invoice next cycle", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addHours( + addMonths(new Date(), 1), + hoursToFinalizeInvoice, + ).getTime(), + waitForSeconds: 30, + }); + + const includedUsage = userMessages.included_usage; + const user1Overage = user1Usage - includedUsage; + const user2Overage = user2Usage - includedUsage; + + const totalUsage = user1Overage + user2Overage + includedUsage; + + const expectedInvoiceTotal = await getExpectedInvoiceTotal({ + customerId, + productId: pro.id, + usage: [{ featureId: TestFeature.Messages, value: totalUsage }], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + expectExpired: true, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices[0].total).toBe(expectedInvoiceTotal); + }); +}); diff --git a/server/tests/contUse/roles/role3.ts b/server/tests/contUse/roles/role3.backup.ts similarity index 100% rename from server/tests/contUse/roles/role3.ts rename to server/tests/contUse/roles/role3.backup.ts diff --git a/server/tests/contUse/roles/role3.test.ts b/server/tests/contUse/roles/role3.test.ts new file mode 100644 index 000000000..5682a818c --- /dev/null +++ b/server/tests/contUse/roles/role3.test.ts @@ -0,0 +1,236 @@ +import { + type CreateEntity, + LegacyVersion, + type LimitedItem, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js"; +import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"; +import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js"; + +const user = TestFeature.Users; +const admin = TestFeature.Admin; + +const userMessages = constructArrearItem({ + featureId: TestFeature.Messages, + price: 0.5, + entityFeatureId: user, +}) as LimitedItem; + +const adminMessages = constructArrearItem({ + featureId: TestFeature.Messages, + includedUsage: 0, + price: 0.1, + entityFeatureId: admin, +}) as LimitedItem; + +export const pro = constructProduct({ + items: [userMessages, adminMessages], + type: "pro", +}); + +const testCase = "role3"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing overages for per entity, diff roles`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 }); + let testClockId: string; + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const user1 = "user1"; + const user2 = "user2"; + const admin1 = "admin1"; + const admin2 = "admin2"; + const firstEntities: CreateEntity[] = [ + { + id: user1, + name: "test", + feature_id: user, + }, + { + id: user2, + name: "test", + feature_id: user, + }, + { + id: admin1, + name: "test", + feature_id: admin, + }, + { + id: admin2, + name: "test", + feature_id: admin, + }, + ]; + + test("should create initial entities, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + entities: firstEntities, + }); + }); + + const user1Usage = 125000; + const user2Usage = 150000; + + // total: 275000, included: 10000, overage: 255000 + test("should track correct usage for seat messages", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: user1Usage, + entity_id: user1, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: user2Usage, + entity_id: user2, + }); + + await timeout(4000); + + const includedUsage = userMessages.included_usage; + + const { balance: userBalance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: user1, + }); + + expect(userBalance).toBe(includedUsage - user1Usage); + + const { balance: user2Balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: user2, + }); + + expect(user2Balance).toBe(includedUsage - user2Usage); + + const { balance: admin1Balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: admin1, + }); + + expect(admin1Balance).toBe(adminMessages.included_usage); + + const { balance: admin2Balance } = await autumn.check({ + customer_id: customerId, + feature_id: TestFeature.Messages, + entity_id: admin2, + }); + + expect(admin2Balance).toBe(adminMessages.included_usage); + }); + + const admin1Usage = 130000; + const admin2Usage = 140000; + // total: 270000, included: 0, overage: 270000 + test("should track correct usage for admin messages", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: admin1Usage, + entity_id: admin1, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: admin2Usage, + entity_id: admin2, + }); + + await timeout(4000); + }); + + test("should have correct invoice next cycle", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addMonths(new Date(), 1).getTime(), + // addHours( + // addMonths(new Date(), 1), + // hoursToFinalizeInvoice + // ).getTime(), + waitForSeconds: 30, + }); + + return; + + const includedUsage = userMessages.included_usage; + const user1Overage = user1Usage - includedUsage; + const user2Overage = user2Usage - includedUsage; + const totalUserUsage = user1Overage + user2Overage + includedUsage; + + const admin1Overage = admin1Usage - adminMessages.included_usage; + const admin2Overage = admin2Usage - adminMessages.included_usage; + const totalAdminUsage = + admin1Overage + admin2Overage + adminMessages.included_usage; + + const expectedInvoiceTotal = await getExpectedInvoiceTotal({ + customerId, + productId: pro.id, + usage: [ + { + featureId: TestFeature.Messages, + entityFeatureId: user, + value: totalUserUsage, + }, + { + featureId: TestFeature.Messages, + entityFeatureId: admin, + value: totalAdminUsage, + }, + ], + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + expectExpired: true, + }); + + const customer = await autumn.customers.get(customerId); + expect(customer.invoices[0].total).toBe(expectedInvoiceTotal); + }); +}); diff --git a/server/tests/contUse/track/track1.ts b/server/tests/contUse/track/track1.backup.ts similarity index 100% rename from server/tests/contUse/track/track1.ts rename to server/tests/contUse/track/track1.backup.ts diff --git a/server/tests/contUse/track/track1.test.ts b/server/tests/contUse/track/track1.test.ts new file mode 100644 index 000000000..3b5012018 --- /dev/null +++ b/server/tests/contUse/track/track1.test.ts @@ -0,0 +1,155 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "track1"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + test("should attach pro", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should create track +3 usage and have correct invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 5, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 3, + }); + + await timeout(15000); + + usage += 3; + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(2); + expect(invoices[0].total).toBe(userItem.price! * 2); + }); + + test("should track -3 and have no new invoice", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: -3, + }); + + await timeout(5000); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(2); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 3, + itemQuantity: usage - 3, + }); + }); + + test("should track +3 and have no new invoice", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 3, + }); + + await timeout(5000); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(2); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + }); +}); diff --git a/server/tests/contUse/track/track2.ts b/server/tests/contUse/track/track2.backup.ts similarity index 100% rename from server/tests/contUse/track/track2.ts rename to server/tests/contUse/track/track2.backup.ts diff --git a/server/tests/contUse/track/track2.test.ts b/server/tests/contUse/track/track2.test.ts new file mode 100644 index 000000000..735073786 --- /dev/null +++ b/server/tests/contUse/track/track2.test.ts @@ -0,0 +1,116 @@ +import { + LegacyVersion, + OnDecrease, + OnIncrease, +} from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "track2"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use (without overage)`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + test("should attach pro", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should track +1 and have no new invoice", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 1, + }); + + usage += 1; + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); + + test("should track -1 and have no new invoice", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: -1, + }); + + usage -= 1; + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + }); +}); diff --git a/server/tests/contUse/track/track3.ts b/server/tests/contUse/track/track3.backup.ts similarity index 100% rename from server/tests/contUse/track/track3.ts rename to server/tests/contUse/track/track3.backup.ts diff --git a/server/tests/contUse/track/track3.test.ts b/server/tests/contUse/track/track3.test.ts new file mode 100644 index 000000000..0d682a772 --- /dev/null +++ b/server/tests/contUse/track/track3.test.ts @@ -0,0 +1,193 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { + expectSubQuantityCorrect, + expectUpcomingItemsCorrect, +} from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.ProrateNextCycle, + on_decrease: OnDecrease.ProrateNextCycle, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "track3"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use, prorate next cycle`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + test("should attach pro", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should create track +3 usage and have correct invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 5, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 3, + }); + + await timeout(15000); + + usage += 3; + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + curUnix, + expectedNumItems: 1, + unitPrice: userItem.price!, + quantity: 2, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); + + test("should track -1 and have no new invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 5, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: -1, + }); + + usage -= 1; + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + unitPrice: userItem.price!, + curUnix, + expectedNumItems: 2, + quantity: -1, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); + + test("should track -1 and have no new invoice", async () => { + const quantity = 2; + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: quantity, + }); + + usage += quantity; + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + unitPrice: userItem.price!, + curUnix, + expectedNumItems: 3, + quantity, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); +}); diff --git a/server/tests/contUse/track/track4.ts b/server/tests/contUse/track/track4.backup.ts similarity index 100% rename from server/tests/contUse/track/track4.ts rename to server/tests/contUse/track/track4.backup.ts diff --git a/server/tests/contUse/track/track4.test.ts b/server/tests/contUse/track/track4.test.ts new file mode 100644 index 000000000..aa0ff7654 --- /dev/null +++ b/server/tests/contUse/track/track4.test.ts @@ -0,0 +1,193 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { + expectSubQuantityCorrect, + expectUpcomingItemsCorrect, +} from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.ProrateNextCycle, + on_decrease: OnDecrease.ProrateNextCycle, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "track4"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing set usage for cont use, prorate next cycle`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + test("should attach pro", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + test("should create set usage to 3 and have correct invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 2).getTime(), + waitForSeconds: 15, + }); + + await autumn.usage({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 3, + }); + + await timeout(15000); + + usage += 3; + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + curUnix, + expectedNumItems: 1, + unitPrice: userItem.price!, + quantity: 2, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); + + test("should set usage to 2 and have no new invoice", async () => { + const newUsage = 2; + + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 15, + }); + + await autumn.usage({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: newUsage, + }); + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage: newUsage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + unitPrice: userItem.price!, + curUnix, + expectedNumItems: 2, + quantity: -1, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); + + test("should set usage to 4 and have no new invoice", async () => { + const newUsage = 4; + await autumn.usage({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: newUsage, + }); + + usage = newUsage; + + const { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + }); + + await expectUpcomingItemsCorrect({ + stripeCli: ctx.stripeCli, + fullCus, + stripeSubs, + unitPrice: userItem.price!, + curUnix, + expectedNumItems: 3, + quantity: 2, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(1); + }); +}); diff --git a/server/tests/contUse/track/track5.ts b/server/tests/contUse/track/track5.backup.ts similarity index 100% rename from server/tests/contUse/track/track5.ts rename to server/tests/contUse/track/track5.backup.ts diff --git a/server/tests/contUse/track/track5.test.ts b/server/tests/contUse/track/track5.test.ts new file mode 100644 index 000000000..331650538 --- /dev/null +++ b/server/tests/contUse/track/track5.test.ts @@ -0,0 +1,211 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { OnDecrease, OnIncrease, ProductItemFeatureType } from "@autumn/shared"; +import chalk from "chalk"; +import { addDays, addHours } from "date-fns"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { defaultApiVersion } from "tests/constants.js"; +import { features } from "tests/global.js"; +import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { constructArrearProratedItem } 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"; + +const seatsItem = constructArrearProratedItem({ + featureId: features.seats.id, + featureType: ProductItemFeatureType.ContinuousUse, + pricePerUnit: 20, + includedUsage: 3, + config: { + on_increase: OnIncrease.ProrateNextCycle, + on_decrease: OnDecrease.ProrateNextCycle, + }, +}); + +const seatsProduct = constructProduct({ + type: "pro", + items: [seatsItem], +}); + +const testCase = "track5"; +const includedUsage = seatsItem.included_usage as number; + +const simulateOneCycle = async ({ + customerId, + stripeCli, + curUnix, + usageValues, + autumn, + testClockId, +}: { + customerId: string; + stripeCli: Stripe; + curUnix: number; + usageValues: number[]; + autumn: AutumnInt; + testClockId: string; +}) => { + const { subs } = await getSubsFromCusId({ + customerId, + db: ctx.db, + org: ctx.org, + env: ctx.env, + stripeCli, + productId: seatsProduct.id, + }); + + const sub = subs[0]; + + let accruedPrice = 0; + for (const usageValue of usageValues) { + const daysToAdvance = Math.round(Math.random() * 10) + 1; + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(curUnix, daysToAdvance).getTime(), + waitForSeconds: 10, + }); + + const customer = await autumn.customers.get(customerId); + const prevBalance = customer.features[seatsItem.feature_id!].balance!; + const prevUsage = includedUsage - prevBalance; + + const usageDiff = usageValue - prevUsage; + + const value1 = Math.floor(usageDiff / 2); + const value2 = usageDiff - value1; + + await autumn.track({ + customer_id: customerId, + feature_id: seatsItem.feature_id!, + value: value1, + }); + + await autumn.track({ + customer_id: customerId, + feature_id: seatsItem.feature_id!, + value: value2, + }); + + const newBalance = includedUsage - usageValue; + const prevOverage = Math.max(0, -prevBalance); + const newOverage = Math.max(0, -newBalance); + + const newPrice = (newOverage - prevOverage) * seatsItem.price!; + + const { start, end } = subToPeriodStartEnd({ sub }); + const proratedPrice = calculateProrationAmount({ + periodStart: start * 1000, + periodEnd: end * 1000, + now: curUnix, + amount: newPrice, + allowNegative: true, + }); + + accruedPrice = new Decimal(accruedPrice).plus(proratedPrice).toNumber(); + } + + const customer = await autumn.customers.get(customerId); + const balance = customer.features[seatsItem.feature_id!].balance!; + + const overage = Math.min(0, includedUsage - balance); + const usagePrice = overage * seatsItem.price!; + const basePrice = getBasePrice({ product: seatsProduct }); + + const totalPrice = new Decimal(accruedPrice) + .plus(usagePrice) + .plus(basePrice) + .toDecimalPlaces(2) + .toNumber(); + + const { start, end } = subToPeriodStartEnd({ sub }); + curUnix = await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addHours(end * 1000, hoursToFinalizeInvoice).getTime(), + waitForSeconds: 30, + }); + + const cusAfter = await autumn.customers.get(customerId); + const invoices = cusAfter.invoices; + const invoice = invoices[0]; + + expect(invoice.total).toBeCloseTo(totalPrice, 2); + + return { + curUnix, + }; +}; + +describe(`${chalk.yellowBright("conUse/track5: Testing update cont use through /usage")}`, () => { + const customerId = testCase; + let testClockId = ""; + const autumn = new AutumnInt({ version: defaultApiVersion }); + let curUnix = Date.now(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [seatsProduct], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + test("should attach in arrear prorated seats", async () => { + await attachAndExpectCorrect({ + customerId, + product: seatsProduct, + db: ctx.db, + org: ctx.org, + env: ctx.env, + autumn, + stripeCli: ctx.stripeCli, + }); + }); + + test("simulate first cycle and have correct invoice / balance", async () => { + const res = await simulateOneCycle({ + customerId, + stripeCli: ctx.stripeCli, + curUnix, + usageValues: [8, 2], + autumn, + testClockId, + }); + + curUnix = res.curUnix; + }); + + test("simulate second cycle and have correct invoice / balance", async () => { + const res = await simulateOneCycle({ + customerId, + stripeCli: ctx.stripeCli, + curUnix, + usageValues: [12, 3], + autumn, + testClockId, + }); + + curUnix = res.curUnix; + }); +}); diff --git a/server/tests/contUse/track/track6.ts b/server/tests/contUse/track/track6.backup.ts similarity index 100% rename from server/tests/contUse/track/track6.ts rename to server/tests/contUse/track/track6.backup.ts diff --git a/server/tests/contUse/track/track6.test.ts b/server/tests/contUse/track/track6.test.ts new file mode 100644 index 000000000..b24d73d97 --- /dev/null +++ b/server/tests/contUse/track/track6.test.ts @@ -0,0 +1,95 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { LegacyVersion, type LimitedItem } from "@autumn/shared"; +import chalk from "chalk"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.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"; + +const userItem = constructFeatureItem({ + featureId: TestFeature.Users, + includedUsage: 5, +}) as LimitedItem; + +export const free = constructProduct({ + items: [userItem], + type: "free", + isDefault: false, +}); + +const testCase = "track6"; + +describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condition`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [free], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + test("should track 5 events in a row and have correct balance", async () => { + let startingBalance = userItem.included_usage; + await autumn.attach({ + customer_id: customerId, + product_id: free.id, + }); + + const promises = []; + for (let i = 0; i < 2; i++) { + console.log("--------------------------------"); + console.log(`Cycle ${i}`); + console.log(`Starting balance: ${startingBalance}`); + const values = []; + for (let i = 0; i < 10; i++) { + const randomVal = + Math.floor(Math.random() * 5) * (Math.random() < 0.3 ? -1 : 1); + promises.push( + autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: randomVal, + }), + ); + startingBalance -= randomVal; + values.push(randomVal); + } + + console.log(`New balance: ${startingBalance}`); + + const results = await Promise.all(promises); + + await timeout(10000); + + const customer = await autumn.customers.get(customerId); + const userFeature = customer.features[TestFeature.Users]; + if (userFeature.balance != startingBalance) { + for (let i = 0; i < values.length; i++) { + console.log(`Value: ${values[i]}, Event ID: ${results[i].id}`); + } + } + expect(userFeature.balance).toBe(startingBalance); + } + }); +}); diff --git a/server/tests/contUse/update/updateContUse1.ts b/server/tests/contUse/update/updateContUse1.backup.ts similarity index 100% rename from server/tests/contUse/update/updateContUse1.ts rename to server/tests/contUse/update/updateContUse1.backup.ts diff --git a/server/tests/contUse/update/updateContUse1.test.ts b/server/tests/contUse/update/updateContUse1.test.ts new file mode 100644 index 000000000..f77318f58 --- /dev/null +++ b/server/tests/contUse/update/updateContUse1.test.ts @@ -0,0 +1,184 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import type Stripe from "stripe"; +import { replaceItems } from "tests/attach/utils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "updateContUse1"; + +describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing update contUse, add included usage`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "test2", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test3", + feature_id: TestFeature.Users, + }, + ]; + + test("should create entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += 3; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + const extraUsage = 2; + const newItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: (userItem.included_usage as number) + extraUsage, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, + }); + + return; + + test("should update product with extra included usage", async () => { + const customItems = replaceItems({ + featureId: TestFeature.Users, + items: pro.items, + newItem, + }); + + usage += extraUsage; + + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + is_custom: true, + items: customItems, + }); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: extraUsage, + }); + + // Will have 1 invoice because price is replaced... + }); + + const entities = [ + { + id: "4", + name: "test4", + feature_id: TestFeature.Users, + }, + { + id: "5", + name: "test5", + feature_id: TestFeature.Users, + }, + ]; + + test("should create 2 entities and have no invoice", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 2).getTime(), + waitForSeconds: 10, + }); + + await autumn.entities.create(customerId, entities); + + // Usage won't change since using replaceables... + // usage += entities.length; + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(2); + }); +}); diff --git a/server/tests/contUse/update/updateContUse2.ts b/server/tests/contUse/update/updateContUse2.backup.ts similarity index 100% rename from server/tests/contUse/update/updateContUse2.ts rename to server/tests/contUse/update/updateContUse2.backup.ts diff --git a/server/tests/contUse/update/updateContUse2.test.ts b/server/tests/contUse/update/updateContUse2.test.ts new file mode 100644 index 000000000..7502ad2b1 --- /dev/null +++ b/server/tests/contUse/update/updateContUse2.test.ts @@ -0,0 +1,156 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { replaceItems } from "tests/attach/utils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "updateContUse2"; + +describe(`${chalk.yellowBright(`contUse/update/${testCase}: Testing update cont use, remove included usage`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + let usage = 0; + const firstEntities = [ + { + id: "1", + name: "test", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "test2", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "test3", + feature_id: TestFeature.Users, + }, + ]; + + test("should create entity, then attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += 3; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + const reduceUsageBy = 1; + const newItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: (userItem.included_usage as number) - reduceUsageBy, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, + }); + + test("should update product with reduced included usage", async () => { + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(new Date(), 1).getTime(), + waitForSeconds: 5, + }); + + const customItems = replaceItems({ + featureId: TestFeature.Users, + items: pro.items, + newItem, + }); + + const preview = await autumn.attachPreview({ + customer_id: customerId, + product_id: pro.id, + is_custom: true, + items: customItems, + }); + + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + is_custom: true, + items: customItems, + }); + + const customer = await autumn.customers.get(customerId); + const invoices = customer.invoices; + expect(invoices.length).toBe(2); + expect(invoices[0].total).toBe(preview.due_today.total); + + // Usage stays the same... + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + }); + return; +}); diff --git a/server/tests/contUse/update/updateContUse3.ts b/server/tests/contUse/update/updateContUse3.backup.ts similarity index 100% rename from server/tests/contUse/update/updateContUse3.ts rename to server/tests/contUse/update/updateContUse3.backup.ts diff --git a/server/tests/contUse/update/updateContUse3.test.ts b/server/tests/contUse/update/updateContUse3.test.ts new file mode 100644 index 000000000..d702ee327 --- /dev/null +++ b/server/tests/contUse/update/updateContUse3.test.ts @@ -0,0 +1,113 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { replaceItems } from "tests/attach/utils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { attachNewContUseAndExpectCorrect } from "tests/utils/expectUtils/expectContUse/expectUpdateContUse.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "updateContUse3"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage when no entities created`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + test("should attach pro", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + }); + }); + + const extraUsage = 2; + const newItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: (userItem.included_usage as number) + extraUsage, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, + }); + + test("should update product with extra included usage", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 1, + }); + + const customItems = replaceItems({ + featureId: TestFeature.Users, + items: pro.items, + newItem, + }); + + await attachNewContUseAndExpectCorrect({ + autumn, + customerId, + product: pro, + customItems, + numInvoices: 2, + }); + + await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage: 1, + numReplaceables: 0, + }); + }); +}); diff --git a/server/tests/contUse/update/updateContUse4.ts b/server/tests/contUse/update/updateContUse4.backup.ts similarity index 100% rename from server/tests/contUse/update/updateContUse4.ts rename to server/tests/contUse/update/updateContUse4.backup.ts diff --git a/server/tests/contUse/update/updateContUse4.test.ts b/server/tests/contUse/update/updateContUse4.test.ts new file mode 100644 index 000000000..34a9fbbc2 --- /dev/null +++ b/server/tests/contUse/update/updateContUse4.test.ts @@ -0,0 +1,216 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, expect, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import { replaceItems } from "tests/attach/utils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { attachNewContUseAndExpectCorrect } from "tests/utils/expectUtils/expectContUse/expectUpdateContUse.js"; +import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); + +const testCase = "updateContUse4"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage, prorate now`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + const firstEntities = [ + { + id: "1", + name: "entity1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "entity2", + feature_id: TestFeature.Users, + }, + ]; + + let usage = 0; + test("should attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += firstEntities.length; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + const extraUsage = 2; + const newItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: (userItem.included_usage as number) + extraUsage, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, + }); + + test("should update product with extra included usage", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 2).getTime(), + waitForSeconds: 15, + }); + + const customItems = replaceItems({ + featureId: TestFeature.Users, + items: pro.items, + newItem, + }); + + const { invoices } = await attachNewContUseAndExpectCorrect({ + autumn, + customerId, + product: pro, + customItems, + numInvoices: 2, + }); + + const { stripeSubs } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + + // Do own calculation too.. + const sub = stripeSubs[0]; + const amount = -userItem.price!; + const { start, end } = subToPeriodStartEnd({ sub }); + let proratedAmount = calculateProrationAmount({ + amount, + periodStart: start * 1000, + periodEnd: end * 1000, + now: curUnix, + allowNegative: true, + }); + proratedAmount = Number(proratedAmount.toFixed(2)); + + expect(invoices[0].total).toBe(proratedAmount); + }); + + const reducedUsage = 3; + const newItem2 = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: (newItem.included_usage as number) - reducedUsage, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, + }); + + test("should update product with reduced included usage", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 1).getTime(), + waitForSeconds: 15, + }); + + const customItems = replaceItems({ + featureId: TestFeature.Users, + items: pro.items, + newItem: newItem2, + }); + + const { invoices } = await attachNewContUseAndExpectCorrect({ + autumn, + customerId, + product: pro, + customItems, + numInvoices: 3, + }); + + const { stripeSubs } = await expectSubQuantityCorrect({ + stripeCli: ctx.stripeCli, + productId: pro.id, + db: ctx.db, + org: ctx.org, + env: ctx.env, + customerId, + usage, + numReplaceables: 0, + }); + + // Do own calculation too.. + const sub = stripeSubs[0]; + const amount = Math.min(reducedUsage, usage) * userItem.price!; + const { start, end } = subToPeriodStartEnd({ sub }); + let proratedAmount = calculateProrationAmount({ + amount, + periodStart: start * 1000, + periodEnd: end * 1000, + now: curUnix, + allowNegative: true, + }); + proratedAmount = Number(proratedAmount.toFixed(2)); + + expect(invoices[0].total).toBe(proratedAmount); + }); +}); diff --git a/server/tests/contUse/update/updateContUse5.ts b/server/tests/contUse/update/updateContUse5.backup.ts similarity index 100% rename from server/tests/contUse/update/updateContUse5.ts rename to server/tests/contUse/update/updateContUse5.backup.ts diff --git a/server/tests/contUse/update/updateContUse5.test.ts b/server/tests/contUse/update/updateContUse5.test.ts new file mode 100644 index 000000000..27078b81e --- /dev/null +++ b/server/tests/contUse/update/updateContUse5.test.ts @@ -0,0 +1,137 @@ +import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared"; +import { beforeAll, describe, test } from "bun:test"; +import chalk from "chalk"; +import { addWeeks } from "date-fns"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearProratedItem } 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"; + +const userItem = constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 1, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, +}); + +export const pro = constructProduct({ + items: [userItem], + type: "pro", +}); +export const proAnnual = constructProduct({ + items: [ + constructArrearProratedItem({ + featureId: TestFeature.Users, + pricePerUnit: 50, + includedUsage: 2, + config: { + on_increase: OnIncrease.BillImmediately, + on_decrease: OnDecrease.None, + }, + }), + ], + type: "pro", + isAnnual: true, +}); + +const testCase = "updateContUse5"; + +describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage, prorate now`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let curUnix = new Date().getTime(); + + beforeAll(async () => { + await initProductsV0({ + ctx, + products: [pro, proAnnual], + prefix: testCase, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomerV3({ + ctx, + customerId, + customerData: {}, + attachPm: "success", + withTestClock: true, + }); + + testClockId = testClockId1!; + }); + + const firstEntities = [ + { + id: "1", + name: "entity1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "entity2", + feature_id: TestFeature.Users, + }, + { + id: "3", + name: "entity3", + feature_id: TestFeature.Users, + }, + ]; + + let usage = 0; + test("should attach pro", async () => { + await autumn.entities.create(customerId, firstEntities); + usage += firstEntities.length; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: pro, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); + + test("should upgrade to pro annual", async () => { + curUnix = await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId, + advanceTo: addWeeks(curUnix, 2).getTime(), + waitForSeconds: 5, + }); + return; + + await attachAndExpectCorrect({ + autumn, + customerId, + product: proAnnual, + stripeCli: ctx.stripeCli, + db: ctx.db, + org: ctx.org, + env: ctx.env, + usage: [ + { + featureId: TestFeature.Users, + value: usage, + }, + ], + }); + }); +}); diff --git a/server/tests/core/reset1.backup.ts b/server/tests/core/reset1.backup.ts new file mode 100644 index 000000000..f22aba0cb --- /dev/null +++ b/server/tests/core/reset1.backup.ts @@ -0,0 +1,143 @@ +import { + type AppEnv, + type Customer, + LegacyVersion, + type LimitedItem, + type Organization, + ProductItemInterval, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addDays, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import { resetAndGetCusEnt } from "tests/advanced/rollovers/rolloverTestUtils.js"; +import { addPrefixToProducts } from "tests/attach/utils.js"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const messagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 400, + interval: ProductItemInterval.Day, + intervalCount: 3, +}) as LimitedItem; + +const wordsItem = constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 100, + interval: ProductItemInterval.Month, + intervalCount: 4, +}) as LimitedItem; + +export const free = constructProduct({ + items: [messagesItem, wordsItem], + type: "free", + isDefault: false, +}); + +const testCase = "reset1"; + +describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let customer: Customer; + let stripeCli: Stripe; + const curUnix = new Date().getTime(); + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [free], + prefix: testCase, + }); + + await createProducts({ + autumn, + products: [free], + customerId, + db, + orgId: org.id, + env, + }); + + const res = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = res.testClockId!; + customer = res.customer; + }); + + it("should attach free product", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: free.id, + }); + }); + + const messageUsage = 250; + const curBalance = messagesItem.included_usage; + + it("should reset messages feature and have correct next reset at", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messageUsage, + }); + + await timeout(3000); + + await resetAndGetCusEnt({ + db, + customer, + productGroup: free.group, + featureId: TestFeature.Messages, + }); + + const cus = await autumn.customers.get(customerId); + const msgesFeature = cus.features[TestFeature.Messages]; + expect(msgesFeature.next_reset_at).to.exist; + expect(msgesFeature.next_reset_at).to.approximately( + addDays(new Date(), 3).getTime(), + 1000 * 30, + ); + }); + + it("should reset words feature and have correct next reset at", async () => { + await resetAndGetCusEnt({ + db, + customer, + productGroup: free.group, + featureId: TestFeature.Words, + }); + + const cus = await autumn.customers.get(customerId); + const wordsFeature = cus.features[TestFeature.Words]; + expect(wordsFeature.next_reset_at).to.exist; + expect(wordsFeature.next_reset_at).to.approximately( + addMonths(new Date(), 4).getTime(), + 1000 * 30 * 60, // account for timezone differences + ); + }); +}); diff --git a/server/tests/core/reset1.test.ts b/server/tests/core/reset1.test.ts new file mode 100644 index 000000000..0a02c73a8 --- /dev/null +++ b/server/tests/core/reset1.test.ts @@ -0,0 +1,136 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + type Customer, + LegacyVersion, + type LimitedItem, + type Organization, + ProductItemInterval, +} from "@autumn/shared"; +import chalk from "chalk"; +import { addDays, addMonths } from "date-fns"; +import type Stripe from "stripe"; +import { resetAndGetCusEnt } from "tests/advanced/rollovers/rolloverTestUtils.js"; +import { addPrefixToProducts } from "tests/attach/utils.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.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"; + +const messagesItem = constructFeatureItem({ + featureId: TestFeature.Messages, + includedUsage: 400, + interval: ProductItemInterval.Day, + intervalCount: 3, +}) as LimitedItem; + +const wordsItem = constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 100, + interval: ProductItemInterval.Month, + intervalCount: 4, +}) as LimitedItem; + +export const free = constructProduct({ + items: [messagesItem, wordsItem], + type: "free", + isDefault: false, +}); + +const testCase = "reset1"; + +describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + let testClockId: string; + let db: DrizzleCli, org: Organization, env: AppEnv; + let customer: Customer; + let stripeCli: Stripe; + const curUnix = new Date().getTime(); + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [free], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [free], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + customer = res.customer; + }); + + test("should attach free product", async () => { + await autumn.attach({ + customer_id: customerId, + product_id: free.id, + }); + }); + + const messageUsage = 250; + const curBalance = messagesItem.included_usage; + + test("should reset messages feature and have correct next reset at", async () => { + await autumn.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: messageUsage, + }); + + await timeout(3000); + + await resetAndGetCusEnt({ + db, + customer, + productGroup: free.group, + featureId: TestFeature.Messages, + }); + + const cus = await autumn.customers.get(customerId); + const msgesFeature = cus.features[TestFeature.Messages]; + expect(msgesFeature.next_reset_at).toBeDefined(); + expect(msgesFeature.next_reset_at).toBeCloseTo( + addDays(new Date(), 3).getTime(), + -4, // tolerance of ~30 seconds (30000ms = 10^4.48) + ); + }); + + test("should reset words feature and have correct next reset at", async () => { + await resetAndGetCusEnt({ + db, + customer, + productGroup: free.group, + featureId: TestFeature.Words, + }); + + const cus = await autumn.customers.get(customerId); + const wordsFeature = cus.features[TestFeature.Words]; + expect(wordsFeature.next_reset_at).toBeDefined(); + expect(wordsFeature.next_reset_at).toBeCloseTo( + addMonths(new Date(), 4).getTime(), + -8, // tolerance of ~30 minutes (1800000ms = 10^6.26, round down to -8) + ); + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade1.backup.ts b/server/tests/merged/downgrade/mergedDowngrade1.backup.ts new file mode 100644 index 000000000..66306eff2 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade1.backup.ts @@ -0,0 +1,206 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Premium, Premium +// Pro, Pro +// Premium, Premium + +// UNCOMMENT FROM HERE +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const init = [ + { entityId: "1", product: premium }, // upgrade to premium + { entityId: "2", product: premium }, // upgrade to premium +]; + +const ops1 = [ + { + entityId: "1", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +// Renew +const ops2 = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade 2 pros")}`, () => { + const customerId = "mergedDowngrade1"; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: customerId, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should attach pro product to both entities", async () => { + await autumn.entities.create(customerId, entities); + + for (const op of init) { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + } + }); + + it("should downgrade both entities to pro and have correct sub + schedule", async () => { + for (const op of ops1) { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } + }); + + it("should renew both entities and have correct sub + schedule", async () => { + for (const op of ops2) { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade1.test.ts b/server/tests/merged/downgrade/mergedDowngrade1.test.ts new file mode 100644 index 000000000..9eedd2886 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade1.test.ts @@ -0,0 +1,199 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } 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 "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Premium, Premium +// Pro, Pro +// Premium, Premium + +// UNCOMMENT FROM HERE +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const init = [ + { entityId: "1", product: premium }, // upgrade to premium + { entityId: "2", product: premium }, // upgrade to premium +]; + +const ops1 = [ + { + entityId: "1", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +// Renew +const ops2 = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade 2 pros")}`, () => { + const customerId = "mergedDowngrade1"; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: customerId, + }); + + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: customerId, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should attach pro product to both entities", async () => { + await autumn.entities.create(customerId, entities); + + for (const op of init) { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + } + }); + + test("should downgrade both entities to pro and have correct sub + schedule", async () => { + for (const op of ops1) { + await autumn.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } + }); + + test("should renew both entities and have correct sub + schedule", async () => { + for (const op of ops2) { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade2.backup.ts b/server/tests/merged/downgrade/mergedDowngrade2.backup.ts new file mode 100644 index 000000000..32fc26dea --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade2.backup.ts @@ -0,0 +1,228 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Premium +// Free +// Free, Premium +// Free, Pro + +const free = constructProduct({ + id: "free", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "free", + isDefault: false, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: free, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: free, status: CusProductStatus.Scheduled }, + ], + shouldBeCanceled: true, + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade2"; +describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade free 1, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, free], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium, free], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + shouldBeCanceled: op.shouldBeCanceled, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + // return; + + it("should advance test clock and have correct products for entity 1 & 2", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + const results = [ + { entityId: "1", product: free, status: CusProductStatus.Active }, + { entityId: "2", product: pro, status: CusProductStatus.Active }, + ]; + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + expectProductAttached({ + customer: entity, + product: result.product, + status: result.status, + }); + + const products = entity.products.filter( + (p: any) => p.group == result.product.group, + ); + expect(products.length).to.equal(1); + } + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + }); + + it("should attach premium to entity 1 (which is free) and have correct products", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + entityId: "1", + }); + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade2.test.ts b/server/tests/merged/downgrade/mergedDowngrade2.test.ts new file mode 100644 index 000000000..af5eb6360 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade2.test.ts @@ -0,0 +1,221 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + 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 "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Premium +// Free +// Free, Premium +// Free, Pro + +const free = constructProduct({ + id: "free", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "free", + isDefault: false, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: free, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: free, status: CusProductStatus.Scheduled }, + ], + shouldBeCanceled: true, + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade2"; +describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade free 1, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, free], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium, free], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + shouldBeCanceled: op.shouldBeCanceled, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + // return; + + test("should advance test clock and have correct products for entity 1 & 2", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + const results = [ + { entityId: "1", product: free, status: CusProductStatus.Active }, + { entityId: "2", product: pro, status: CusProductStatus.Active }, + ]; + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + expectProductAttached({ + customer: entity, + product: result.product, + status: result.status, + }); + + const products = entity.products.filter( + (p: any) => p.group == result.product.group, + ); + expect(products.length).toBe(1); + } + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + }); + + test("should attach premium to entity 1 (which is free) and have correct products", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + entityId: "1", + }); + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade3.backup.ts b/server/tests/merged/downgrade/mergedDowngrade3.backup.ts new file mode 100644 index 000000000..f9ff2e621 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade3.backup.ts @@ -0,0 +1,172 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + constructFeatureItem, +} from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Pro, Pro +// Free, Premium + +const free = constructProduct({ + id: "free", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "free", + isDefault: false, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: free, + results: [ + { product: pro, status: CusProductStatus.Active }, + { product: free, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +const testCase = "mergedDowngrade3"; +describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pro 2, downgrade free pro 1, upgrade pro 2 ")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, free], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium, free], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade3.test.ts b/server/tests/merged/downgrade/mergedDowngrade3.test.ts new file mode 100644 index 000000000..cdce2a5dc --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade3.test.ts @@ -0,0 +1,165 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { + constructArrearItem, + 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 "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// Pro, Pro +// Free, Premium + +const free = constructProduct({ + id: "free", + items: [constructFeatureItem({ featureId: TestFeature.Words })], + type: "free", + isDefault: false, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: free, + results: [ + { product: pro, status: CusProductStatus.Active }, + { product: free, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +const testCase = "mergedDowngrade3"; +describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pro 2, downgrade free pro 1, upgrade pro 2 ")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, free], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium, free], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade4.backup.ts b/server/tests/merged/downgrade/mergedDowngrade4.backup.ts new file mode 100644 index 000000000..557484a01 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade4.backup.ts @@ -0,0 +1,196 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// PremiumAnnual, Premium +// PremiumAnnual, Pro + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade4"; +describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedule activates")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium, premiumAnnual], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + it("should advance test clock and have correct premium downgraded for entity 2", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + // 1. Check that only + const results = [ + { + entityId: "1", + product: premiumAnnual, + status: CusProductStatus.Active, + }, + { entityId: "2", product: pro, status: CusProductStatus.Active }, + ]; + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + expectProductAttached({ + customer: entity, + product: result.product, + status: result.status, + }); + + const products = entity.products.filter( + (p: any) => p.group == result.product.group, + ); + expect(products.length).to.equal(1); + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade4.test.ts b/server/tests/merged/downgrade/mergedDowngrade4.test.ts new file mode 100644 index 000000000..1b9f153ec --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade4.test.ts @@ -0,0 +1,189 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } 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 "../mergeUtils/expectSubCorrect.js"; + +// OPERATIONS: +// PremiumAnnual, Premium +// PremiumAnnual, Pro + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade4"; +describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedule activates")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium, premiumAnnual], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + test("should advance test clock and have correct premium downgraded for entity 2", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + // 1. Check that only + const results = [ + { + entityId: "1", + product: premiumAnnual, + status: CusProductStatus.Active, + }, + { entityId: "2", product: pro, status: CusProductStatus.Active }, + ]; + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + expectProductAttached({ + customer: entity, + product: result.product, + status: result.status, + }); + + const products = entity.products.filter( + (p: any) => p.group == result.product.group, + ); + expect(products.length).toBe(1); + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade5.test.ts b/server/tests/merged/downgrade/mergedDowngrade5.test.ts index 1546e47b3..4bfafb940 100644 --- a/server/tests/merged/downgrade/mergedDowngrade5.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade5.test.ts @@ -100,7 +100,7 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`, let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/downgrade/mergedDowngrade6.test.ts b/server/tests/merged/downgrade/mergedDowngrade6.test.ts index 0864aef21..dd4d6b97f 100644 --- a/server/tests/merged/downgrade/mergedDowngrade6.test.ts +++ b/server/tests/merged/downgrade/mergedDowngrade6.test.ts @@ -106,7 +106,7 @@ describe(`${chalk.yellowBright("mergedDowngrade6: Testing downgrade changes")}`, let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/downgrade/mergedDowngrade8.backup.ts b/server/tests/merged/downgrade/mergedDowngrade8.backup.ts new file mode 100644 index 000000000..b9ea55b4a --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade8.backup.ts @@ -0,0 +1,184 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +// UNCOMMENT FROM HERE +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +// const init = [ +// { entityId: "1", product: premiumAnnual }, // upgrade to premium +// { entityId: "2", product: premium }, // upgrade to premium +// ]; + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: pro, + results: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +const testCase = "mergedDowngrade8"; +describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade 2 monthly + annual")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: customerId, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium, premiumAnnual], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).to.equal(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade8.test.ts b/server/tests/merged/downgrade/mergedDowngrade8.test.ts new file mode 100644 index 000000000..06c1a1461 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade8.test.ts @@ -0,0 +1,177 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } 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 "../mergeUtils/expectSubCorrect.js"; + +// UNCOMMENT FROM HERE +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +// const init = [ +// { entityId: "1", product: premiumAnnual }, // upgrade to premium +// { entityId: "2", product: premium }, // upgrade to premium +// ]; + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: pro, + results: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, +]; + +const testCase = "mergedDowngrade8"; +describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade 2 monthly + annual")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: customerId, + }); + + await initProductsV0({ + ctx, + products: [pro, premium, premiumAnnual], + prefix: customerId, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await autumn.attach({ + customer_id: customerId, + product_id: op.product.id, + entity_id: op.entityId, + }); + + const entity = await autumn.entities.get(customerId, op.entityId); + for (const result of op.results) { + expectProductAttached({ + customer: entity, + product: result.product, + entityId: op.entityId, + }); + } + expect( + entity.products.filter((p: any) => p.group == premium.group).length, + ).toBe(op.results.length); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade9.backup.ts b/server/tests/merged/downgrade/mergedDowngrade9.backup.ts new file mode 100644 index 000000000..e249ef7eb --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade9.backup.ts @@ -0,0 +1,232 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +// UNCOMMENT FROM HERE +const premium = constructProduct({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +// const init = [ +// { entityId: "1", product: premiumAnnual }, // upgrade to premium +// { entityId: "2", product: premium }, // upgrade to premium +// ]; + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: pro, + results: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade9"; +describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade 2 monthly + annual & advance test clock")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: customerId, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium, premiumAnnual], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + }); + // await autumn.attach({ + // customer_id: customerId, + // product_id: op.product.id, + // entity_id: op.entityId, + // }); + // const entity = await autumn.entities.get(customerId, op.entityId); + // for (const result of op.results) { + // expectProductAttached({ + // customer: entity, + // product: result.product, + // entityId: op.entityId, + // }); + // } + // expect( + // entity.products.filter((p: any) => p.group == premium.group).length + // ).to.equal(op.results.length); + // await expectSubToBeCorrect({ + // db, + // customerId, + // org, + // env, + // }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + it("should advance test clock and have correct products for entity 1 & 2", async () => { + const results = [ + { + entityId: "1", + products: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + products: [{ product: pro, status: CusProductStatus.Active }], + }, + ]; + + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + for (const product of result.products) { + expectProductAttached({ + customer: entity, + product: product.product, + status: product.status, + }); + } + const products = entity.products.filter( + (p: any) => p.group == premium.group, + ); + expect(products.length).to.equal(result.products.length); + } + }); + + it("should attach premium to entity 2 and have correct products", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + entityId: "2", + }); + }); +}); diff --git a/server/tests/merged/downgrade/mergedDowngrade9.test.ts b/server/tests/merged/downgrade/mergedDowngrade9.test.ts new file mode 100644 index 000000000..2bc0d91a7 --- /dev/null +++ b/server/tests/merged/downgrade/mergedDowngrade9.test.ts @@ -0,0 +1,225 @@ +import { beforeAll, describe, expect, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } 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({ + id: "premium", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", +}); + +const premiumAnnual = constructProduct({ + id: "premiumAnnual", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "premium", + isAnnual: true, +}); + +const pro = constructProduct({ + id: "pro", + items: [constructArrearItem({ featureId: TestFeature.Words })], + type: "pro", +}); + +// const init = [ +// { entityId: "1", product: premiumAnnual }, // upgrade to premium +// { entityId: "2", product: premium }, // upgrade to premium +// ]; + +const ops = [ + { + entityId: "1", + product: premiumAnnual, + results: [{ product: premiumAnnual, status: CusProductStatus.Active }], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + }, + { + entityId: "1", + product: pro, + results: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + product: pro, + results: [ + { product: premium, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, +]; + +const testCase = "mergedDowngrade9"; +describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade 2 monthly + annual & advance test clock")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium, premiumAnnual], + prefix: customerId, + }); + + await initProductsV0({ + ctx, + products: [pro, premium, premiumAnnual], + prefix: customerId, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + }); + // await autumn.attach({ + // customer_id: customerId, + // product_id: op.product.id, + // entity_id: op.entityId, + // }); + // const entity = await autumn.entities.get(customerId, op.entityId); + // for (const result of op.results) { + // expectProductAttached({ + // customer: entity, + // product: result.product, + // entityId: op.entityId, + // }); + // } + // expect( + // entity.products.filter((p: any) => p.group == premium.group).length + // ).toBe(op.results.length); + // await expectSubToBeCorrect({ + // db, + // customerId, + // org, + // env, + // }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + test("should advance test clock and have correct products for entity 1 & 2", async () => { + const results = [ + { + entityId: "1", + products: [ + { product: premiumAnnual, status: CusProductStatus.Active }, + { product: pro, status: CusProductStatus.Scheduled }, + ], + }, + { + entityId: "2", + products: [{ product: pro, status: CusProductStatus.Active }], + }, + ]; + + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + for (const result of results) { + const entity = await autumn.entities.get(customerId, result.entityId); + for (const product of result.products) { + expectProductAttached({ + customer: entity, + product: product.product, + status: product.status, + }); + } + const products = entity.products.filter( + (p: any) => p.group == premium.group, + ); + expect(products.length).toBe(result.products.length); + } + }); + + test("should attach premium to entity 2 and have correct products", async () => { + await attachAndExpectCorrect({ + autumn, + customerId, + product: premium, + stripeCli, + db, + org, + env, + entityId: "2", + }); + }); +}); diff --git a/server/tests/merged/group/mergedGroup1.test.ts b/server/tests/merged/group/mergedGroup1.test.ts index bc62bc4d6..9fbbc056c 100644 --- a/server/tests/merged/group/mergedGroup1.test.ts +++ b/server/tests/merged/group/mergedGroup1.test.ts @@ -92,7 +92,7 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups" let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/group/mergedGroup2.test.ts b/server/tests/merged/group/mergedGroup2.test.ts index c5a0a47e4..4efd87394 100644 --- a/server/tests/merged/group/mergedGroup2.test.ts +++ b/server/tests/merged/group/mergedGroup2.test.ts @@ -83,7 +83,7 @@ describe(`${chalk.yellowBright("mergedGroup2: Testing products from diff groups" let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/mergeUtils/expectSubCorrect.backup.ts b/server/tests/merged/mergeUtils/expectSubCorrect.backup.ts new file mode 100644 index 000000000..b79838341 --- /dev/null +++ b/server/tests/merged/mergeUtils/expectSubCorrect.backup.ts @@ -0,0 +1,491 @@ +import { + type AppEnv, + CusProductStatus, + cusProductToEnts, + cusProductToPrices, + cusProductToProduct, + type FullCustomer, + type Organization, +} from "@autumn/shared"; +import { notNullish } from "@shared/utils/utils.js"; +import { expect } from "chai"; +import type Stripe from "stripe"; +import { defaultApiVersion } from "tests/constants.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js"; +import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js"; +import { + cusProductInPhase, + logPhaseItems, + similarUnix, +} from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; +import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { getUniqueUpcomingSchedulePairs } from "@/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.js"; +import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { + formatPrice, + getPriceEntitlement, + getPriceOptions, +} from "@/internal/products/prices/priceUtils.js"; +import { isFreeProduct } from "@/internal/products/productUtils.js"; +import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js"; +import { cusProductToSubIds } from "../mergeUtils.test.js"; + +const compareActualItems = async ({ + actualItems, + expectedItems, + type, + fullCus, + db, + phaseStartsAt, +}: { + actualItems: any[]; + expectedItems: any[]; + type: "sub" | "schedule"; + fullCus: FullCustomer; + phaseStartsAt?: number; + db: DrizzleCli; +}) => { + for (const expectedItem of expectedItems) { + const actualItem = actualItems.find( + (item: any) => item.price === (expectedItem as any).price, + ); + + if (!actualItem) { + // Search for price by stripe id + const price = await PriceService.getByStripeId({ + db, + stripePriceId: expectedItem.price, + }); + console.log(`(${type}) Missing item:`, expectedItem); + // if (price) { + // console.log(`Autumn price:`, `${price.id} - ${formatPrice({ price })}`); + // } + + // Actual items + console.log(`(${type}) Actual items (${actualItems.length}):`); + await logPhaseItems({ + db, + items: actualItems, + }); + + console.log(`(${type}) Expected items (${expectedItems.length}):`); + await logPhaseItems({ + db, + items: expectedItems, + }); + } + + expect(actualItem).to.exist; + + if (actualItem?.quantity !== (expectedItem as any).quantity) { + if (phaseStartsAt) { + console.log(`Phase starts at: ${formatUnixToDateTime(phaseStartsAt)}`); + } + + console.log("Actual items:"); + await logPhaseItems({ + db, + items: actualItems, + }); + + console.log("Expected items:"); + await logPhaseItems({ + db, + items: expectedItems, + }); + + console.log( + `Item quantity mismatch: ${actualItem?.quantity} !== ${expectedItem.quantity}`, + ); + + const price = await PriceService.getByStripeId({ + db, + stripePriceId: expectedItem.price, + }); + if (price) { + console.log( + `Autumn price:`, + `${price?.product.name} - ${formatPrice({ price })}`, + ); + } + + console.log("--------------------------------"); + } + + expect(actualItem?.quantity).to.equal( + (expectedItem as any).quantity, + `actual items quantity should be equals to ${expectedItem.quantity}`, + ); + } + + expect(actualItems.length).to.equal(expectedItems.length); +}; + +export const expectSubToBeCorrect = async ({ + db, + customerId, + org, + env, + + entityId, + shouldBeCanceled, + shouldBeTrialing = false, + flags, + subId, + rewards, +}: { + db: DrizzleCli; + customerId: string; + org: Organization; + env: AppEnv; + + entityId?: string; + shouldBeCanceled?: boolean; + shouldBeTrialing?: boolean; + flags?: { + checkNotTrialing?: boolean; + }; + subId?: string; + rewards?: string[]; +}) => { + const stripeCli = createStripeCli({ org, env }); + const fullCus = await CusService.getFull({ + db, + idOrInternalId: customerId, + orgId: org.id, + env, + withEntities: true, + }); + + // 1. Only 1 sub ID available + let cusProducts = fullCus.customer_products; + if (!subId) { + const subIds = cusProductToSubIds({ cusProducts }); + subId = subIds[0]; + expect(subIds.length, "should only have 1 sub ID available").to.equal(1); + } else { + cusProducts = cusProducts.filter((cp) => + cp.subscription_ids?.includes(subId!), + ); + } + + // Get the items that should be in the sub + const supposedSubItems = []; + + const scheduleUnixes = getUniqueUpcomingSchedulePairs({ + cusProducts, + now: Date.now(), + }); + + const supposedPhases: any[] = scheduleUnixes.map((unix) => { + return { + start_date: unix, // milliseconds + items: [], + }; + }); + + // console.log(`\n\nChecking sub correct`); + const printCusProduct = false; + if (printCusProduct) { + console.log(`\n\nChecking sub correct`); + } + + for (const cusProduct of cusProducts) { + const prices = cusProductToPrices({ cusProduct }); + const ents = cusProductToEnts({ cusProduct }); + const product = cusProductToProduct({ cusProduct }); + + // Add to schedules + const scheduleIndexes: number[] = []; + const apiVersion = cusProduct.api_semver || defaultApiVersion; + + if (isFreeProduct(product.prices)) { + expect(cusProduct.subscription_ids, "free product should have no subs").to + .be.empty; + continue; + } + + if (printCusProduct) { + console.log( + `Cus product: ${cusProduct.product.name}, Status: ${cusProduct.status}, Entity ID: ${cusProduct.entity_id}`, + ); + console.log(`Starts at: ${formatUnixToDateTime(cusProduct.starts_at)}`); + } + + scheduleUnixes.forEach((unix, index) => { + if ( + cusProduct.status === CusProductStatus.Scheduled && + cusProductInPhase({ phaseStartMillis: unix, cusProduct }) + ) { + return scheduleIndexes.push(index); + } + + if (cusProduct.status === CusProductStatus.Scheduled) return; + + if (cusProduct.product.is_add_on) { + // 1. If it's canceled + if (cusProduct.canceled && (cusProduct.ended_at || 0) > unix) { + return scheduleIndexes.push(index); + } else if (!cusProduct.canceled) { + return scheduleIndexes.push(index); + } + + return; + } + + // 2. If main product, check that schedule is AFTER this phase + const curScheduledProduct = cusProducts.find( + (cp) => + cp.product.group === product.group && + cp.status === CusProductStatus.Scheduled && + (cp.internal_entity_id + ? cp.internal_entity_id === cusProduct.internal_entity_id + : nullish(cp.internal_entity_id)), + ); + + if (!curScheduledProduct) return scheduleIndexes.push(index); + + // If scheduled product NOT in phase, add main product to schedule + if ( + !cusProductInPhase({ + phaseStartMillis: unix, + cusProduct: curScheduledProduct, + }) + ) { + scheduleIndexes.push(index); + } + }); + + if (printCusProduct) { + console.log(`Schedule indexes:`, scheduleIndexes); + console.log("--------------------------------"); + } + + // const hasScheduledProduct = + cusProduct.status !== CusProductStatus.Scheduled && + !cusProduct.product.is_add_on && + cusProducts.some( + (cp) => + cp.product.group === product.group && + ACTIVE_STATUSES.includes(cp.status), + ); + + const addToSub = cusProduct.status !== CusProductStatus.Scheduled; + + for (const price of prices) { + const relatedEnt = getPriceEntitlement(price, ents); + const options = getPriceOptions(price, cusProduct.options); + const existingUsage = getExistingUsageFromCusProducts({ + entitlement: relatedEnt, + cusProducts, + entities: fullCus.entities, + carryExistingUsages: true, + internalEntityId: cusProduct.internal_entity_id || undefined, + }); + + const res = priceToStripeItem({ + price, + relatedEnt, + product, + org, + options, + existingUsage, + withEntity: !!entityId, + isCheckout: false, + apiVersion, + productOptions: cusProduct.quantity + ? { + product_id: product.id, + quantity: cusProduct.quantity, + } + : undefined, + }); + + if (res?.lineItem && nullish(res.lineItem.quantity)) { + res.lineItem.quantity = 0; + } + + // console.log("API VERSION:", apiVersion); + // console.log("LINE ITEM:", res?.lineItem); + if (options?.upcoming_quantity && res?.lineItem) { + res.lineItem.quantity = options.upcoming_quantity; + } + + const lineItem: any = res?.lineItem; + if (lineItem && res?.lineItem) { + if (addToSub) { + const existingIndex = supposedSubItems.findIndex( + (si: any) => si.price === lineItem.price, + ); + + if (existingIndex !== -1) { + supposedSubItems[existingIndex].quantity += lineItem.quantity; + } else { + supposedSubItems.push({ + ...res.lineItem, + priceStr: `${product.id}-${formatPrice({ price })}`, + }); + } + } + + for (const scheduleIndex of scheduleIndexes) { + const phase = supposedPhases[scheduleIndex]; + const existingIndex = phase.items.findIndex( + (item: any) => item.price === lineItem.price, + ); + + if (existingIndex !== -1) { + phase.items[existingIndex].quantity += lineItem.quantity!; + } else { + phase.items.push({ + price: lineItem.price, + quantity: lineItem.quantity!, + }); + } + } + } + } + } + + const sub = await stripeCli.subscriptions.retrieve(subId, { + expand: ["discounts.coupon"], + }); + + const actualItems = sub.items.data.map((item: any) => ({ + price: item.price.id, + quantity: item.quantity || 0, + })); + + const subCouponIds = sub.discounts?.map( + (discount: any) => discount.coupon.id, + ); + if (rewards) { + for (const reward of rewards) { + const corresponding = subCouponIds.find( + (subCouponId: any) => subCouponId === reward, + ); + expect(corresponding, `reward ${reward} should be in sub`).to.exist; + } + expect(subCouponIds.length).to.equal(rewards.length); + } + + await compareActualItems({ + actualItems, + expectedItems: supposedSubItems, + type: "sub", + fullCus, + db, + }); + + if (shouldBeTrialing) { + expect(sub.status, "sub should be trialing").to.equal("trialing"); + } + + if (flags?.checkNotTrialing) { + expect(sub.status, "sub should not be trialing").to.not.equal("trialing"); + } + + // Should be canceled + const cusSubShouldBeCanceled = cusProducts.every((cp) => { + if (cp.subscription_ids?.includes(subId!)) { + // 1. Get scheduled product + const { curScheduledProduct } = getExistingCusProducts({ + cusProducts, + product: cp.product, + internalEntityId: cp.internal_entity_id, + }); + + if (curScheduledProduct) { + const scheduledProduct = cusProductToProduct({ + cusProduct: curScheduledProduct, + }); + if (!isFreeProduct(scheduledProduct.prices)) { + return false; + } + } + + return cp.canceled; + } + + return true; + }); + + // console.log("Sub should be canceled:", cusSubShouldBeCanceled); + + const finalShouldBeCanceled = notNullish(shouldBeCanceled) + ? shouldBeCanceled! + : cusSubShouldBeCanceled; + + // console.log("Final should be canceled:", finalShouldBeCanceled); + + if (finalShouldBeCanceled) { + expect(sub.schedule, "sub should NOT have a schedule").to.be.null; + // expect(sub.cancel_at, "sub should be canceled").to.exist; + expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true; + return; + } + + const schedule = + supposedPhases.length > 0 + ? await stripeCli.subscriptionSchedules.retrieve(sub.schedule as string, { + expand: ["phases.items.price"], + }) + : null; + + // console.log("--------------------------------"); + // console.log("Supposed phases:"); + // await logPhases({ + // phases: supposedPhases, + // db, + // }); + + // console.log("--------------------------------"); + // console.log("Actual phases:"); + + // await logPhases({ + // phases: (schedule?.phases as any) || [], + // db, + // }); + + for (let i = 0; i < supposedPhases.length; i++) { + const supposedPhase = supposedPhases[i]; + + if (supposedPhase.items.length === 0) continue; + + const actualPhase = schedule?.phases?.[i + 1]; + expect(schedule?.phases.length).to.be.greaterThan(i + 1); + + expect( + similarUnix({ + unix1: supposedPhase.start_date, + unix2: actualPhase!.start_date * 1000, + }), + ).to.be.true; + + const actualItems = + actualPhase?.items.map((item) => ({ + price: (item.price as Stripe.Price).id, + quantity: item.quantity, + })) || []; + + await compareActualItems({ + actualItems, + expectedItems: supposedPhase.items, + type: "schedule", + fullCus, + db, + phaseStartsAt: supposedPhase.start_date, + }); + } + + expect(sub.cancel_at, "sub should not be canceled").to.be.null; + // if (shouldBeCanceled) { + // expect(sub.cancel_at, "sub should be canceled").to.exist; + // } else { + // } +}; diff --git a/server/tests/merged/mergeUtils/expectSubCorrect.ts b/server/tests/merged/mergeUtils/expectSubCorrect.ts index b79838341..d2446fa8a 100644 --- a/server/tests/merged/mergeUtils/expectSubCorrect.ts +++ b/server/tests/merged/mergeUtils/expectSubCorrect.ts @@ -1,3 +1,4 @@ +import { expect } from "bun:test"; import { type AppEnv, CusProductStatus, @@ -8,7 +9,6 @@ import { type Organization, } from "@autumn/shared"; import { notNullish } from "@shared/utils/utils.js"; -import { expect } from "chai"; import type Stripe from "stripe"; import { defaultApiVersion } from "tests/constants.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; @@ -80,7 +80,7 @@ const compareActualItems = async ({ }); } - expect(actualItem).to.exist; + expect(actualItem).toBeDefined(); if (actualItem?.quantity !== (expectedItem as any).quantity) { if (phaseStartsAt) { @@ -117,13 +117,12 @@ const compareActualItems = async ({ console.log("--------------------------------"); } - expect(actualItem?.quantity).to.equal( + expect(actualItem?.quantity).toBe( (expectedItem as any).quantity, - `actual items quantity should be equals to ${expectedItem.quantity}`, ); } - expect(actualItems.length).to.equal(expectedItems.length); + expect(actualItems.length).toBe(expectedItems.length); }; export const expectSubToBeCorrect = async ({ @@ -167,7 +166,7 @@ export const expectSubToBeCorrect = async ({ if (!subId) { const subIds = cusProductToSubIds({ cusProducts }); subId = subIds[0]; - expect(subIds.length, "should only have 1 sub ID available").to.equal(1); + expect(subIds.length).toBe(1); } else { cusProducts = cusProducts.filter((cp) => cp.subscription_ids?.includes(subId!), @@ -205,8 +204,7 @@ export const expectSubToBeCorrect = async ({ const apiVersion = cusProduct.api_semver || defaultApiVersion; if (isFreeProduct(product.prices)) { - expect(cusProduct.subscription_ids, "free product should have no subs").to - .be.empty; + expect(cusProduct.subscription_ids).toEqual([]); continue; } @@ -369,9 +367,9 @@ export const expectSubToBeCorrect = async ({ const corresponding = subCouponIds.find( (subCouponId: any) => subCouponId === reward, ); - expect(corresponding, `reward ${reward} should be in sub`).to.exist; + expect(corresponding).toBeDefined(); } - expect(subCouponIds.length).to.equal(rewards.length); + expect(subCouponIds.length).toBe(rewards.length); } await compareActualItems({ @@ -383,11 +381,11 @@ export const expectSubToBeCorrect = async ({ }); if (shouldBeTrialing) { - expect(sub.status, "sub should be trialing").to.equal("trialing"); + expect(sub.status).toBe("trialing"); } if (flags?.checkNotTrialing) { - expect(sub.status, "sub should not be trialing").to.not.equal("trialing"); + expect(sub.status).not.toBe("trialing"); } // Should be canceled @@ -424,9 +422,9 @@ export const expectSubToBeCorrect = async ({ // console.log("Final should be canceled:", finalShouldBeCanceled); if (finalShouldBeCanceled) { - expect(sub.schedule, "sub should NOT have a schedule").to.be.null; - // expect(sub.cancel_at, "sub should be canceled").to.exist; - expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true; + expect(sub.schedule).toBeNull(); + // expect(sub.cancel_at).toBeDefined(); + expect(subIsCanceled({ sub })).toBe(true); return; } @@ -458,14 +456,14 @@ export const expectSubToBeCorrect = async ({ if (supposedPhase.items.length === 0) continue; const actualPhase = schedule?.phases?.[i + 1]; - expect(schedule?.phases.length).to.be.greaterThan(i + 1); + expect(schedule?.phases.length).toBeGreaterThan(i + 1); expect( similarUnix({ unix1: supposedPhase.start_date, unix2: actualPhase!.start_date * 1000, }), - ).to.be.true; + ).toBe(true); const actualItems = actualPhase?.items.map((item) => ({ @@ -483,9 +481,9 @@ export const expectSubToBeCorrect = async ({ }); } - expect(sub.cancel_at, "sub should not be canceled").to.be.null; + expect(sub.cancel_at).toBeNull(); // if (shouldBeCanceled) { - // expect(sub.cancel_at, "sub should be canceled").to.exist; + // expect(sub.cancel_at).toBeDefined(); // } else { // } }; diff --git a/server/tests/merged/prepaid/mergedPrepaid1.backup.ts b/server/tests/merged/prepaid/mergedPrepaid1.backup.ts new file mode 100644 index 000000000..b0c3aa5bb --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid1.backup.ts @@ -0,0 +1,175 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 5, + }, + ], + }, + // Update prepaid quantity (decrease) + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 1, + }, + ], + }, +]; + +const testCase = "mergedPrepaid1"; +describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/prepaid/mergedPrepaid1.test.ts b/server/tests/merged/prepaid/mergedPrepaid1.test.ts new file mode 100644 index 000000000..44a51e900 --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid1.test.ts @@ -0,0 +1,169 @@ +import { beforeAll, describe, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { 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"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 5, + }, + ], + }, + // Update prepaid quantity (decrease) + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 1, + }, + ], + }, +]; + +const testCase = "mergedPrepaid1"; +describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); +}); diff --git a/server/tests/merged/prepaid/mergedPrepaid2.backup.ts b/server/tests/merged/prepaid/mergedPrepaid2.backup.ts new file mode 100644 index 000000000..9c630682a --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid2.backup.ts @@ -0,0 +1,200 @@ +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + OnDecrease, + OnIncrease, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 2, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 1, + }, + ], + }, + // // Update prepaid quantity (decrease) + // { + // entityId: "2", + // product: pro, + // results: [{ product: pro, status: CusProductStatus.Active }], + // options: [ + // { + // feature_id: TestFeature.Credits, + // quantity: billingUnits * 1, + // }, + // ], + // }, +]; + +const testCase = "mergedPrepaid2"; +describe(`${chalk.yellowBright("mergedPrepaid2: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + it("should have correct balances after update", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + }); +}); diff --git a/server/tests/merged/prepaid/mergedPrepaid2.test.ts b/server/tests/merged/prepaid/mergedPrepaid2.test.ts new file mode 100644 index 000000000..a45457f07 --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid2.test.ts @@ -0,0 +1,194 @@ +import { beforeAll, describe, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + OnDecrease, + OnIncrease, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { 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"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.None, + }, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 2, + }, + ], + }, + { + entityId: "2", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 1, + }, + ], + }, + // // Update prepaid quantity (decrease) + // { + // entityId: "2", + // product: pro, + // results: [{ product: pro, status: CusProductStatus.Active }], + // options: [ + // { + // feature_id: TestFeature.Credits, + // quantity: billingUnits * 1, + // }, + // ], + // }, +]; + +const testCase = "mergedPrepaid2"; +describe(`${chalk.yellowBright("mergedPrepaid2: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + test("should have correct balances after update", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + }); +}); diff --git a/server/tests/merged/prepaid/mergedPrepaid3.backup.ts b/server/tests/merged/prepaid/mergedPrepaid3.backup.ts new file mode 100644 index 000000000..f7cb0221c --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid3.backup.ts @@ -0,0 +1,195 @@ +// PREPAID WITH DOWNGRADE (SCHEDULED...) + +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + OnDecrease, + OnIncrease, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 2, + }, + ], + }, +]; + +const testCase = "mergedPrepaid3"; +describe(`${chalk.yellowBright("mergedPrepaid3: Testing merged subs, upgrade 1 & 2 to premium, downgrade 1 to pro")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + before(async function () { + await setupBefore(this); + const { autumnJs } = this; + db = this.db; + org = this.org; + env = this.env; + + stripeCli = this.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await createProducts({ + autumn: autumnJs, + products: [pro, premium], + db, + orgId: org.id, + env, + customerId, + }); + + const { testClockId: testClockId1 } = await initCustomer({ + autumn: autumnJs, + customerId, + db, + org, + env, + attachPm: "success", + }); + + testClockId = testClockId1!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + it("should have correct products after update", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + const entity1 = await autumn.entities.get(customerId, "1"); + expectProductAttached({ + customer: entity1, + product: pro, + entityId: "1", + }); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + }); +}); diff --git a/server/tests/merged/prepaid/mergedPrepaid3.test.ts b/server/tests/merged/prepaid/mergedPrepaid3.test.ts new file mode 100644 index 000000000..bb940afa0 --- /dev/null +++ b/server/tests/merged/prepaid/mergedPrepaid3.test.ts @@ -0,0 +1,189 @@ +// PREPAID WITH DOWNGRADE (SCHEDULED...) + +import { beforeAll, describe, test } from "bun:test"; +import { + type AppEnv, + CusProductStatus, + LegacyVersion, + OnDecrease, + OnIncrease, + type Organization, +} from "@autumn/shared"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js"; +import ctx from "tests/utils/testInitUtils/createTestContext.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { 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 "../mergeUtils/expectSubCorrect.js"; + +const billingUnits = 100; +const creditItem = constructPrepaidItem({ + featureId: TestFeature.Credits, + includedUsage: 100, + price: 10, + billingUnits, + config: { + on_increase: OnIncrease.ProrateImmediately, + on_decrease: OnDecrease.ProrateImmediately, + }, +}); + +const premium = constructProduct({ + id: "premium", + items: [creditItem], + type: "premium", +}); + +const pro = constructProduct({ + id: "pro", + items: [creditItem], + type: "pro", +}); + +const ops = [ + { + entityId: "1", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 4, + }, + ], + }, + { + entityId: "2", + product: premium, + results: [{ product: premium, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 3, + }, + ], + }, + + // Update prepaid quantity (increase) + { + entityId: "1", + product: pro, + results: [{ product: pro, status: CusProductStatus.Active }], + options: [ + { + feature_id: TestFeature.Credits, + quantity: billingUnits * 2, + }, + ], + }, +]; + +const testCase = "mergedPrepaid3"; +describe(`${chalk.yellowBright("mergedPrepaid3: Testing merged subs, upgrade 1 & 2 to premium, downgrade 1 to pro")}`, () => { + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 }); + + let stripeCli: Stripe; + let testClockId: string; + let curUnix: number; + let db: DrizzleCli; + let org: Organization; + let env: AppEnv; + + beforeAll(async () => { + db = ctx.db; + org = ctx.org; + env = ctx.env; + + stripeCli = ctx.stripeCli; + + addPrefixToProducts({ + products: [pro, premium], + prefix: testCase, + }); + + await initProductsV0({ + ctx, + products: [pro, premium], + prefix: testCase, + customerId, + }); + + const res = await initCustomerV3({ + ctx, + customerId, + withTestClock: true, + }); + + testClockId = res.testClockId!; + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + test("should run operations", async () => { + await autumn.entities.create(customerId, entities); + + for (let index = 0; index < ops.length; index++) { + const op = ops[index]; + try { + await attachAndExpectCorrect({ + autumn, + customerId, + product: op.product, + stripeCli, + db, + org, + env, + entityId: op.entityId, + options: op.options, + }); + } catch (error) { + console.log( + `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`, + ); + throw error; + } + } + }); + + test("should have correct products after update", async () => { + await advanceToNextInvoice({ + stripeCli, + testClockId, + }); + + const entity1 = await autumn.entities.get(customerId, "1"); + expectProductAttached({ + customer: entity1, + product: pro, + entityId: "1", + }); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + }); +}); diff --git a/server/tests/merged/separate/separate1.test.ts b/server/tests/merged/separate/separate1.test.ts index d6915f126..e29ae77cb 100644 --- a/server/tests/merged/separate/separate1.test.ts +++ b/server/tests/merged/separate/separate1.test.ts @@ -68,7 +68,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/separate/separate2.test.ts b/server/tests/merged/separate/separate2.test.ts index bb07c2473..fa7cefd27 100644 --- a/server/tests/merged/separate/separate2.test.ts +++ b/server/tests/merged/separate/separate2.test.ts @@ -88,7 +88,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions beca let stripeCli: Stripe; const curUnix = new Date().getTime(); - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/mergedTrial1.test.ts b/server/tests/merged/trial/mergedTrial1.test.ts index a7203b22a..5ddf84f89 100644 --- a/server/tests/merged/trial/mergedTrial1.test.ts +++ b/server/tests/merged/trial/mergedTrial1.test.ts @@ -42,7 +42,7 @@ describe(`${chalk.yellowBright("mergedTrial1: Testing trial")}`, () => { let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/mergedTrial2.test.ts b/server/tests/merged/trial/mergedTrial2.test.ts index 7658c8712..72dd7e133 100644 --- a/server/tests/merged/trial/mergedTrial2.test.ts +++ b/server/tests/merged/trial/mergedTrial2.test.ts @@ -51,7 +51,7 @@ describe(`${chalk.yellowBright("mergedTrial2: Testing add second trial product a let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/mergedTrial3.test.ts b/server/tests/merged/trial/mergedTrial3.test.ts index 27a61b2d9..c7be3d0e4 100644 --- a/server/tests/merged/trial/mergedTrial3.test.ts +++ b/server/tests/merged/trial/mergedTrial3.test.ts @@ -58,7 +58,7 @@ describe(`${chalk.yellowBright("mergedTrial3: Testing upgrade to product with tr let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/mergedTrial4.test.ts b/server/tests/merged/trial/mergedTrial4.test.ts index 346b1d042..03b8e1b88 100644 --- a/server/tests/merged/trial/mergedTrial4.test.ts +++ b/server/tests/merged/trial/mergedTrial4.test.ts @@ -57,7 +57,7 @@ describe(`${chalk.yellowBright("mergedTrial4: Testing cancel immediately on merg let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/mergedTrial5.test.ts b/server/tests/merged/trial/mergedTrial5.test.ts index 3bbaf031a..d6e8d0653 100644 --- a/server/tests/merged/trial/mergedTrial5.test.ts +++ b/server/tests/merged/trial/mergedTrial5.test.ts @@ -62,7 +62,7 @@ describe(`${chalk.yellowBright("mergedTrial5: Testing cancel at end of cycle and let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/trial1.test.ts b/server/tests/merged/trial/trial1.test.ts index 93c0c0dab..f7162b059 100644 --- a/server/tests/merged/trial/trial1.test.ts +++ b/server/tests/merged/trial/trial1.test.ts @@ -65,7 +65,7 @@ describe(`${chalk.yellowBright("trial1: Testing main trial branch, upgrade from let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/trial2.test.ts b/server/tests/merged/trial/trial2.test.ts index 2207a216b..850505790 100644 --- a/server/tests/merged/trial/trial2.test.ts +++ b/server/tests/merged/trial/trial2.test.ts @@ -67,7 +67,7 @@ describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/trial/trial3.test.ts b/server/tests/merged/trial/trial3.test.ts index ac95d4155..d198929ba 100644 --- a/server/tests/merged/trial/trial3.test.ts +++ b/server/tests/merged/trial/trial3.test.ts @@ -64,7 +64,7 @@ describe(`${chalk.yellowBright("trial3: Testing cancel trial product")}`, () => let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/upgrade/mergedUpgrade1.test.ts b/server/tests/merged/upgrade/mergedUpgrade1.test.ts index f649a46f9..ba27d3f00 100644 --- a/server/tests/merged/upgrade/mergedUpgrade1.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade1.test.ts @@ -68,7 +68,7 @@ describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/upgrade/mergedUpgrade2.test.ts b/server/tests/merged/upgrade/mergedUpgrade2.test.ts index b178b1e86..bbbaed2c6 100644 --- a/server/tests/merged/upgrade/mergedUpgrade2.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade2.test.ts @@ -82,7 +82,7 @@ describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a schedul let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/upgrade/mergedUpgrade3.test.ts b/server/tests/merged/upgrade/mergedUpgrade3.test.ts index 07e4ef0ec..f6a956a13 100644 --- a/server/tests/merged/upgrade/mergedUpgrade3.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade3.test.ts @@ -91,7 +91,7 @@ describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a schedul let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db; diff --git a/server/tests/merged/upgrade/mergedUpgrade4.test.ts b/server/tests/merged/upgrade/mergedUpgrade4.test.ts index 32a49aa77..1f9cd5f5d 100644 --- a/server/tests/merged/upgrade/mergedUpgrade4.test.ts +++ b/server/tests/merged/upgrade/mergedUpgrade4.test.ts @@ -85,7 +85,7 @@ describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel" let org: Organization; let env: AppEnv; - before(async function () { + beforeAll(async function () { await setupBefore(this); const { autumnJs } = this; db = this.db;