Files
cfw-autumn/server/src/internal/customers/handlers/handleUpdateEntitlement.ts
2025-07-23 09:47:59 +01:00

182 lines
5.0 KiB
TypeScript

import { handleRequestError } from "@/utils/errorUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import RecaseError from "@/utils/errorUtils.js";
import { ErrCode, FullCustomerEntitlement } from "@autumn/shared";
import { Decimal } from "decimal.js";
import { StatusCodes } from "http-status-codes";
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 } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
const getCusOrgAndCusPrice = async ({
db,
req,
cusEnt,
}: {
db: DrizzleCli;
req: ExtendedRequest;
cusEnt: FullCustomerEntitlement;
}) => {
const [cusPrice, customer, org] = await Promise.all([
CusPriceService.getRelatedToCusEnt({
db,
cusEnt,
}),
CusService.getByInternalId({
db,
internalId: cusEnt.internal_customer_id,
}),
OrgService.getFromReq(req),
]);
return { cusPrice, customer, org };
};
export const handleUpdateEntitlement = async (req: any, res: any) => {
try {
const { db, logtail: logger } = req;
const { customer_entitlement_id } = req.params;
const { balance, next_reset_at, entity_id } = req.body;
if (isNaN(parseFloat(balance))) {
throw new RecaseError({
message: "Invalid balance",
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
if (
next_reset_at !== null &&
(!Number.isInteger(next_reset_at) || next_reset_at < 0)
) {
throw new RecaseError({
message: "Next reset at must be a valid unix timestamp or null",
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
// Check if org owns the entitlement
const cusEnt = await CusEntService.getStrict({
db,
id: customer_entitlement_id,
orgId: req.orgId,
env: req.env,
withCusProduct: true,
});
const cusProduct = await CusProductService.get({
db,
id: cusEnt.customer_product_id,
orgId: req.orgId,
env: req.env,
});
if (balance < 0 && !cusEnt.usage_allowed) {
throw new RecaseError({
message: "Entitlement does not allow usage",
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
if (cusEnt.unlimited) {
throw new RecaseError({
message: "Entitlement is unlimited",
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
let { balance: masterBalance } = getCusEntBalance({
cusEnt,
entityId: entity_id,
});
const deducted = new Decimal(masterBalance!).minus(balance).toNumber();
let originalBalance = structuredClone(masterBalance);
let { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt({
cusEnt: {
...cusEnt,
customer_product: cusProduct!,
},
toDeduct: deducted,
addAdjustment: true,
allowNegativeBalance: cusEnt.usage_allowed || false,
entityId: entity_id,
});
let updates = {
balance: newBalance,
next_reset_at,
entities: newEntities,
adjustment: newAdjustment,
};
const { cusPrice, customer, org } = await getCusOrgAndCusPrice({
db,
req,
cusEnt,
});
if (cusPrice && customer) {
let fullCusProduct = await CusProductService.get({
db,
id: cusEnt.customer_product_id,
orgId: req.orgId,
env: req.env,
});
const { newReplaceables, deletedReplaceables } = await adjustAllowance({
db,
env: req.env,
org: org,
affectedFeature: cusEnt.entitlement.feature,
cusEnt: {
...cusEnt,
customer_product: fullCusProduct!,
},
cusPrices: [cusPrice],
customer: customer,
originalBalance: originalBalance!,
newBalance: balance,
logger: req.logtail,
});
if (newReplaceables && newReplaceables.length > 0) {
updates.balance = newBalance! - newReplaceables.length;
}
if (deletedReplaceables && deletedReplaceables.length > 0) {
updates.balance = newBalance! + deletedReplaceables.length;
}
}
await CusEntService.update({
db,
id: customer_entitlement_id,
updates,
});
res.status(200).json({ success: true });
} catch (error) {
handleRequestError({
req,
error,
res,
action: "update customer entitlement",
});
}
};