260 lines
5.9 KiB
TypeScript
260 lines
5.9 KiB
TypeScript
import { Router } from "express";
|
|
|
|
import {
|
|
APIVersion,
|
|
AppEnv,
|
|
CreateEventSchema,
|
|
CusProductStatus,
|
|
EntityData,
|
|
ErrCode,
|
|
EventInsert,
|
|
Feature,
|
|
FeatureType,
|
|
FullCustomer,
|
|
Organization,
|
|
} from "@autumn/shared";
|
|
|
|
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
|
import { generateId } from "@/utils/genUtils.js";
|
|
import { EventService } from "./EventService.js";
|
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
|
import { handleUsageEvent } from "./usageRouter.js";
|
|
import { StatusCodes } from "http-status-codes";
|
|
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
|
|
import { FeatureService } from "@/internal/features/FeatureService.js";
|
|
import { creditSystemContainsFeature } from "@/internal/features/creditSystemUtils.js";
|
|
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
|
import { JobName } from "@/queue/JobName.js";
|
|
import { orgToVersion } from "@/utils/versionUtils.js";
|
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
|
import { getEventTimestamp } from "./eventUtils.js";
|
|
|
|
export const eventsRouter: Router = Router();
|
|
|
|
const getEventAndCustomer = async ({
|
|
req,
|
|
db,
|
|
org,
|
|
env,
|
|
features,
|
|
customer_id,
|
|
customer_data,
|
|
event_data,
|
|
logger,
|
|
entityId,
|
|
entityData,
|
|
}: {
|
|
req: ExtendedRequest;
|
|
db: DrizzleCli;
|
|
org: Organization;
|
|
features: Feature[];
|
|
env: AppEnv;
|
|
customer_id: string;
|
|
customer_data: any;
|
|
event_data: any;
|
|
entityId: string;
|
|
logger: any;
|
|
entityData?: EntityData;
|
|
}) => {
|
|
if (!customer_id) {
|
|
throw new RecaseError({
|
|
message: "Customer ID is required",
|
|
code: ErrCode.InvalidInputs,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
let customer: FullCustomer;
|
|
|
|
// 2. Check if customer ID is valid
|
|
customer = await getOrCreateCustomer({
|
|
req,
|
|
customerId: customer_id,
|
|
customerData: customer_data,
|
|
entityId,
|
|
inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
|
|
entityData,
|
|
});
|
|
|
|
// 3. Insert event
|
|
const parsedEvent = CreateEventSchema.parse(event_data);
|
|
const timestamp = getEventTimestamp(parsedEvent.timestamp);
|
|
|
|
const newEvent: EventInsert = {
|
|
...parsedEvent,
|
|
properties: parsedEvent.properties || {},
|
|
id: generateId("evt"),
|
|
org_id: org.id,
|
|
org_slug: org.slug,
|
|
env: env,
|
|
internal_customer_id: customer.internal_id,
|
|
created_at: timestamp.getTime(),
|
|
timestamp: timestamp,
|
|
};
|
|
|
|
let event = await EventService.insert({ db, event: newEvent });
|
|
|
|
return { customer, event };
|
|
};
|
|
|
|
const getAffectedFeatures = async ({
|
|
req,
|
|
eventName,
|
|
}: {
|
|
req: any;
|
|
eventName: string;
|
|
}) => {
|
|
let features = await FeatureService.getFromReq(req);
|
|
|
|
let featuresWithEvent = features.filter((feature) => {
|
|
return (
|
|
feature.type == FeatureType.Metered &&
|
|
feature.config.filters.some((filter: any) => {
|
|
return filter.value.includes(eventName);
|
|
})
|
|
);
|
|
});
|
|
|
|
let creditSystems = features.filter((cs: Feature) => {
|
|
return (
|
|
cs.type == FeatureType.CreditSystem &&
|
|
featuresWithEvent.some((f) =>
|
|
creditSystemContainsFeature({
|
|
creditSystem: cs,
|
|
meteredFeatureId: f.id,
|
|
}),
|
|
)
|
|
);
|
|
});
|
|
|
|
return [...featuresWithEvent, ...creditSystems];
|
|
};
|
|
|
|
export const handleEventSent = async ({
|
|
req,
|
|
customer_id,
|
|
customer_data,
|
|
event_data,
|
|
}: {
|
|
req: any;
|
|
customer_id: string;
|
|
customer_data: any;
|
|
event_data: any;
|
|
}) => {
|
|
if (event_data.feature_id) {
|
|
return handleUsageEvent({
|
|
req,
|
|
});
|
|
}
|
|
|
|
const { env, db } = req;
|
|
|
|
let eventName = event_data.event_name;
|
|
|
|
const org = await OrgService.getFromReq(req);
|
|
const features = await FeatureService.getFromReq(req);
|
|
|
|
const affectedFeatures = await getAffectedFeatures({
|
|
req,
|
|
eventName,
|
|
});
|
|
|
|
if (affectedFeatures.length == 0) {
|
|
throw new RecaseError({
|
|
message: `No features found for event_name ${event_data.event_name}`,
|
|
code: ErrCode.InvalidEventName,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
const { customer, event } = await getEventAndCustomer({
|
|
req,
|
|
db,
|
|
org,
|
|
env,
|
|
customer_id,
|
|
customer_data,
|
|
event_data,
|
|
logger: req.logtail,
|
|
entityId: event_data.entity_id,
|
|
entityData: event_data.entity_data,
|
|
features,
|
|
});
|
|
|
|
const payload = {
|
|
internalCustomerId: customer.internal_id,
|
|
customerId: customer.id,
|
|
entityId: event_data.entity_id,
|
|
features: affectedFeatures,
|
|
event,
|
|
org,
|
|
env,
|
|
};
|
|
|
|
await addTaskToQueue({
|
|
jobName: JobName.UpdateBalance,
|
|
payload,
|
|
});
|
|
|
|
return { event, affectedFeatures, org };
|
|
};
|
|
|
|
eventsRouter.post("", async (req: any, res: any) => {
|
|
try {
|
|
const body = req.body;
|
|
|
|
if (!body.event_name && !body.feature_id) {
|
|
throw new RecaseError({
|
|
message: "event_name or feature_id is required",
|
|
code: ErrCode.InvalidInputs,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
if (!body.customer_id) {
|
|
throw new RecaseError({
|
|
message: "customer_id is required",
|
|
code: ErrCode.InvalidInputs,
|
|
statusCode: StatusCodes.BAD_REQUEST,
|
|
});
|
|
}
|
|
|
|
let { event, org }: any = await handleEventSent({
|
|
req,
|
|
customer_id: body.customer_id,
|
|
customer_data: body.customer_data,
|
|
event_data: body,
|
|
});
|
|
|
|
let apiVersion = orgToVersion({
|
|
org,
|
|
reqApiVersion: req.apiVersion,
|
|
});
|
|
|
|
let response: any = {
|
|
id: event?.id,
|
|
code: "event_received",
|
|
customer_id: body.customer_id,
|
|
entity_id: body.entity_id,
|
|
};
|
|
|
|
if (body.feature_id) {
|
|
response.feature_id = body.feature_id;
|
|
} else {
|
|
response.event_name = event.event_name;
|
|
}
|
|
|
|
if (apiVersion >= APIVersion.v1_1) {
|
|
res.status(200).json(response);
|
|
} else {
|
|
res.status(200).json({ success: true });
|
|
}
|
|
|
|
return;
|
|
} catch (error) {
|
|
handleRequestError({ req, res, error, action: "POST event failed" });
|
|
return;
|
|
}
|
|
});
|