浏览代码

Добавлена регистрация на активности

Vadim 3 月之前
父节点
当前提交
b7a30af8fe

+ 53 - 3
src/api/v_0.1.0/client/client-activities-api.ts

@@ -10,7 +10,7 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       categories: z.array(actTypes.ActCategory),
-      activities: z.array(actTypes.Activity),
+      activities: z.array(actTypes.ActivityWithValidators),
     }),
   };
 
@@ -23,7 +23,7 @@ class ClientActivitiesApi {
     res: z.object({
       code: z.enum(["success"]),
       categories: z.array(actTypes.ActCategory),
-      activities: z.array(actTypes.Activity),
+      activities: z.array(actTypes.ActivityWithValidators),
     }),
   };
 
@@ -60,7 +60,7 @@ class ClientActivitiesApi {
     }),
   };
 
-  POST_RegisterPe = {
+  POST_RegisterToAct = {
     req: {
       params: z.object({
         activityCode: z.string(),
@@ -80,6 +80,56 @@ class ClientActivitiesApi {
       activityRegId: z.string().uuid(),
     }),
   };
+
+  GET_ActRegs = {
+    res: z.object({
+      code: z.enum(["success"]),
+      actRegs: z.array(actTypes.ActivityReg),
+    }),
+  };
+
+  GET_ActReg = {
+    req: {
+      params: z.object({
+        activityRegId: z.string().uuid(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+
+      actReg: z.discriminatedUnion("role", [
+        actTypes.ActivityRegWithFields.extend({ role: z.literal("owner") }),
+        actTypes.ActivityReg.extend({ role: z.literal("member") }),
+      ]),
+    }),
+  };
+
+  GET_Activity = {
+    req: {
+      params: z.object({
+        activityCode: z.string(),
+      }),
+    },
+    res: z.object({
+      code: z.enum(["success"]),
+      act: z.object({
+        activityId: z.string().uuid(),
+        code: z.string(),
+        publicName: z.string(),
+        eventInstId: z.string().uuid(),
+        categoryId: z.string().uuid().nullable(),
+        isUserReg: z.boolean(),
+        paymentConfig: z.enum([
+          "PER_REGISTRATION",
+          "PER_PARTICIPANT",
+          "FREE",
+          "NONE",
+        ]),
+        registrationProductId: z.string().uuid().nullable(),
+        participantProductId: z.string().uuid().nullable(),
+      }),
+    }),
+  };
 }
 
 export const clientActivitiesApi = new ClientActivitiesApi();

+ 34 - 1
src/api/v_0.1.0/types/act-types.ts

@@ -25,7 +25,7 @@ class ActTypes {
     serverData: z.unknown(),
   });
 
-  Activity = z.object({
+  ActivityWithValidators = z.object({
     activityId: z.string().uuid(),
     code: z.string(),
     publicName: z.string(),
@@ -68,6 +68,39 @@ class ActTypes {
       }),
     ),
   });
+
+  ActivityReg = z.object({
+    activityRegId: z.string().uuid(),
+    activityId: z.string().uuid(),
+    activityCode: z.string(),
+    activityPublicName: z.string(),
+    peId: z.string().uuid().nullable(),
+    peName: z.string().nullable(),
+    peOwnerId: z.string().uuid().nullable(),
+    userId: z.string().uuid().nullable(),
+    isPaid: z.boolean(),
+    statusHistory: z.array(
+      z.object({
+        statusHistoryId: z.string().uuid(),
+        actRegStatusId: z.string().uuid(),
+        name: z.string(),
+        code: z.string(),
+        note: z.string().nullable(),
+        setDate: z.string(),
+        color: z.string().nullable(),
+        isPaymentOpen: z.boolean(),
+      }),
+    ),
+  });
+
+  ActivityRegWithFields = this.ActivityReg.extend({
+    fields: z.array(
+      CustomFieldWithValue.extend({
+        arffId: z.string().uuid(),
+        isChangeResetStatus: z.boolean(),
+      }),
+    ),
+  });
 }
 
 export const actTypes = new ActTypes();

+ 27 - 0
src/db/db-schema.ts

@@ -163,6 +163,16 @@ const DbSchema = {
       categoryId: z.string().uuid().nullable(),
       blockId: z.string().uuid().nullable(),
       isUserReg: z.boolean(),
+paymentConfig: z.enum([
+        "PER_REGISTRATION",
+        "PER_PARTICIPANT",
+        "FREE",
+        "NONE",
+      ]),
+      registrationProductId: z.string().uuid().nullable(),
+      participantProductId: z.string().uuid().nullable(),
+      initialRegStatusId: z.string().uuid(),
+      nextRegStatusIdAfterPayment: z.string().uuid(),
     },
     activityPeTypes: {
       activityId: z.string().uuid(),
@@ -196,12 +206,29 @@ const DbSchema = {
       fieldDefinitionId: z.string().uuid(),
       fieldTitleOverride: z.string().nullable(),
       isCopyUserValue: z.boolean(),
+      isChangeResetStatus: z.boolean(),
     },
     activityRegs: {
       activityRegId: z.string().uuid(),
       activityId: z.string().uuid(),
       peId: z.string().uuid().nullable(),
       userId: z.string().uuid().nullable(),
+      isPaid: z.boolean(),
+      number: z.string(),
+    },
+    actRegStatuses: {
+      actRegStatusId: z.string().uuid(),
+      code: z.string(),
+      name: z.string(),
+      color: z.string().nullable(),
+      isPaymentOpen: z.boolean(),
+    },
+    actRegStatusHistory: {
+      statusHistoryId: z.string().uuid(),
+      activityRegId: z.string().uuid(),
+      actRegStatusId: z.string().uuid(),
+      note: z.string().nullable(),
+      setDate: z.string().datetime(),
     },
     arFieldValues: {
       // Таблица ar_field_values из новой БД

+ 110 - 10
src/modules/client/activities/c-act-controller.ts

@@ -26,6 +26,7 @@ import { v7 } from "uuid";
 import { cPeService } from "./participant-entities/c-pe-service.js";
 import { cActService } from "./c-act-service.js";
 import { validatePeForAct } from "./validators/act-pe-validators.js";
+import { generateRandomNumber } from "#utils/other-utils.js";
 
 class ClientActivitiesController {
   async getEventActivities(
@@ -284,13 +285,13 @@ class ClientActivitiesController {
     );
   }
 
-  async registerPe(req: Request, res: Response) {
+  async registerToAct(req: Request, res: Response) {
     const { fields, peId } =
-      api.client.activities.POST_RegisterPe.req.body.parse(
+      api.client.activities.POST_RegisterToAct.req.body.parse(
         JSON.parse(req.body.body),
       );
     const { activityCode } =
-      api.client.activities.POST_RegisterPe.req.params.parse(req.params);
+      api.client.activities.POST_RegisterToAct.req.params.parse(req.params);
 
     const files = req.files;
 
@@ -441,14 +442,21 @@ class ClientActivitiesController {
     //
     // вставляем в базу и сохраняем файлы
     const activityRegId = v7();
+    let activityRegNumber = generateRandomNumber();
+
+    while (await cActService.checkActivityRegNumber(activityRegNumber)) {
+      activityRegNumber = generateRandomNumber();
+    }
+
     await updPool.transaction(async (tr) => {
       if (actRegData.isUserReg) {
         // для user
-        tr.query(sql.unsafe`
+        // сама регистрация
+        await tr.query(sql.unsafe`
           insert into act.activity_regs
-            (activity_reg_id, activity_id, user_id)
+            (activity_reg_id, activity_id, user_id, number)
           values
-            (${activityRegId}, ${actRegData.activityId}, ${user.userId})
+            (${activityRegId}, ${actRegData.activityId}, ${user.userId}, ${activityRegNumber})
         `);
       } else {
         // для pe
@@ -457,14 +465,28 @@ class ClientActivitiesController {
             "peIdNotFound",
             "ID сущности участия не передан",
           );
-        tr.query(sql.unsafe`
+
+        // сама регистрация
+        await tr.query(sql.unsafe`
         insert into act.activity_regs
-          (activity_reg_id, activity_id, pe_id)
+          (activity_reg_id, activity_id, pe_id, number)
         values
-          (${activityRegId}, ${actRegData.activityId}, ${peId})
+          (${activityRegId}, ${actRegData.activityId}, ${peId}, ${activityRegNumber})
       `);
       }
 
+      const initialRegStatusId = await cActService.getInitialRegStatusId(
+        actRegData.activityId,
+      );
+      // устанавливаем начальный статус
+      // FIXME: проверить во всех транзациях что я дождаюсь ответа
+      await tr.query(sql.unsafe`
+          insert into act.act_reg_status_history
+            (status_history_id, activity_reg_id, act_reg_status_id, note)
+          values
+            (${v7()}, ${activityRegId}, ${initialRegStatusId}, 'Отправлена заявка на регистрацию')
+        `);
+
       await cCustomFieldsValidateService.saveCustomFieldValuesInTransaction({
         tr,
         parentId: activityRegId,
@@ -477,7 +499,7 @@ class ClientActivitiesController {
       // TODO: отправка уведомления
     });
     RouterUtils.validAndSendResponse(
-      api.client.activities.POST_RegisterPe.res,
+      api.client.activities.POST_RegisterToAct.res,
       res,
       {
         code: "success",
@@ -485,6 +507,84 @@ class ClientActivitiesController {
       },
     );
   }
+
+  async getActRegs(req: Request, res: Response) {
+    const user = sessionService.getUserFromReq(req);
+    const actRegs = await cActService.getActRegs(user.userId);
+    RouterUtils.validAndSendResponse(
+      api.client.activities.GET_ActRegs.res,
+      res,
+      {
+        code: "success",
+        actRegs: [...actRegs],
+      },
+    );
+  }
+
+  async getActReg(req: Request, res: Response) {
+    const { activityRegId } = api.client.activities.GET_ActReg.req.params.parse(
+      req.params,
+    );
+
+    const user = sessionService.getUserFromReq(req);
+    const role = await cActService.checkActRegOwner(user.userId, activityRegId);
+    if (!role) throw ApiError.ForbiddenError();
+
+    let actReg: z.infer<
+      typeof api.client.activities.GET_ActReg.res.shape.actReg
+    > | null;
+    if (role === "owner") {
+      const r = await cActService.getActReg(activityRegId);
+      if (!r)
+        throw ApiError.BadRequest(
+          "actRegNotFound",
+          "Регистрация на мероприятии не найдена",
+        );
+      actReg = {
+        ...r,
+        role,
+      };
+    } else {
+      const r = await cActService.getActRegForPeMember(activityRegId);
+      if (!r)
+        throw ApiError.BadRequest(
+          "actRegNotFound",
+          "Регистрация на мероприятии не найдена",
+        );
+
+      actReg = {
+        ...r,
+        role,
+      };
+    }
+
+    RouterUtils.validAndSendResponse(
+      api.client.activities.GET_ActReg.res,
+      res,
+      {
+        code: "success",
+        actReg,
+      },
+    );
+  }
+
+  async getActivity(req: Request, res: Response) {
+    const { activityCode } =
+      api.client.activities.GET_Activity.req.params.parse(req.params);
+    const act = await cActService.getActivity(activityCode);
+
+    if (!act)
+      throw ApiError.BadRequest("actNotFound", "Мероприятие не найдено");
+
+    RouterUtils.validAndSendResponse(
+      api.client.activities.GET_Activity.res,
+      res,
+      {
+        code: "success",
+        act,
+      },
+    );
+  }
 }
 
 export const clientActController = new ClientActivitiesController();

+ 16 - 2
src/modules/client/activities/c-act-router.ts

@@ -11,17 +11,31 @@ router.get(
   "/eventActivities",
   RouterUtils.asyncHandler(clientActController.getEventActivities),
 );
+
+router.get("/regs", RouterUtils.asyncHandler(clientActController.getActRegs));
+
+router.get(
+  "/reg/:activityRegId",
+  RouterUtils.asyncHandler(clientActController.getActReg),
+);
+
 router.get(
   "/cat/:categoryCode",
   RouterUtils.asyncHandler(clientActController.getCategory),
 );
+
 router.get(
   "/:activityCode/regData",
   RouterUtils.asyncHandler(clientActController.getActRegData),
 );
 
 router.post(
-  "/:activityCode/registerPe",
+  "/:activityCode/register",
   upload.any(),
-  RouterUtils.asyncHandler(clientActController.registerPe),
+  RouterUtils.asyncHandler(clientActController.registerToAct),
+);
+
+router.get(
+  "/:activityCode",
+  RouterUtils.asyncHandler(clientActController.getActivity),
 );

+ 339 - 4
src/modules/client/activities/c-act-service.ts

@@ -1,13 +1,16 @@
 import { apiTypes } from "#api/current-api.js";
-import { selPool } from "#db/db.js";
+import { CustomFieldWithValue } from "#api/v_0.1.0/types/custom-fields-types.js";
+import { DbSchema } from "#db/db-schema.js";
+import { selPool, updPool } from "#db/db.js";
+import { ApiError } from "#exceptions/api-error.js";
 import { sql } from "slonik";
 import { z } from "zod";
 
 class CActService {
-  addDataToActValidator = async (
+  async addDataToActValidator(
     validator: z.infer<typeof apiTypes.activities.ActValidator>,
     activityId: string,
-  ): Promise<z.infer<typeof apiTypes.activities.ActValidatorWithData>> => {
+  ): Promise<z.infer<typeof apiTypes.activities.ActValidatorWithData>> {
     switch (validator.code) {
       case "max-regs": {
         const currentRegs = await selPool.oneFirst(sql.type(
@@ -35,7 +38,339 @@ class CActService {
         return validator;
       }
     }
-  };
+  }
+
+  async getActRegs(userId: string) {
+    const actRegs = await selPool.any(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        activityId: DbSchema.act.activityRegs.activityId,
+        activityCode: DbSchema.act.activities.code,
+        activityPublicName: DbSchema.act.activities.publicName,
+        peId: DbSchema.act.activityRegs.peId,
+        peName: DbSchema.act.partEntities.name.nullable(),
+        peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
+        userId: DbSchema.act.activityRegs.userId.nullable(),
+        isPaid: DbSchema.act.activityRegs.isPaid,
+        statusHistory: z.array(
+          z.object({
+            statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
+            name: DbSchema.act.actRegStatuses.name,
+            code: DbSchema.act.actRegStatuses.code,
+            note: DbSchema.act.actRegStatusHistory.note,
+            setDate: DbSchema.act.actRegStatusHistory.setDate,
+            actRegStatusId: DbSchema.act.actRegStatuses.actRegStatusId,
+            color: DbSchema.act.actRegStatuses.color,
+            isPaymentOpen: DbSchema.act.actRegStatuses.isPaymentOpen,
+          }),
+        ),
+      }),
+    )`
+      select
+        ar.activity_reg_id "activityRegId",
+        ar.activity_id "activityId",
+        ar.activity_code "activityCode",
+        ar.activity_public_name "activityPublicName",
+        ar.pe_id "peId",
+        ar.pe_name "peName",
+        ar.pe_owner_id "peOwnerId",
+        ar.user_id "userId",
+        ar.is_paid "isPaid",
+        ar.status_history "statusHistory"
+      from
+        act.act_regs_with_status ar
+      where
+        ar.user_id = ${userId}
+      or 
+          ar.pe_owner_id = ${userId}
+      or 
+          EXISTS (
+              select 1
+              from act.pe_members pm_check
+              where pm_check.pe_id = ar.pe_id
+              and pm_check.user_id = ${userId}
+          );
+    `);
+    return actRegs;
+  }
+
+  async getActReg(activityRegId: string) {
+    const actReg = await selPool.maybeOne(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        activityId: DbSchema.act.activityRegs.activityId,
+        activityCode: DbSchema.act.activities.code,
+        activityPublicName: DbSchema.act.activities.publicName,
+        peId: DbSchema.act.activityRegs.peId,
+        peName: DbSchema.act.partEntities.name.nullable(),
+        peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
+        userId: DbSchema.act.activityRegs.userId.nullable(),
+        isPaid: DbSchema.act.activityRegs.isPaid,
+        statusHistory: z.array(
+          z.object({
+            statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
+            name: DbSchema.act.actRegStatuses.name,
+            code: DbSchema.act.actRegStatuses.code,
+            note: DbSchema.act.actRegStatusHistory.note,
+            setDate: DbSchema.act.actRegStatusHistory.setDate,
+            actRegStatusId: DbSchema.act.actRegStatuses.actRegStatusId,
+            color: DbSchema.act.actRegStatuses.color,
+            isPaymentOpen: DbSchema.act.actRegStatuses.isPaymentOpen,
+          }),
+        ),
+        fields: z.array(
+          CustomFieldWithValue.extend({
+            arffId: DbSchema.act.activityRegFormFields.arffId,
+            isChangeResetStatus:
+              DbSchema.act.activityRegFormFields.isChangeResetStatus,
+          }),
+        ),
+      }),
+    )`
+      select
+        ar.activity_reg_id "activityRegId",
+        ar.activity_id "activityId",
+        ar.activity_code "activityCode",
+        ar.activity_public_name "activityPublicName",
+        ar.pe_id "peId",
+        ar.pe_name "peName",
+        ar.pe_owner_id "peOwnerId",
+        ar.user_id "userId",
+        ar.is_paid "isPaid",
+        ar.status_history "statusHistory",
+        ar.fields "fields"
+      from
+        act.act_regs_with_values ar
+      where
+        ar.activity_reg_id = ${activityRegId}
+    `);
+    return actReg;
+  }
+
+  async getActRegForPeMember(activityRegId: string) {
+    const actReg = await selPool.maybeOne(sql.type(
+      z.object({
+        activityRegId: DbSchema.act.activityRegs.activityRegId,
+        activityId: DbSchema.act.activityRegs.activityId,
+        activityCode: DbSchema.act.activities.code,
+        activityPublicName: DbSchema.act.activities.publicName,
+        peId: DbSchema.act.activityRegs.peId,
+        peName: DbSchema.act.partEntities.name.nullable(),
+        peOwnerId: DbSchema.act.partEntities.ownerId.nullable(),
+        userId: DbSchema.act.activityRegs.userId.nullable(),
+        isPaid: DbSchema.act.activityRegs.isPaid,
+        statusHistory: z.array(
+          z.object({
+            statusHistoryId: DbSchema.act.actRegStatusHistory.statusHistoryId,
+            name: DbSchema.act.actRegStatuses.name,
+            code: DbSchema.act.actRegStatuses.code,
+            note: DbSchema.act.actRegStatusHistory.note,
+            setDate: DbSchema.act.actRegStatusHistory.setDate,
+            actRegStatusId: DbSchema.act.actRegStatuses.actRegStatusId,
+            color: DbSchema.act.actRegStatuses.color,
+            isPaymentOpen: DbSchema.act.actRegStatuses.isPaymentOpen,
+          }),
+        ),
+      }),
+    )`
+      select
+        ar.activity_reg_id "activityRegId",
+        ar.activity_id "activityId",
+        ar.activity_code "activityCode",
+        ar.activity_public_name "activityPublicName",
+        ar.pe_id "peId",
+        ar.pe_name "peName",
+        ar.pe_owner_id "peOwnerId",
+        ar.user_id "userId",
+        ar.is_paid "isPaid",
+        ar.status_history "statusHistory"
+      from
+        act.act_regs_with_values ar
+      where
+        ar.activity_reg_id = ${activityRegId}
+    `);
+    return actReg;
+  }
+
+  async getInitialRegStatusId(activityId: string) {
+    const initialRegStatusId = await selPool.oneFirst(sql.type(
+      z.object({
+        initialRegStatusId: DbSchema.act.activities.initialRegStatusId,
+      }),
+    )`
+      select
+        initial_reg_status_id "initialRegStatusId"
+      from
+        act.activities
+      where
+        activity_id = ${activityId}
+    `);
+    return initialRegStatusId;
+  }
+
+  async getActivity(activityCode: string) {
+    return await selPool.maybeOne(sql.type(
+      z.object({
+        activityId: DbSchema.act.activities.activityId,
+        code: DbSchema.act.activities.code,
+        publicName: DbSchema.act.activities.publicName,
+        eventInstId: DbSchema.act.activities.eventInstId,
+        categoryId: DbSchema.act.activities.categoryId,
+        isUserReg: DbSchema.act.activities.isUserReg,
+        paymentConfig: DbSchema.act.activities.paymentConfig,
+        registrationProductId: DbSchema.act.activities.registrationProductId,
+        participantProductId: DbSchema.act.activities.participantProductId,
+        nextRegStatusIdAfterPayment:
+          DbSchema.act.activities.nextRegStatusIdAfterPayment,
+      }),
+    )`
+      select
+        a.activity_id "activityId",
+        a.code "code",
+        a.public_name "publicName",
+        a.event_inst_id "eventInstId",
+        a.category_id "categoryId",
+        a.is_user_reg "isUserReg",
+        a.payment_config "paymentConfig",
+        a.registration_product_id "registrationProductId",
+        a.participant_product_id "participantProductId",
+        a.next_reg_status_id_after_payment "nextRegStatusIdAfterPayment"
+      from
+        act.activities a
+      where
+        a.code = ${activityCode}
+    `);
+  }
+
+  async checkActivityRegNumber(activityRegNumber: string) {
+    const isExist = await selPool.exists(sql.unsafe`
+      select
+        number
+      from
+        act.activity_regs
+      where
+        number = ${activityRegNumber}
+    `);
+    return !!isExist;
+  }
+
+  async updateActRegPaymentStatus(activityRegId: string) {
+    const actReg = await this.getActReg(activityRegId);
+    if (!actReg) {
+      throw ApiError.BadRequest("actRegNotFound", "Не найдена регистрация");
+    }
+
+    const activity = await this.getActivity(actReg.activityCode);
+    if (!activity) {
+      throw ApiError.BadRequest("activityNotFound", "Не найдена активность");
+    }
+
+    // оплата за всю регистрацию
+    if (activity.paymentConfig === "PER_REGISTRATION") {
+      await updPool.query(sql.unsafe`
+          update act.activity_regs
+          set is_paid = true
+          where activity_reg_id = ${activityRegId}
+        `);
+
+      await updPool.query(sql.unsafe`
+          insert into act.act_reg_status_history (
+            activity_reg_id,
+            act_reg_status_id,
+            note
+          )
+          values (
+            ${activityRegId},
+            ${activity.nextRegStatusIdAfterPayment},
+            'Оплачено'
+          )
+        `);
+
+      // TODO: QR
+    }
+
+    // оплата за каждого участника
+    if (activity.paymentConfig === "PER_PARTICIPANT") {
+      if (!actReg.peId) {
+        throw Error("peId not found");
+      }
+
+      const members = await selPool.any(sql.type(
+        z.object({
+          peMemberId: DbSchema.act.peMembers.peMemberId,
+          userId: DbSchema.act.peMembers.userId,
+        }),
+      )`
+        select
+          pm.pe_member_id "peMemberId",
+          pm.user_id "userId"
+        from
+          act.pe_members pm
+        where
+          pm.pe_id = ${actReg.peId}
+      `);
+
+      const memberIds = members.map((member) => member.peMemberId);
+
+      const paidMemberRows = await selPool.any(sql.unsafe`
+        select distinct
+          oi.pe_member_id  -- Выбираем ID тех, кто заплатил
+        from
+          shop.order_items oi
+        where
+          oi.pe_member_id = ANY(${sql.array(memberIds, "uuid")})
+          and oi.status = 'PAID'
+      `);
+
+      if (memberIds.length !== paidMemberRows.length) {
+        await updPool.query(sql.unsafe`
+          update act.activity_regs
+          set is_paid = false
+          where activity_reg_id = ${activityRegId}
+        `);
+      } else {
+        await updPool.query(sql.unsafe`
+          update act.activity_regs
+          set is_paid = true
+          where activity_reg_id = ${activityRegId}
+        `);
+        await updPool.query(sql.unsafe`
+          insert into act.act_reg_status_history (
+            activity_reg_id,
+            act_reg_status_id,
+            note
+          )
+          values (
+            ${activityRegId},
+            ${activity.nextRegStatusIdAfterPayment},
+            'Оплачены все участники'
+          )
+        `);
+      }
+    }
+  }
+
+  async checkActRegOwner(userId: string, activityRegId: string) {
+    const actReg = await this.getActReg(activityRegId);
+    if (!actReg) {
+      throw ApiError.BadRequest("actRegNotFound", "Не найдена регистрация");
+    }
+
+    if (actReg.userId === userId) {
+      return "owner";
+    }
+
+    const isMemeber = await selPool.exists(sql.unsafe`
+      select 1
+      from act.pe_members
+      where pe_id = ${actReg.peId}
+      and user_id = ${userId}
+    `);
+
+    if (isMemeber) return "member";
+
+    return undefined;
+  }
 }
 
 export const cActService = new CActService();

+ 18 - 0
src/modules/client/activities/participant-entities/c-pe-controller.ts

@@ -17,6 +17,7 @@ import { ApiError } from "#exceptions/api-error.js";
 import { v4, v7 } from "uuid";
 import { cPeService } from "./c-pe-service.js";
 import { cCustomFieldsValidateService } from "#modules/client/custom-fields/c-cf-validate-service.js";
+import dayjs from "dayjs";
 
 class ClientPeController {
   async getEventPeTypes(
@@ -428,6 +429,8 @@ class ClientPeController {
 
     if (!invite)
       throw ApiError.BadRequest("inviteNotFound", "Приглашение не найдено");
+    if (dayjs(invite.expirationDate).isBefore(dayjs()))
+      throw ApiError.BadRequest("inviteExpired", "Приглашение истекло");
 
     RouterUtils.validAndSendResponse(api.client.pe.GET_InviteInfo.res, res, {
       code: "success",
@@ -467,6 +470,21 @@ class ClientPeController {
       return;
     }
 
+    if (
+      invite.expirationDate &&
+      dayjs(invite.expirationDate).isBefore(dayjs())
+    ) {
+      RouterUtils.validAndSendResponse(
+        api.client.pe.POST_AcceptInvite.res,
+        res,
+        {
+          code: "inviteExpired",
+        },
+        400,
+      );
+      return;
+    }
+
     // TODO: много лишних данных
     const peMembers = await cPeService.getMembers(invite.peId);
     const isFound = peMembers.find((m) => m.userId === user.userId);

+ 4 - 6
src/modules/client/activities/participant-entities/c-pe-service.ts

@@ -120,7 +120,6 @@ class CPeService {
   async getPeForMember(peId: string) {
     return await selPool.maybeOne(sql.type(
       z.object({
-        peMemberId: DbSchema.act.peMembers.peMemberId,
         peId: DbSchema.act.partEntities.peId,
         peTypeId: DbSchema.act.partEntities.peTypeId,
         peTypeCode: DbSchema.act.peTypes.code,
@@ -131,7 +130,6 @@ class CPeService {
       }),
     )`
       select
-        pm.pe_member_id "peMemberId",
         pe.pe_id "peId",
         pt.pe_type_id "peTypeId",
         pt.code "peTypeCode",
@@ -140,9 +138,7 @@ class CPeService {
         pe.owner_id "ownerId",
         pe.name
       from
-        act.pe_members pm
-      left join act.part_entities pe on
-        pe.pe_id = pm.pe_member_id
+		act.part_entities pe
       left join act.pe_types pt on
         pt.pe_type_id = pe.pe_type_id
       where
@@ -210,6 +206,7 @@ class CPeService {
         peOwnerId: DbSchema.act.partEntities.ownerId,
         limitVal: DbSchema.act.peInvites.limitVal,
         countVal: DbSchema.act.peInvites.countVal,
+        expirationDate: DbSchema.act.peInvites.expirationDate,
       }),
     )`
       select
@@ -219,7 +216,8 @@ class CPeService {
         pe."name" "peName",
         pe.owner_id "peOwnerId",
         i.limit_val "limitVal",
-        i.count_val "countVal"
+        i.count_val "countVal",
+        i.expiration_date "expirationDate"
       from
         act.pe_invites i
       join act.part_entities pe on

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

@@ -2,7 +2,7 @@ import { apiTypes } from "#api/current-api.js";
 import { z } from "zod";
 
 export const validatePeForAct = (
-  activity: z.infer<typeof apiTypes.activities.Activity>,
+  activity: z.infer<typeof apiTypes.activities.ActivityWithValidators>,
   pe: z.infer<typeof apiTypes.activities.PeForActivity>,
 ) => {
   let isValid = true;