Преглед на файлове

Улучшена валидация активности и вынесена только на сервер

Vadim преди 2 месеца
родител
ревизия
5a9fa63176

+ 33 - 16
src/api/v_0.1.0/client/client-activities-api.ts

@@ -12,7 +12,12 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       categories: z.array(actTypes.ActCategory),
-      activities: z.array(actTypes.ActivityWithValidators),
+      activities: z.array(
+        actTypes.Activity.extend({
+          isOpen: z.boolean(),
+          messages: z.array(z.string()),
+        }),
+      ),
     }),
   };
 
@@ -25,7 +30,12 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       categories: z.array(actTypes.ActCategory),
-      activities: z.array(actTypes.ActivityWithValidators),
+      activities: z.array(
+        actTypes.Activity.extend({
+          isOpen: z.boolean(),
+          messages: z.array(z.string()),
+        }),
+      ),
     }),
   };
 
@@ -38,13 +48,22 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       actRegData: z.object({
-        activityId: z.string().uuid(),
+        activityId: z.string(),
         code: z.string(),
         publicName: z.string(),
         eventInstId: z.string(),
         categoryId: z.string().nullable(),
         categoryCode: z.string(),
-        validators: z.array(actTypes.ActValidator),
+        isUserReg: z.boolean(),
+
+        isOpen: z.boolean(),
+        messages: z.array(z.string()),
+
+        fields: z.array(
+          CustomFieldWithUserCopyValue.extend({
+            arffId: z.string().uuid(),
+          }),
+        ),
         peTypes: z.array(
           z.object({
             peTypeId: z.string().uuid(),
@@ -52,12 +71,18 @@ class ClientActivitiesApi {
             name: z.string(),
           }),
         ),
-        fields: z.array(
-          CustomFieldWithUserCopyValue.extend({
-            arffId: z.string().uuid(),
+
+        validatedPes: z.array(
+          z.object({
+            peId: z.string().uuid(),
+            peTypeId: z.string().uuid(),
+            peTypeCode: z.string(),
+            peTypeName: z.string(),
+            name: z.string(),
+            isValid: z.boolean(),
+            messages: z.array(z.string()),
           }),
         ),
-        isUserReg: z.boolean(),
       }),
     }),
   };
@@ -151,14 +176,6 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       actReg: actTypes.ActivityReg.extend({
-        validators: z.array(actTypes.ActValidatorWithData),
-        peTypes: z.array(
-          z.object({
-            peTypeId: z.string().uuid(),
-            code: z.string(),
-            name: z.string(),
-          }),
-        ),
         fields: z.array(
           CustomFieldWithValidatorsAndValue.extend({
             arffId: z.string().uuid(),

+ 0 - 34
src/api/v_0.1.0/client/client-pe-api.ts

@@ -298,40 +298,6 @@ class ClientPartEntitiesApi {
     }),
   };
 
-  GET_MyPesForActivity = {
-    res: z.object({
-      code: z.enum(["success"]),
-      pes: z.array(
-        z.object({
-          peId: z.string().uuid(),
-          peTypeId: z.string().uuid(),
-          peTypeCode: z.string(),
-          peTypeName: z.string(),
-          eventInstId: z.string().uuid(),
-          ownerId: z.string().uuid(),
-          name: z.string(),
-          members: z.array(
-            z.object({
-              peMemberId: z.string().uuid(),
-              userId: z.string().uuid(),
-              email: z.string().email(),
-              fields: z.array(
-                CustomFieldWithValue.extend({
-                  userEfId: z.string().uuid(),
-                }),
-              ),
-            }),
-          ),
-          fields: z.array(
-            CustomFieldWithValue.extend({
-              peFfId: z.string().uuid(),
-            }),
-          ),
-        }),
-      ),
-    }),
-  };
-
   DELETE_PeMember = {
     req: {
       params: z.object({

+ 5 - 2
src/api/v_0.1.0/types/act-types.ts

@@ -25,13 +25,12 @@ class ActTypes {
     serverData: z.unknown(),
   });
 
-  ActivityWithValidators = z.object({
+  Activity = z.object({
     activityId: z.string().uuid(),
     code: z.string(),
     publicName: z.string(),
     categoryId: z.string().uuid().nullable(),
     categoryCode: z.string(),
-    validators: z.array(this.ActValidatorWithData),
     isUserReg: z.boolean(),
     peTypes: z.array(
       z.object({
@@ -42,6 +41,10 @@ class ActTypes {
     ),
   });
 
+  ActivityWithValidators = this.Activity.extend({
+    validators: z.array(this.ActValidatorWithData),
+  });
+
   PeForActivity = z.object({
     peId: z.string().uuid(),
     peTypeId: z.string().uuid(),

+ 1 - 1
src/db/db-schema.ts

@@ -98,7 +98,7 @@ const DbSchema = {
     eventDates: {
       evDateId: z.string().uuid(),
       eventInstId: z.string().uuid(),
-      date: z.string().datetime(), // timestamp в БД
+      date: z.string().date(),
     },
     locations: {
       locationId: z.string().uuid(),

+ 98 - 38
src/modules/client/activities/c-act-controller.ts

@@ -88,15 +88,25 @@ class ClientActivitiesController {
           and a.category_id is null
     `);
 
-    const activitiesWithValidatorsData = await Promise.all(
+    const validatedActivities = await Promise.all(
       activities.map(async (activity) => {
+        const validatorsWithData = await cActService.addDataToActValidators({
+          validators: activity.validators,
+          activityId: activity.activityId,
+        });
+        const validatedActivity = validateActValidators(validatorsWithData);
+
         return {
-          ...activity,
-          validators: await Promise.all(
-            activity.validators.map(async (validator) =>
-              cActService.addDataToActValidator(validator, activity.activityId),
-            ),
-          ),
+          activityId: activity.activityId,
+          code: activity.code,
+          publicName: activity.publicName,
+          categoryId: activity.categoryId,
+          categoryCode: activity.categoryCode,
+          peTypes: activity.peTypes,
+          isUserReg: activity.isUserReg,
+
+          isOpen: validatedActivity.isOpen,
+          messages: validatedActivity.messages,
         };
       }),
     );
@@ -107,7 +117,7 @@ class ClientActivitiesController {
       {
         code: "success",
         categories: [...categories],
-        activities: [...activitiesWithValidatorsData],
+        activities: [...validatedActivities],
       },
     );
   }
@@ -168,15 +178,25 @@ class ClientActivitiesController {
             a.category_code = ${categoryCode}
       `);
 
-    const activitiesWithValidatorsData = await Promise.all(
+    const validatedActivities = await Promise.all(
       activities.map(async (activity) => {
+        const validatorsWithData = await cActService.addDataToActValidators({
+          validators: activity.validators,
+          activityId: activity.activityId,
+        });
+        const validatedActivity = validateActValidators(validatorsWithData);
+
         return {
-          ...activity,
-          validators: await Promise.all(
-            activity.validators.map(async (validator) =>
-              cActService.addDataToActValidator(validator, activity.activityId),
-            ),
-          ),
+          activityId: activity.activityId,
+          code: activity.code,
+          publicName: activity.publicName,
+          categoryId: activity.categoryId,
+          categoryCode: activity.categoryCode,
+          peTypes: activity.peTypes,
+          isUserReg: activity.isUserReg,
+
+          isOpen: validatedActivity.isOpen,
+          messages: validatedActivity.messages,
         };
       }),
     );
@@ -187,7 +207,7 @@ class ClientActivitiesController {
       {
         code: "success",
         categories: [...categories],
-        activities: [...activitiesWithValidatorsData],
+        activities: [...validatedActivities],
       },
     );
   }
@@ -212,12 +232,61 @@ class ClientActivitiesController {
         "Данные для регистрации на мероприятии не найдены",
       );
 
+    // validate act
+    const validatorsWithData = await cActService.addDataToActValidators({
+      validators: actRegData.validators,
+      activityId: actRegData.activityId,
+    });
+
+    const validatedActivity = validateActValidators(validatorsWithData);
+    if (!validatedActivity.isOpen)
+      throw ApiError.BadRequest(
+        "actValidatorFailed",
+        validatedActivity.messages.join(", "),
+      );
+    //
+
+    // validate pes
+    const pes = await cPeService.getUserOwnerPesWithFieldsAndMembers({
+      userId: user.userId,
+      eventInstId: event.eventInstId,
+    });
+
+    const validatedPes = await Promise.all(
+      pes.map(async (pe) => {
+        const validationResult = await validatePeForAct(actRegData, pe);
+        return {
+          peId: pe.peId,
+          peTypeId: pe.peTypeId,
+          peTypeCode: pe.peTypeCode,
+          peTypeName: pe.peTypeName,
+          name: pe.name,
+
+          isValid: validationResult.isValid,
+          messages: validationResult.messages,
+        };
+      }),
+    );
+
     RouterUtils.validAndSendResponse(
       api.client.activities.GET_ActRegData.res,
       res,
       {
         code: "success",
-        actRegData: actRegData,
+        actRegData: {
+          isOpen: validatedActivity.isOpen,
+          messages: validatedActivity.messages,
+          activityId: actRegData.activityId,
+          code: actRegData.code,
+          publicName: actRegData.publicName,
+          eventInstId: actRegData.eventInstId,
+          categoryId: actRegData.categoryId,
+          categoryCode: actRegData.categoryCode,
+          isUserReg: actRegData.isUserReg,
+          peTypes: actRegData.peTypes,
+          fields: actRegData.fields,
+          validatedPes: validatedPes,
+        },
       },
     );
   }
@@ -259,16 +328,12 @@ class ClientActivitiesController {
 
     //
     // -- ВАЛИДАЦИЯ --
-    const validatorsWithData = await Promise.all(
-      actRegData.validators.map(
-        async (validator) =>
-          await cActService.addDataToActValidator(
-            validator,
-            actRegData.activityId,
-          ),
-      ),
-    );
+
     // валидация активности
+    const validatorsWithData = await cActService.addDataToActValidators({
+      validators: actRegData.validators,
+      activityId: actRegData.activityId,
+    });
 
     const validatedActivity = validateActValidators(validatorsWithData);
     if (!validatedActivity.isOpen)
@@ -293,7 +358,7 @@ class ClientActivitiesController {
         ...pe,
         members: [...members],
       };
-      const validationResult = validatePeForAct(actRegData, fullPe);
+      const validationResult = await validatePeForAct(actRegData, fullPe);
       if (!validationResult.isValid)
         throw ApiError.BadRequest(
           "peValidatorFailed",
@@ -495,7 +560,7 @@ class ClientActivitiesController {
     const { activityRegId } =
       api.client.activities.GET_ActRegForPatch.req.params.parse(req.params);
     const actReg =
-      await cActService.getActRegWithFieldsAndValidatorsAndValuesAndActValidators(
+      await cActService.getActRegWithFieldsAndValuesWithValidators(
         activityRegId,
       );
 
@@ -558,15 +623,10 @@ class ClientActivitiesController {
 
     //
     // -- ВАЛИДАЦИЯ --
-    const validatorsWithData = await Promise.all(
-      actRegData.validators.map(
-        async (validator) =>
-          await cActService.addDataToActValidator(
-            validator,
-            actRegData.activityId,
-          ),
-      ),
-    );
+    const validatorsWithData = await cActService.addDataToActValidators({
+      validators: actRegData.validators,
+      activityId: actRegData.activityId,
+    });
     // валидация активности
 
     const validatedActivity = validateActValidators(validatorsWithData);
@@ -591,7 +651,7 @@ class ClientActivitiesController {
         ...pe,
         members: [...members],
       };
-      const validationResult = validatePeForAct(actRegData, fullPe);
+      const validationResult = await validatePeForAct(actRegData, fullPe);
       if (!validationResult.isValid)
         throw ApiError.BadRequest(
           "peValidatorFailed",

+ 18 - 17
src/modules/client/activities/c-act-service.ts

@@ -48,6 +48,21 @@ class CActService {
     }
   }
 
+  async addDataToActValidators({
+    validators,
+    activityId,
+  }: {
+    validators: z.infer<typeof apiTypes.activities.ActValidator>[];
+    activityId: string;
+  }) {
+    return await Promise.all(
+      validators.map(
+        async (validator) =>
+          await cActService.addDataToActValidator(validator, activityId),
+      ),
+    );
+  }
+
   async getActRegs(userId: string) {
     const actRegs = await selPool.any(sql.type(
       z.object({
@@ -596,7 +611,7 @@ class CActService {
                   'mask', f.mask,
                   'options', f."options",
                   'validators', f.validators,
-                  'orderNumber', af.order_number,
+                  'orderNumber', af.order_number
                 )) as fields
               from
                 act.activity_reg_form_fields af
@@ -609,9 +624,7 @@ class CActService {
           `);
   }
 
-  async getActRegWithFieldsAndValidatorsAndValuesAndActValidators(
-    activityRegId: string,
-  ) {
+  async getActRegWithFieldsAndValuesWithValidators(activityRegId: string) {
     return selPool.maybeOne(sql.type(
       z.object({
         activityRegId: DbSchema.act.activityRegs.activityRegId,
@@ -624,7 +637,6 @@ class CActService {
         userId: DbSchema.act.activityRegs.userId.nullable(),
         isPaid: DbSchema.act.activityRegs.isPaid,
         isCanceled: DbSchema.act.activityRegs.isCanceled,
-        validators: z.array(apiTypes.activities.ActValidator),
         statusHistory: z.array(
           z.object({
             statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
@@ -645,13 +657,6 @@ class CActService {
           }),
         ),
         isUserReg: DbSchema.act.activities.isUserReg,
-        peTypes: z.array(
-          z.object({
-            peTypeId: DbSchema.act.peTypes.peTypeId,
-            code: DbSchema.act.peTypes.code,
-            name: DbSchema.act.peTypes.name,
-          }),
-        ),
       }),
     )`
       select
@@ -667,9 +672,7 @@ class CActService {
         ar.is_canceled "isCanceled",
         ar.status_history "statusHistory",
         f.fields "fields",
-        awv.validators,
-	      ar.is_user_reg "isUserReg",
-        awv.pe_types "peTypes"
+	      ar.is_user_reg "isUserReg"
       from
         act.act_regs_with_status ar
       left join lateral (
@@ -701,8 +704,6 @@ class CActService {
         where
           f_1.activity_id = ar.activity_id) f on
         true
-      left join act.act_with_validators awv on
-        awv.activity_id = ar.activity_id
       where ar.activity_reg_id = ${activityRegId}
     `);
   }

+ 1 - 54
src/modules/client/activities/participant-entities/c-pe-controller.ts

@@ -4,7 +4,7 @@ import { DbSchema } from "#db-schema";
 import { sql } from "slonik";
 
 // api
-import { api, apiTypes } from "#api";
+import { api } from "#api";
 
 // other
 import { z } from "zod";
@@ -727,59 +727,6 @@ class ClientPeController {
     );
   }
 
-  async getMyPesForActivity(req: Request, res: Response) {
-    const user = sessionService.getUserFromReq(req);
-
-    const event = await sessionService.getCurrentEventFromReq(req);
-
-    const pes = await selPool.any(sql.type(apiTypes.activities.PeForActivity)`
-      select
-        pe.pe_id "peId",
-        pe.event_inst_id "eventInstId",
-        pe.owner_id "ownerId",
-        pe.pe_type_id "peTypeId",
-        pt.code "peTypeCode",
-        pt."name" "peTypeName",
-        pe."name",
-        coalesce(m.members, '[]'::jsonb) members,
-        v.fields
-      from
-        act.part_entities pe
-      join act.pe_types pt on
-        pt.pe_type_id = pe.pe_type_id
-        -- members
-      left join lateral (
-        select
-          jsonb_agg(jsonb_build_object(
-              'peMemberId', m.pe_member_id,
-              'userId', m.user_id,
-              'email', m.email,
-              'fields', m.fields
-          )) as members
-        from
-          act.pe_members_with_fields_and_values m
-        where
-          m.pe_id = pe.pe_id
-      ) m on
-        true
-        -- fields
-      left join act.pe_with_fields_and_values v on
-        v.pe_id = pe.pe_id
-      where
-        pe.event_inst_id = ${event.eventInstId}
-        and pe.owner_id = ${user.userId}
-    `);
-
-    RouterUtils.validAndSendResponse(
-      api.client.pe.GET_MyPesForActivity.res,
-      res,
-      {
-        code: "success",
-        pes: [...pes],
-      },
-    );
-  }
-
   async excludeMemberFromPe(req: Request, res: Response) {
     const user = sessionService.getUserFromReq(req);
     const { peMemberId } = api.client.pe.DELETE_PeMember.req.params.parse(

+ 0 - 5
src/modules/client/activities/participant-entities/c-pe-router.ts

@@ -24,11 +24,6 @@ router.post(
 
 router.get("/myPes/", RouterUtils.asyncHandler(clientPeController.getMyPes));
 
-router.get(
-  "/myPesForActivity",
-  RouterUtils.asyncHandler(clientPeController.getMyPesForActivity),
-);
-
 router.patch(
   "/peMembersRequests",
   RouterUtils.asyncHandler(clientPeController.patchPeMembersRequests),

+ 73 - 0
src/modules/client/activities/participant-entities/c-pe-service.ts

@@ -503,6 +503,79 @@ class CPeService {
         pm.pe_member_id = ${peMemberId}
     `);
   }
+
+  async getUserOwnerPesWithFieldsAndMembers({
+    userId,
+    eventInstId,
+  }: {
+    userId: string;
+    eventInstId: string;
+  }) {
+    return await selPool.any(sql.type(
+      z.object({
+        peId: DbSchema.act.partEntities.peId,
+        eventInstId: DbSchema.act.partEntities.eventInstId,
+        ownerId: DbSchema.act.partEntities.ownerId,
+        peTypeId: DbSchema.act.partEntities.peTypeId,
+        peTypeCode: DbSchema.act.peTypes.code,
+        peTypeName: DbSchema.act.peTypes.name,
+        name: DbSchema.act.partEntities.name,
+        members: z.array(
+          z.object({
+            peMemberId: DbSchema.act.peMembers.peMemberId,
+            userId: DbSchema.act.peMembers.userId,
+            email: DbSchema.usr.users.email,
+            fields: z.array(
+              CustomFieldWithValue.extend({
+                userEfId: DbSchema.ev.userEventFields.userEfId,
+              }),
+            ),
+          }),
+        ),
+        fields: z.array(
+          CustomFieldWithValue.extend({
+            peFfId: DbSchema.act.peFormFields.peFfId,
+          }),
+        ),
+      }),
+    )`
+          select
+            pe.pe_id "peId",
+            pe.event_inst_id "eventInstId",
+            pe.owner_id "ownerId",
+            pe.pe_type_id "peTypeId",
+            pt.code "peTypeCode",
+            pt."name" "peTypeName",
+            pe."name",
+            coalesce(m.members, '[]'::jsonb) members,
+            v.fields
+          from
+            act.part_entities pe
+          join act.pe_types pt on
+            pt.pe_type_id = pe.pe_type_id
+            -- members
+          left join lateral (
+            select
+              jsonb_agg(jsonb_build_object(
+                  'peMemberId', m.pe_member_id,
+                  'userId', m.user_id,
+                  'email', m.email,
+                  'fields', m.fields
+              )) as members
+            from
+              act.pe_members_with_fields_and_values m
+            where
+              m.pe_id = pe.pe_id
+          ) m on
+            true
+            -- fields
+          left join act.pe_with_fields_and_values v on
+            v.pe_id = pe.pe_id
+          where
+            pe.event_inst_id = ${eventInstId}
+            and pe.owner_id = ${userId}
+        `);
+  }
 }
 
 export const cPeService = new CPeService();

+ 77 - 4
src/modules/client/activities/validators/act-pe-validators.ts

@@ -1,7 +1,12 @@
 import { apiTypes } from "#api/current-api.js";
+import { DbSchema } from "#db/db-schema.js";
+import { selPool } from "#db/db.js";
+import { logger } from "#plugins/logger.js";
+import dayjs from "dayjs";
+import { sql } from "slonik";
 import { z } from "zod";
 
-export const validatePeForAct = (
+export const validatePeForAct = async (
   activity: z.infer<typeof apiTypes.activities.ActivityWithValidators>,
   pe: z.infer<typeof apiTypes.activities.PeForActivity>,
 ) => {
@@ -20,7 +25,11 @@ export const validatePeForAct = (
   // валидаторы
   const validators = activity.validators;
   for (const validator of validators.filter((v) => v.isPeValidator)) {
-    const validatorData = validatePeOnValidator(validator, pe);
+    const validatorData = await validatePeOnActValidator(
+      validator,
+      pe,
+      activity,
+    );
     if (!validatorData.isValid) {
       isValid = false;
       messages.push(validatorData.message);
@@ -33,10 +42,11 @@ export const validatePeForAct = (
   };
 };
 
-const validatePeOnValidator = (
+const validatePeOnActValidator = async (
   validator: z.infer<typeof apiTypes.activities.ActValidatorWithData>,
   pe: z.infer<typeof apiTypes.activities.PeForActivity>,
-): { isValid: true } | { isValid: false; message: string } => {
+  activity: z.infer<typeof apiTypes.activities.ActivityWithValidators>,
+): Promise<{ isValid: true } | { isValid: false; message: string }> => {
   switch (validator.code) {
     case "min-part-members": {
       const isValid = pe.members.length >= Number(validator.value);
@@ -68,6 +78,69 @@ const validatePeOnValidator = (
       }
     }
 
+    case "max-age-percentage": {
+      const maxAge = Number(validator.value);
+      const maxPercentage = Number(validator.value2);
+
+      let exceeding = 0;
+
+      for (const member of pe.members) {
+        const birthDate = member.fields.find(
+          (f) => f.code === "birth-date",
+        )?.value;
+
+        if (typeof birthDate !== "string") {
+          logger.error({
+            message: `Возраст пользователя ${member.userId} не указан`,
+            member,
+          });
+          continue;
+        }
+
+        const eventStartDateResult = await selPool.maybeOneFirst(sql.type(
+          z.object({
+            startDate: DbSchema.ev.eventDates.date,
+          }),
+        )`
+            select
+              ed.date "startDate"
+            from
+              act.activities a
+            left join ev.event_dates ed on
+              ed.event_inst_id = a.event_inst_id
+            where
+              a.activity_id = ${activity.activityId}
+            order by
+              ed.date asc
+            limit 1
+        `);
+
+        const eventStartDate = eventStartDateResult || dayjs().toISOString();
+
+        const age = dayjs(eventStartDate).diff(birthDate, "year");
+
+        const isExceeding = age > maxAge;
+        if (isExceeding) {
+          exceeding++;
+        }
+      }
+
+      const percentage = (exceeding / pe.members.length) * 100;
+
+      if (percentage <= maxPercentage) {
+        return {
+          isValid: true,
+        };
+      } else {
+        return {
+          isValid: false,
+          message:
+            validator.errorMessage ||
+            `Доля участников старше ${maxAge} лет не должна превышать ${maxPercentage}%, на момент чемпионата`,
+        };
+      }
+    }
+
     default: {
       return {
         isValid: true,

+ 31 - 0
src/modules/client/activities/validators/act-validators.ts

@@ -1,6 +1,7 @@
 import { z } from "zod";
 
 import { apiTypes } from "#api/current-api.js";
+import dayjs from "dayjs";
 
 export const validateActValidators = (
   validators: z.infer<typeof apiTypes.activities.ActValidatorWithData>[],
@@ -55,6 +56,36 @@ const validateActValidator = (
       }
     }
 
+    case "start-reg-date": {
+      const isValid = dayjs().isAfter(dayjs(validator.value));
+
+      if (isValid) {
+        return {
+          isValid: true,
+        };
+      } else {
+        return {
+          isValid: false,
+          message: `Регистрация откроется ${validator.value}`,
+        };
+      }
+    }
+
+    case "end-reg-date": {
+      const isValid = dayjs().isBefore(dayjs(validator.value));
+
+      if (isValid) {
+        return {
+          isValid: true,
+        };
+      } else {
+        return {
+          isValid: false,
+          message: `Регистрация была доступна до ${validator.value}`,
+        };
+      }
+    }
+
     default: {
       return {
         isValid: true,