From ee662e9c658df715a0ce7c2f82dc259d31a8f41f Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 3 Oct 2025 18:12:23 +0100 Subject: [PATCH 1/3] fix: v1.1 customer schema --- server/src/check.ts | 6 +- .../attachParamsUtils/getPricesAndEnts.ts | 4 +- .../attach/attachUtils/handleAttachErrors.ts | 1 + .../attach/attachUtils/mapOptionsList.ts | 6 +- .../customers/cusUtils/getCustomerDetails.ts | 6 +- .../features/utils/mapFeatureUtils.ts | 24 ++++---- .../internal/migrations/migrationRouter.ts | 14 ++--- .../migrationSteps/migrateCustomer.ts | 27 +++++---- .../migrationSteps/migrateCustomers.ts | 55 ++++++++++--------- .../prices/priceUtils/findPriceUtils.ts | 21 ++++--- shared/api/customers/apiCustomer.ts | 4 ++ shared/api/features/apiFeature.ts | 2 +- .../product/components/AttachModal.tsx | 6 +- 13 files changed, 93 insertions(+), 83 deletions(-) diff --git a/server/src/check.ts b/server/src/check.ts index 1bd055c3f..d8ac778b8 100644 --- a/server/src/check.ts +++ b/server/src/check.ts @@ -43,9 +43,9 @@ const skipIds = [ "DxG668K7uDd0Vahk54YWjvCGVgf2", ]; -orgSlugs = ["capy"]; -const customerId = null; -// customerId = "55765cf6-84af-11f0-a94e-3eef481a796b"; +orgSlugs = ["feather"]; +let customerId = null; +customerId = "3aa8ebee-28cb-4293-97e8-becf55bc2159"; const getSingleCustomer = async ({ stripeCli, diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts index 64e2acd61..ea840dc7d 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts @@ -39,6 +39,8 @@ export const getPricesAndEnts = async ({ internalEntityId: customer.entity?.internal_id, }); + const curCusProduct = curSameProduct || curMainProduct; + // Not custom if (!is_custom) { const prices = products.flatMap((p: FullProduct) => p.prices); @@ -63,7 +65,7 @@ export const getPricesAndEnts = async ({ optionsInput: optionsInput || [], features, prices, - curCusProduct: curMainProduct, + curCusProduct, }), prices, entitlements, diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts index db0c330a0..a0c80747f 100644 --- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts @@ -81,6 +81,7 @@ const handlePrepaidErrors = async ({ if (billingType === BillingType.UsageInAdvance) { // Get options for price const priceEnt = getPriceEntitlement(price, entitlements); + const options = getEntOptions(optionsList, priceEnt); // 1. If not checkout, quantity should be defined diff --git a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts index 7388c85db..8212075c0 100644 --- a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts +++ b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts @@ -68,10 +68,10 @@ export const mapOptionsList = ({ if (isOneOff(prices) || isFreeProduct(prices)) return newOptionsList; const curOptionsList = curCusProduct?.options || []; + console.log("Current options list:", curOptionsList); for (const option of curOptionsList) { const inNewOptions = newOptionsList.find( - (newOption) => - newOption.internal_feature_id === option.internal_feature_id, + (newOption) => newOption.feature_id === option.feature_id, ); const prepaidPriceExists = findPrepaidPrice({ @@ -84,5 +84,7 @@ export const mapOptionsList = ({ } } + console.log(`New options list:`, newOptionsList); + return newOptionsList; }; diff --git a/server/src/internal/customers/cusUtils/getCustomerDetails.ts b/server/src/internal/customers/cusUtils/getCustomerDetails.ts index 2e71eaafc..b0ff8ed3d 100644 --- a/server/src/internal/customers/cusUtils/getCustomerDetails.ts +++ b/server/src/internal/customers/cusUtils/getCustomerDetails.ts @@ -1,5 +1,6 @@ import { APICustomerSchema, + APICustomerV1Schema, APIVersion, type AppEnv, CusEntResponseSchema, @@ -141,8 +142,11 @@ export const getCustomerDetails = async ({ expand, }); + const apiCustomerSchema = + apiVersion >= APIVersion.v1_2 ? APICustomerSchema : APICustomerV1Schema; + const cusResponse = { - ...APICustomerSchema.parse({ + ...apiCustomerSchema.parse({ ...customer, stripe_id: customer.processor?.id, features: entList, diff --git a/server/src/internal/features/utils/mapFeatureUtils.ts b/server/src/internal/features/utils/mapFeatureUtils.ts index 8bc19d38e..7fdb72ebe 100644 --- a/server/src/internal/features/utils/mapFeatureUtils.ts +++ b/server/src/internal/features/utils/mapFeatureUtils.ts @@ -1,30 +1,30 @@ import { - APIFeature, + type APIFeature, + APIFeatureSchema, APIFeatureType, - AppEnv, - CreditSchemaItem, - Feature, + type AppEnv, + type CreditSchemaItem, + type Feature, FeatureType, - FeatureUsageType, + type FeatureUsageType, } from "@autumn/shared"; -import { APIFeatureSchema } from "@autumn/shared"; +import RecaseError from "@/utils/errorUtils.js"; import { constructBooleanFeature, constructCreditSystem, constructMeteredFeature, } from "./constructFeatureUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; export const toAPIFeature = ({ feature }: { feature: Feature }) => { // return FeatureResponseSchema.parse(feature); // 1. Get feature type let featureType = feature.type; - if (feature.type == FeatureType.Metered) { + if (feature.type === FeatureType.Metered) { featureType = feature.config.usage_type; } - let creditSchema = undefined; - if (feature.type == FeatureType.CreditSystem) { + let creditSchema; + if (feature.type === FeatureType.CreditSystem) { creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({ metered_feature_id: s.metered_feature_id, credit_cost: s.credit_amount, @@ -53,11 +53,11 @@ export const fromAPIFeature = ({ orgId: string; env: AppEnv; }) => { - let isMetered = + const isMetered = apiFeature.type == APIFeatureType.SingleUsage || apiFeature.type == APIFeatureType.ContinuousUse; - let featureType: FeatureType = isMetered + const featureType: FeatureType = isMetered ? FeatureType.Metered : (apiFeature.type as unknown as FeatureType); diff --git a/server/src/internal/migrations/migrationRouter.ts b/server/src/internal/migrations/migrationRouter.ts index cdb11f1fa..47a558f9c 100644 --- a/server/src/internal/migrations/migrationRouter.ts +++ b/server/src/internal/migrations/migrationRouter.ts @@ -74,13 +74,13 @@ export const handleMigrate = async ( }); } - if (fromProduct.is_add_on || toProduct.is_add_on) { - throw new RecaseError({ - message: `Cannot migrate customers for add on products`, - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } + // if (fromProduct.is_add_on || toProduct.is_add_on) { + // throw new RecaseError({ + // message: `Cannot migrate customers for add on products`, + // code: ErrCode.InvalidRequest, + // statusCode: 400, + // }); + // } for (const price of toProduct.prices) { const billingType = getBillingType(price.config); diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts index 2cf8937d1..d05101959 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts @@ -1,21 +1,19 @@ -import { - MigrationJob, - Customer, - Organization, +import type { AppEnv, - FullProduct, - FullCusProduct, Feature, + FullCusProduct, + FullProduct, + MigrationJob, + Organization, } from "@autumn/shared"; -import { DrizzleCli } from "@/db/initDrizzle.js"; - -import { CusService } from "@/internal/customers/CusService.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/stripe/utils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js"; import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js"; -import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; export const migrateCustomer = async ({ db, @@ -42,7 +40,7 @@ export const migrateCustomer = async ({ }) => { try { const stripeCli = createStripeCli({ org, env }); - let fullCus = await CusService.getFull({ + const fullCus = await CusService.getFull({ db, idOrInternalId: customerId, orgId, @@ -51,7 +49,7 @@ export const migrateCustomer = async ({ }); // 1. Build req object - let req = { + const req = { db, orgId, env, @@ -63,7 +61,8 @@ export const migrateCustomer = async ({ const cusProducts = fullCus.customer_products; const filteredCusProducts = cusProducts.filter( - (cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id, + (cp: FullCusProduct) => + cp.product.internal_id === fromProduct.internal_id, ); for (const cusProduct of filteredCusProducts) { diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts index 13680238f..b6ec885b7 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts @@ -1,19 +1,20 @@ import dotenv from "dotenv"; + dotenv.config(); + import { - Customer, - Feature, - FullProduct, - MigrationJob, + type Customer, + type Feature, + type FullProduct, + type MigrationJob, MigrationJobStep, } from "@autumn/shared"; -import { MigrationService } from "../MigrationService.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { migrateCustomer } from "./migrateCustomer.js"; -import { sendMigrationEmail } from "../../emails/sendMigrationEmail.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { MigrationService } from "../MigrationService.js"; +import { migrateCustomer } from "./migrateCustomer.js"; export const migrateCustomers = async ({ db, @@ -41,17 +42,17 @@ export const migrateCustomers = async ({ }); let batchCount = 0; - let { org_id: orgId, env } = migrationJob; + const { org_id: orgId, env } = migrationJob; - let org = await OrgService.get({ + const org = await OrgService.get({ db, orgId, }); // Create stripe prices if they don't exist - let stripeCli = createStripeCli({ org, env }); - let batchCreate = []; - for (let price of toProduct.prices) { + const stripeCli = createStripeCli({ org, env }); + const batchCreate = []; + for (const price of toProduct.prices) { batchCreate.push( createStripePriceIFNotExist({ db, @@ -67,12 +68,12 @@ export const migrateCustomers = async ({ await Promise.all(batchCreate); - let batchSize = 5; + const batchSize = 5; for (let i = 0; i < customers.length; i += batchSize) { - let batchCustomers = customers.slice(i, i + batchSize); - let batchPromises = []; - for (let customer of batchCustomers) { + const batchCustomers = customers.slice(i, i + batchSize); + const batchPromises = []; + for (const customer of batchCustomers) { if (!customer.id) continue; batchPromises.push( migrateCustomer({ @@ -90,9 +91,9 @@ export const migrateCustomers = async ({ ); } - let results = await Promise.all(batchPromises); - let numPassed = results.filter((r) => r).length; - let numFailed = results.filter((r) => !r).length; + const results = await Promise.all(batchPromises); + const numPassed = results.filter((r) => r).length; + const numFailed = results.filter((r) => !r).length; logger.info( `Job: ${migrationJob.id} - Migrated ${i + batchCustomers.length}/${ customers.length @@ -100,14 +101,14 @@ export const migrateCustomers = async ({ ); // Get current number of customers migrated - let curMigrationJob = await MigrationService.getJob({ + const curMigrationJob = await MigrationService.getJob({ db, id: migrationJob.id, }); - let curSucceeded = + const curSucceeded = curMigrationJob.step_details[MigrationJobStep.MigrateCustomers] ?.succeeded || 0; - let curFailed = + const curFailed = curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]?.failed || 0; @@ -134,7 +135,7 @@ export const migrateCustomers = async ({ } // Get number of errors - let migrationDetails: any = {}; + const migrationDetails: any = {}; // try { // let errors = await MigrationService.getErrors({ // db, @@ -152,7 +153,7 @@ export const migrateCustomers = async ({ // logger.error(error); // } - let curMigrationJob = await MigrationService.getJob({ + const curMigrationJob = await MigrationService.getJob({ db, id: migrationJob.id, }); diff --git a/server/src/internal/products/prices/priceUtils/findPriceUtils.ts b/server/src/internal/products/prices/priceUtils/findPriceUtils.ts index 914bc36dc..203854da7 100644 --- a/server/src/internal/products/prices/priceUtils/findPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/findPriceUtils.ts @@ -1,17 +1,15 @@ import { BillingType, ErrCode, - FullProduct, - Price, + type Feature, + type FullProduct, + type Price, PriceType, - Product, - UsagePriceConfig, + type Product, + type UsagePriceConfig, } from "@autumn/shared"; - -import { Feature } from "@autumn/shared"; -import { getBillingType } from "../priceUtils.js"; -import Stripe from "stripe"; import RecaseError from "@/utils/errorUtils.js"; +import { getBillingType } from "../priceUtils.js"; export const findPrepaidPrice = ({ prices, @@ -21,15 +19,16 @@ export const findPrepaidPrice = ({ internalFeatureId?: string; }) => { return prices.find((p: Price) => { - if (p.config.type != PriceType.Usage) return false; + if (p.config.type !== PriceType.Usage) return false; const billingType = getBillingType(p.config); + const config = p.config as UsagePriceConfig; - if (billingType != BillingType.UsageInAdvance) return false; + if (billingType !== BillingType.UsageInAdvance) return false; if (internalFeatureId) { - return config.internal_feature_id == internalFeatureId; + return config.internal_feature_id === internalFeatureId; } else return true; }); }; diff --git a/shared/api/customers/apiCustomer.ts b/shared/api/customers/apiCustomer.ts index 1b1fa5e81..3c6eaf101 100644 --- a/shared/api/customers/apiCustomer.ts +++ b/shared/api/customers/apiCustomer.ts @@ -53,4 +53,8 @@ export const APICustomerSchema = z description: "Customer object returned by the API", }); +export const APICustomerV1Schema = APICustomerSchema.extend({ + features: z.any(), +}); + export type APICustomer = z.infer; diff --git a/shared/api/features/apiFeature.ts b/shared/api/features/apiFeature.ts index 63c167ce8..74f841aa8 100644 --- a/shared/api/features/apiFeature.ts +++ b/shared/api/features/apiFeature.ts @@ -11,7 +11,7 @@ export const APIFeatureSchema = z .object({ id: z.string(), name: z.string().nullish(), - type: z.nativeEnum(APIFeatureType), + type: z.enum(APIFeatureType), display: z .object({ singular: z.string(), diff --git a/vite/src/views/customers/customer/product/components/AttachModal.tsx b/vite/src/views/customers/customer/product/components/AttachModal.tsx index 42de1ae1c..8f61a0fd5 100644 --- a/vite/src/views/customers/customer/product/components/AttachModal.tsx +++ b/vite/src/views/customers/customer/product/components/AttachModal.tsx @@ -151,14 +151,12 @@ export const AttachModal = ({ const redirectUrl = getRedirectUrl(`/customers/${cusId}`, env); - // console.log("Version", version); - // console.log("Product version", product.version); - const attachBody = getAttachBody({ customerId: customer.id || customer.internal_id, entityId, product, - optionsInput: options, + optionsInput: + preview?.branch !== AttachBranch.NewVersion ? options : undefined, attachState, useInvoice, enableProductImmediately, From 5088482e1166256ba0e5d10cad33635961e29497 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 3 Oct 2025 18:29:06 +0100 Subject: [PATCH 2/3] fix: passing in app env in header --- .../honoMiddlewares/betterAuthMiddleware.ts | 2 +- .../handleUpdateProduct.ts | 1 + .../product-items/createFeaturesFromItems.ts | 26 +++---- .../productItemUtils/handleNewProductItems.ts | 45 ++++++------ .../product-items/validateProductItems.ts | 72 +++++++++---------- server/src/utils/routerUtils.ts | 5 +- 6 files changed, 76 insertions(+), 75 deletions(-) diff --git a/server/src/honoMiddlewares/betterAuthMiddleware.ts b/server/src/honoMiddlewares/betterAuthMiddleware.ts index 2a7048538..42ce02855 100644 --- a/server/src/honoMiddlewares/betterAuthMiddleware.ts +++ b/server/src/honoMiddlewares/betterAuthMiddleware.ts @@ -57,6 +57,7 @@ export const betterAuthMiddleware = async (c: Context, next: Next) => { } // Step 4: Fetch org and features from database + ctx.env = c.req.header("app_env") as AppEnv; const data = await OrgService.getWithFeatures({ db: ctx.db, orgId: orgId, @@ -76,7 +77,6 @@ export const betterAuthMiddleware = async (c: Context, next: Next) => { // Step 5: Store in context ctx.org = org; - ctx.env = c.req.header("app_env") as AppEnv; ctx.features = features; ctx.userId = userId; ctx.authType = AuthType.Dashboard; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index a2f992d36..e290294d4 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -100,6 +100,7 @@ export const handleUpdateProductV2 = createRoute({ const itemsExist = notNullish(body.items); const cusProductExists = cusProductsCurVersion.length > 0; + if (cusProductExists && itemsExist) { if (disable_version === "true") { throw new RecaseError({ diff --git a/server/src/internal/products/product-items/createFeaturesFromItems.ts b/server/src/internal/products/product-items/createFeaturesFromItems.ts index 067616373..2b1713115 100644 --- a/server/src/internal/products/product-items/createFeaturesFromItems.ts +++ b/server/src/internal/products/product-items/createFeaturesFromItems.ts @@ -1,21 +1,20 @@ import { - AppEnv, + type AppEnv, ErrCode, - Feature, + type Feature, FeatureType, FeatureUsageType, - ProductItem, + type ProductItem, ProductItemFeatureType, } from "@autumn/shared"; -import { isBooleanFeatureItem } from "./productItemUtils/getItemType.js"; import { validateFeatureId } from "@/internal/features/featureUtils.js"; import { constructBooleanFeature, constructMeteredFeature, } from "@/internal/features/utils/constructFeatureUtils.js"; - import RecaseError from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; +import { isBooleanFeatureItem } from "./productItemUtils/getItemType.js"; export const createFeaturesFromItems = ({ items, @@ -28,22 +27,23 @@ export const createFeaturesFromItems = ({ orgId: string; env: AppEnv; }) => { - let newFeatures: Feature[] = []; + const newFeatures: Feature[] = []; for (const item of items) { if (!item.feature_id) { continue; } - let feature = curFeatures.find((f) => f.id == item.feature_id); + const feature = curFeatures.find((f) => f.id === item.feature_id); + if (feature) { if (nullish(item.feature_type)) { continue; } // 1. Check that feature_type matches - if (item.feature_type == ProductItemFeatureType.Static) { - let booleanFail = - item.feature_type == ProductItemFeatureType.Static && - feature.type != FeatureType.Boolean; + if (item.feature_type === ProductItemFeatureType.Static) { + const booleanFail = + item.feature_type === ProductItemFeatureType.Static && + feature.type !== FeatureType.Boolean; if (booleanFail) { throw new RecaseError({ @@ -53,7 +53,7 @@ export const createFeaturesFromItems = ({ }); } } else { - let usageFail = item.feature_type != feature.config?.usage_type; + const usageFail = item.feature_type !== feature.config?.usage_type; if (usageFail) { throw new RecaseError({ message: `Feature ${item.feature_id} already exists but is not a ${item.feature_type} feature`, @@ -88,7 +88,7 @@ export const createFeaturesFromItems = ({ orgId, env, usageType: - item.feature_type == ProductItemFeatureType.ContinuousUse + item.feature_type === ProductItemFeatureType.ContinuousUse ? FeatureUsageType.Continuous : FeatureUsageType.Single, }); diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts index 97a935f6b..6311276b5 100644 --- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts +++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts @@ -1,4 +1,4 @@ -import { +import type { AppEnv, Entitlement, Feature, @@ -6,14 +6,13 @@ import { Product, ProductItem, } from "@autumn/shared"; -import { itemToPriceAndEnt } from "./itemToPriceAndEnt.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { FeatureService } from "@/internal/features/FeatureService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; import { EntitlementService } from "../../entitlements/EntitlementService.js"; import { validateProductItems } from "../validateProductItems.js"; -import { FeatureService } from "@/internal/features/FeatureService.js"; import { isFeatureItem } from "./getItemType.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { formatPrice } from "../../prices/priceUtils.js"; +import { itemToPriceAndEnt } from "./itemToPriceAndEnt.js"; const updateDbPricesAndEnts = async ({ db, @@ -62,8 +61,8 @@ const updateDbPricesAndEnts = async ({ ]); // Check if any custom prices use this entitlement... - let deletedEntIds = deletedEnts.map((ent) => ent.id!); - let customPrices = await PriceService.getCustomInEntIds({ + const deletedEntIds = deletedEnts.map((ent) => ent.id!); + const customPrices = await PriceService.getCustomInEntIds({ db, entitlementIds: deletedEntIds, }); @@ -75,9 +74,9 @@ const updateDbPricesAndEnts = async ({ ids: deletedEntIds, }); } else { - let updateOrDelete: any = []; + const updateOrDelete: any = []; for (const ent of deletedEnts) { - let hasCustomPrice = customPrices.some( + const hasCustomPrice = customPrices.some( (price) => price.entitlement_id == ent.id, ); @@ -179,7 +178,7 @@ export const handleNewProductItems = async ({ } // Validate product items... - let { allFeatures, newFeatures } = validateProductItems({ + const { allFeatures, newFeatures } = validateProductItems({ newItems, features, orgId: product.org_id!, @@ -188,14 +187,14 @@ export const handleNewProductItems = async ({ features = allFeatures; - let newPrices: Price[] = []; - let newEnts: Entitlement[] = []; + const newPrices: Price[] = []; + const newEnts: Entitlement[] = []; - let updatedPrices: Price[] = []; - let updatedEnts: Entitlement[] = []; + const updatedPrices: Price[] = []; + const updatedEnts: Entitlement[] = []; - let deletedPrices: Price[] = curPrices.filter((price) => { - let item = newItems.find((item) => item.price_id == price.id); + const deletedPrices: Price[] = curPrices.filter((price) => { + const item = newItems.find((item) => item.price_id === price.id); if (!item) { return true; } @@ -203,20 +202,20 @@ export const handleNewProductItems = async ({ return isFeatureItem(item); }); - let deletedEnts: Entitlement[] = curEnts.filter( + const deletedEnts: Entitlement[] = curEnts.filter( (ent) => !newItems.some((item) => item.entitlement_id == ent.id), ); - let samePrices: Price[] = []; - let sameEnts: Entitlement[] = []; + const samePrices: Price[] = []; + const sameEnts: Entitlement[] = []; for (const item of newItems) { - let feature = features.find((f) => f.id == item.feature_id); - let curEnt = curEnts.find((ent) => ent.id == item.entitlement_id); - let curPrice = curPrices.find((price) => price.id == item.price_id); + const feature = features.find((f) => f.id == item.feature_id); + const curEnt = curEnts.find((ent) => ent.id == item.entitlement_id); + const curPrice = curPrices.find((price) => price.id == item.price_id); // 2. Update price and entitlement? - let { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } = + const { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } = itemToPriceAndEnt({ item, orgId: product.org_id!, diff --git a/server/src/internal/products/product-items/validateProductItems.ts b/server/src/internal/products/product-items/validateProductItems.ts index e4d32da60..0e910c442 100644 --- a/server/src/internal/products/product-items/validateProductItems.ts +++ b/server/src/internal/products/product-items/validateProductItems.ts @@ -1,29 +1,27 @@ -import RecaseError from "@/utils/errorUtils.js"; import { - ProductItem, + type AppEnv, EntInterval, ErrCode, - TierInfinite, - ProductItemSchema, - Infinite, - ProductItemInterval, - Feature, + type Feature, FeatureType, - AppEnv, + Infinite, OnIncrease, + type ProductItem, + ProductItemInterval, + ProductItemSchema, UsageModel, - RolloverDuration, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +import RecaseError from "@/utils/errorUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; +import { createFeaturesFromItems } from "./createFeaturesFromItems.js"; +import { itemToEntInterval } from "./itemIntervalUtils.js"; import { isBooleanFeatureItem, isFeatureItem, isFeaturePriceItem, isPriceItem, } from "./productItemUtils/getItemType.js"; -import { itemToEntInterval } from "./itemIntervalUtils.js"; -import { createFeaturesFromItems } from "./createFeaturesFromItems.js"; const validateProductItem = ({ item, @@ -107,9 +105,9 @@ const validateProductItem = ({ // 5. If it's a price, can't have day, minute or hour interval if (isFeaturePriceItem(item) || isPriceItem(item)) { if ( - item.interval == ProductItemInterval.Day || - item.interval == ProductItemInterval.Minute || - item.interval == ProductItemInterval.Hour + item.interval === ProductItemInterval.Day || + item.interval === ProductItemInterval.Minute || + item.interval === ProductItemInterval.Hour ) { throw new RecaseError({ message: `Price can't have day, minute or hour interval`, @@ -120,8 +118,8 @@ const validateProductItem = ({ } if ( - item.usage_model == UsageModel.Prepaid && - item.config?.on_increase == OnIncrease.BillImmediately + item.usage_model === UsageModel.Prepaid && + item.config?.on_increase === OnIncrease.BillImmediately ) { throw new RecaseError({ message: `Bill immediately is not supported for prepaid just yet, contact us at hey@useautumn.com if you're interested!`, @@ -150,7 +148,7 @@ export const validateProductItems = ({ orgId: string; env: AppEnv; }) => { - let { allFeatures, newFeatures } = createFeaturesFromItems({ + const { allFeatures, newFeatures } = createFeaturesFromItems({ items: newItems, curFeatures: features, orgId, @@ -169,21 +167,21 @@ export const validateProductItems = ({ // 1. Check values for (let index = 0; index < newItems.length; index++) { validateProductItem({ item: newItems[index], features }); - let feature = features.find((f) => f.id == newItems[index].feature_id); + const feature = features.find((f) => f.id === newItems[index].feature_id); - if (feature && feature.type == FeatureType.Metered) { + if (feature && feature.type === FeatureType.Metered) { newItems[index].feature_type = feature.config?.usage_type; } } for (let index = 0; index < newItems.length; index++) { - let item = newItems[index]; - let entInterval = itemToEntInterval(item); + const item = newItems[index]; + const entInterval = itemToEntInterval(item); const intervalCount = item.interval_count || 1; - if (isFeaturePriceItem(item) && entInterval == EntInterval.Lifetime) { - let otherItem = newItems.find((i: any, index2: any) => { - return i.feature_id == item.feature_id && index2 != index; + if (isFeaturePriceItem(item) && entInterval === EntInterval.Lifetime) { + const otherItem = newItems.find((i: any, index2: any) => { + return i.feature_id === item.feature_id && index2 !== index; }); if (otherItem && isFeaturePriceItem(otherItem)) { @@ -197,11 +195,11 @@ export const validateProductItems = ({ // Boolean duplicate if (isBooleanFeatureItem(item)) { - let otherItem = newItems.find((i: any, index2: any) => { + const otherItem = newItems.find((i: any, index2: any) => { return ( - i.feature_id == item.feature_id && - index2 != index && - item.entity_feature_id == i.entity_feature_id + i.feature_id === item.feature_id && + index2 !== index && + item.entity_feature_id === i.entity_feature_id ); }); @@ -214,13 +212,13 @@ export const validateProductItems = ({ } } - let otherItem = newItems.find((i: any, index2: any) => { + const otherItem = newItems.find((i: any, index2: any) => { return ( - i.feature_id == item.feature_id && - index2 != index && - itemToEntInterval(i) == entInterval && - (i.interval_count || 1) == intervalCount && - i.entity_feature_id == item.entity_feature_id + i.feature_id === item.feature_id && + index2 !== index && + itemToEntInterval(i) === entInterval && + (i.interval_count || 1) === intervalCount && + i.entity_feature_id === item.entity_feature_id ); }); @@ -244,7 +242,7 @@ export const validateProductItems = ({ }); } - if (item.usage_model && item.usage_model == otherItem?.usage_model) { + if (item.usage_model && item.usage_model === otherItem?.usage_model) { throw new RecaseError({ message: `You're trying to add the same feature (${item.feature_id}), with the same reset interval. You should either change the reset interval of one of the items, or make one of them a prepaid quantity`, code: ErrCode.InvalidInputs, @@ -253,8 +251,8 @@ export const validateProductItems = ({ } if (isPriceItem(item)) { - let otherItem = newItems.find((i: any, index2: any) => { - return i.interval === item.interval && index2 != index; + const otherItem = newItems.find((i: any, index2: any) => { + return i.interval === item.interval && index2 !== index; }); if (otherItem) { diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts index 718408c5c..bce349f46 100644 --- a/server/src/utils/routerUtils.ts +++ b/server/src/utils/routerUtils.ts @@ -289,7 +289,10 @@ export const routeHandler = async ({ }); } - if (error.message.includes("not a valid email address")) { + if ( + error.message.includes("not a valid email address") || + error.message.includes("email: Invalid input") + ) { req.logtail.warn(`Invalid email address`); return res.status(400).json({ message: error.message, From 788fa32313f65c45fd593c3e5646f2dfb0c99d13 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 3 Oct 2025 20:00:27 +0100 Subject: [PATCH 3/3] fix: added past due status to cus product --- shared/api/customers/components/apiCusProduct.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/api/customers/components/apiCusProduct.ts b/shared/api/customers/components/apiCusProduct.ts index 045f40cc1..8691f2bbf 100644 --- a/shared/api/customers/components/apiCusProduct.ts +++ b/shared/api/customers/components/apiCusProduct.ts @@ -5,7 +5,7 @@ export const APICusProductSchema = z.object({ id: z.string(), name: z.string().nullable(), group: z.string().nullable(), - status: z.enum(["active", "expired", "scheduled", "trialing"]), + status: z.enum(["active", "expired", "scheduled", "trialing", "past_due"]), canceled_at: z.number().nullish(), started_at: z.number(),