* wip * Add preview mode in router * only fetch bookings of this month for weighted rr * test set up * only get bookings of current month * reverts test setup * first changes for weight adjustments * reset booking count + adjust calibration * depreciate weightAdjustment * get only bookings created this month * fix typo * make sure createdAt for hosts is correct * use earliest possibel date as fallback * add missing createdAt date to tests * fix typo * clean up changes in tests * fix typo * change end date to current date * fix: Fall back to empty host array when no hosts are found * fix: Restructure code a little * fixed test, incorrectly used now outdated var * perf: remove Dayjs from getLuckyUser * Refactor getHostsWithCalibration for optimised performance, intentionally break test as findMany is always an array * Better mock for host.findMany * Remove team-event-types.test.ts, move to appropriate package * TypeScript cannot auto-infer that an array is non-empty when assigning to a var * fix: Type Fixes and DistributionMethod enum add * Optimise tests * Added test to show that bookings made before a newHost was added affect the lucky user result * Throw error when the usersWithHighestPriority is empty, which should never happen * WIP * remove comment * update migrations * get attributes weights and virtual queue data * use attribute weights and use bookings of virtual queue only * clean up migrations * Add shortfall column and add tests * code clean up from feedback * wrapper function for getLuckyUser * code clean up * fetch routingFormResponse in handleNewBooking * fix type errors * fix type errors in tests * fix getAttributesQueryValue import for tests * fix totalWeight * add test for attributes weights and virtual queues * clean up code * add test for prepareQueuesAndAttributesData * remove console.log * use lazy import * Add more tests and more columns to matching members queue * fix issue from merge * always send usersAndTheirBookingShortfalls --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com>
210 lines
7.2 KiB
TypeScript
210 lines
7.2 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { routingFormAppDataSchemas } from "./appDataSchemas";
|
|
|
|
export const zodNonRouterField = z.object({
|
|
id: z.string(),
|
|
label: z.string(),
|
|
identifier: z.string().optional(),
|
|
placeholder: z.string().optional(),
|
|
type: z.string(),
|
|
/**
|
|
* @deprecated in favour of `options`
|
|
*/
|
|
selectText: z.string().optional(),
|
|
required: z.boolean().optional(),
|
|
deleted: z.boolean().optional(),
|
|
options: z
|
|
.array(
|
|
z.object({
|
|
label: z.string(),
|
|
// To keep backwards compatibility with the options generated from legacy selectText, we allow saving null as id
|
|
// It helps in differentiating whether the routing logic should consider the option.label as value or option.id as value.
|
|
// This is important for legacy routes which has option.label saved in conditions and it must keep matching with the value of the option
|
|
id: z.string().or(z.null()),
|
|
})
|
|
)
|
|
.optional(),
|
|
});
|
|
|
|
export const zodRouterField = zodNonRouterField.extend({
|
|
routerId: z.string(),
|
|
});
|
|
|
|
// This ordering is important - If routerId is present then it should be in the parsed object. Moving zodNonRouterField to first position doesn't do that
|
|
export const zodField = z.union([zodRouterField, zodNonRouterField]);
|
|
export const zodFields = z.array(zodField).optional();
|
|
|
|
export const zodNonRouterFieldView = zodNonRouterField;
|
|
export const zodRouterFieldView = zodRouterField.extend({
|
|
routerField: zodNonRouterFieldView,
|
|
router: z.object({
|
|
name: z.string(),
|
|
description: z.string(),
|
|
id: z.string(),
|
|
}),
|
|
});
|
|
/**
|
|
* Has some additional fields that are not supposed to be saved to DB but are required for the UI
|
|
*/
|
|
export const zodFieldView = z.union([zodNonRouterFieldView, zodRouterFieldView]);
|
|
|
|
export const zodFieldsView = z.array(zodFieldView).optional();
|
|
const queryValueSchema = z.object({
|
|
id: z.string().optional(),
|
|
type: z.union([z.literal("group"), z.literal("switch_group")]),
|
|
children1: z.any(),
|
|
properties: z.any(),
|
|
});
|
|
|
|
export enum RouteActionType {
|
|
CustomPageMessage = "customPageMessage",
|
|
ExternalRedirectUrl = "externalRedirectUrl",
|
|
EventTypeRedirectUrl = "eventTypeRedirectUrl",
|
|
}
|
|
|
|
export const routeActionTypeSchema = z.nativeEnum(RouteActionType);
|
|
|
|
export const children1Schema = z.object({
|
|
children1: z
|
|
.record(
|
|
z.object({
|
|
type: z.string().optional(),
|
|
properties: z
|
|
.object({
|
|
field: z.any().optional(),
|
|
operator: z.any().optional(),
|
|
value: z.any().optional(),
|
|
valueSrc: z.any().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
)
|
|
.optional()
|
|
// Be very careful and lenient here. Just ensure that the rule isn't invalid without breaking anything
|
|
.superRefine((children1, ctx) => {
|
|
if (!children1) return;
|
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
typeof value === "object" && value !== null;
|
|
Object.entries(children1).forEach(([, _rule]) => {
|
|
const rule = _rule as unknown;
|
|
if (!isObject(rule) || rule.type !== "rule") return;
|
|
if (!isObject(rule.properties)) return;
|
|
|
|
const value = rule.properties.value || [];
|
|
const valueSrc = rule.properties.valueSrc;
|
|
if (!(value instanceof Array) || !(valueSrc instanceof Array)) {
|
|
return;
|
|
}
|
|
|
|
if (!valueSrc.length) {
|
|
// If valueSrc is empty, value could be empty for operators like is_empty, is_not_empty
|
|
return;
|
|
}
|
|
|
|
// MultiSelect array can be 2D array
|
|
const flattenedValues = value.flat();
|
|
|
|
const validValues = flattenedValues.filter((value: unknown) => {
|
|
// Might want to restrict it to filter out null and empty string as well. But for now we know that Prisma errors only for undefined values when saving it in JSON field
|
|
// Also, it is possible that RAQB has some requirements to support null or empty string values.
|
|
if (value === undefined) return false;
|
|
return true;
|
|
});
|
|
|
|
if (!validValues.length) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
message: "Looks like you are trying to create a rule with no value",
|
|
});
|
|
}
|
|
});
|
|
}),
|
|
});
|
|
/**
|
|
* Stricter schema for validating before saving to DB
|
|
* It doesn't decide what will be saved, it is just to validate the data before saving
|
|
*/
|
|
export const queryValueSaveValidationSchema = queryValueSchema
|
|
.omit({ children1: true })
|
|
.merge(children1Schema)
|
|
.nullish();
|
|
|
|
export const attributeRoutingConfigSchema = z
|
|
.object({
|
|
skipContactOwner: z.boolean().optional(),
|
|
salesforce: routingFormAppDataSchemas["salesforce"],
|
|
})
|
|
.nullish();
|
|
|
|
export const zodNonRouterRoute = z.object({
|
|
id: z.string(),
|
|
name: z.string().optional(),
|
|
attributeRoutingConfig: attributeRoutingConfigSchema,
|
|
|
|
// TODO: It should be renamed to formFieldsQueryValue but it would take some effort
|
|
/**
|
|
* RAQB query value for form fields
|
|
* BRANDED to ensure we don't give it Attributes
|
|
*/
|
|
queryValue: queryValueSchema.brand<"formFieldsQueryValue">(),
|
|
/**
|
|
* RAQB query value for attributes. It is only applicable for Team Events as it is used to find matching team members
|
|
* BRANDED to ensure we don't give it Form Fields
|
|
*/
|
|
attributesQueryValue: queryValueSchema.brand<"attributesQueryValue">().optional(),
|
|
/**
|
|
* RAQB query value for fallback of `attributesQueryValue`
|
|
* BRANDED to ensure we don't give it Form Fields, It needs Attributes
|
|
*/
|
|
fallbackAttributesQueryValue: queryValueSchema.brand<"attributesQueryValue">().optional(),
|
|
/**
|
|
* Whether the route is a fallback if no other routes match
|
|
*/
|
|
isFallback: z.boolean().optional(),
|
|
action: z.object({
|
|
type: routeActionTypeSchema,
|
|
eventTypeId: z.number().optional(),
|
|
value: z.string(),
|
|
}),
|
|
});
|
|
|
|
export const zodNonRouterRouteView = zodNonRouterRoute;
|
|
|
|
export const zodRouterRoute = z.object({
|
|
// This is the id of the Form being used as router
|
|
id: z.string(),
|
|
name: z.string().optional(),
|
|
isRouter: z.literal(true),
|
|
});
|
|
|
|
export const zodRoute = z.union([zodNonRouterRoute, zodRouterRoute]);
|
|
|
|
export const zodRouterRouteView = zodRouterRoute.extend({
|
|
//TODO: Extend it from form
|
|
name: z.string(),
|
|
description: z.string().nullable(),
|
|
routes: z.array(z.union([zodRoute, z.null()])),
|
|
});
|
|
|
|
export const zodRoutes = z.union([z.array(zodRoute), z.null()]).optional();
|
|
|
|
export const zodRouteView = z.union([zodNonRouterRouteView, zodRouterRouteView]);
|
|
|
|
export const zodRoutesView = z.union([z.array(zodRouteView), z.null()]).optional();
|
|
|
|
// TODO: This is a requirement right now that zod.ts file (if it exists) must have appDataSchema export(which is only required by apps having EventTypeAppCard interface)
|
|
// This is a temporary solution and will be removed in future
|
|
export const appDataSchema = z.any();
|
|
|
|
export const appKeysSchema = z.object({});
|
|
|
|
// This is different from FormResponse in types.d.ts in that it has label optional. We don't seem to be using label at this point, so we might want to use this only while saving the response when Routing Form is submitted
|
|
// Record key is formFieldId
|
|
export const routingFormResponseInDbSchema = z.record(
|
|
z.object({
|
|
label: z.string().optional(),
|
|
value: z.union([z.string(), z.number(), z.array(z.string())]),
|
|
})
|
|
);
|