From 9ab33fb268fe9b1113396e0b1469da9f28ca985b Mon Sep 17 00:00:00 2001 From: John Yeo Date: Fri, 12 Sep 2025 16:17:59 -0700 Subject: [PATCH] fix: entity and customer.products.updated webhooks --- .../external/autumn/autumnWebhookRouter.ts | 29 +++- .../priceToUsageInAdvance.ts | 9 +- .../handleCheckoutCompleted.ts | 1 + .../getOptionsFromCheckout.ts | 8 +- .../handleSchedulePhaseCompleted.ts | 5 +- .../handleSubUpdated/handleSubCanceled.ts | 6 +- .../handleSubUpdated/handleSubRenewed.ts | 7 + .../handlers/handleProductsUpdated.ts | 42 +++--- .../analytics/runActionHandlerTask.ts | 8 +- .../internal/api/entities/getEntityUtils.ts | 33 +++-- .../add-product/createFullCusProduct.ts | 1 + .../addProductFlow/handlePaidProduct.ts | 2 + .../attachFunctions/handleRenewProduct.ts | 29 +++- .../scheduleFlow/handleScheduleFlow2.ts | 33 ++++- .../upgradeFlow/handleUpgradeFlow.ts | 1 + .../attach/checkout/handleCheckout.ts | 2 - .../customers/cusCache/subCacheUtils.ts | 21 +++ .../cusProducts/cusEnts/cusEntUtils.ts | 126 ----------------- .../cusEnts/cusEntUtils/getExistingUsage.ts | 2 +- .../cusProducts/cusEnts/entBalanceUtils.ts | 16 +-- .../cusFeatureResponseUtils/getCusBalances.ts | 2 +- .../getCusProductResponse.ts | 15 +- .../processFullCusProducts.ts | 6 +- .../customers/cusUtils/getCustomerDetails.ts | 1 - .../handlers/handleCreateBillingPortal.ts | 128 ++++++++++++++---- .../handlers/handleTransferProduct.ts | 18 ++- .../handleDecreaseAndTransfer.ts | 2 +- .../handlers/handleUpdateBalances.ts | 6 +- .../handlers/handleUpdateEntitlement.ts | 5 +- server/src/trigger/handleThresholdReached.ts | 12 +- shared/utils/cusEntUtils/balanceUtils.ts | 71 ++++++++++ shared/utils/index.ts | 3 + .../entitlements/UpdateCusEntitlement.tsx | 44 ++++-- 33 files changed, 432 insertions(+), 262 deletions(-) create mode 100644 server/src/internal/customers/cusCache/subCacheUtils.ts create mode 100644 shared/utils/cusEntUtils/balanceUtils.ts diff --git a/server/src/external/autumn/autumnWebhookRouter.ts b/server/src/external/autumn/autumnWebhookRouter.ts index 5bb0cd24c..e3c6dc90d 100644 --- a/server/src/external/autumn/autumnWebhookRouter.ts +++ b/server/src/external/autumn/autumnWebhookRouter.ts @@ -61,9 +61,36 @@ autumnWebhookRouter.post( switch (type) { case WebhookEventType.CustomerProductsUpdated: + console.log(`--------------------------------`); + // console.log(`Received customer.products.updated webhook`); + // console.log(JSON.stringify(data, null, 2)); console.log( - `Type: ${type}, Scenario: ${data?.scenario}, Product: ${data?.updated_product?.id}` + `Customer:`, + data?.customer.id, + `Products:`, + data?.customer.products.map((p: any) => ({ + id: p.id, + entity_id: p.entity_id, + status: p.status, + quantity: p.quantity, + })) ); + + if (data?.entity) { + console.log( + `Entity: ${data.entity.id}, Products:`, + data.entity.products.map((p: any) => ({ + id: p.id, + status: p.status, + quantity: p.quantity, + })) + ); + } + + console.log( + `Update product ID: ${data?.updated_product?.id}, Scenario: ${data?.scenario}` + ); + console.log(`--------------------------------`); break; case WebhookEventType.CustomerThresholdReached: console.log(`Type: ${type}`); diff --git a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts index 2794f4103..b50dfe932 100644 --- a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts +++ b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts @@ -20,11 +20,12 @@ export const priceToOneOffAndTiered = ({ }) => { const config = price.config as UsagePriceConfig; let quantity = options?.quantity!; - let overage = quantity * config.billing_units! - relatedEnt.allowance!; + let overage = new Decimal(quantity).mul(config.billing_units!).toNumber(); + // let overage = quantity * config.billing_units! - relatedEnt.allowance!; - if (overage <= 0) { - return null; - } + // if (overage <= 0) { + // return null; + // } const amount = getPriceForOverage(price, overage); if (!config.stripe_product_id) { diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index f76c66aa4..5bdbedb17 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -125,6 +125,7 @@ export const handleCheckoutSessionCompleted = async ({ const anchorToUnix = checkoutSub ? getEarliestPeriodEnd({ sub: checkoutSub! }) * 1000 : undefined; + if (attachParams.productsList) { console.log("Inserting products list"); for (const productOptions of attachParams.productsList) { diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts index dee8b52b6..de863259a 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts @@ -1,5 +1,6 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { + formatPrice, getBillingType, getPriceEntitlement, priceIsOneOffAndTiered, @@ -32,9 +33,7 @@ export const getOptionsFromCheckoutSession = async ({ for (const price of prices) { let config = price.config as UsagePriceConfig; - if (getBillingType(config) != BillingType.UsageInAdvance) { - continue; - } + if (getBillingType(config) != BillingType.UsageInAdvance) continue; const lineItem = findStripeItemForPrice({ price, @@ -47,7 +46,8 @@ export const getOptionsFromCheckoutSession = async ({ let relatedEnt = getPriceEntitlement(price, ents); if (priceIsOneOffAndTiered(price, relatedEnt)) { - quantity = (lineItem.quantity || 0) + (relatedEnt.allowance || 0); + // quantity = lineItem.quantity || 0; + continue; } else { quantity = lineItem.quantity || 0; } diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts index 5f02029a8..b16e73320 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts @@ -121,11 +121,10 @@ export const handleSchedulePhaseCompleted = async ({ scheduled_ids: [], }, }); - } catch (error) { + } catch (error: any) { logger.error( - `schedule.phase.completed: failed to cancel schedule ${schedule.id}` + `schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}` ); - logger.error({ error }); } } // const currentPhase = schedule.phases.find( diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts index 94900bc0e..82c66fe5d 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts @@ -102,9 +102,7 @@ export const handleSubCanceled = async ({ const { db, env, logtail: logger } = req; - if (!canceledFromPortal || updatedCusProducts.length == 0) { - return; - } + if (!canceledFromPortal || updatedCusProducts.length == 0) return; await updateCusProductCanceled({ db, @@ -194,6 +192,8 @@ export const handleSubCanceled = async ({ } for (let cusProd of updatedCusProducts) { + console.log("Sending webhook for canceled product: ", cusProd.product.id); + try { await addProductsUpdatedWebhookTask({ req, diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts index df91bd565..0def51dbe 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts @@ -7,6 +7,7 @@ import { AttachScenario, FullCusProduct } from "@autumn/shared"; import Stripe from "stripe"; import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js"; const isSubRenewed = ({ previousAttributes, sub, @@ -70,12 +71,18 @@ export const handleSubRenewed = async ({ if (!renewed || updatedCusProducts.length == 0) return; + const subScenario = await getSubScenarioFromCache({ subId: sub.id }); + console.log(`Renewed: ${renewed}, subScenario: ${subScenario}`); + if (subScenario === AttachScenario.Renew) return; + const customer = updatedCusProducts[0].customer; let cusProducts = await CusProductService.list({ db, internalCustomerId: customer!.internal_id, }); + console.log(`handling sub.renewed!`); + if (isMultiProductSub({ sub, cusProducts }) || sub.schedule) return; await CusProductService.updateByStripeSubId({ diff --git a/server/src/internal/analytics/handlers/handleProductsUpdated.ts b/server/src/internal/analytics/handlers/handleProductsUpdated.ts index ba10f4967..6098162b5 100644 --- a/server/src/internal/analytics/handlers/handleProductsUpdated.ts +++ b/server/src/internal/analytics/handlers/handleProductsUpdated.ts @@ -2,9 +2,9 @@ import { ActionType, AppEnv, AuthType, - CusProductStatus, FullCusProduct, FullProduct, + notNullish, Organization, } from "@autumn/shared"; @@ -23,7 +23,8 @@ import { ActionService } from "@/internal/analytics/ActionService.js"; import { constructAction } from "@/internal/analytics/actionUtils.js"; import { parseReqForAction } from "@/internal/analytics/actionUtils.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; -import { cusProductToPrices, cusProductToProduct } from "@autumn/shared"; +import { cusProductToProduct } from "@autumn/shared"; +import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js"; interface ActionDetails { request_id: string; @@ -121,19 +122,8 @@ export const handleProductsUpdated = async ({ // Product: let product = cusProduct.product; - // const prices = cusProductToPrices({ cusProduct }); - // const ents = cusProductToEnts({ cusProduct }); - // let freeTrial = cusProduct.free_trial; - let fullProduct: FullProduct = cusProductToProduct({ cusProduct }); - // { - // ...product, - // prices, - // entitlements: ents, - // free_trial: freeTrial || null, - // }; - let customer = await CusService.getFull({ db, idOrInternalId: data.customerId || data.internalCustomerId, @@ -165,9 +155,6 @@ export const handleProductsUpdated = async ({ features, }); - // 1. Log action to DB - - // console.log(`handling products.updated for customer ${customer.id}`); try { if (req) { let action = constructAction({ @@ -205,6 +192,28 @@ export const handleProductsUpdated = async ({ } } + let entityRes = null; + if (notNullish(customer?.entity)) { + entityRes = await getSingleEntityResponse({ + entityId: customer.entity!.id, + org, + env, + fullCus: customer, + entity: customer.entity!, + features, + }); + } + + // console.log(`Sending svix event for customer ${customer.id}`); + // console.log( + // "Products:", + // cusDetails.products.map((p) => ({ + // id: p.id, + // status: p.status, + // quantity: p.quantity, + // })) + // ); + // 2. Send Svix event await sendSvixEvent({ org, @@ -213,6 +222,7 @@ export const handleProductsUpdated = async ({ data: { scenario, customer: cusDetails, + entity: entityRes, updated_product: productRes, }, }); diff --git a/server/src/internal/analytics/runActionHandlerTask.ts b/server/src/internal/analytics/runActionHandlerTask.ts index eb2528b45..619b3010c 100644 --- a/server/src/internal/analytics/runActionHandlerTask.ts +++ b/server/src/internal/analytics/runActionHandlerTask.ts @@ -43,11 +43,11 @@ export const runActionHandlerTask = async ({ }); break; } - } catch (error) { + } catch (error: any) { logger.error("Error processing action handler job:", { - jobName: job.name, - error, - payload, + // jobName: job.name, + // payload, + message: error.message, }); } finally { await releaseLock({ lockKey, useBackup }); diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index be129258f..a6539a8f0 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -1,10 +1,5 @@ import type { DrizzleCli } from "@/db/initDrizzle.js"; import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js"; -import { - ACTIVE_STATUSES, - RELEVANT_STATUSES, -} from "@/internal/customers/cusProducts/CusProductService.js"; -import { CusService } from "@/internal/customers/CusService.js"; import { getCusFeaturesResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.js"; import { processFullCusProducts } from "@/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.js"; @@ -13,7 +8,6 @@ import { nullish } from "@/utils/genUtils.js"; import { type AppEnv, Feature, - CusProductStatus, type Entity, EntityExpand, type EntityResponse, @@ -32,6 +26,7 @@ export const getSingleEntityResponse = async ({ org, env, fullCus, + entity, features, withAutumnId = false, }: { @@ -39,13 +34,10 @@ export const getSingleEntityResponse = async ({ org: Organization; env: AppEnv; fullCus: FullCustomer; + entity: Entity; features: Feature[]; withAutumnId?: boolean; }) => { - let entity = fullCus.entities.find( - (e: Entity) => e.id == entityId || e.internal_id == entityId - ); - const apiVersion = APIVersion.v1_2; if (!entity) { @@ -80,7 +72,7 @@ export const getSingleEntityResponse = async ({ let { main, addOns } = await processFullCusProducts({ fullCusProducts: entityCusProducts, - entities: fullCus.entities, + entity, subs: entitySubs, org, apiVersion: APIVersion.v1_2, @@ -136,7 +128,7 @@ export const getEntityResponse = async ({ logger: any; skipCache?: boolean; }) => { - let customer = await getCusWithCache({ + let fullCus = await getCusWithCache({ db, idOrInternalId: customerId, org, @@ -147,7 +139,7 @@ export const getEntityResponse = async ({ skipCache, }); - if (!customer) { + if (!fullCus) { throw new RecaseError({ message: `Customer ${customerId} not found`, code: ErrCode.CustomerNotFound, @@ -157,11 +149,18 @@ export const getEntityResponse = async ({ const entityResponses: EntityResponse[] = []; for (const entityId of entityIds) { + const entity = fullCus.entities.find( + (e: Entity) => e.id == entityId || e.internal_id == entityId + ); + + if (!entity) continue; + let entityResponse = await getSingleEntityResponse({ entityId, org, env, - fullCus: customer, + fullCus, + entity, features, withAutumnId, }); @@ -171,8 +170,8 @@ export const getEntityResponse = async ({ return { entities: entityResponses, - customer, - fullEntities: customer.entities, - invoices: customer.invoices, + customer: fullCus, + fullEntities: fullCus.entities, + invoices: fullCus.invoices, }; }; diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts index a7b4bca95..8ec607581 100644 --- a/server/src/internal/customers/add-product/createFullCusProduct.ts +++ b/server/src/internal/customers/add-product/createFullCusProduct.ts @@ -399,6 +399,7 @@ export const createFullCusProduct = async ({ } // 3. Deduct existing usages + let deductedCusEnts = addExistingUsagesToCusEnts({ cusEnts: cusEnts, entitlements: entitlements, diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts index b7af249fc..c99a70dfb 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts @@ -80,6 +80,7 @@ export const handlePaidProduct = async ({ let trialEndsAt = undefined; // 1. If merge sub + if (mergeSub && !config.disableMerge) { if (mergeCusProduct?.free_trial) { trialEndsAt = isTrialing({ @@ -152,6 +153,7 @@ export const handlePaidProduct = async ({ billingCycleAnchorUnix = attachParams.billingAnchor; } + // console.log("Item set: ", itemSet); try { sub = await createStripeSub2({ db: req.db, diff --git a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts index a6764f036..fafa0eec3 100644 --- a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts @@ -9,7 +9,7 @@ import { paramsToCurSub, } from "../attachUtils/convertAttachParams.js"; import { paramsToScheduleItems } from "../mergeUtils/paramsToScheduleItems.js"; -import { AttachConfig, SuccessCode } from "@autumn/shared"; +import { AttachConfig, AttachScenario, SuccessCode } from "@autumn/shared"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { cusProductToSchedule, @@ -19,6 +19,8 @@ import { subToNewSchedule } from "../mergeUtils/subToNewSchedule.js"; import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { updateCurSchedule } from "../mergeUtils/updateCurSchedule.js"; import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { addSubIdToCache } from "../../cusCache/subCacheUtils.js"; export const handleRenewProduct = async ({ req, @@ -33,9 +35,9 @@ export const handleRenewProduct = async ({ }) => { const logger = req.logtail; const { stripeCli, customer: fullCus } = attachParams; + const { curScheduledProduct } = attachParamToCusProducts({ attachParams }); const curCusProduct = attachParamsToCurCusProduct({ attachParams }); - const { curScheduledProduct } = attachParamToCusProducts({ attachParams }); const product = attachParams.products[0]; const cusProducts = attachParams.customer.customer_products; @@ -91,6 +93,11 @@ export const handleRenewProduct = async ({ } if (curSubId) { + // Add sub id to upstash + await addSubIdToCache({ + subId: curSubId, + scenario: AttachScenario.Renew, + }); await stripeCli.subscriptions.update(curSubId, { cancel_at: null, }); @@ -193,6 +200,24 @@ export const handleRenewProduct = async ({ } } + if (curCusProduct) { + try { + await addProductsUpdatedWebhookTask({ + req, + internalCustomerId: curCusProduct.internal_customer_id, + org: attachParams.org, + env: attachParams.customer.env, + customerId: + attachParams.customer.id || attachParams.customer.internal_id, + scenario: AttachScenario.Renew, + cusProduct: curCusProduct, + logger, + }); + } catch (error) { + logger.error("RENEW FLOW: failed to add to webhook queue", { error }); + } + } + if (curScheduledProduct) { await CusProductService.delete({ db: req.db, diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts index 59363f826..1fa90b73d 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts @@ -26,6 +26,7 @@ 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, @@ -138,6 +139,9 @@ export const handleScheduleFunction2 = async ({ logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`); await stripeCli.subscriptions.update(curSub!.id, { cancel_at: expectedEnd!, + cancellation_details: { + comment: "autumn_downgrade", + }, }); } @@ -150,13 +154,36 @@ export const handleScheduleFunction2 = async ({ nextResetAt: expectedEnd! * 1000, disableFreeTrial: true, isDowngrade: true, - scenario: newProductFree - ? AttachScenario.Cancel - : AttachScenario.Downgrade, + sendWebhook: false, + // scenario: newProductFree + // ? AttachScenario.Cancel + // : AttachScenario.Downgrade, logger, }); } + if (curCusProduct) { + try { + await addProductsUpdatedWebhookTask({ + req, + internalCustomerId: curCusProduct.internal_customer_id, + org: attachParams.org, + env: attachParams.customer.env, + customerId: + attachParams.customer.id || attachParams.customer.internal_id, + + scenario: isFreeProduct(attachParams.prices) + ? AttachScenario.Cancel + : AttachScenario.Downgrade, + + cusProduct: curCusProduct, + logger, + }); + } catch (error) { + logger.error("SCHEDULE FLOW: failed to add to webhook queue", { error }); + } + } + let apiVersion = attachParams.apiVersion || APIVersion.v1; if (res) { diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index 756f63a6d..0dffd8faf 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -207,6 +207,7 @@ export const handleUpgradeFlow = async ({ carryExistingUsages: config.carryUsage, carryOverTrial: config.carryTrial, anchorToUnix: anchorToUnix, + scenario: AttachScenario.Upgrade, logger, }); } diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts index c5a5c0243..410fd1764 100644 --- a/server/src/internal/customers/attach/checkout/handleCheckout.ts +++ b/server/src/internal/customers/attach/checkout/handleCheckout.ts @@ -184,8 +184,6 @@ export const handleCheckout = (req: any, res: any) => if (checkoutRes.next_cycle) { const nextCycle = checkoutRes.next_cycle; - console.log("Due at:", formatUnixToDate(nextCycle.starts_at!)); - console.log("Total:", nextCycle.total); } res.status(200).json({ diff --git a/server/src/internal/customers/cusCache/subCacheUtils.ts b/server/src/internal/customers/cusCache/subCacheUtils.ts new file mode 100644 index 000000000..918c4fcda --- /dev/null +++ b/server/src/internal/customers/cusCache/subCacheUtils.ts @@ -0,0 +1,21 @@ +import { initUpstash } from "./upstashUtils.js"; + +export const addSubIdToCache = async ({ + subId, + scenario, +}: { + subId: string; + scenario: string; +}) => { + const upstash = await initUpstash(); + if (!upstash) return; + + await upstash.set(`sub:${subId}`, scenario, { + ex: 180, // 3 minutes + }); +}; +export const getSubScenarioFromCache = async ({ subId }: { subId: string }) => { + const upstash = await initUpstash(); + if (!upstash) return null; + return (await upstash.get(`sub:${subId}`)) as string | null; +}; diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts index bcf0ed9ff..ffb460abc 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts @@ -119,132 +119,6 @@ export const getCusEntBalance = ({ }; }; -// export const sortCusEntsForDeduction = ( -// cusEnts: (FullCustomerEntitlement & { -// customer_product?: FullCusProduct; -// })[], -// reverseOrder: boolean = false -// ) => { -// let intervalOrder: Record = { -// [EntInterval.Minute]: 0, // 1 minute -// [EntInterval.Hour]: 1, // 1 hour -// [EntInterval.Day]: 2, // 1 day -// [EntInterval.Week]: 3, // 1 week -// [EntInterval.Month]: 4, // 1 month -// [EntInterval.Quarter]: 5, // 3 months -// [EntInterval.Year]: 6, // 1 year -// [EntInterval.SemiAnnual]: 7, // 6 months -// [EntInterval.Lifetime]: 8, // 1 time -// }; - -// // console.log( -// // `Cus ents before (${reverseOrder ? "reversed" : "normal"})`, -// // cusEnts.map( -// // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}` -// // ) -// // ); -// cusEnts.sort((a, b) => { -// const aEnt = a.entitlement; -// const bEnt = b.entitlement; - -// // 1. If boolean, go first -// if (aEnt.feature.type == FeatureType.Boolean) { -// return -1; -// } - -// if (bEnt.feature.type == FeatureType.Boolean) { -// return 1; -// } - -// // 1. If a is credit system and b is not, a should go last -// if ( -// aEnt.feature.type == FeatureType.CreditSystem && -// bEnt.feature.type != FeatureType.CreditSystem -// ) { -// return 1; -// } - -// // 2. If a is not credit system and b is, a should go first -// if ( -// aEnt.feature.type != FeatureType.CreditSystem && -// bEnt.feature.type == FeatureType.CreditSystem -// ) { -// return -1; -// } - -// // 2. Sort by unlimited (unlimited goes first) -// if ( -// aEnt.allowance_type == AllowanceType.Unlimited && -// bEnt.allowance_type != AllowanceType.Unlimited -// ) { -// return -1; -// } - -// if ( -// aEnt.allowance_type != AllowanceType.Unlimited && -// bEnt.allowance_type == AllowanceType.Unlimited -// ) { -// return 1; -// } - -// // If one has usage_allowed, it should go last -// if (!a.usage_allowed && b.usage_allowed) { -// return -1; -// } - -// if (!b.usage_allowed && a.usage_allowed) { -// return 1; -// } - -// // If one has a next_reset_at, it should go first -// let nextResetFirst = reverseOrder ? 1 : -1; - -// if (a.next_reset_at && !b.next_reset_at) { -// return nextResetFirst; -// } - -// // If b has a next_reset_at, it should go first -// if (!a.next_reset_at && b.next_reset_at) { -// return -nextResetFirst; -// } - -// // 3. Sort by interval -// let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count); -// let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count); -// if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) { -// if (reverseOrder) { -// return bVal.sub(aVal).toNumber(); -// // return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval]; -// } else { -// return aVal.sub(bVal).toNumber(); -// // return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval]; -// } -// } - -// // Check if a is main product -// let aIsAddOn = a.customer_product?.product?.is_add_on; -// let bIsAddOn = b.customer_product?.product?.is_add_on; - -// if (aIsAddOn && !bIsAddOn) { -// return 1; -// } - -// if (!aIsAddOn && bIsAddOn) { -// return -1; -// } - -// // 4. Sort by created_at -// return a.created_at - b.created_at; -// }); - -// // console.log( -// // `Cus ents after (${reverseOrder ? "reversed" : "normal"})`, -// // cusEnts.map( -// // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}` -// // ) -// // ); -// }; - // Get related cusPrice export const getRelatedCusPrice = ( cusEnt: FullCustomerEntitlement, diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts index 975f0f3c8..8f00ac66c 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts @@ -221,7 +221,7 @@ export const addExistingUsagesToCusEnts = ({ for (const cusEnt of fullCusEnts) { let ent = cusEnt.entitlement; - // let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; + let fromEntities = existingUsages[key].fromEntities; // if (cusEntKey !== key) continue; diff --git a/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts index bdab7db6d..a18fb668e 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts @@ -1,7 +1,6 @@ import RecaseError from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; -import { ErrCode, FullCustomerEntitlement } from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; +import { FullCustomerEntitlement } from "@autumn/shared"; export const getEntityBalance = ({ cusEnt, @@ -13,14 +12,7 @@ export const getEntityBalance = ({ let entityBalance = cusEnt.entities?.[entityId!]?.balance; let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0; - if (nullish(entityBalance)) { - return { balance: 0, adjustment: 0 }; - // throw new RecaseError({ - // message: `Entity balance not found for entityId: ${entityId}`, - // code: ErrCode.EntityBalanceNotFound, - // statusCode: StatusCodes.BAD_REQUEST, - // }); - } + if (nullish(entityBalance)) return { balance: 0, adjustment: 0 }; return { balance: entityBalance, @@ -45,11 +37,11 @@ export const getSummedEntityBalances = ({ return { balance: Object.values(cusEnt.entities!).reduce( (acc, curr) => acc + curr.balance, - 0, + 0 ), adjustment: Object.values(cusEnt.entities!).reduce( (acc, curr) => acc + curr.adjustment, - 0, + 0 ), unused: 0, count: Object.values(cusEnt.entities!).length, diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts index dc123ec3f..e8acd526d 100644 --- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts +++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts @@ -15,11 +15,11 @@ import { notNullish, notNullOrUndefined } from "@/utils/genUtils.js"; import { BREAK_API_VERSION } from "@/utils/constants.js"; import { - getCusEntBalance, getRelatedCusPrice, getResetBalance, getUnlimitedAndUsageAllowed, } from "../../cusProducts/cusEnts/cusEntUtils.js"; +import { getCusEntBalance } from "@autumn/shared"; export interface CusFeatureBalance { feature_id: string; diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts index e055b3430..184aeb7e5 100644 --- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts +++ b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts @@ -41,16 +41,16 @@ export const getCusProductResponse = async ({ cusProduct, subs, org, - entities = [], apiVersion, features, + entity, }: { cusProduct: FullCusProduct; org: Organization; subs?: Subscription[]; - entities?: Entity[]; apiVersion: number; features: Feature[]; + entity?: Entity; }) => { // Process prices @@ -184,11 +184,12 @@ export const getCusProductResponse = async ({ // stripe_subscription_ids: cusProduct.subscription_ids || [], started_at: cusProduct.starts_at, - entity_id: cusProduct.internal_entity_id - ? entities?.find( - (e: Entity) => e.internal_id == cusProduct.internal_entity_id - )?.id - : cusProduct.entity_id || undefined, + entity_id: entity?.id || cusProduct.entity_id || undefined, + // entity_id: cusProduct.internal_entity_id + // ? entities?.find( + // (e: Entity) => e.internal_id == cusProduct.internal_entity_id + // )?.id + // : cusProduct.entity_id || undefined, ...stripeSubData, items: v2Product.items, diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts index 4a17db42f..99c062b5c 100644 --- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts +++ b/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts @@ -48,14 +48,14 @@ export const processFullCusProducts = async ({ fullCusProducts, subs, org, - entities = [], + entity, apiVersion, features, }: { fullCusProducts: any; subs: any; org: Organization; - entities?: Entity[]; + entity?: Entity; apiVersion: number; features: Feature[]; }) => { @@ -67,7 +67,7 @@ export const processFullCusProducts = async ({ cusProduct, subs, org, - entities, + entity, apiVersion, features, }); diff --git a/server/src/internal/customers/cusUtils/getCustomerDetails.ts b/server/src/internal/customers/cusUtils/getCustomerDetails.ts index cf74533ea..e99339f11 100644 --- a/server/src/internal/customers/cusUtils/getCustomerDetails.ts +++ b/server/src/internal/customers/cusUtils/getCustomerDetails.ts @@ -82,7 +82,6 @@ export const getCustomerDetails = async ({ subs, org, apiVersion, - entities: customer.entities, features, }); diff --git a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts index eccf88d20..160402a31 100644 --- a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts +++ b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts @@ -7,6 +7,40 @@ import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { ErrCode } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +import Stripe from "stripe"; + +const createDefaultBillingPortalConfiguration = async (stripeCli: Stripe) => { + try { + const configuration = await stripeCli.billingPortal.configurations.create({ + features: { + customer_update: { + allowed_updates: ["email", "address"], + enabled: true, + }, + invoice_history: { + enabled: true, + }, + payment_method_update: { + enabled: true, + }, + subscription_cancel: { + enabled: true, + }, + }, + // business_profile: { + // privacy_policy_url: "https://example.com/privacy", + // terms_of_service_url: "https://example.com/terms", + // }, + }); + return configuration; + } catch (error: any) { + throw new RecaseError({ + message: `Failed to create billing portal configuration: ${error.message}`, + code: ErrCode.StripeError, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } +}; export const handleCreateBillingPortal = async (req: any, res: any) => routeHandler({ @@ -37,23 +71,19 @@ export const handleCreateBillingPortal = async (req: any, res: any) => const stripeCli = createStripeCli({ org, env: req.env }); + // Determine the Stripe customer ID to use + let stripeCustomerId: string; + if (!customer.processor?.id) { - let newCus; try { - newCus = await createStripeCusIfNotExists({ + const newCus = await createStripeCusIfNotExists({ db: req.db, org, env: req.env, customer, logger: req.logtail, }); - } catch (error: any) { - throw new RecaseError({ - message: `Failed to create Stripe customer`, - code: ErrCode.StripeError, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - }); - } finally { + if (!newCus) { throw new RecaseError({ message: `Failed to create Stripe customer`, @@ -62,26 +92,78 @@ export const handleCreateBillingPortal = async (req: any, res: any) => }); } - const portal = await stripeCli.billingPortal.sessions.create({ - customer: newCus.id, - return_url: returnUrl || toSuccessUrl({ org, env: req.env }), - }); - - res.status(200).json({ - customer_id: customer.id, - url: portal.url, + stripeCustomerId = newCus.id; + } catch (error: any) { + throw new RecaseError({ + message: `Failed to create Stripe customer`, + code: ErrCode.StripeError, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, }); } } else { - const portal = await stripeCli.billingPortal.sessions.create({ - customer: customer.processor.id, + stripeCustomerId = customer.processor.id; + } + + // Create billing portal session + let portal; + try { + portal = await stripeCli.billingPortal.sessions.create({ + customer: stripeCustomerId, return_url: returnUrl || toSuccessUrl({ org, env: req.env }), }); + } catch (error: any) { + console.log(`Code: ${error.code}, Message: ${error.message}`); - res.status(200).json({ - customer_id: customer.id, - url: portal.url, - }); + // Check if the error is due to missing default configuration + if ( + error.message && + error.message.includes("default configuration has not been created") + ) { + try { + // Create a default billing portal configuration + req.logtail?.info( + `Creating default billing portal configuration for customer ${customer.id}` + ); + + const configuration = + await createDefaultBillingPortalConfiguration(stripeCli); + + req.logtail?.info( + "Successfully created billing portal configuration", + { + configurationId: configuration.id, + orgId: org.id, + } + ); + + // Retry creating the portal session with the new configuration + portal = await stripeCli.billingPortal.sessions.create({ + customer: stripeCustomerId, + return_url: returnUrl || toSuccessUrl({ org, env: req.env }), + configuration: configuration.id, + }); + } catch (configError: any) { + req.logtail?.error( + "Failed to create billing portal configuration", + { + error: configError.message, + orgId: org.id, + } + ); + throw new RecaseError({ + message: `Failed to create billing portal configuration: ${configError.message}`, + code: ErrCode.StripeError, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } + } else { + throw error; + } } + + res.status(200).json({ + customer_id: customer.id, + url: portal.url, + }); }, }); diff --git a/server/src/internal/customers/handlers/handleTransferProduct.ts b/server/src/internal/customers/handlers/handleTransferProduct.ts index 343d46a57..47d8fdb45 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct.ts @@ -3,12 +3,13 @@ import { z } from "zod"; import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { CusService } from "../CusService.js"; -import { ErrCode } from "@autumn/shared"; +import { AttachScenario, ErrCode } from "@autumn/shared"; import { CusProductService } from "../cusProducts/CusProductService.js"; import { nullish } from "@/utils/genUtils.js"; import { handleDecreaseAndTransfer } from "./handleTransferProduct/handleDecreaseAndTransfer.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { deleteCusCache } from "../cusCache/updateCachedCus.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; const TransferProductSchema = z.object({ from_entity_id: z.string().nullish(), to_entity_id: z.string(), @@ -119,6 +120,21 @@ export const handleTransferProduct = async (req: any, res: any) => internal_entity_id: toEntity.internal_id, }, }); + + await addProductsUpdatedWebhookTask({ + req, + internalCustomerId: customer.internal_id, + org: req.org, + env: req.env, + customerId: customer.id || customer.internal_id, + scenario: AttachScenario.New, + cusProduct: { + ...cusProduct, + entity_id: toEntity.id, + internal_entity_id: toEntity.internal_id, + }, + logger: req.logger, + }); } await deleteCusCache({ diff --git a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts index 664d8b2cc..bbf726be4 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts @@ -100,6 +100,6 @@ export const handleDecreaseAndTransfer = async ({ }, product ), - sendWebhook: false, + scenario: AttachScenario.New, }); }; diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts index bef4248e4..b12b937f7 100644 --- a/server/src/internal/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts @@ -13,10 +13,8 @@ import { } from "@/trigger/updateBalanceTask.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { - getCusEntBalance, - getUnlimitedAndUsageAllowed, -} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { getCusEntBalance } from "@autumn/shared"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { notNullish } from "@/utils/genUtils.js"; diff --git a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts index 16e51791e..8a25358be 100644 --- a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts +++ b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts @@ -9,10 +9,7 @@ import { adjustAllowance } from "@/trigger/adjustAllowance.js"; import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js"; import { CusService } from "@/internal/customers/CusService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { - getCusEntBalance, - getRelatedCusPrice, -} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { getCusEntBalance } from "@autumn/shared"; import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; diff --git a/server/src/trigger/handleThresholdReached.ts b/server/src/trigger/handleThresholdReached.ts index b156063fc..f4b50e7a8 100644 --- a/server/src/trigger/handleThresholdReached.ts +++ b/server/src/trigger/handleThresholdReached.ts @@ -70,16 +70,12 @@ export const sendSvixThresholdReachedEvent = async ({ }); if (fullCus.entity) { - const entities = await EntityService.list({ - db, - internalCustomerId: fullCus.internal_id, - }); - fullCus.entities = entities; await getSingleEntityResponse({ org, env, features, fullCus, + entity: fullCus.entity, entityId: fullCus.entity.id, }); } @@ -240,15 +236,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, features, + entity: fullCus.entity, fullCus, entityId: fullCus.entity.id, }); diff --git a/shared/utils/cusEntUtils/balanceUtils.ts b/shared/utils/cusEntUtils/balanceUtils.ts new file mode 100644 index 000000000..7cd3bef18 --- /dev/null +++ b/shared/utils/cusEntUtils/balanceUtils.ts @@ -0,0 +1,71 @@ +import { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js"; +import { notNullish, nullish } from "../utils.js"; + +export const getSummedEntityBalances = ({ + cusEnt, +}: { + cusEnt: FullCustomerEntitlement; +}) => { + if (nullish(cusEnt.entities)) { + return { + balance: 0, + adjustment: 0, + unused: 0, + count: 0, + }; + } + + return { + balance: Object.values(cusEnt.entities!).reduce( + (acc, curr) => acc + curr.balance, + 0 + ), + adjustment: Object.values(cusEnt.entities!).reduce( + (acc, curr) => acc + curr.adjustment, + 0 + ), + unused: 0, + count: Object.values(cusEnt.entities!).length, + }; +}; + +export const getCusEntBalance = ({ + cusEnt, + entityId, +}: { + cusEnt: FullCustomerEntitlement; + entityId?: string | null; +}) => { + let entitlement = cusEnt.entitlement; + let ent = cusEnt.entitlement; + let feature = ent.feature; + + if (notNullish(entitlement.entity_feature_id)) { + if (nullish(entityId)) { + return getSummedEntityBalances({ + cusEnt, + }); + } else { + let entityBalance = cusEnt.entities?.[entityId!]?.balance; + let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0; + + if (nullish(entityBalance)) { + return { balance: 0, adjustment: 0, unused: 0, count: 1 }; + } + + return { + balance: entityBalance, + adjustment, + unused: 0, + count: 1, + }; + } + } + + return { + balance: cusEnt.balance, + adjustment: cusEnt.adjustment, + unused: cusEnt.replaceables?.length || 0, + count: 1, + }; +}; diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 7fff843ae..10fe92537 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -9,6 +9,9 @@ export * from "./cusProductUtils/cusProductConstants.js"; export * from "./cusProductUtils/cusProductUtils.js"; export * from "./utils.js"; +// Cus ent utils +export * from "./cusEntUtils/balanceUtils.js"; + // Item utils export * from "./productV2Utils/productItemUtils/mapToItem.js"; export * from "./productV2Utils/productItemUtils/productItemUtils.js"; diff --git a/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx b/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx index 4877a547d..2e7c7ccd3 100644 --- a/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx +++ b/vite/src/views/customers/customer/entitlements/UpdateCusEntitlement.tsx @@ -22,6 +22,7 @@ import { getBackendErr, notNullish } from "@/utils/genUtils"; import CopyButton from "@/components/general/CopyButton"; import { AlertCircle, Info, InfoIcon } from "lucide-react"; import { useCusQuery } from "../hooks/useCusQuery"; +import { getCusEntBalance } from "@autumn/shared"; function UpdateCusEntitlement({ selectedCusEntitlement, @@ -32,17 +33,40 @@ function UpdateCusEntitlement({ }) { const { customer, refetch } = useCusQuery(); const { entityId } = useCustomerContext(); - // const { customer, env, cusMutate, entityId } = useCustomerContext(); + const cusEnt = selectedCusEntitlement; + console.log( + "Balance: ", + cusEnt + ? getCusEntBalance({ + cusEnt: cusEnt!, + entityId, + }).balance + : null + ); + const [updateLoading, setUpdateLoading] = useState(false); const axiosInstance = useAxiosInstance(); + console.log( + `Cus ent: ${cusEnt?.entitlement.feature_id}, Balances: `, + cusEnt + ? getCusEntBalance({ + cusEnt: cusEnt!, + entityId, + }) + : null + ); + const [updateFields, setUpdateFields] = useState({ - balance: - entityId && notNullish(cusEnt?.entities?.[entityId]?.balance) - ? cusEnt?.entities?.[entityId]?.balance - : cusEnt?.balance, + balance: cusEnt + ? getCusEntBalance({ + cusEnt, + entityId, + }).balance + : null, + next_reset_at: cusEnt?.next_reset_at, }); @@ -55,10 +79,12 @@ function UpdateCusEntitlement({ useEffect(() => { setUpdateFields({ - balance: - entityId && notNullish(cusEnt?.entities?.[entityId]?.balance) - ? cusEnt?.entities?.[entityId]?.balance - : cusEnt?.balance, + balance: cusEnt + ? getCusEntBalance({ + cusEnt: cusEnt!, + entityId, + }).balance + : null, next_reset_at: cusEnt?.next_reset_at, }); }, [selectedCusEntitlement]);