From 25c5ac39cd7c045dacb4e6613e8d2e88309ed4e1 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 9 Sep 2025 10:20:33 -0700 Subject: [PATCH] feat: sending allowance_used threshold type --- .../external/autumn/autumnWebhookRouter.ts | 25 ++- .../attachParams/getAttachParams.ts | 1 - server/src/trigger/handleThresholdReached.ts | 156 +++++++++++++++++- 3 files changed, 172 insertions(+), 10 deletions(-) diff --git a/server/src/external/autumn/autumnWebhookRouter.ts b/server/src/external/autumn/autumnWebhookRouter.ts index 9b6f146f6..5bb0cd24c 100644 --- a/server/src/external/autumn/autumnWebhookRouter.ts +++ b/server/src/external/autumn/autumnWebhookRouter.ts @@ -1,4 +1,5 @@ -import { WebhookEventType } from "@autumn/shared"; +import RecaseError from "@/utils/errorUtils.js"; +import { ErrCode, WebhookEventType } from "@autumn/shared"; import express, { Router } from "express"; import { Webhook } from "svix"; @@ -15,11 +16,15 @@ const verifyAutumnWebhook = async (req: any, res: any) => { const svix_signature = headers["svix-signature"]; if (!svix_id || !svix_timestamp || !svix_signature) { - res.status(400).json({ - success: false, + throw new RecaseError({ message: "Error: Missing svix headers", + code: ErrCode.InvalidInputs, }); - return; + // res.status(400).json({ + // success: false, + // message: "Error: Missing svix headers", + // }); + // return; } let evt: any; @@ -30,12 +35,16 @@ const verifyAutumnWebhook = async (req: any, res: any) => { "svix-signature": svix_signature as string, }); } catch (err) { - console.log("Error: Could not verify webhook"); - res.status(400).json({ - success: false, + throw new RecaseError({ message: "Error: Could not verify webhook", + code: ErrCode.InvalidInputs, }); - return; + // console.log("Error: Could not verify webhook"); + // res.status(400).json({ + // success: false, + // message: "Error: Could not verify webhook", + // }); + // return; } return evt; diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts index 0e3abd259..c977573a8 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts @@ -5,7 +5,6 @@ import { orgToVersion } from "@/utils/versionUtils.js"; import { APIVersion } from "@autumn/shared"; import { AttachParams } from "../../../cusProducts/AttachParams.js"; import { nullish } from "@/utils/genUtils.js"; -import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; export const getAttachParams = async ({ req, diff --git a/server/src/trigger/handleThresholdReached.ts b/server/src/trigger/handleThresholdReached.ts index c6265fb3b..3d79e5aff 100644 --- a/server/src/trigger/handleThresholdReached.ts +++ b/server/src/trigger/handleThresholdReached.ts @@ -1,5 +1,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { sendSvixEvent } from "@/external/svix/svixHelpers.js"; +import { EntityService } from "@/internal/api/entities/EntityService.js"; import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js"; import { getV2CheckResponse } from "@/internal/api/entitled/checkUtils/getV2CheckResponse.js"; import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js"; @@ -38,6 +39,139 @@ export const mergeNewCusEntsIntoCusProducts = ({ return cusProducts; }; +export const sendSvixThresholdReachedEvent = async ({ + db, + org, + env, + features, + logger, + feature, + fullCus, + thresholdType, +}: { + db: DrizzleCli; + org: Organization; + env: AppEnv; + features: Feature[]; + logger: any; + feature: Feature; + fullCus: FullCustomer; + thresholdType: "limit_reached" | "allowance_used"; +}) => { + const cusDetails = await getCustomerDetails({ + db, + customer: fullCus, + org, + env, + features, + logger, + cusProducts: fullCus.customer_products, + expand: [], + }); + + if (fullCus.entity) { + const entities = await EntityService.list({ + db, + internalCustomerId: fullCus.internal_id, + }); + fullCus.entities = entities; + await getSingleEntityResponse({ + org, + env, + features, + fullCus, + entityId: fullCus.entity.id, + }); + } + + await sendSvixEvent({ + org: org, + env: env, + eventType: WebhookEventType.CustomerThresholdReached, + data: { + threshold_type: thresholdType, + customer: cusDetails, + feature: toAPIFeature({ feature }), + }, + }); + + logger.info(`Sent Svix event for threshold reached (type: ${thresholdType})`); + return; +}; + +export const handleAllowanceUsed = async ({ + db, + org, + env, + features, + logger, + cusEnts, + newCusEnts, + feature, + fullCus, +}: { + db: DrizzleCli; + org: Organization; + env: AppEnv; + cusEnts: FullCusEntWithFullCusProduct[]; + newCusEnts: FullCusEntWithFullCusProduct[]; + feature: Feature; + fullCus: FullCustomer; + features: Feature[]; + logger: any; +}) => { + // Allowance used... + // Make sure overage allowed is false + for (const cusEnt of cusEnts) { + cusEnt.usage_allowed = false; + } + + for (const cusEnt of newCusEnts) { + cusEnt.usage_allowed = false; + } + + const prevCheckResponse = await getV2CheckResponse({ + fullCus, + cusEnts, + creditSystems: [], + feature, + org, + cusProducts: fullCus.customer_products, + apiVersion: APIVersion.v1_2, + }); + + const v2CheckResponse = await getV2CheckResponse({ + fullCus, + cusEnts: newCusEnts, + creditSystems: [], + feature, + org, + cusProducts: fullCus.customer_products, + apiVersion: APIVersion.v1_2, + }); + + // console.log(`Handling allowance used for feature: ${feature.id}`); + // console.log( + // `Prev: allowed (${prevCheckResponse.allowed}), balance (${prevCheckResponse.balance})` + // ); + // console.log( + // `Current: allowed (${v2CheckResponse.allowed}), balance (${v2CheckResponse.balance})` + // ); + + if (prevCheckResponse.allowed === true && v2CheckResponse.allowed === false) { + await sendSvixThresholdReachedEvent({ + db, + org, + env, + features, + logger, + feature, + fullCus, + thresholdType: "allowance_used", + }); + } +}; + export const handleThresholdReached = async ({ db, feature, @@ -104,6 +238,11 @@ export const handleThresholdReached = async ({ }); if (fullCus.entity) { + const entities = await EntityService.list({ + db, + internalCustomerId: fullCus.internal_id, + }); + fullCus.entities = entities; await getSingleEntityResponse({ org, env, @@ -124,8 +263,23 @@ export const handleThresholdReached = async ({ }, }); - logger.info("Sent Svix event for threshold reached"); + logger.info( + "Sent Svix event for threshold reached (type: limit_reached)" + ); + return; } + await handleAllowanceUsed({ + db, + org, + env, + features, + logger, + cusEnts, + newCusEnts, + feature, + fullCus, + }); + return; } catch (error: any) { logger.error("Failed to handle threshold reached", { error,