Files
cfw-autumn/server/src/internal/api/entities/entityUtils.ts
2025-12-13 12:18:06 +00:00

172 lines
3.5 KiB
TypeScript

import {
type AppEnv,
BillingType,
type Customer,
type Entity,
EntityExpand,
ErrCode,
type Feature,
type FullCustomerEntitlement,
type FullCustomerPrice,
type Organization,
type UsagePriceConfig,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { submitUsageToStripe } from "@/external/stripe/stripeMeterUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
getBillingType,
roundUsage,
} from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const getLinkedCusEnt = ({
linkedFeature,
cusEnts,
}: {
linkedFeature: any;
cusEnts: any;
}) => {
// Get linked cus ent...
const linkedCusEnt = cusEnts.find(
(e: any) => e.entitlement.feature.id === linkedFeature.id,
);
if (!linkedCusEnt) {
return null;
}
return linkedCusEnt;
};
export const entityFeatureIdExists = ({
cusEnt,
}: {
cusEnt: FullCustomerEntitlement;
}) => {
const ent = cusEnt.entitlement;
return notNullish(ent.entity_feature_id);
};
export const entityMatchesFeature = ({
feature,
entity,
}: {
feature: Feature;
entity: Entity;
}) => {
return feature.id === entity.feature_id;
};
export const isLinkedToEntity = ({
cusEnt,
entity,
}: {
cusEnt: FullCustomerEntitlement;
entity: Entity;
}) => {
return cusEnt.entitlement.entity_feature_id === entity.feature_id;
};
export const removeEntityFromCusEnt = async ({
db,
cusEnt,
entity,
logger,
cusPrice,
customer,
org,
env,
}: {
db: DrizzleCli;
cusEnt: FullCustomerEntitlement;
entity: Entity;
logger: any;
cusPrice?: FullCustomerPrice;
customer: Customer;
org: Organization;
env: AppEnv;
}) => {
// isLinked
const isLinked = isLinkedToEntity({
cusEnt,
entity,
});
if (!isLinked) {
return;
}
const entitlement = cusEnt.entitlement;
console.log(
`Linked cus ent: ${entitlement.feature.id}, isLinked: ${isLinked}`,
);
// Delete cus ent ids
const newEntities = structuredClone(cusEnt.entities!);
// TODO: Send usage to stripe if cus price exists
const stripeCli = createStripeCli({
org,
env,
});
if (cusPrice) {
const config = cusPrice.price.config as UsagePriceConfig;
const billingType = getBillingType(config);
if (billingType === BillingType.UsageInArrear) {
let usage = -newEntities[entity.id]?.balance;
usage = roundUsage({
usage,
billingUnits: config.billing_units!,
});
await submitUsageToStripe({
price: cusPrice.price,
usage,
customer,
feature: entitlement.feature,
logger,
stripeCli,
});
}
}
delete newEntities[entity.id];
await CusEntService.update({
db,
id: cusEnt.id,
updates: {
entities: newEntities,
},
});
logger.info(
`Feature: ${entitlement.feature.id}, customer: ${cusEnt.customer_id}, deleted entities from cus ent`,
);
};
export const parseEntityExpand = (expand: string): EntityExpand[] => {
if (expand) {
const options = expand.split(",");
const result: EntityExpand[] = [];
for (const option of options) {
if (!Object.values(EntityExpand).includes(option as EntityExpand)) {
throw new RecaseError({
message: `Invalid expand option: ${option}`,
code: ErrCode.InvalidExpand,
statusCode: StatusCodes.BAD_REQUEST,
});
}
result.push(option as EntityExpand);
}
return result;
} else {
return [];
}
};