From 2eeb83ed42ccf43fdc2553b5560d1df79cccd984 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 19 Oct 2025 10:23:49 +0100 Subject: [PATCH] fix: tests --- server/shell/g1.sh | 6 +- server/shell/g5.sh | 21 +++--- .../stripe/handleStripeWebhookEvent.ts | 3 +- .../external/webhooks/connectWebhookRouter.ts | 7 +- .../handlers/handleListPlatformOrgs.ts | 46 +++++++++++++ .../handlers/handleListPlatformUsers.ts | 38 ++++------- .../platformBeta/handlers/platformOrgUtils.ts | 44 ++++++++++++ .../platformBeta/platformBetaRouter.ts | 6 +- server/tests/advanced/coupons/coupon2.ts | 3 +- .../advanced/multiFeature/multiFeature3.ts | 68 +++++++++---------- server/tests/advanced/referrals/referrals2.ts | 35 +++++++--- server/tests/advanced/referrals/referrals4.ts | 42 ++++++------ server/tests/attach/basic/basic3.ts | 4 +- .../multiInvoice/multiInvoice1.test.ts | 15 +--- .../utils/expectUtils/expectMultiAttach.ts | 7 +- shared/api/common/queryHelpers.ts | 45 ++++++++++++ shared/api/platform/platformModels.ts | 40 +++++++---- shared/api/versionUtils/ApiVersionClass.ts | 7 +- .../api/versionUtils/versionRegistryUtils.ts | 6 +- 19 files changed, 297 insertions(+), 146 deletions(-) create mode 100644 server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts rename server/src/internal/platform/{platformLegacy => platformBeta}/handlers/handleListPlatformUsers.ts (65%) create mode 100644 server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 6d0569571..98896f245 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -5,9 +5,9 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/config.sh" # If contains setup then run $MOCHA_SETUP -if [[ "$1" == *"setup"* ]]; then - MOCHA_PARALLEL=true $MOCHA_SETUP -fi +# if [[ "$1" == *"setup"* ]]; then +# MOCHA_PARALLEL=true $MOCHA_SETUP +# fi $MOCHA_CMD \ 'tests/attach/basic/*.ts' \ diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 4eb6760ba..f083cf29f 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -7,20 +7,19 @@ source "$(dirname "$0")/config.sh" if [[ "$1" == *"setup"* ]]; then MOCHA_PARALLEL=true $MOCHA_SETUP fi -# $MOCHA_CMD 'tests/advanced/referrals/*.ts' 'tests/advanced/coupons/*.ts' -# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ -# 'tests/advanced/coupons/*.ts' \ -# 'tests/attach/updateQuantity/*.ts' \ -# 'tests/advanced/referrals/*.ts' \ -# 'tests/advanced/referrals/paid/*.ts' \ -# 'tests/advanced/rollovers/*.ts' \ -# 'tests/advanced/customInterval/*.ts' +$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/coupons/*.ts' \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/referrals/paid/*.ts' \ + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' -# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ -# 'tests/advanced/usageLimit/*.ts' +$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/usageLimit/*.ts' -# $MOCHA_CMD 'tests/advanced/usage/*.ts' +$MOCHA_CMD 'tests/advanced/usage/*.ts' \ No newline at end of file diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts index 8f9501597..056a479ef 100644 --- a/server/src/external/stripe/handleStripeWebhookEvent.ts +++ b/server/src/external/stripe/handleStripeWebhookEvent.ts @@ -1,4 +1,4 @@ -import { type AppEnv, type Organization } from "@autumn/shared"; +import type { AppEnv, Organization } from "@autumn/shared"; import chalk from "chalk"; import { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; @@ -266,6 +266,7 @@ export const handleStripeWebhookEvent = async ({ }); } catch (error) { logger.error(`Stripe webhook, error refreshing cache!`, { error }); + return { success: true }; } return { success: true }; diff --git a/server/src/external/webhooks/connectWebhookRouter.ts b/server/src/external/webhooks/connectWebhookRouter.ts index 2bc4f5e50..2bd7a3233 100644 --- a/server/src/external/webhooks/connectWebhookRouter.ts +++ b/server/src/external/webhooks/connectWebhookRouter.ts @@ -41,7 +41,10 @@ export const handleConnectWebhook = async (c: Context) => { } const accountId = event.account; - if (!accountId) return c.json({ error: "Account ID not found" }, 200); + if (!accountId) { + logger.error(`Account ID not found in webhook event`); + return c.json({ error: "Account ID not found" }, 200); + } const { org, features } = await OrgService.getByAccountId({ db, @@ -78,6 +81,6 @@ export const handleConnectWebhook = async (c: Context) => { return c.json({ message: "Webhook received" }, 200); } catch (error) { logger.error(`Stripe webhook, error: ${error}`, { error }); - return c.json({ message: "Internal server error" }, 500); + return c.json({ message: "Webhook received, internal server error" }, 200); // 200 to avoid retries / shutdown of webhook... } }; diff --git a/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts b/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts new file mode 100644 index 000000000..1d49feb11 --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts @@ -0,0 +1,46 @@ +import { + type ApiPlatformOrg, + type ListPlatformOrgsQuery, + ListPlatformOrgsQuerySchema, + organizations, +} from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { toPlatformOrg } from "./platformOrgUtils.js"; + +/** + * Route: GET /platform/orgs - List organizations created by master org + */ +export const handleListPlatformOrgs = createRoute({ + query: ListPlatformOrgsQuerySchema, + handler: async (c) => { + const query = c.req.valid("query") as ListPlatformOrgsQuery; + const ctx = c.get("ctx"); + const { db, org: masterOrg } = ctx; + + const orgs = await db + .select() + .from(organizations) + .where(eq(organizations.created_by, masterOrg.id)) + .limit(query.limit) + .offset(query.offset); + + const orgsList: ApiPlatformOrg[] = orgs.map((org) => + toPlatformOrg({ + org: { + slug: org.slug, + name: org.name, + createdAt: org.createdAt, + }, + masterOrgId: masterOrg.id, + }), + ); + + return c.json({ + list: orgsList, + total: orgs.length, + limit: query.limit, + offset: query.offset, + }); + }, +}); diff --git a/server/src/internal/platform/platformLegacy/handlers/handleListPlatformUsers.ts b/server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts similarity index 65% rename from server/src/internal/platform/platformLegacy/handlers/handleListPlatformUsers.ts rename to server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts index 3dcd8cae7..e1cc281ef 100644 --- a/server/src/internal/platform/platformLegacy/handlers/handleListPlatformUsers.ts +++ b/server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts @@ -9,6 +9,7 @@ import { import { eq } from "drizzle-orm"; import { cte } from "@/db/cteUtils/buildCte.js"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { toPlatformOrg } from "./platformOrgUtils.js"; /** * Route: GET /platform/users - List users created by master org @@ -19,7 +20,7 @@ export const listPlatformUsers = createRoute({ const query = c.req.valid("query") as ListPlatformUsersQuery; const ctx = c.get("ctx"); - const { db, org, logger } = ctx; + const { db, org } = ctx; const shouldExpandOrgs = query.expand?.includes("organizations"); @@ -54,11 +55,16 @@ export const listPlatformUsers = createRoute({ created_at: new Date(user.created_at).getTime(), ...(shouldExpandOrgs && user.organizations && { - organizations: user.organizations.map((org: any) => ({ - slug: cleanOrgSlug(org.slug, org.id), - name: org.name, - created_at: new Date(org.createdAt).getTime(), - })), + organizations: user.organizations.map((org: any) => + toPlatformOrg({ + org: { + slug: org.slug, + name: org.name, + createdAt: org.createdAt, + }, + masterOrgId: ctx.org?.id || "", + }), + ), }), })); @@ -70,23 +76,3 @@ export const listPlatformUsers = createRoute({ }); }, }); - -/** - * Remove master org ID prefix from organization slug - */ -function cleanOrgSlug(slug: string, orgId: string): string { - let cleanedSlug = slug; - const prefix = `${orgId}_`; - if (cleanedSlug.startsWith(prefix)) { - cleanedSlug = cleanedSlug.slice(prefix.length); - } - // Handle the case where slug is prepended with "slug_orgId" - const altPrefix1 = `_${orgId}`; - const altPrefix2 = `|${orgId}`; - if (cleanedSlug.endsWith(altPrefix1)) { - cleanedSlug = cleanedSlug.slice(0, -altPrefix1.length); - } else if (cleanedSlug.endsWith(altPrefix2)) { - cleanedSlug = cleanedSlug = cleanedSlug.slice(0, -altPrefix2.length); - } - return cleanedSlug; -} diff --git a/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts b/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts new file mode 100644 index 000000000..0885ff2ca --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts @@ -0,0 +1,44 @@ +import type { ApiPlatformOrg } from "@autumn/shared"; + +/** + * Remove master org ID prefix from organization slug + */ +function cleanOrgSlug({ + slug, + orgId, +}: { + slug: string; + orgId: string; +}): string { + let cleanedSlug = slug; + const prefix = `${orgId}_`; + if (cleanedSlug.startsWith(prefix)) { + cleanedSlug = cleanedSlug.slice(prefix.length); + } + // Handle the case where slug is prepended with "slug_orgId" + const altPrefix1 = `_${orgId}`; + const altPrefix2 = `|${orgId}`; + if (cleanedSlug.endsWith(altPrefix1)) { + cleanedSlug = cleanedSlug.slice(0, -altPrefix1.length); + } else if (cleanedSlug.endsWith(altPrefix2)) { + cleanedSlug = cleanedSlug = cleanedSlug.slice(0, -altPrefix2.length); + } + return cleanedSlug; +} + +/** + * Convert raw org data to ApiPlatformOrg format + */ +export function toPlatformOrg({ + org, + masterOrgId, +}: { + org: { slug: string; name: string; createdAt: string | Date }; + masterOrgId: string; +}): ApiPlatformOrg { + return { + slug: cleanOrgSlug({ slug: org.slug, orgId: masterOrgId }), + name: org.name, + created_at: new Date(org.createdAt).getTime(), + }; +} diff --git a/server/src/internal/platform/platformBeta/platformBetaRouter.ts b/server/src/internal/platform/platformBeta/platformBetaRouter.ts index 9fde2f9d6..d2cf54de0 100644 --- a/server/src/internal/platform/platformBeta/platformBetaRouter.ts +++ b/server/src/internal/platform/platformBeta/platformBetaRouter.ts @@ -1,9 +1,10 @@ import { Autumn } from "autumn-js"; import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; -import { listPlatformUsers } from "../platformLegacy/handlers/handleListPlatformUsers.js"; import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js"; import { handleGetPlatformOAuth } from "./handlers/handleGetPlatformOAuth.js"; +import { handleListPlatformOrgs } from "./handlers/handleListPlatformOrgs.js"; +import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js"; import { handleUpdateOrganizationStripe } from "./handlers/handleUpdateOrganizationStripe.js"; const platformBetaRouter = new Hono(); @@ -61,7 +62,7 @@ platformBetaRouter.use("*", async (c, next) => { * POST /organization * Creates a new organization for platform users */ -platformBetaRouter.post("/organization", ...handleCreatePlatformOrg); +platformBetaRouter.post("/organizations", ...handleCreatePlatformOrg); /** * POST /oauth_url @@ -80,4 +81,5 @@ platformBetaRouter.post( platformBetaRouter.get("/users", ...listPlatformUsers); +platformBetaRouter.get("/organizations", ...handleListPlatformOrgs); export { platformBetaRouter }; diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts index fca812a3e..cadd28dd8 100644 --- a/server/tests/advanced/coupons/coupon2.ts +++ b/server/tests/advanced/coupons/coupon2.ts @@ -57,7 +57,6 @@ const reward: CreateReward = { describe( chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`), () => { - let logger: any; const customerId = testCase; let stripeCli: Stripe; let testClockId: string; @@ -67,7 +66,7 @@ describe( let env: AppEnv; let db: DrizzleCli; - let couponAmount = reward.discount_config!.discount_value; + let couponAmount = reward.discount_config?.discount_value ?? 0; before(async function () { await setupBefore(this); diff --git a/server/tests/advanced/multiFeature/multiFeature3.ts b/server/tests/advanced/multiFeature/multiFeature3.ts index 9a090ca1a..49f0a5e01 100644 --- a/server/tests/advanced/multiFeature/multiFeature3.ts +++ b/server/tests/advanced/multiFeature/multiFeature3.ts @@ -1,35 +1,34 @@ -import chalk from "chalk"; -import { expect } from "chai"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { features } from "tests/global.js"; -import { setupBefore } from "tests/before.js"; - +/** biome-ignore-all lint/suspicious/noExportsInTest: needed */ import { - AppEnv, + type AppEnv, BillingInterval, EntInterval, ProductItemFeatureType, UsageModel, } from "@autumn/shared"; -import { createProducts } from "tests/utils/productUtils.js"; -import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; +import { setupBefore } from "tests/before.js"; +import { features } from "tests/global.js"; import { getLifetimeFreeCusEnt, getUsageCusEnt, } from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; - +import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.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 { constructFeatureItem, constructFeaturePriceItem, } from "@/internal/products/product-items/productItemUtils.js"; import { timeout } from "@/utils/genUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { addMonths } from "date-fns"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly -let pro = { +const pro = { id: "multiFeature3Pro", name: "Multi Feature 3 Pro", items: { @@ -62,19 +61,19 @@ export const getLifetimeAndUsageCusEnts = async ({ env: AppEnv; featureId: string; }) => { - let mainCusProduct = await getMainCusProduct({ + const mainCusProduct = await getMainCusProduct({ customerId, db, orgId, env, }); - let lifetimeCusEnt = getLifetimeFreeCusEnt({ + const lifetimeCusEnt = getLifetimeFreeCusEnt({ cusProduct: mainCusProduct!, featureId, }); - let usageCusEnt = getUsageCusEnt({ + const usageCusEnt = getUsageCusEnt({ cusProduct: mainCusProduct!, featureId, }); @@ -86,8 +85,8 @@ export const getLifetimeAndUsageCusEnts = async ({ describe(`${chalk.yellowBright( "multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock", )}`, () => { - let autumn: AutumnInt = new AutumnInt(); - let customerId = "multiFeature3Customer"; + const autumn: AutumnInt = new AutumnInt(); + const customerId = "multiFeature3Customer"; let totalUsage = 0; @@ -95,17 +94,16 @@ describe(`${chalk.yellowBright( before(async function () { await setupBefore(this); - let { customer, testClockId: _testClockId } = - await initCustomerWithTestClock({ - customerId, - db: this.db, - org: this.org, - env: this.env, - }); + const res = await initCustomerV2({ + autumn, + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }); - testClockId = _testClockId; - - autumn = this.autumn; + testClockId = res.testClockId; await createProducts({ autumn, @@ -122,7 +120,7 @@ describe(`${chalk.yellowBright( product_id: pro.id, }); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, @@ -135,7 +133,7 @@ describe(`${chalk.yellowBright( expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage); }); - let overageValue = 30; + const overageValue = 30; it("should use lifetime allowance + overage", async function () { let value = pro.items.lifetime.included_usage as number; value += overageValue; @@ -150,7 +148,7 @@ describe(`${chalk.yellowBright( await timeout(3000); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, @@ -163,14 +161,14 @@ describe(`${chalk.yellowBright( }); it("cycle 1:should have correct usage after first cycle", async function () { - let advanceTo = addMonths(new Date(), 1).getTime(); + const advanceTo = addMonths(new Date(), 1).getTime(); await advanceTestClock({ stripeCli: this.stripeCli, testClockId, advanceTo, }); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, diff --git a/server/tests/advanced/referrals/referrals2.ts b/server/tests/advanced/referrals/referrals2.ts index 3fcda1718..1aa238e1c 100644 --- a/server/tests/advanced/referrals/referrals2.ts +++ b/server/tests/advanced/referrals/referrals2.ts @@ -1,6 +1,8 @@ import { + type AppEnv, type Customer, ErrCode, + type Organization, type ReferralCode, type RewardRedemption, } from "@autumn/shared"; @@ -12,8 +14,9 @@ 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 { initCustomerWithTestClock } from "tests/utils/testInitUtils.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 @@ -29,18 +32,21 @@ describe(`${chalk.yellowBright( 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 initCustomerWithTestClock({ - customerId: mainCustomerId, - db: this.db, - 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; @@ -95,8 +101,17 @@ describe(`${chalk.yellowBright( } // Check stripe customer - const stripeCus = (await stripeCli.customers.retrieve( + 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); diff --git a/server/tests/advanced/referrals/referrals4.ts b/server/tests/advanced/referrals/referrals4.ts index 741d63dcd..2de7fc1a4 100644 --- a/server/tests/advanced/referrals/referrals4.ts +++ b/server/tests/advanced/referrals/referrals4.ts @@ -1,31 +1,31 @@ -import { features, products, referralPrograms } from "../../global.js"; +import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; import { assert } from "chai"; import chalk from "chalk"; -import { setupBefore } from "tests/before.js"; -import { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; -import { timeout } from "tests/utils/genUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { Stripe } from "stripe"; -import { initCustomer } from "tests/utils/init.js"; -import { compareProductEntitlements } from "tests/utils/compare.js"; import { addDays, addHours } from "date-fns"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; +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", )}`, () => { - let mainCustomerId = "main-referral-4"; + const mainCustomerId = "main-referral-4"; // let redeemers = ["referral4-r1", "referral4-r2"]; - let redeemerId = "referral4-r1"; + const redeemerId = "referral4-r1"; let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let referralCode: ReferralCode; - let redemptions: RewardRedemption[] = []; + const redemptions: RewardRedemption[] = []; let mainCustomer: Customer; let redeemer: Customer; @@ -48,7 +48,7 @@ describe(`${chalk.yellowBright( product_id: products.proWithTrial.id, }); - let { testClockId: testClockId1, customer } = + const { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: redeemerId, db: this.db, @@ -60,7 +60,7 @@ describe(`${chalk.yellowBright( redeemer = customer; }); - it("should create referral code", async function () { + it("should create referral code", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.freeProduct.id, @@ -69,8 +69,8 @@ describe(`${chalk.yellowBright( assert.exists(referralCode.code); }); - it("should create redemption for each redeemer and fail if redeemed again", async function () { - let redemption: RewardRedemption = await autumn.referrals.redeem({ + 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, }); @@ -78,7 +78,7 @@ describe(`${chalk.yellowBright( redemptions.push(redemption); }); - it("should not be triggered because of trial", async function () { + it("should not be triggered because of trial", async () => { await autumn.attach({ customer_id: redeemerId, product_id: products.proWithTrial.id, @@ -87,13 +87,13 @@ describe(`${chalk.yellowBright( await timeout(3000); // Get redemption object - let redemption = await autumn.redemptions.get(redemptions[0].id); + const redemption = await autumn.redemptions.get(redemptions[0].id); assert.equal(redemption.triggered, false); }); - it("should be triggered after trial ends", async function () { - let advanceTo = addHours( + it("should be triggered after trial ends", async () => { + const advanceTo = addHours( addDays(new Date(), 7), hoursToFinalizeInvoice, ).getTime(); @@ -104,7 +104,7 @@ describe(`${chalk.yellowBright( waitForSeconds: 30, }); - let redemption = await autumn.redemptions.get(redemptions[0].id); + const redemption = await autumn.redemptions.get(redemptions[0].id); assert.equal(redemption.triggered, true); diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts index 78661c861..369f6c299 100644 --- a/server/tests/attach/basic/basic3.ts +++ b/server/tests/attach/basic/basic3.ts @@ -1,3 +1,4 @@ +import type { AppEnv, Organization } from "@autumn/shared"; import { expect } from "chai"; import chalk from "chalk"; import { setupBefore } from "tests/before.js"; @@ -7,6 +8,7 @@ import { compareMainProduct } from "tests/utils/compare.js"; import { timeout } from "tests/utils/genUtils.js"; import { createProducts } from "tests/utils/productUtils.js"; import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; @@ -51,7 +53,7 @@ const testCase = "basic3"; describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt(); - let db, org, env; + let db: DrizzleCli, org: Organization, env: AppEnv; before(async function () { await setupBefore(this); diff --git a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts index bc0eb0c6d..f2a4cd0c9 100644 --- a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts +++ b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts @@ -38,19 +38,6 @@ const pro = constructProduct({ trial: true, }); -const ops = [ - { - entityId: "1", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, -]; - const testCase = "multiInvoice1"; describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => { const customerId = testCase; @@ -98,7 +85,7 @@ describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invo testClockId = testClockId1!; }); - it("should run multi attach through checkout and have correct sub", async () => { + it("should run multi attach through invoice checkout flow", async () => { const productsList = [ { product_id: pro.id, diff --git a/server/tests/utils/expectUtils/expectMultiAttach.ts b/server/tests/utils/expectUtils/expectMultiAttach.ts index 855e85c4e..f8280d20c 100644 --- a/server/tests/utils/expectUtils/expectMultiAttach.ts +++ b/server/tests/utils/expectUtils/expectMultiAttach.ts @@ -6,6 +6,7 @@ import { type ProductOptions, type ProductV2, } from "@autumn/shared"; +import type { Customer, Entity } from "autumn-js"; import { expect } from "chai"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; @@ -75,8 +76,10 @@ export const expectMultiAttachCorrect = async ({ await timeout(5000); } + await timeout(2500); + for (const result of results) { - let customer; + let customer: Customer | Entity; if (result.entityId) { customer = await autumn.entities.get(customerId, result.entityId); } else { @@ -84,7 +87,7 @@ export const expectMultiAttachCorrect = async ({ } expectProductAttached({ - customer, + customer: customer as Customer, product: result.product, status: result.status, entityId: result.entityId, diff --git a/shared/api/common/queryHelpers.ts b/shared/api/common/queryHelpers.ts index 07d21530b..89b73d857 100644 --- a/shared/api/common/queryHelpers.ts +++ b/shared/api/common/queryHelpers.ts @@ -27,3 +27,48 @@ export function queryStringArray(schema: T) { return val; }, z.array(schema)); } + +/** + * Helper to handle query string integers that come in as strings and need to be converted to numbers. + * Query parameters are always strings, so this helper parses them to integers for validation. + * + * @example + * ```ts + * const schema = z.object({ + * limit: queryInteger({ min: 1, max: 100 }).default(10), + * offset: queryInteger({ min: 0 }).default(0), + * }); + * ``` + */ +export function queryInteger(options?: { + min?: number; + max?: number; + error?: string; +}) { + let schema = z.number().int({ message: options?.error }); + + if (options?.min !== undefined) { + schema = schema.min(options.min, { + message: options?.error || `must be at least ${options.min}`, + }); + } + + if (options?.max !== undefined) { + schema = schema.max(options.max, { + message: options?.error || `must be at most ${options.max}`, + }); + } + + return z.preprocess((val) => { + // If already a number, return as-is + if (typeof val === "number") { + return val; + } + // Parse string to integer + if (typeof val === "string") { + const parsed = Number.parseInt(val, 10); + return Number.isNaN(parsed) ? val : parsed; + } + return val; + }, schema); +} diff --git a/shared/api/platform/platformModels.ts b/shared/api/platform/platformModels.ts index d6d658047..a5970c591 100644 --- a/shared/api/platform/platformModels.ts +++ b/shared/api/platform/platformModels.ts @@ -1,22 +1,13 @@ -import { queryStringArray } from "@api/common/queryHelpers.js"; +import { queryInteger, queryStringArray } from "@api/common/queryHelpers.js"; import { z } from "zod/v4"; /** * Query params for GET /platform/users endpoint */ export const ListPlatformUsersQuerySchema = z.object({ - limit: z - .number() - .int({ error: "limit must be an integer" }) - .min(1, { error: "limit must be at least 1" }) - .max(100, { error: "limit must be at most 100" }) - .default(10), + limit: queryInteger({ min: 1, max: 100 }).default(10), - offset: z - .number({ error: "offset must be a number" }) - .int({ error: "offset must be an integer" }) - .min(0, { error: "offset must be at least 0" }) - .default(0), + offset: queryInteger({ min: 0 }).default(0), expand: queryStringArray(z.enum(["organizations"])) .optional() @@ -72,3 +63,28 @@ export const ListPlatformUsersResponseSchema = z.object({ export type ListPlatformUsersResponse = z.infer< typeof ListPlatformUsersResponseSchema >; + +/** + * Query params for GET /platform/orgs endpoint + */ +export const ListPlatformOrgsQuerySchema = z.object({ + limit: queryInteger({ min: 1, max: 100 }).default(10), + + offset: queryInteger({ min: 0 }).default(0), +}); + +export type ListPlatformOrgsQuery = z.infer; + +/** + * Response schema for GET /platform/orgs + */ +export const ListPlatformOrgsResponseSchema = z.object({ + list: z.array(ApiPlatformOrgSchema), + total: z.number().describe("Total number of organizations returned"), + limit: z.number().describe("Limit used in the query"), + offset: z.number().describe("Offset used in the query"), +}); + +export type ListPlatformOrgsResponse = z.infer< + typeof ListPlatformOrgsResponseSchema +>; diff --git a/shared/api/versionUtils/ApiVersionClass.ts b/shared/api/versionUtils/ApiVersionClass.ts index dda6232f8..4cc15b700 100644 --- a/shared/api/versionUtils/ApiVersionClass.ts +++ b/shared/api/versionUtils/ApiVersionClass.ts @@ -1,6 +1,9 @@ -import { ApiVersion, API_VERSIONS } from "./ApiVersion.js"; +import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js"; import type { VersionMetadata } from "./versionRegistry.js"; -import { getVersionMetadata, getVersionsSorted } from "./versionRegistryUtils.js"; +import { + getVersionMetadata, + getVersionsSorted, +} from "./versionRegistryUtils.js"; /** * ApiVersionClass - Encapsulates version comparison logic diff --git a/shared/api/versionUtils/versionRegistryUtils.ts b/shared/api/versionUtils/versionRegistryUtils.ts index 2ac64d209..42f63613a 100644 --- a/shared/api/versionUtils/versionRegistryUtils.ts +++ b/shared/api/versionUtils/versionRegistryUtils.ts @@ -1,4 +1,4 @@ -import { API_VERSIONS, ApiVersion } from "./ApiVersion.js"; +import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js"; import { VERSION_REGISTRY, type VersionMetadata } from "./versionRegistry.js"; /** @@ -24,7 +24,9 @@ export function getVersionMetadata({ return VERSION_REGISTRY[version]; } -export function isValidVersion(params: { version: string }): params is { version: ApiVersion } { +export function isValidVersion(params: { + version: string; +}): params is { version: ApiVersion } { return API_VERSIONS.includes(params.version as ApiVersion); }