diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index a6778208c..d18e37f6a 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -1,24 +1,23 @@ -import { SupabaseClient } from "@supabase/supabase-js"; import { - AppEnv, + type AppEnv, CusExpand, - CusProductStatus, - Customer, + type CusProductStatus, + type Customer, customers, - EntityExpand, - FullCusProduct, - FullCustomer, - Organization, + type EntityExpand, + type FullCusProduct, + type FullCustomer, + type Organization, } from "@autumn/shared"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@/errors/errCodes.js"; -import { StatusCodes } from "http-status-codes"; -import { and, eq, ilike, or, sql } from "drizzle-orm"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { getFullCusQuery } from "./getFullCusQuery.js"; import { trace } from "@opentelemetry/api"; +import { and, eq, ilike, or, sql } from "drizzle-orm"; +import { StatusCodes } from "http-status-codes"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { ErrCode } from "@/errors/errCodes.js"; +import RecaseError from "@/utils/errorUtils.js"; import { withSpan } from "../analytics/tracer/spanUtils.js"; import { RELEVANT_STATUSES } from "./cusProducts/CusProductService.js"; +import { getFullCusQuery } from "./getFullCusQuery.js"; const tracer = trace.getTracer("express"); @@ -76,11 +75,11 @@ export class CusService { entityId, ); - let result = await db.execute(query); + const result = await db.execute(query); if (!result || result.length == 0) { if (allowNotFound) { - // @ts-ignore + // @ts-expect-error return null as FullCustomer; } @@ -91,7 +90,7 @@ export class CusService { }); } - let data = result[0]; + const data = result[0]; data.created_at = Number(data.created_at); for (const product of data.customer_products as FullCusProduct[]) { @@ -131,9 +130,7 @@ export class CusService { ), }); - if (!customer) { - return null; - } + if (!customer) return null; return customer as Customer; } diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index bad33a055..b202fe235 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -1,21 +1,19 @@ +import { APIVersion, type AttachConfig, SuccessCode } from "@autumn/shared"; +import type Stripe from "stripe"; +import { ErrCode } from "@/errors/errCodes.js"; +import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; +import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; +import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; +import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; +import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js"; import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; import { - AttachParams, + type AttachParams, AttachResultSchema, } from "../cusProducts/AttachParams.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js"; -import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { ErrCode } from "@/errors/errCodes.js"; -import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; -import { APIVersion, AttachConfig } from "@autumn/shared"; -import { SuccessCode } from "@autumn/shared"; -import { notNullish } from "@/utils/genUtils.js"; - -import Stripe from "stripe"; -import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; export const handleCreateCheckout = async ({ req, @@ -99,8 +97,8 @@ export const handleCreateCheckout = async ({ } : undefined; - let checkoutParams = attachParams.checkoutSessionParams || {}; - let allowPromotionCodes = + const checkoutParams = attachParams.checkoutSessionParams || {}; + const allowPromotionCodes = notNullish(checkoutParams.discounts) || notNullish(rewards) ? undefined : checkoutParams.allow_promotion_codes || true; @@ -113,9 +111,9 @@ export const handleCreateCheckout = async ({ } // Prepare checkout session parameters - let checkout; + let checkout: Stripe.Checkout.Session; - let paymentMethodSet = + const paymentMethodSet = notNullish(checkoutParams.payment_method_types) || notNullish(checkoutParams.payment_method_configuration); @@ -165,7 +163,7 @@ export const handleCreateCheckout = async ({ `✅ Successfully created checkout for customer ${customer.id || customer.internal_id}`, ); } catch (error: any) { - let msg = error.message; + const msg = error.message; if ( msg && msg.includes("No valid payment method types") && @@ -188,7 +186,7 @@ export const handleCreateCheckout = async ({ return checkout; } - let apiVersion = attachParams.apiVersion || APIVersion.v1; + const apiVersion = attachParams.apiVersion || APIVersion.v1; if (apiVersion >= APIVersion.v1_1) { res.status(200).json( AttachResultSchema.parse({ diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts index 64a07d4a4..2ccdc1ed3 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts @@ -1,32 +1,34 @@ -import { - AttachParams, - AttachResultSchema, -} from "@/internal/customers/cusProducts/AttachParams.js"; import { APIVersion, - AttachConfig, + type AttachConfig, AttachScenario, + ErrCode, SuccessCode, } from "@autumn/shared"; - +import { StatusCodes } from "http-status-codes"; +import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; +import { + type AttachParams, + AttachResultSchema, +} from "@/internal/customers/cusProducts/AttachParams.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { attachToInsertParams, isFreeProduct, } from "@/internal/products/productUtils.js"; -import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; +import RecaseError from "@/utils/errorUtils.js"; import { attachParamsToCurCusProduct, - paramsToCurSub, + getCustomerSub, paramsToCurSubSchedule, } from "../../attachUtils/convertAttachParams.js"; -import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js"; +import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js"; import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js"; import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js"; -import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; -import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js"; -import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; export const handleScheduleFunction2 = async ({ req, @@ -45,47 +47,64 @@ export const handleScheduleFunction2 = async ({ const product = attachParams.products[0]; const { stripeCli } = attachParams; - const curCusProduct = attachParamsToCurCusProduct({ attachParams }); - const curSub = await paramsToCurSub({ attachParams }); - const subItems = curSub?.items.data.filter((item) => - subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item }), - ); + const curCusProduct = attachParamsToCurCusProduct({ + attachParams, + }); - const expectedEnd = getLatestPeriodEnd({ subItems }); + const { sub: curSub } = await getCustomerSub({ attachParams }); // 1. Cancel current subscription and fetch items from other cus products...? let schedule = await paramsToCurSubSchedule({ attachParams }); + if (!curSub) { + throw new RecaseError({ + message: `SCHEDULE FLOW, curSub is undefined`, + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } - const newProductFree = isFreeProduct(attachParams.prices); + if (!curCusProduct) { + throw new RecaseError({ + message: `SCHEDULE FLOW, curCusProduct is undefined`, + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } + + const subItems = curSub?.items.data.filter((item) => + subItemInCusProduct({ cusProduct: curCusProduct, subItem: item }), + ); + const expectedEnd = getLatestPeriodEnd({ subItems }); if (schedule) { const newItems = await paramsToScheduleItems({ req, - schedule: schedule!, + schedule: schedule, attachParams, config, - billingPeriodEnd: expectedEnd!, + billingPeriodEnd: expectedEnd, }); const currentPhaseIndex = getCurrentPhaseIndex({ + // biome-ignore lint/suspicious/noExplicitAny: ok schedule: { phases: newItems.phases } as any, now: attachParams.now, }); - if (currentPhaseIndex == newItems.phases.length - 1) { + if (currentPhaseIndex === newItems.phases.length - 1) { logger.info( `SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`, ); - await stripeCli.subscriptionSchedules.release(schedule!.id); + await stripeCli.subscriptionSchedules.release(schedule.id); await CusProductService.updateByStripeScheduledId({ db: req.db, - stripeScheduledId: schedule!.id, + stripeScheduledId: schedule.id, updates: { scheduled_ids: [] }, }); await CusProductService.update({ db: req.db, - cusProductId: curCusProduct!.id, + cusProductId: curCusProduct.id, updates: { canceled: true, canceled_at: Date.now(), @@ -100,14 +119,14 @@ export const handleScheduleFunction2 = async ({ attachParams, schedule, newPhases: newItems.phases || [], - sub: curSub!, + sub: curSub, }); await CusProductService.update({ db: req.db, - cusProductId: curCusProduct!.id, + cusProductId: curCusProduct.id, updates: { - scheduled_ids: [schedule!.id], + scheduled_ids: [schedule.id], canceled_at: Date.now(), canceled: true, ended_at: expectedEnd * 1000, @@ -118,15 +137,15 @@ export const handleScheduleFunction2 = async ({ logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`); schedule = await subToNewSchedule({ req, - sub: curSub!, + sub: curSub, attachParams, config, - endOfBillingPeriod: expectedEnd!, + endOfBillingPeriod: expectedEnd, }); await CusProductService.update({ db: req.db, - cusProductId: curCusProduct!.id, + cusProductId: curCusProduct.id, updates: { canceled: true, canceled_at: Date.now(), @@ -137,8 +156,8 @@ export const handleScheduleFunction2 = async ({ if (!schedule) { logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`); - await stripeCli.subscriptions.update(curSub!.id, { - cancel_at: expectedEnd!, + await stripeCli.subscriptions.update(curSub.id, { + cancel_at: expectedEnd, cancellation_details: { comment: "autumn_downgrade", }, @@ -149,9 +168,9 @@ export const handleScheduleFunction2 = async ({ await createFullCusProduct({ db: req.db, attachParams: attachToInsertParams(attachParams, product), - startsAt: expectedEnd! * 1000, + startsAt: expectedEnd * 1000, subscriptionScheduleIds: schedule ? [schedule.id] : [], - nextResetAt: expectedEnd! * 1000, + nextResetAt: expectedEnd * 1000, disableFreeTrial: true, isDowngrade: true, sendWebhook: false, @@ -184,14 +203,14 @@ export const handleScheduleFunction2 = async ({ } } - let apiVersion = attachParams.apiVersion || APIVersion.v1; + const apiVersion = attachParams.apiVersion || APIVersion.v1; if (res) { if (apiVersion >= APIVersion.v1_1) { res.status(200).json( AttachResultSchema.parse({ code: SuccessCode.DowngradeScheduled, - message: `Successfully downgraded from ${curCusProduct!.product.name} to ${product.name}`, + message: `Successfully downgraded from ${curCusProduct.product.name} to ${product.name}`, product_ids: [product.id], customer_id: attachParams.customer.id || attachParams.customer.internal_id, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index 675f867bf..a6c6d0a08 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -1,40 +1,39 @@ -import { - AttachParams, - AttachResultSchema, -} from "@/internal/customers/cusProducts/AttachParams.js"; -import { - attachParamsToCurCusProduct, - attachParamsToProduct, - paramsToCurSub, - paramsToCurSubSchedule, -} from "../../attachUtils/convertAttachParams.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { attachToInsertParams } from "@/internal/products/productUtils.js"; import { APIVersion, AttachBranch, - AttachConfig, + type AttachConfig, AttachScenario, CusProductStatus, cusProductToProduct, - logCusProducts, ProrationBehavior, } from "@autumn/shared"; -import { ExtendedRequest } from "@/utils/models/Request.js"; - +import type Stripe from "stripe"; +import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; +import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; +import { + type AttachParams, + AttachResultSchema, +} from "@/internal/customers/cusProducts/AttachParams.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; import { attachToInvoiceResponse, insertInvoiceFromAttach, } from "@/internal/invoices/invoiceUtils.js"; -import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { updateStripeSub2 } from "./updateStripeSub2.js"; -import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { attachToInsertParams } from "@/internal/products/productUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { + attachParamsToCurCusProduct, + paramsToCurSub, + paramsToCurSubSchedule, +} from "../../attachUtils/convertAttachParams.js"; import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js"; -import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { shouldCancelSub } from "./upgradeFlowUtils.js"; import { handleUpgradeFlowSchedule } from "./handleUpgradeFlowSchedule.js"; -import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { updateStripeSub2 } from "./updateStripeSub2.js"; +import { shouldCancelSub } from "./upgradeFlowUtils.js"; export const handleUpgradeFlow = async ({ req, @@ -59,7 +58,7 @@ export const handleUpgradeFlow = async ({ } let sub = curSub; - let latestInvoice = undefined; + let latestInvoice: Stripe.Invoice | undefined; const itemSet = await getStripeSubItems2({ attachParams, @@ -75,12 +74,18 @@ export const handleUpgradeFlow = async ({ const { subItems } = newItemSet; - const products = attachParams.fromCancel - ? [cusProductToProduct({ cusProduct: attachParams.cusProduct! })] - : attachParams.products; + const products = + attachParams.fromCancel && attachParams.cusProduct + ? [cusProductToProduct({ cusProduct: attachParams.cusProduct })] + : attachParams.products; for (const product of products) { - if (product.is_add_on) continue; + if ( + product.is_add_on || + branch === AttachBranch.NewVersion || + branch === AttachBranch.SameCustomEnts + ) + continue; const { curScheduledProduct } = getExistingCusProducts({ product, @@ -97,31 +102,29 @@ export const handleUpgradeFlow = async ({ } let canceled = false; - // SCENARIO 1, NO SUB: - // Don't really need this... - if (branch == AttachBranch.SameCustomEnts) { + if (branch === AttachBranch.SameCustomEnts) { config.proration = ProrationBehavior.None; } if (!curSub) { logger.info("UPGRADE FLOW: no sub (from cancel maybe...?)"); // Do something about current sub... - } else if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) { + } else if (shouldCancelSub({ sub: curSub, newSubItems: subItems })) { logger.info( - `UPGRADE FLOW: canceling sub ${curSub!.id}, proration: ${config.proration}`, + `UPGRADE FLOW: canceling sub ${curSub.id}, proration: ${config.proration}`, ); canceled = true; const { stripeCli } = attachParams; - await stripeCli.subscriptions.cancel(curSub!.id, { - prorate: config.proration == ProrationBehavior.Immediately, - invoice_now: config.proration == ProrationBehavior.Immediately, + await stripeCli.subscriptions.cancel(curSub.id, { + prorate: config.proration === ProrationBehavior.Immediately, + invoice_now: config.proration === ProrationBehavior.Immediately, cancellation_details: { comment: "autumn_cancel", }, }); } else if (subItems.length > 0) { - logger.info(`UPGRADE FLOW, updating sub ${curSub!.id}`); + logger.info(`UPGRADE FLOW, updating sub ${curSub.id}`); itemSet.subItems = subItems; // await logPhaseItems({ @@ -133,7 +136,7 @@ export const handleUpgradeFlow = async ({ req, attachParams, config, - curSub: curSub!, + curSub: curSub, itemSet, fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product... }); @@ -163,14 +166,14 @@ export const handleUpgradeFlow = async ({ attachParams.replaceables = res.replaceables || []; sub = res.updatedSub; - latestInvoice = res.latestInvoice; + latestInvoice = res.latestInvoice || undefined; } if (curCusProduct) { logger.info(`UPGRADE FLOW: expiring previous cus product`); await CusProductService.update({ db: req.db, - cusProductId: curCusProduct!.id, + cusProductId: curCusProduct.id, updates: { subscription_ids: canceled ? undefined : [], status: CusProductStatus.Expired, @@ -197,24 +200,36 @@ export const handleUpgradeFlow = async ({ if (attachParams.products.length > 0) { logger.info(`UPGRADE FLOW: creating new cus product`); const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined; + console.log("Sub status:", sub?.status); + + let canceledAt: number | undefined; + if (sub && subIsCanceled({ sub })) { + canceledAt = sub.canceled_at + ? sub.canceled_at * 1000 + : curCusProduct?.canceled_at || undefined; + } + await createFullCusProduct({ db: req.db, attachParams: attachToInsertParams( attachParams, attachParams.products[0], ), - subscriptionIds: curCusProduct!.subscription_ids || [], + subscriptionIds: curCusProduct?.subscription_ids || [], disableFreeTrial: config.disableTrial, carryExistingUsages: config.carryUsage, carryOverTrial: config.carryTrial, anchorToUnix: anchorToUnix, scenario: AttachScenario.Upgrade, + canceledAt: canceledAt, + subscriptionStatus: + sub?.status === "past_due" ? CusProductStatus.PastDue : undefined, logger, }); } if (res) { - let apiVersion = attachParams.org.api_version || APIVersion.v1; + const apiVersion = attachParams.org.api_version || APIVersion.v1; if (apiVersion >= APIVersion.v1_1) { res.status(200).json( AttachResultSchema.parse({ diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts index 480fe6a9d..f6e7852f1 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts @@ -1,10 +1,9 @@ -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AttachBody } from "@autumn/shared"; -import { processAttachBody } from "./processAttachBody.js"; -import { orgToVersion } from "@/utils/versionUtils.js"; -import { APIVersion } from "@autumn/shared"; -import { AttachParams } from "../../../cusProducts/AttachParams.js"; +import { APIVersion, type AttachBody } from "@autumn/shared"; import { nullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { orgToVersion } from "@/utils/versionUtils.js"; +import type { AttachParams } from "../../../cusProducts/AttachParams.js"; +import { processAttachBody } from "./processAttachBody.js"; export const getAttachParams = async ({ req, diff --git a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts index b145b1a55..ffe07c245 100644 --- a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts @@ -1,7 +1,7 @@ -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { cusProductToProduct } from "@autumn/shared"; +import type Stripe from "stripe"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { CusProductStatus, cusProductToProduct } from "@autumn/shared"; -import Stripe from "stripe"; export const attachParamsToCurCusProduct = ({ attachParams, @@ -81,7 +81,7 @@ export const getCustomerSub = async ({ }) => { const { stripeCli } = attachParams; const fullCus = attachParams.customer; - let cusProducts = fullCus.customer_products; + const cusProducts = fullCus.customer_products; const targetGroup = attachParams.products[0].group; const targetEntityId = attachParams.internalEntityId || null; @@ -156,7 +156,7 @@ export const getCustomerSchedule = async ({ }) => { const { stripeCli } = attachParams; const fullCus = attachParams.customer; - let cusProducts = fullCus.customer_products; + const cusProducts = fullCus.customer_products; const targetGroup = attachParams.products[0].group; const targetEntityId = attachParams.internalEntityId || null; @@ -226,8 +226,11 @@ export const paramsToCurSub = async ({ }) => { const { stripeCli } = attachParams; const curCusProduct = attachParamsToCurCusProduct({ attachParams }); + console.log("Cur cus product:", curCusProduct); + console.log("Sub IDs:", curCusProduct?.subscription_ids); const subIds = curCusProduct?.subscription_ids || []; + if (subIds.length === 0) { return undefined; } @@ -263,7 +266,7 @@ export const paramsToCurSubSchedule = async ({ }, ); - if (schedule.status == "canceled") { + if (schedule.status === "canceled") { return undefined; } diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index 50af79a26..1bbae4859 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -121,7 +121,11 @@ export const getAttachConfig = async ({ invoiceCheckout || (noPaymentMethod && !invoiceAndEnable && - branch !== AttachBranch.MultiAttachUpdate); + ![ + AttachBranch.MultiAttachUpdate, + AttachBranch.NewVersion, + AttachBranch.SameCustomEnts, + ].includes(branch)); const onlyCheckout = !isFree && checkoutFlow && !freeTrialWithoutCardRequired; const disableMerge = branch === AttachBranch.MainIsTrial || onlyCheckout; @@ -136,7 +140,12 @@ export const getAttachConfig = async ({ paymentMethodRequired = false; } - if (flags.invoiceOnly) paymentMethodRequired = false; + if ( + flags.invoiceOnly || + branch === AttachBranch.NewVersion || + branch === AttachBranch.SameCustomEnts + ) + paymentMethodRequired = false; const config: AttachConfig = { branch, diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts index 174be35b8..f59c066e9 100644 --- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts @@ -1,29 +1,30 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@/errors/errCodes.js"; -import { StatusCodes } from "http-status-codes"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; import { + type AttachBody, AttachBranch, - AttachConfig, + type AttachConfig, AttachErrCode, - UsagePriceConfig, + BillingType, + cusProductsToCusEnts, + cusProductToPrices, + type FullCusProduct, + type UsagePriceConfig, } from "@autumn/shared"; -import { AttachBody } from "@autumn/shared"; -import { AttachFlags } from "../models/AttachFlags.js"; - +import { Decimal } from "decimal.js"; +import { StatusCodes } from "http-status-codes"; +import { ErrCode } from "@/errors/errCodes.js"; +import { findPriceForFeature } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { + getBillingType, getEntOptions, + getPriceEntitlement, priceIsOneOffAndTiered, } from "@/internal/products/prices/priceUtils.js"; -import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js"; -import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import { BillingType } from "@autumn/shared"; +import RecaseError from "@/utils/errorUtils.js"; import { notNullish, nullOrUndefined } from "@/utils/genUtils.js"; -import { attachParamToCusProducts } from "./convertAttachParams.js"; -import { cusProductsToCusEnts, cusProductToPrices } from "@autumn/shared"; -import { findPriceForFeature } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { getResetBalance } from "../../cusProducts/cusEnts/cusEntUtils.js"; -import { Decimal } from "decimal.js"; +import type { AttachFlags } from "../models/AttachFlags.js"; +import { attachParamToCusProducts } from "./convertAttachParams.js"; import { handleMultiAttachErrors } from "./handleAttachErrors/handleMultiAttachErrors.js"; const handleNonCheckoutErrors = ({ @@ -75,12 +76,12 @@ const handlePrepaidErrors = async ({ // 2. Check if options are valid for (const price of prices) { - const billingType = getBillingType(price.config!); + const billingType = getBillingType(price.config); if (billingType === BillingType.UsageInAdvance) { // Get options for price - let priceEnt = getPriceEntitlement(price, entitlements); - let options = getEntOptions(optionsList, priceEnt); + const priceEnt = getPriceEntitlement(price, entitlements); + const options = getEntOptions(optionsList, priceEnt); // 1. If not checkout, quantity should be defined @@ -107,7 +108,7 @@ const handlePrepaidErrors = async ({ } // 3. Quantity cannot be negative - if (notNullish(options?.quantity) && options?.quantity! < 0) { + if (notNullish(options?.quantity) && options.quantity < 0) { throw new RecaseError({ message: `Quantity cannot be negative`, code: ErrCode.InvalidOptions, @@ -124,11 +125,15 @@ const handlePrepaidErrors = async ({ }); } - let usageLimit = priceEnt.usage_limit; - let totalQuantity = - options?.quantity! * (price.config as UsagePriceConfig).billing_units!; + const config = price.config as UsagePriceConfig; + const usageLimit = priceEnt.usage_limit; + const totalQuantity = + (options?.quantity || 0) * (config.billing_units || 1); - if (usageLimit && totalQuantity + priceEnt.allowance! > usageLimit) { + if ( + usageLimit && + totalQuantity + (priceEnt.allowance || 0) > usageLimit + ) { throw new RecaseError({ message: `Quantity + included usage exceeds usage limit of ${usageLimit} for feature ${priceEnt.feature_id}`, code: ErrCode.InvalidOptions, @@ -152,7 +157,7 @@ const handleUpdateQuantityErrors = async ({ return; } - const cusProduct = curSameProduct || curMainProduct!; + const cusProduct = (curSameProduct || curMainProduct) as FullCusProduct; const cusEnts = cusProductsToCusEnts({ cusProducts: [cusProduct] }); const prices = cusProductToPrices({ cusProduct }); @@ -170,12 +175,12 @@ const handleUpdateQuantityErrors = async ({ const totalUsage = cusEnts .reduce((acc, curr) => { if ( - curr.entitlement.internal_feature_id == option.internal_feature_id + curr.entitlement.internal_feature_id === option.internal_feature_id ) { const allowance = getResetBalance({ entitlement: curr.entitlement, options: cusProduct.options.find( - (o) => o.internal_feature_id == option.internal_feature_id, + (o) => o.internal_feature_id === option.internal_feature_id, ), relatedPrice: price, }); @@ -226,7 +231,7 @@ export const handleAttachErrors = async ({ // Invoice no payment enabled: onlyCheckout if (onlyCheckout || flags.isPublic) { - let upgradeDowngradeFlows = [ + const upgradeDowngradeFlows = [ AttachBranch.Upgrade, AttachBranch.Downgrade, AttachBranch.MainIsTrial, @@ -238,7 +243,7 @@ export const handleAttachErrors = async ({ action: "perform upgrade or downgrade", }); } - let updateProductFlows = [ + const updateProductFlows = [ AttachBranch.NewVersion, AttachBranch.SameCustom, AttachBranch.UpdatePrepaidQuantity, @@ -253,7 +258,7 @@ export const handleAttachErrors = async ({ } // 2. If same custom ents, not allowed if is public flow... - if (branch == AttachBranch.SameCustomEnts) { + if (branch === AttachBranch.SameCustomEnts) { if (flags.isPublic) { throw new RecaseError({ message: diff --git a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts b/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts index c57b42584..f896e6414 100644 --- a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts +++ b/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts @@ -1,12 +1,10 @@ -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { paramsToScheduleItems } from "./paramsToScheduleItems.js"; -import Stripe from "stripe"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { AttachConfig, FullCusProduct } from "@autumn/shared"; -import { createSubSchedule } from "../attachFunctions/scheduleFlow/createSubSchedule.js"; +import type { AttachConfig, FullCusProduct } from "@autumn/shared"; +import type Stripe from "stripe"; import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; import { CusProductService } from "../../cusProducts/CusProductService.js"; -import { logPhases } from "./phaseUtils/phaseUtils.js"; +import { paramsToScheduleItems } from "./paramsToScheduleItems.js"; import { getCusProductsToRemove } from "./paramsToSubItems.js"; export const subToNewSchedule = async ({ @@ -49,7 +47,7 @@ export const subToNewSchedule = async ({ }); const { stripeCli } = attachParams; - let newSchedule: Stripe.SubscriptionSchedule | undefined = undefined; + let newSchedule: Stripe.SubscriptionSchedule | undefined; // if (sub.cancel_at) { // logger.info(`UNCANCELING SUB ${sub.id}`); diff --git a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts index f061b37d0..72fbce7ea 100644 --- a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts +++ b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts @@ -1,26 +1,21 @@ -import { updateCustomerDetails } from "./cusUtils.js"; -import { handleCreateCustomer } from "../handlers/handleCreateCustomer.js"; - -import { CusService } from "../CusService.js"; import { - AppEnv, CusExpand, CusProductStatus, - CustomerData, - EntityData, - ErrCode, - Feature, - FullCustomer, - Organization, + type CustomerData, + type Entity, + type EntityData, + type FullCustomer, } from "@autumn/shared"; - -import { ExtendedRequest } from "@/utils/models/Request.js"; import { autoCreateEntity } from "@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { CusService } from "../CusService.js"; +import { getCusWithCache } from "../cusCache/getCusWithCache.js"; import { deleteCusCache, refreshCusCache, } from "../cusCache/updateCachedCus.js"; -import { getCusWithCache } from "../cusCache/getCusWithCache.js"; +import { handleCreateCustomer } from "../handlers/handleCreateCustomer.js"; +import { updateCustomerDetails } from "./cusUtils.js"; export const getOrCreateCustomer = async ({ req, @@ -51,9 +46,9 @@ export const getOrCreateCustomer = async ({ entityData?: EntityData; withCache?: boolean; }): Promise => { - let customer; + let customer: FullCustomer | undefined; - const { db, org, features, env, logtail: logger } = req; + const { db, org, env, logtail: logger } = req; if (!withEntities) { withEntities = expand?.includes(CusExpand.Entities) || false; @@ -88,7 +83,7 @@ export const getOrCreateCustomer = async ({ if (!customer) { try { - customer = await handleCreateCustomer({ + customer = (await handleCreateCustomer({ req, cusData: { id: customerId, @@ -98,11 +93,11 @@ export const getOrCreateCustomer = async ({ metadata: customerData?.metadata || {}, stripe_id: customerData?.stripe_id, }, - }); + })) as FullCustomer; customer = await CusService.getFull({ db, - idOrInternalId: customerId || customer!.internal_id, + idOrInternalId: customerId || customer.internal_id, orgId: org.id, env, inStatuses, @@ -114,12 +109,12 @@ export const getOrCreateCustomer = async ({ await deleteCusCache({ db, - customerId: customer.id!, + customerId: customer.id || customer.internal_id, org, env, }); } catch (error: any) { - if (error?.data?.code == "23505") { + if (error?.data?.code === "23505") { customer = await CusService.getFull({ db, idOrInternalId: customerId, @@ -145,27 +140,30 @@ export const getOrCreateCustomer = async ({ logger, }); + // Customer is defined by this point! + customer = customer as FullCustomer; + if (entityId && !customer.entity) { logger.info(`Auto creating entity ${entityId} for customer ${customerId}`); - let newEntity = await autoCreateEntity({ + const newEntity = (await autoCreateEntity({ req, customer, entityId, entityData: { id: entityId, name: entityData?.name, - feature_id: entityData?.feature_id!, + feature_id: entityData?.feature_id || "", }, logger, - }); + })) as Entity; customer.entities = [...(customer.entities || []), newEntity]; customer.entity = newEntity; await refreshCusCache({ db, - customerId: customer.id!, + customerId: customer.id || customer.internal_id, org, env: customer.env, }); diff --git a/server/src/internal/customers/handlers/handleCreateCustomer.ts b/server/src/internal/customers/handlers/handleCreateCustomer.ts index 97ddba755..2ff58cee5 100644 --- a/server/src/internal/customers/handlers/handleCreateCustomer.ts +++ b/server/src/internal/customers/handlers/handleCreateCustomer.ts @@ -1,20 +1,20 @@ -import { CusService } from "@/internal/customers/CusService.js"; -import RecaseError from "@/utils/errorUtils.js"; import { - AppEnv, - CreateCustomer, + type AppEnv, + type CreateCustomer, CreateCustomerSchema, - Customer, + type Customer, ErrCode, - FullProduct, - Organization, + type FullProduct, + type Organization, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; -import { notNullish } from "@/utils/genUtils.js"; -import { initProductInStripe } from "@/internal/products/productUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { initProductInStripe } from "@/internal/products/productUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { createNewCustomer } from "../cusUtils/createNewCustomer.js"; export const initStripeCusAndProducts = async ({ @@ -78,7 +78,7 @@ const handleIdIsNull = async ({ } // 2. Check if email already exists - let existingCustomers = await CusService.getByEmail({ + const existingCustomers = await CusService.getByEmail({ db, email: newCus.email, orgId: org.id, @@ -123,10 +123,12 @@ export const handleCreateCustomerWithId = async ({ }) => { const { db, org, env, logger } = req; - // 1. Get by ID - let existingCustomer = await CusService.get({ + if (!newCus.id) + throw new Error("Calling handleCreateCustomerWithId with id null"); + + const existingCustomer = await CusService.get({ db, - idOrInternalId: newCus.id!, + idOrInternalId: newCus.id, orgId: org.id, env, }); @@ -140,9 +142,9 @@ export const handleCreateCustomerWithId = async ({ // 2. Check if email exists if (notNullish(newCus.email) && newCus.email !== "") { - let cusWithEmail = await CusService.getByEmail({ + const cusWithEmail = await CusService.getByEmail({ db, - email: newCus.email!, + email: newCus.email, orgId: org.id, env, }); @@ -152,17 +154,17 @@ export const handleCreateCustomerWithId = async ({ `POST /customers, email ${newCus.email} and ID null found, updating ID to ${newCus.id} (org: ${org.slug})`, ); - let updatedCustomer = await CusService.update({ + const updatedCustomer = await CusService.update({ db, internalCusId: cusWithEmail[0].internal_id, update: { - id: newCus.id!, + id: newCus.id, name: newCus.name, fingerprint: newCus.fingerprint, }, }); - return updatedCustomer; + return updatedCustomer as Customer; } } @@ -186,7 +188,7 @@ export const handleCreateCustomer = async ({ const newCus = CreateCustomerSchema.parse(cusData); // 1. If no ID and email is not NULL - let createdCustomer; + let createdCustomer: Customer; if (newCus.id === null) { createdCustomer = await handleIdIsNull({ diff --git a/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts b/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts index f79ac870e..71fc1d977 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts @@ -1,10 +1,9 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { ErrCode, FullCustomer } from "@autumn/shared"; -import { CreateEntity } from "@autumn/shared"; -import { createEntityForCusProduct } from "./createEntityForCusProduct.js"; +import { type CreateEntity, ErrCode, type FullCustomer } from "@autumn/shared"; import { EntityService } from "@/internal/api/entities/EntityService.js"; +import RecaseError from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { constructEntity } from "../../entityUtils/entityUtils.js"; +import { createEntityForCusProduct } from "./createEntityForCusProduct.js"; export const autoCreateEntity = async ({ req, @@ -56,7 +55,7 @@ export const autoCreateEntity = async ({ }); } - let replaceEntity = await EntityService.getNull({ + const replaceEntity = await EntityService.getNull({ db, orgId: customer.org_id, env: customer.env, @@ -67,7 +66,7 @@ export const autoCreateEntity = async ({ if (replaceEntity) { return await EntityService.update({ db, - internalId: replaceEntity.internal_id!, + internalId: replaceEntity.internal_id, update: { id: entityId, name: entityData.name, @@ -75,7 +74,7 @@ export const autoCreateEntity = async ({ }); } else { try { - const result = await EntityService.insert({ + const results = await EntityService.insert({ db, data: [ constructEntity({ @@ -87,8 +86,10 @@ export const autoCreateEntity = async ({ }), ], }); + + return results; } catch (error: any) { - if (error.code == "23505") { + if (error.code === "23505") { return await EntityService.get({ db, id: entityId, diff --git a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts index 4e2c3a6ff..ed3bdf143 100644 --- a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts @@ -1,14 +1,14 @@ import { CusProductStatus, + customerProducts, ErrCode, MigrationJobStep, - Product, + type Product, } from "@autumn/shared"; -import { MigrationService } from "../MigrationService.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { customerProducts } from "@autumn/shared"; import { and, asc, eq, gt, inArray } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { MigrationService } from "../MigrationService.js"; const getAllCustomersOnProduct = async ({ db, @@ -22,7 +22,7 @@ const getAllCustomersOnProduct = async ({ let lastId: string | null = null; while (true) { - let data; + let data: any[] = []; try { data = await db.query.customerProducts.findMany({ where: and( @@ -49,7 +49,7 @@ const getAllCustomersOnProduct = async ({ if (!data || data.length === 0) break; - let filtered = data.reduce((acc: any[], curr: any) => { + const filtered = data.reduce((acc: any[], curr: any) => { const existingIndex = acc.findIndex( (item) => item.customer.id === curr.customer.id, ); @@ -76,12 +76,10 @@ export const getMigrationCustomers = async ({ db, migrationJobId, fromProduct, - logger, }: { db: DrizzleCli; migrationJobId: string; fromProduct: Product; - logger: any; }) => { await MigrationService.updateJob({ db, @@ -91,23 +89,19 @@ export const getMigrationCustomers = async ({ }, }); - let { cusProducts } = await getAllCustomersOnProduct({ + const { cusProducts } = await getAllCustomersOnProduct({ db, internalProductId: fromProduct.internal_id, }); - let totalCount = cusProducts.length; - let canceledCount = cusProducts.filter( + const totalCount = cusProducts.length; + const canceledCount = cusProducts.filter( (cusProd) => cusProd.canceled_at !== null, ).length; - let customCount = cusProducts.filter((cusProd) => cusProd.is_custom).length; - - let filteredCusProducts = cusProducts.filter( - (cusProd) => cusProd.canceled_at === null && !cusProd.is_custom, - ); - - let customers = filteredCusProducts.map((cusProd) => cusProd.customer); + const customCount = cusProducts.filter((cusProd) => cusProd.is_custom).length; + const filteredCusProducts = cusProducts.filter((cp) => !cp.is_custom); + const customers = filteredCusProducts.map((cusProd) => cusProd.customer); await MigrationService.updateJob({ db, diff --git a/server/src/internal/migrations/runMigrationTask.ts b/server/src/internal/migrations/runMigrationTask.ts index 600d87ef1..e33bb245b 100644 --- a/server/src/internal/migrations/runMigrationTask.ts +++ b/server/src/internal/migrations/runMigrationTask.ts @@ -1,11 +1,11 @@ -import { MigrationService } from "./MigrationService.js"; +/** biome-ignore-all lint/suspicious/noExplicitAny: ok */ +import { MigrationJobStep } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { FeatureService } from "../features/FeatureService.js"; import { ProductService } from "../products/ProductService.js"; - +import { MigrationService } from "./MigrationService.js"; import { getMigrationCustomers } from "./migrationSteps/getMigrationCustomers.js"; import { migrateCustomers } from "./migrationSteps/migrateCustomers.js"; -import { MigrationJobStep } from "@autumn/shared"; -import { FeatureService } from "../features/FeatureService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; export const runMigrationTask = async ({ db, @@ -26,10 +26,10 @@ export const runMigrationTask = async ({ id: migrationJobId, }); - let { org_id: orgId, env } = migrationJob; + const { org_id: orgId, env } = migrationJob; // Get from and to products - let [fromProduct, toProduct] = await Promise.all([ + const [fromProduct, toProduct] = await Promise.all([ ProductService.getFull({ db, idOrInternalId: migrationJob.from_internal_product_id, @@ -45,14 +45,13 @@ export const runMigrationTask = async ({ ]); // STEP 1: GET ALL CUSTOMERS AND INSERT INTO MIGRATIONS... - let customers = await getMigrationCustomers({ + const customers = await getMigrationCustomers({ db, migrationJobId, fromProduct, - logger, }); - let features = await FeatureService.list({ + const features = await FeatureService.list({ db, orgId, env, diff --git a/server/src/internal/products/entitlements/entitlementUtils.ts b/server/src/internal/products/entitlements/entitlementUtils.ts index 709765a56..53e694ca3 100644 --- a/server/src/internal/products/entitlements/entitlementUtils.ts +++ b/server/src/internal/products/entitlements/entitlementUtils.ts @@ -1,24 +1,23 @@ -import RecaseError from "@/utils/errorUtils.js"; +/** biome-ignore-all lint/suspicious/noDoubleEquals: != allowed for comparison... */ import { - EntInterval, - FreeTrial, - Entitlement, AllowanceType, - EntitlementWithFeature, - FeatureType, - Feature, + EntInterval, + type Entitlement, + type EntitlementWithFeature, ErrCode, - UsagePriceConfig, + type Feature, + FeatureType, + type FreeTrial, + type FullEntitlement, + type FullProduct, + type Price, PriceType, - Price, - FullProduct, - FullEntitlement, - Rollover, - RolloverConfig, + type RolloverConfig, + type UsagePriceConfig, } from "@autumn/shared"; - import { addDays } from "date-fns"; +import RecaseError from "@/utils/errorUtils.js"; export const entIntervalToTrialDuration = ({ interval, @@ -113,23 +112,23 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => { return false; } // 3. Check if they have same interval - let diffs = { + const diffs = { interval: { - condition: ent1.interval !== ent2.interval, + condition: ent1.interval != ent2.interval, message: `Interval different: ${ent1.interval} !== ${ent2.interval}`, }, intervalCount: { - condition: ent1.interval_count !== ent2.interval_count, + condition: ent1.interval_count != ent2.interval_count, message: `Interval count different: ${ent1.interval_count} !== ${ent2.interval_count}`, }, allowance: { condition: ent1.allowance_type !== AllowanceType.Unlimited && - ent1.allowance !== ent2.allowance, + ent1.allowance != ent2.allowance, message: `Allowance different: ${ent1.allowance} !== ${ent2.allowance}`, }, carryFromPrevious: { - condition: ent1.carry_from_previous !== ent2.carry_from_previous, + condition: ent1.carry_from_previous != ent2.carry_from_previous, message: `Carry from previous different: ${ent1.carry_from_previous} !== ${ent2.carry_from_previous}`, }, entityFeatureId: { @@ -137,9 +136,10 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => { message: `Entity feature ID different: ${ent1.entity_feature_id} !== ${ent2.entity_feature_id}`, }, usageLimit: { - condition: ent1.usage_limit !== ent2.usage_limit, + condition: ent1.usage_limit != ent2.usage_limit, message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`, }, + rollover: { condition: !rolloversAreSame({ rollover1: ent1.rollover, @@ -149,7 +149,7 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => { }, }; - let entsAreDiff = Object.values(diffs).some((d) => d.condition); + const entsAreDiff = Object.values(diffs).some((d) => d.condition); if (entsAreDiff) { console.log("Entitlements different"); @@ -174,14 +174,14 @@ export const getEntRelatedPrice = ( return false; } - let config = price.config as UsagePriceConfig; + const config = price.config as UsagePriceConfig; if (allowFeatureMatch) { return entitlement.internal_feature_id == config.internal_feature_id; } - let entIdMatch = entitlement.id == price.entitlement_id; - let productIdMatch = + const entIdMatch = entitlement.id == price.entitlement_id; + const productIdMatch = entitlement.internal_product_id == price.internal_product_id; return entIdMatch && productIdMatch; }); @@ -204,7 +204,7 @@ export const getEntsWithFeature = ({ features: Feature[]; }) => { return ents.map((ent) => { - let feature = features.find( + const feature = features.find( (f) => f.internal_id === ent.internal_feature_id, ); if (!feature) { diff --git a/server/src/utils/genUtils.ts b/server/src/utils/genUtils.ts index 56eceb9a9..6e453309e 100644 --- a/server/src/utils/genUtils.ts +++ b/server/src/utils/genUtils.ts @@ -1,8 +1,8 @@ +import { UTCDate } from "@date-fns/utc"; import { format } from "date-fns"; import KSUID from "ksuid"; -import RecaseError from "./errorUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; -import { UTCDate } from "@date-fns/utc"; +import RecaseError from "./errorUtils.js"; export const generateId = (prefix: string) => { if (!prefix) { @@ -38,19 +38,23 @@ export const keyToTitle = (key: string) => { .replace(/\b\w/g, (char) => char.toUpperCase()); }; -export const notNullOrUndefined = (value: any) => { +export const notNullOrUndefined = ( + value: T | null | undefined, +): value is T => { return value !== null && value !== undefined; }; -export const nullOrUndefined = (value: any) => { +export const nullOrUndefined = (value: T | null | undefined): value is T => { return value === null || value === undefined; }; -export const nullish = (value: any) => { +export const nullish = ( + value: T | null | undefined, +): value is null | undefined => { return value === null || value === undefined; }; -export const notNullish = (value: any) => { +export const notNullish = (value: T | null | undefined): value is T => { return !nullish(value); }; @@ -128,7 +132,7 @@ export const slugify = ( ) => { return text .toLowerCase() - .replace(/ /g, type == "underscore" ? "_" : "-") + .replace(/ /g, type === "underscore" ? "_" : "-") .replace(/[^\w\s-]/g, ""); }; diff --git a/shared/utils/productDisplayUtils/getItemType.ts b/shared/utils/productDisplayUtils/getItemType.ts index 1e04d3b66..5de8647a8 100644 --- a/shared/utils/productDisplayUtils/getItemType.ts +++ b/shared/utils/productDisplayUtils/getItemType.ts @@ -1,5 +1,5 @@ import { - ProductItem, + type ProductItem, ProductItemType, } from "../../models/productV2Models/productItemModels/productItemModels.js"; import { notNullish, nullish } from "../utils.js"; @@ -7,7 +7,7 @@ import { notNullish, nullish } from "../utils.js"; export const isBooleanFeatureItem = (item: ProductItem) => { return ( notNullish(item.feature_id) && - (nullish(item.price) || item.price == 0) && + (nullish(item.price) || item.price === 0) && nullish(item.tiers) && nullish(item.interval) && nullish(item.included_usage) @@ -17,7 +17,7 @@ export const isBooleanFeatureItem = (item: ProductItem) => { export const isFeatureItem = (item: ProductItem) => { return ( notNullish(item.feature_id) && - (nullish(item.price) || item.price == 0) && + (nullish(item.price) || item.price === 0) && nullish(item.tiers) ); }; diff --git a/shared/utils/utils.ts b/shared/utils/utils.ts index 5f8b96b3a..79b840d35 100644 --- a/shared/utils/utils.ts +++ b/shared/utils/utils.ts @@ -1,2 +1,8 @@ -export const notNullish = (value: any) => value !== null && value !== undefined; -export const nullish = (value: any) => value === null || value === undefined; +export const nullish = ( + value: T | null | undefined, +): value is null | undefined => { + return value === null || value === undefined; +}; + +export const notNullish = (value: T | null | undefined): value is T => + value !== null && value !== undefined;