219 lines
4.9 KiB
TypeScript
219 lines
4.9 KiB
TypeScript
import { Router } from "express";
|
|
import {
|
|
CusProductStatus,
|
|
Customer,
|
|
ErrCode,
|
|
EventInsert,
|
|
FeatureType,
|
|
FeatureUsageType,
|
|
} from "@autumn/shared";
|
|
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
|
import { generateId, nullish } from "@/utils/genUtils.js";
|
|
|
|
import { EventService } from "./EventService.js";
|
|
import { StatusCodes } from "http-status-codes";
|
|
import { JobName } from "@/queue/JobName.js";
|
|
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
|
|
import { creditSystemContainsFeature } from "@/internal/features/creditSystemUtils.js";
|
|
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
|
import { getEventTimestamp } from "./eventUtils.js";
|
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
|
import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js";
|
|
|
|
export const eventsRouter: Router = Router();
|
|
export const usageRouter: Router = Router();
|
|
|
|
const getCusFeatureAndOrg = async ({
|
|
req,
|
|
customerId,
|
|
featureId,
|
|
entityId,
|
|
customerData,
|
|
}: {
|
|
req: ExtendedRequest;
|
|
customerId: string;
|
|
featureId: string;
|
|
entityId: string;
|
|
customerData: any;
|
|
}) => {
|
|
// 1. Get customer
|
|
const { org, features } = req;
|
|
|
|
let [customer] = await Promise.all([
|
|
getOrCreateCustomer({
|
|
req,
|
|
customerId,
|
|
customerData,
|
|
inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
|
|
|
|
entityId,
|
|
entityData: req.body.entity_data,
|
|
}),
|
|
]);
|
|
|
|
let feature = features.find((f) => f.id == featureId);
|
|
let creditSystems = features.filter(
|
|
(f) =>
|
|
f.type == FeatureType.CreditSystem &&
|
|
creditSystemContainsFeature({
|
|
creditSystem: f,
|
|
meteredFeatureId: featureId,
|
|
}),
|
|
);
|
|
|
|
if (!feature) {
|
|
throw new RecaseError({
|
|
message: `Feature ${featureId} not found`,
|
|
code: ErrCode.FeatureNotFound,
|
|
statusCode: StatusCodes.NOT_FOUND,
|
|
});
|
|
}
|
|
|
|
return { customer, org, feature, creditSystems };
|
|
};
|
|
|
|
const createAndInsertEvent = async ({
|
|
req,
|
|
customer,
|
|
featureId,
|
|
value,
|
|
set_usage,
|
|
properties,
|
|
idempotencyKey,
|
|
}: {
|
|
req: any;
|
|
customer: Customer;
|
|
featureId: string;
|
|
value?: number;
|
|
set_usage?: boolean;
|
|
properties: any;
|
|
idempotencyKey?: string;
|
|
}) => {
|
|
if (!customer.id) {
|
|
throw new RecaseError({
|
|
message: "Customer ID is required",
|
|
code: ErrCode.InvalidInputs,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
const timestamp = getEventTimestamp(req.body.timestamp);
|
|
|
|
const newEvent: EventInsert = {
|
|
id: generateId("evt"),
|
|
org_id: req.orgId,
|
|
org_slug: req.org.slug,
|
|
env: req.env,
|
|
internal_customer_id: customer.internal_id,
|
|
|
|
created_at: timestamp.getTime(),
|
|
timestamp: timestamp,
|
|
|
|
idempotency_key: idempotencyKey,
|
|
customer_id: customer.id,
|
|
event_name: featureId,
|
|
properties,
|
|
value,
|
|
set_usage: set_usage || false,
|
|
};
|
|
|
|
return await EventService.insert({ db: req.db, event: newEvent });
|
|
};
|
|
|
|
export const handleUsageEvent = async ({
|
|
req,
|
|
setUsage = false,
|
|
}: {
|
|
req: any;
|
|
setUsage?: boolean;
|
|
}) => {
|
|
let {
|
|
customer_id,
|
|
customer_data,
|
|
properties,
|
|
feature_id,
|
|
value,
|
|
entity_id,
|
|
idempotency_key,
|
|
} = req.body;
|
|
if (!customer_id || !feature_id) {
|
|
throw new RecaseError({
|
|
message: "customer_id and feature_id are required",
|
|
code: ErrCode.InvalidInputs,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
properties = properties || {};
|
|
|
|
const { customer, org, feature, creditSystems } = await getCusFeatureAndOrg({
|
|
req,
|
|
customerId: customer_id,
|
|
featureId: feature_id,
|
|
customerData: customer_data,
|
|
entityId: entity_id,
|
|
});
|
|
|
|
let newEvent = await createAndInsertEvent({
|
|
req,
|
|
customer,
|
|
featureId: feature_id,
|
|
value,
|
|
set_usage: setUsage,
|
|
properties,
|
|
idempotencyKey: idempotency_key,
|
|
});
|
|
|
|
const features = [feature, ...creditSystems];
|
|
|
|
if (nullish(value) || isNaN(parseFloat(value))) {
|
|
value = 1;
|
|
} else {
|
|
value = parseFloat(value);
|
|
}
|
|
|
|
const payload = {
|
|
customerId: customer.id,
|
|
internalCustomerId: customer.internal_id,
|
|
features,
|
|
org,
|
|
env: req.env,
|
|
properties,
|
|
value,
|
|
set_usage: setUsage,
|
|
entityId: entity_id,
|
|
};
|
|
|
|
const featureUsageType = feature.config?.usage_type;
|
|
// console.log(`Feature Usage Type: ${featureUsageType}`);
|
|
if (featureUsageType === FeatureUsageType.Continuous) {
|
|
await runUpdateUsageTask({
|
|
payload,
|
|
logger: console,
|
|
db: req.db,
|
|
throwError: true,
|
|
});
|
|
} else {
|
|
await addTaskToQueue({
|
|
jobName: JobName.UpdateUsage,
|
|
payload,
|
|
});
|
|
}
|
|
|
|
return { event: newEvent, affectedFeatures: features, org };
|
|
};
|
|
|
|
usageRouter.post("", async (req: any, res: any) => {
|
|
try {
|
|
await handleUsageEvent({ req, setUsage: true });
|
|
res.status(StatusCodes.OK).json({ success: true });
|
|
} catch (error) {
|
|
return handleRequestError({
|
|
req,
|
|
res,
|
|
error,
|
|
action: "handleUsageEvent",
|
|
});
|
|
}
|
|
});
|