diff --git a/server/shell/g4.sh b/server/shell/g4.sh index 61c146949..80a5e328c 100755 --- a/server/shell/g4.sh +++ b/server/shell/g4.sh @@ -12,12 +12,12 @@ fi $MOCHA_CMD 'tests/merged/group/*.ts' -$MOCHA_CMD 'tests/merged/add/*.ts' \ -'tests/merged/downgrade/*.ts' \ -'tests/merged/prepaid/*.ts' \ -'tests/merged/separate/*.ts' \ -'tests/merged/upgrade/*.ts' \ -'tests/merged/trial/*.ts' +# $MOCHA_CMD 'tests/merged/add/*.ts' \ +# 'tests/merged/downgrade/*.ts' \ +# 'tests/merged/prepaid/*.ts' \ +# 'tests/merged/separate/*.ts' \ +# 'tests/merged/upgrade/*.ts' \ +# 'tests/merged/trial/*.ts' $MOCHA_CMD 'tests/merged/addOn/*.ts' \ diff --git a/server/src/check.ts b/server/src/check.ts index c3772b063..74ac1dac0 100644 --- a/server/src/check.ts +++ b/server/src/check.ts @@ -32,14 +32,17 @@ import { getRelatedCusPrice } from "./internal/customers/cusProducts/cusEnts/cus import { checkCusSubCorrect } from "./utils/checkUtils/checkCustomerCorrect.js"; import { EntityService } from "./internal/api/entities/EntityService.js"; -const { db, client } = initDrizzle({ maxConnections: 5 }); +const { db } = initDrizzle({ maxConnections: 5 }); let orgSlugs = process.env.ORG_SLUGS!.split(","); const skipEmails = process.env.SKIP_EMAILS!.split(","); -const skipIds = ["cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx"]; +const skipIds = [ + "cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx", + "DxG668K7uDd0Vahk54YWjvCGVgf2", +]; -orgSlugs = ["lumenary"]; -const customerId = "305fb694-6cdc-4148-bb9b-c73770629f75"; +orgSlugs = ["supermemory"]; +const customerId = "co1VPgUU59q43d5P2rFt4c"; const getSingleCustomer = async ({ stripeCli, diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 1c1996245..7a9393f9c 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -9,6 +9,7 @@ import { EntityExpand, ErrCode, Invoice, + OrgConfig, } from "@autumn/shared"; import { CancelParams, @@ -48,11 +49,13 @@ export class AutumnInt { secretKey, baseUrl, version, + orgConfig, }: { apiKey?: string; secretKey?: string; baseUrl?: string; version?: string | APIVersion; + orgConfig?: Partial; } = {}) { // this.apiKey = apiKey || process.env.AUTUMN_API_KEY || ""; this.apiKey = @@ -67,6 +70,10 @@ export class AutumnInt { this.headers["x-api-version"] = version.toString(); } + if (orgConfig) { + this.headers["org-config"] = JSON.stringify(orgConfig); + } + this.baseUrl = baseUrl || "http://localhost:8080/v1"; } diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index 7b2e43c7d..d97486b52 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -22,6 +22,7 @@ import { type Organization, type Subscription, CusProductResponse, + notNullish, } from "@autumn/shared"; export const getEntityResponse = async ({ @@ -85,9 +86,19 @@ export const getEntityResponse = async ({ ); let entityCusProducts = customer.customer_products.filter( - (p: FullCusProduct) => - entities.some((e: Entity) => e.internal_id == p.internal_entity_id) || - nullish(p.internal_entity_id) + (p: FullCusProduct) => { + if (org.config.entity_product) { + return entities.some( + (e: Entity) => + e.internal_id == p.internal_entity_id && + notNullish(p.internal_entity_id) + ); + } + return ( + entities.some((e: Entity) => e.internal_id == p.internal_entity_id) || + nullish(p.internal_entity_id) + ); + } ); let subs = customer.subscriptions || []; diff --git a/server/src/internal/api/entities/handlers/handleGetEntity.ts b/server/src/internal/api/entities/handlers/handleGetEntity.ts index 52ee6993f..2bd9fc30d 100644 --- a/server/src/internal/api/entities/handlers/handleGetEntity.ts +++ b/server/src/internal/api/entities/handlers/handleGetEntity.ts @@ -39,8 +39,6 @@ export const handleGetEntity = async (req: any, res: any) => features, logger, }); - // const end = performance.now(); - // logger.info(`getEntityResponse took ${(end - start).toFixed(2)}ms`); let entity = entities[0]; let withInvoices = expand.includes(EntityExpand.Invoices); diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts index 728861e94..29e5436bf 100644 --- a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts @@ -173,6 +173,8 @@ export const handleMultiAttachFlow = async ({ (p) => p.id === productOptions.product_id )!; + if (productOptions.quantity === 0) continue; + const anchorToUnix = curSub ? getLatestPeriodEnd({ sub: curSub! }) * 1000 : undefined; diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts index be0d7eb24..d41b22292 100644 --- a/server/src/internal/orgs/orgUtils.ts +++ b/server/src/internal/orgs/orgUtils.ts @@ -1,12 +1,23 @@ import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js"; import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv, ErrCode, FrontendOrg, Organization } from "@autumn/shared"; +import { + AppEnv, + ErrCode, + FrontendOrg, + Organization, + organizations, + OrgConfig, +} from "@autumn/shared"; import { createStripeCli } from "@/external/stripe/utils.js"; import { OrgService } from "./OrgService.js"; import { FeatureService } from "../features/FeatureService.js"; import { notNullish } from "@/utils/genUtils.js"; import Stripe from "stripe"; import { toSuccessUrl } from "./orgUtils/convertOrgUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { CacheManager } from "@/external/caching/CacheManager.js"; +import { eq } from "drizzle-orm"; +import { clearOrgCache } from "./orgUtils/clearOrgCache.js"; export const shouldReconnectStripe = async ({ org, @@ -167,3 +178,34 @@ export const getOrgAndFeatures = async ({ req }: { req: any }) => { return { org, features }; }; + +export const updateOrgConfig = async ({ + db, + org, + config, + disconnectCache = true, +}: { + db: DrizzleCli; + org: Organization; + config: Partial; + disconnectCache?: boolean; +}) => { + await db + .update(organizations) + .set({ + config: { + ...org.config, + ...config, + }, + }) + .where(eq(organizations.id, org.id)); + + await clearOrgCache({ + db, + orgId: org.id, + }); + + if (disconnectCache) { + await CacheManager.disconnect(); + } +}; diff --git a/server/src/middleware/apiAuthMiddleware.ts b/server/src/middleware/apiAuthMiddleware.ts index a0477986c..e0e067b08 100644 --- a/server/src/middleware/apiAuthMiddleware.ts +++ b/server/src/middleware/apiAuthMiddleware.ts @@ -92,6 +92,18 @@ export const verifySecretKey = async (req: any, res: any, next: any) => { req.authType = AuthType.SecretKey; req.userId = userId; + const orgConfig = await req.headers["org-config"]; + if (orgConfig) { + console.log("Org config found!: ", orgConfig); + let newConfigFields = JSON.parse(orgConfig); + try { + req.org.config = { + ...org.config, + ...newConfigFields, + }; + } catch (error) {} + } + next(); }; diff --git a/server/tests/core/multiAttach/multiAttach1.test.ts b/server/tests/core/multiAttach/multiAttach1.test.ts index 38b3a4d5f..2e47ef650 100644 --- a/server/tests/core/multiAttach/multiAttach1.test.ts +++ b/server/tests/core/multiAttach/multiAttach1.test.ts @@ -64,7 +64,10 @@ const ops = [ const testCase = "multiAttach1"; describe(`${chalk.yellowBright("multiAttach1: Testing multi attach for trial products and update product quantities mid trial")}`, () => { let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let autumn: AutumnInt = new AutumnInt({ + version: APIVersion.v1_4, + orgConfig: { entity_product: true }, + }); let stripeCli: Stripe; let testClockId: string; diff --git a/server/tests/core/multiAttach/multiAttach3.test.ts b/server/tests/core/multiAttach/multiAttach3.test.ts index d0af5514b..64f164134 100644 --- a/server/tests/core/multiAttach/multiAttach3.test.ts +++ b/server/tests/core/multiAttach/multiAttach3.test.ts @@ -51,7 +51,10 @@ let pro = constructProduct({ const testCase = "multiAttach3"; describe(`${chalk.yellowBright("multiAttach3: Testing multi attach for trial products transfer to entity, then cancel products on entities...")}`, () => { let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let autumn: AutumnInt = new AutumnInt({ + version: APIVersion.v1_4, + orgConfig: { entity_product: true }, + }); let stripeCli: Stripe; let testClockId: string; @@ -137,12 +140,12 @@ describe(`${chalk.yellowBright("multiAttach3: Testing multi attach for trial pro const results = [ { product: pro, - quantity: 4, + quantity: 5, status: CusProductStatus.Trialing, }, { product: premium, - quantity: 2, + quantity: 3, status: CusProductStatus.Trialing, }, { @@ -172,6 +175,7 @@ describe(`${chalk.yellowBright("multiAttach3: Testing multi attach for trial pro }); await expectResultsCorrect({ + autumn, customerId, results, }); diff --git a/server/tests/core/multiAttach/multiAttach5.test.ts b/server/tests/core/multiAttach/multiAttach5.test.ts index 91b18e583..b1cde7e14 100644 --- a/server/tests/core/multiAttach/multiAttach5.test.ts +++ b/server/tests/core/multiAttach/multiAttach5.test.ts @@ -11,6 +11,7 @@ import { AppEnv, CusProductStatus, Organization, + organizations, } from "@autumn/shared"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; @@ -26,6 +27,12 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addDays } from "date-fns"; import { expect } from "chai"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; +import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; +import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js"; +import { eq } from "drizzle-orm"; +import { CacheManager } from "@/external/caching/CacheManager.js"; let premium = constructProduct({ id: "premium", @@ -67,6 +74,17 @@ describe(`${chalk.yellowBright("multiAttach5: Testing multi attach and get custo stripeCli = this.stripeCli; + await OrgService.update({ + db, + orgId: org.id, + updates: { + config: { + ...org.config, + entity_product: true, + }, + }, + }); + addPrefixToProducts({ products: [pro, premium], prefix: testCase, @@ -145,90 +163,156 @@ describe(`${chalk.yellowBright("multiAttach5: Testing multi attach and get custo product_id: pro.id, }); - // await expectResultsCorrect({ - // customerId, - // results: [ - // { - // product: pro, - // quantity: 1, - // entityId: "1", - // status: CusProductStatus.Active, - // }, - // ], - // }); + const results = [ + { + product_id: pro.id, + quantity: 4, + product: pro, + status: CusProductStatus.Active, + }, + { + product_id: premium.id, + quantity: 3, + product: premium, + status: CusProductStatus.Active, + }, + { + product_id: pro.id, + quantity: 1, + product: pro, + entityId: "1", + status: CusProductStatus.Active, + }, + { + product_id: pro.id, + quantity: 1, + product: pro, + entityId: "2", + status: CusProductStatus.Active, + }, + ]; - // await expectSubToBeCorrect({ - // db, - // customerId, - // org, - // env, - // }); + await expectResultsCorrect({ + customerId, + results, + }); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + + const entity1 = await autumn.entities.get(customerId, "1"); + const entity2 = await autumn.entities.get(customerId, "2"); + expectFeaturesCorrect({ + customer: entity1, + product: pro, + }); + expectFeaturesCorrect({ + customer: entity2, + product: pro, + }); }); + + it("should try to reduce quantity of pro to less than number of entities and fail", async function () { + await expectAutumnError({ + func: async () => { + await autumn.attach({ + customer_id: customerId, + products: [ + { + product_id: pro.id, + quantity: 1, + }, + ], + }); + }, + }); + }); + + it("should update pro product quantity and have correct amount", async function () { + await expectMultiAttachCorrect({ + customerId, + products: [ + { + product_id: pro.id, + quantity: 6, + }, + ], + results: [ + { + product: pro, + quantity: 6, + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 1, + entityId: "1", + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 1, + entityId: "2", + status: CusProductStatus.Active, + }, + ], + db, + org, + env, + }); + }); + + it("should decrease pro product quantity and have correct amount", async function () { + await expectMultiAttachCorrect({ + customerId, + products: [ + { + product_id: pro.id, + quantity: 5, + }, + ], + results: [ + { + product: pro, + quantity: 5, + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 1, + entityId: "1", + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 1, + entityId: "2", + status: CusProductStatus.Active, + }, + ], + db, + org, + env, + }); + }); + + after(async function () { + await OrgService.update({ + db, + orgId: org.id, + updates: { + config: { + ...org.config, + entity_product: false, + }, + }, + }); + await CacheManager.disconnect(); + }); + return; - - it("should cancel one entity's sub at end of cycle and have correct schedule...", async function () { - await autumn.cancel({ - customer_id: customerId, - product_id: pro.id, - entity_id: "2", - }); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - }); - - it("should cancel one entity's sub immediately", async function () { - await autumn.cancel({ - customer_id: customerId, - product_id: premium.id, - entity_id: "1", - cancel_immediately: true, - // @ts-ignore - prorate: false, - }); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - }); - - it("should advance test clock to end of trial and have correct sub", async function () { - await advanceTestClock({ - stripeCli, - testClockId, - advanceTo: addDays(new Date(), 8).getTime(), - waitForSeconds: 30, - }); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - - const customer = await autumn.customers.get(customerId); - const latestInvoice = customer.invoices[0]; - - // Should only have paid for 4 pro and 2 premium... - const invoiceTotal = - getBasePrice({ product: pro }) * 4 + - getBasePrice({ product: premium }) * 2; - - expect(invoiceTotal).to.equal(latestInvoice.total); - - await expectSubToBeCorrect({ - db, - customerId, - org, - env, - }); - }); }); diff --git a/server/tests/core/multiAttach/multiAttach6.test.ts b/server/tests/core/multiAttach/multiAttach6.test.ts new file mode 100644 index 000000000..9f5ff2387 --- /dev/null +++ b/server/tests/core/multiAttach/multiAttach6.test.ts @@ -0,0 +1,280 @@ +import chalk from "chalk"; +import { setupBefore } from "tests/before.js"; +import { Stripe } from "stripe"; +import { createProducts } from "tests/utils/productUtils.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { + APIVersion, + AppEnv, + CusProductStatus, + FullCusProduct, + nullish, + Organization, +} from "@autumn/shared"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; +import { + expectMultiAttachCorrect, + expectResultsCorrect, +} from "tests/utils/expectUtils/expectMultiAttach.js"; +import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; + +import { expect } from "chai"; +import { OrgService } from "@/internal/orgs/OrgService.js"; + +import { CacheManager } from "@/external/caching/CacheManager.js"; +import { CusService } from "@/internal/customers/CusService.js"; + +let premium = constructProduct({ + id: "premium", + items: [ + constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }), + ], + type: "premium", +}); + +let pro = constructProduct({ + id: "pro", + items: [ + constructFeatureItem({ + featureId: TestFeature.Words, + includedUsage: 300, + }), + ], + type: "pro", +}); + +const testCase = "multiAttach6"; +describe(`${chalk.yellowBright("multiAttach6: Testing multi attach and get customer")}`, () => { + let customerId = testCase; + let autumn: AutumnInt = new AutumnInt({ version: APIVersion.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; + + await OrgService.update({ + db, + orgId: org.id, + updates: { + config: { + ...org.config, + entity_product: true, + }, + }, + }); + + 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!; + }); + + it("should run multi attach through checkout and have correct sub", async function () { + const productsList = [ + { + product_id: pro.id, + quantity: 4, + product: pro, + status: CusProductStatus.Active, + }, + { + product_id: premium.id, + quantity: 3, + product: premium, + status: CusProductStatus.Active, + }, + ]; + + await expectMultiAttachCorrect({ + customerId, + products: productsList, + results: productsList, + db, + org, + env, + }); + }); + + const entities = [ + { + id: "1", + name: "Entity 1", + feature_id: TestFeature.Users, + }, + { + id: "2", + name: "Entity 2", + feature_id: TestFeature.Users, + }, + ]; + + it("should transfer to entity 1 and 2", async function () { + await autumn.entities.create(customerId, entities); + + await autumn.transfer(customerId, { + to_entity_id: "1", + product_id: pro.id, + }); + + await autumn.transfer(customerId, { + to_entity_id: "2", + product_id: pro.id, + }); + + const results = [ + { + product_id: pro.id, + quantity: 4, + product: pro, + status: CusProductStatus.Active, + }, + { + product_id: premium.id, + quantity: 3, + product: premium, + status: CusProductStatus.Active, + }, + { + product_id: pro.id, + quantity: 1, + product: pro, + entityId: "1", + status: CusProductStatus.Active, + }, + { + product_id: pro.id, + quantity: 1, + product: pro, + entityId: "2", + status: CusProductStatus.Active, + }, + ]; + + await expectResultsCorrect({ + customerId, + results, + }); + + await expectSubToBeCorrect({ + db, + customerId, + org, + env, + }); + }); + + it("should try to reduce quantity of pro to 0 and have no top level cus product...", async function () { + await autumn.attach({ + customer_id: customerId, + products: [ + { + product_id: pro.id, + quantity: 2, + }, + ], + }); + + const results = [ + { + product: pro, + quantity: 1, + entityId: "1", + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 1, + entityId: "2", + status: CusProductStatus.Active, + }, + { + product: pro, + quantity: 2, + status: CusProductStatus.Active, + }, + ]; + + await expectResultsCorrect({ + customerId, + results, + }); + + const fullCus = await CusService.getFull({ + db, + orgId: org.id, + env, + idOrInternalId: customerId, + }); + + const proProduct = fullCus.customer_products.find( + (p: FullCusProduct) => + p.product_id === pro.id && nullish(p.internal_entity_id) + ); + expect(proProduct).to.be.undefined; + }); + + it("should increase pro product quantity and have correct amount", async function () { + await autumn.attach({ + customer_id: customerId, + products: [ + { + product_id: pro.id, + quantity: 4, + }, + ], + }); + }); + + after(async function () { + await OrgService.update({ + db, + orgId: org.id, + updates: { + config: { + ...org.config, + entity_product: false, + }, + }, + }); + await CacheManager.disconnect(); + }); + + return; +}); diff --git a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts index fd5528466..ceb8d0a3c 100644 --- a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts +++ b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts @@ -27,6 +27,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addDays } from "date-fns"; import { expect } from "chai"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; +import { updateOrgConfig } from "@/internal/orgs/orgUtils.js"; let premium = constructProduct({ id: "premium", @@ -50,7 +51,10 @@ let pro = constructProduct({ const testCase = "multiUpgrade1"; describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade")}`, () => { let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let autumn: AutumnInt = new AutumnInt({ + version: APIVersion.v1_4, + orgConfig: { entity_product: true }, + }); let stripeCli: Stripe; let testClockId: string; @@ -61,6 +65,7 @@ describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade" before(async function () { await setupBefore(this); + const { autumnJs } = this; db = this.db; org = this.org; @@ -111,6 +116,7 @@ describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade" ]; await expectMultiAttachCorrect({ + autumn, customerId, products: productsList, results: productsList, @@ -131,7 +137,7 @@ describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade" const results = [ { product: pro, - quantity: 4, + quantity: 5, status: CusProductStatus.Active, }, { @@ -156,6 +162,7 @@ describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade" }); await expectResultsCorrect({ + autumn, customerId, results, }); @@ -193,7 +200,27 @@ describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade" env, }); + const results = [ + { + product: pro, + quantity: 4, + status: CusProductStatus.Active, + }, + { + product: premium, + quantity: 4, + status: CusProductStatus.Active, + }, + { + product: premium, + quantity: 1, + entityId: "1", + status: CusProductStatus.Active, + }, + ]; + await expectResultsCorrect({ + autumn, customerId, results, }); diff --git a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts index 41858e807..80c176c44 100644 --- a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts +++ b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts @@ -14,19 +14,12 @@ import { } from "@autumn/shared"; import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; -import { - addPrefixToProducts, - getBasePrice, -} from "tests/utils/testProductUtils/testProductUtils.js"; +import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js"; import { expectMultiAttachCorrect, expectResultsCorrect, } from "tests/utils/expectUtils/expectMultiAttach.js"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { addDays } from "date-fns"; -import { expect } from "chai"; -import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; let premium = constructProduct({ id: "premium", @@ -50,7 +43,10 @@ let pro = constructProduct({ const testCase = "multiUpgrade2"; describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update quantities downward")}`, () => { let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + let autumn: AutumnInt = new AutumnInt({ + version: APIVersion.v1_4, + orgConfig: { entity_product: true }, + }); let stripeCli: Stripe; let testClockId: string; @@ -111,6 +107,7 @@ describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update q ]; await expectMultiAttachCorrect({ + autumn, customerId, products: productsList, results: productsList, @@ -131,7 +128,7 @@ describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update q const results = [ { product: pro, - quantity: 4, + quantity: 5, status: CusProductStatus.Active, }, { @@ -156,6 +153,7 @@ describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update q }); await expectResultsCorrect({ + autumn, customerId, results, }); @@ -172,23 +170,23 @@ describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update q const productsList = [ { product_id: pro.id, - quantity: 3, + quantity: 6, }, { product_id: premium.id, - quantity: 2, + quantity: 1, }, ]; const results = [ { product: pro, - quantity: 3, + quantity: 6, status: CusProductStatus.Active, }, { product: premium, - quantity: 2, + quantity: 1, status: CusProductStatus.Active, }, { @@ -200,6 +198,7 @@ describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update q ]; await expectMultiAttachCorrect({ + autumn, customerId, products: productsList, results, diff --git a/server/tests/utils/expectUtils/expectMultiAttach.ts b/server/tests/utils/expectUtils/expectMultiAttach.ts index 903f75660..2fc9a961a 100644 --- a/server/tests/utils/expectUtils/expectMultiAttach.ts +++ b/server/tests/utils/expectUtils/expectMultiAttach.ts @@ -33,6 +33,7 @@ import { Decimal } from "decimal.js"; import { completeInvoiceCheckout } from "../stripeUtils/completeInvoiceCheckout.js"; export const expectMultiAttachCorrect = async ({ + autumn, customerId, entityId, products, @@ -44,6 +45,7 @@ export const expectMultiAttachCorrect = async ({ org, env, }: { + autumn?: AutumnInt; customerId: string; entityId?: string; products: ProductOptions[]; @@ -51,6 +53,7 @@ export const expectMultiAttachCorrect = async ({ product: ProductV2; quantity: number; status: CusProductStatus; + entityId?: string; }[]; rewards?: string[]; expectedRewards?: string[]; @@ -59,7 +62,7 @@ export const expectMultiAttachCorrect = async ({ org: Organization; env: AppEnv; }) => { - const autumn = new AutumnInt({ version: APIVersion.v1_2 }); + autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 }); const checkoutRes = await autumn.checkout({ customer_id: customerId, products: products, @@ -91,12 +94,17 @@ export const expectMultiAttachCorrect = async ({ for (const result of results) { let customer; - customer = await autumn.customers.get(customerId); + if (result.entityId) { + customer = await autumn.entities.get(customerId, result.entityId); + } else { + customer = await autumn.customers.get(customerId); + } expectProductAttached({ customer, product: result.product, status: result.status, + entityId: result.entityId, }); } @@ -118,21 +126,33 @@ export const expectMultiAttachCorrect = async ({ }; export const expectResultsCorrect = async ({ + autumn, customerId, results, }: { + autumn?: AutumnInt; customerId: string; - results: { product: ProductV2; quantity: number; status: CusProductStatus }[]; + results: { + product: ProductV2; + quantity: number; + status: CusProductStatus; + entityId?: string; + }[]; }) => { - const autumn = new AutumnInt({ version: APIVersion.v1_2 }); + autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 }); for (const result of results) { let customer; - customer = await autumn.customers.get(customerId); + if (result.entityId) { + customer = await autumn.entities.get(customerId, result.entityId); + } else { + customer = await autumn.customers.get(customerId); + } expectProductAttached({ customer, product: result.product, status: result.status, + quantity: result.quantity, }); } }; diff --git a/server/tests/utils/expectUtils/expectProductAttached.ts b/server/tests/utils/expectUtils/expectProductAttached.ts index c043b8536..ed02da03f 100644 --- a/server/tests/utils/expectUtils/expectProductAttached.ts +++ b/server/tests/utils/expectUtils/expectProductAttached.ts @@ -11,6 +11,7 @@ export const expectProductAttached = ({ status, entityId, isCanceled = false, + quantity, }: { customer: Customer; product?: ProductV2; @@ -18,6 +19,7 @@ export const expectProductAttached = ({ status?: CusProductStatus; entityId?: string; isCanceled?: boolean; + quantity?: number; }) => { const cusProducts = customer.products; const finalProductId = productId || product?.id; @@ -45,6 +47,11 @@ export const expectProductAttached = ({ ).to.not.equal(CusProductStatus.Expired); } + if (quantity) { + // @ts-ignore + expect(productAttached?.quantity).to.equal(quantity); + } + if (entityId) { // @ts-ignore expect(productAttached?.entity_id).to.equal(entityId); diff --git a/shared/models/orgModels/orgConfig.ts b/shared/models/orgModels/orgConfig.ts index d696643e1..f786a1bf7 100644 --- a/shared/models/orgModels/orgConfig.ts +++ b/shared/models/orgModels/orgConfig.ts @@ -21,6 +21,7 @@ export const OrgConfigSchema = z.object({ allow_paid_default: z.boolean().default(false), cache_customer: z.boolean().default(false), invoice_memos: z.boolean().default(false), + entity_product: z.boolean().default(false), }); export type OrgConfig = z.infer;