// db import { selPool, updPool } from "#db"; import { DbSchema } from "#db-schema"; import { sql } from "slonik"; // api import { api, apiTypes } from "#api"; // other import { z } from "zod"; import { RouterUtils } from "#utils/router-utils.js"; import { Request, Response } from "express"; import sessionService from "#modules/users/auth/services/session-service.js"; 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"; import { cActService } from "../c-act-service.js"; class ClientPeController { async getEventPeTypes( req: Request, res: Response, // next: NextFunction ) { const event = await sessionService.getCurrentEventFromReq(req); const peTypes = await selPool.any(sql.type( z.object({ peTypeId: DbSchema.act.peTypes.peTypeId, code: DbSchema.act.peTypes.code, name: DbSchema.act.peTypes.name, }), )` select pe_type_id as "peTypeId", code, "name" from act.pe_types pt where pt.event_inst_id = ${event.eventInstId} `); RouterUtils.validAndSendResponse(api.client.pe.GET_EventPeTypes.res, res, { code: "success", peTypes: [...peTypes], }); } async getPeTypeForCreate( req: Request, res: Response, // next: NextFunction ) { const event = await sessionService.getCurrentEventFromReq(req); const user = sessionService.getUserFromReq(req); const { peTypeCode } = api.client.pe.GET_PeType.req.parse(req.params); const eventId = event.eventId; const userId = user.userId; const peType = await cPeService.getPeTypeWithFieldsAndUserCopyValues( userId, peTypeCode, eventId, ); if (!peType) throw ApiError.BadRequest( "peTypeNotFound", "Тип сущности участия не найден", ); RouterUtils.validAndSendResponse(api.client.pe.GET_PeType.res, res, { code: "success", peType: peType, }); } async createPe(req: Request, res: Response) { const event = await sessionService.getCurrentEventFromReq(req); const user = sessionService.getUserFromReq(req); const { fields, peTypeCode, name } = api.client.pe.POST_PartEntity.req.formData.body.parse( JSON.parse(req.body.body), ); const files = req.files; const peType = await cPeService.getPeTypeWithFields(peTypeCode); if (!peType) throw ApiError.BadRequest( "peTypeNotFound", "Тип сущности участия не найден", ); const refFields = peType.fields.map((f) => ({ ...f, idKey: "peFfId", })); // валидация const validationResult = await cCustomFieldsValidateService.processAndValidateFields({ inputFields: fields, referenceFields: refFields, files, idKey: "peFfId", addOldValue: false, }); if (!validationResult.isValid) throw ApiError.BadRequest( "fieldsValidationFailed", JSON.stringify(validationResult.messages), ); const validatedFields = validationResult.checkedfields; // // // вставляем в базу и сохраняем файлы const peId = v7(); await updPool.transaction(async (tr) => { await tr.query(sql.unsafe` insert into act.part_entities (pe_id, pe_type_id, event_inst_id, owner_id, name) values (${peId}, ${peType.peTypeId}, ${event.eventInstId}, ${user.userId}, ${name}) `); if (peType.isJoinAfterCreate) { await tr.query(sql.unsafe` insert into act.pe_members (pe_member_id, pe_id, user_id) values (${v7()}, ${peId}, ${user.userId}) `); } await cCustomFieldsValidateService.saveCustomFieldValuesInTransaction({ tr, parentId: peId, action: "peCreate", inputFields: validatedFields, files, isDeleteBefore: false, }); }); RouterUtils.validAndSendResponse(api.client.pe.POST_PartEntity.res, res, { code: "success", peId, }); } async getPeForPatch(req: Request, res: Response) { const { peId } = api.client.pe.GET_PeForPatch.req.parse(req.params); const user = sessionService.getUserFromReq(req); const pe = await cPeService.getPeWithValidatorsAndValues(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError(); RouterUtils.validAndSendResponse(api.client.pe.GET_PeForPatch.res, res, { code: "success", pe: pe, }); } async patchPe(req: Request, res: Response) { const { peId } = api.client.pe.PATCH_PartEntity.req.params.parse( req.params, ); const { name, fields } = api.client.pe.PATCH_PartEntity.req.formData.body.parse( JSON.parse(req.body.body), ); const files = req.files; const user = sessionService.getUserFromReq(req); const peType = await cPeService.getPeWithValidatorsAndValues(peId); if (!peType) throw ApiError.BadRequest( "peTypeNotFound", "Тип сущности участия не найден", ); // проверка доступа if (peType.ownerId !== user.userId) throw ApiError.ForbiddenError(); const refFields = peType.fields .map((f) => ({ ...f, idKey: "peFfId", })) // только изменяемые .filter((f) => fields.some((ff) => ff.peFfId === f.peFfId)); // валидация const validationResult = await cCustomFieldsValidateService.processAndValidateFields({ inputFields: fields, referenceFields: refFields, files, idKey: "peFfId", addOldValue: true, }); if (!validationResult.isValid) throw ApiError.BadRequest( "fieldsValidationFailed", JSON.stringify(validationResult.messages), ); const validatedFields = validationResult.checkedfields; // // // вставляем в базу и сохраняем файлы await updPool.transaction(async (tr) => { if (name) { await tr.query(sql.unsafe` update act.part_entities set name = ${name} where pe_id = ${peId} `); } await cCustomFieldsValidateService.saveCustomFieldValuesInTransaction({ tr, parentId: peId, action: "peCreate", inputFields: validatedFields, files, isDeleteBefore: true, }); }); RouterUtils.validAndSendResponse(api.client.pe.PATCH_PartEntity.res, res, { code: "success", }); } async getMyPes(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const event = await sessionService.getCurrentEventFromReq(req); const ownerPes = await selPool.any(sql.type( z.object({ peId: DbSchema.act.partEntities.peId, peTypeId: DbSchema.act.partEntities.peTypeId, peTypeCode: DbSchema.act.peTypes.code, peTypeName: DbSchema.act.peTypes.name, eventInstId: DbSchema.act.partEntities.eventInstId, ownerId: DbSchema.act.partEntities.ownerId, name: DbSchema.act.partEntities.name, }), )` 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" from act.part_entities pe join act.pe_types pt on pt.pe_type_id = pe.pe_type_id where pe.event_inst_id = ${event.eventInstId} and pe.owner_id = ${user.userId} `); const memberPes = await selPool.any(sql.type( z.object({ peMemberId: DbSchema.act.peMembers.peMemberId, peId: DbSchema.act.partEntities.peId, peTypeId: DbSchema.act.partEntities.peTypeId, peTypeCode: DbSchema.act.peTypes.code, peTypeName: DbSchema.act.peTypes.name, ownerId: DbSchema.act.partEntities.ownerId, name: DbSchema.act.partEntities.name, }), )` select pm.pe_member_id "peMemberId", pe.pe_id "peId", pe.event_inst_id "peInstId", pe.owner_id "ownerId", pe.pe_type_id "peTypeId", pt.code "peTypeCode", pt."name" "peTypeName", pe."name" from act.pe_members pm join act.part_entities pe on pe.pe_id = pm.pe_id join act.pe_types pt on pt.pe_type_id = pe.pe_type_id where pe.event_inst_id = ${event.eventInstId} and pm.user_id = ${user.userId} and pm.is_active = true `); RouterUtils.validAndSendResponse(api.client.pe.GET_MyPes.res, res, { code: "success", owner: [...ownerPes], memeber: [...memberPes], }); } async getPe(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { peId } = api.client.pe.GET_PartEntity.req.parse(req.params); const isOwner = await selPool.exists( sql.unsafe` select pe_id from act.part_entities pe where pe.owner_id = ${user.userId} and pe.pe_id = ${peId} `, ); // валделец if (isOwner) { const pe = await cPeService.getPeWithValues(peId); const members = await cPeService.getPeMembersWithFields(peId); const invites = await cPeService.getInvites(peId); const peMembersRequests = await cPeService.getPeMembersRequests(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError(); RouterUtils.validAndSendResponse(api.client.pe.GET_PartEntity.res, res, { code: "success", pe: { ...pe, members: [...members], invites: [...invites], peMembersRequests: [...peMembersRequests], userRole: "owner", }, }); return; } // участник const isMember = await selPool.exists( sql.unsafe` select pm.pe_id from act.pe_members pm where pm.user_id = ${user.userId} and pm.pe_id = ${peId} and pm.is_active = true `, ); if (!isMember) throw ApiError.ForbiddenError(); const pe = await cPeService.getPeForMember(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); RouterUtils.validAndSendResponse(api.client.pe.GET_PartEntity.res, res, { code: "success", pe: { ...pe, userRole: "member" }, }); } async joinToPe(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { peId } = api.client.pe.POST_JoinToPe.req.params.parse(req.params); const isOwner = await cPeService.checkPeOwner(user.userId, peId); if (!isOwner) throw ApiError.ForbiddenError(); const peMember = await selPool.maybeOne(sql.type( z.object({ isActive: DbSchema.act.peMembers.isActive, peMemberId: DbSchema.act.peMembers.peMemberId, }), )` select pm.pe_member_id "peMemberId", pm.is_active "isActive" from act.pe_members pm where pm.pe_id = ${peId} and pm.user_id = ${user.userId} `); if (peMember && peMember.isActive) { throw ApiError.BadRequest( "peMemberAlreadyExists", "Участник уже в сущности участия", ); } const memberId = v7(); await updPool.transaction(async (tr) => { if (peMember) { await tr.query(sql.unsafe` update act.pe_members set is_active = true where pe_member_id = ${peMember.peMemberId} `); } else { await tr.query(sql.unsafe` insert into act.pe_members (pe_member_id, pe_id, user_id, is_active) values (${memberId}, ${peId}, ${user.userId}, true) `); } await cPeService.updateAllActRegStatusByPe(peId); }); RouterUtils.validAndSendResponse(api.client.pe.POST_JoinToPe.res, res, { code: "success", memberId, }); } async createInvite(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { name, limitVal, expirationDate } = api.client.pe.POST_Invite.req.body.parse(req.body); const { peId } = api.client.pe.POST_Invite.req.params.parse(req.params); const isOwner = await cPeService.checkPeOwner(user.userId, peId); if (!isOwner) throw ApiError.ForbiddenError(); const peInviteId = v7(); const peInviteUuid = v4(); await updPool.transaction(async (t) => { await t.query(sql.unsafe` insert into act.pe_invites (pe_invite_id, pe_invite_uuid, pe_id, limit_val, name, expiration_date) values (${peInviteId}, ${peInviteUuid}, ${peId}, ${limitVal}, ${name}, ${expirationDate}) `); }); RouterUtils.validAndSendResponse(api.client.pe.POST_Invite.res, res, { code: "success", peInviteId, }); } async getInvites(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { peId } = api.client.pe.GET_Invites.req.parse(req.params); const isOwner = await cPeService.checkPeOwner(user.userId, peId); if (!isOwner) throw ApiError.ForbiddenError(); const invites = await cPeService.getInvites(peId); RouterUtils.validAndSendResponse(api.client.pe.GET_Invites.res, res, { code: "success", invites: [...invites], }); } async getInviteInfo(req: Request, res: Response) { const { peInviteUuid } = api.client.pe.GET_InviteInfo.req.params.parse( req.params, ); const user = sessionService.getUserFromReq(req); const invite = await cPeService.getInviteInfo(peInviteUuid); if (!invite) throw ApiError.BadRequest("inviteNotFound", "Приглашение не найдено"); const request = await cPeService.checkPeMemberInvite({ peInviteId: invite.peInviteId, userId: user.userId, }); if (dayjs(invite.expirationDate).isBefore(dayjs())) throw ApiError.BadRequest("inviteExpired", "Приглашение истекло"); if (request) throw ApiError.BadRequest("requestAlreadyExists", "Запрос уже отправлен"); RouterUtils.validAndSendResponse(api.client.pe.GET_InviteInfo.res, res, { code: "success", invite, }); } async acceptInvite(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { peInviteUuid } = api.client.pe.POST_AcceptInvite.req.params.parse( req.params, ); const invite = await cPeService.getInviteInfo(peInviteUuid); // приглашение не найдено if (!invite) { RouterUtils.validAndSendResponse( api.client.pe.POST_AcceptInvite.res, res, { code: "inviteNotFound", }, 400, ); return; } const pe = await selPool.maybeOne(sql.unsafe` select pe.pe_id, pt.max_members, members."membersCount" from act.part_entities pe left join act.pe_types pt on pt.pe_type_id = pe.pe_type_id left join ( select pe_id, count(*) as "membersCount" from act.pe_members where is_active = true group by pe_id ) members on members.pe_id = pe.pe_id where pe.pe_id = ${invite.peId} `); if (!pe) { throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); } // лимит превышен if ( (invite.limitVal && invite.countVal >= invite.limitVal) || (pe.maxMembers !== null && pe.membersCount >= pe.maxMembers) ) { RouterUtils.validAndSendResponse( api.client.pe.POST_AcceptInvite.res, res, { code: "inviteLimitExceeded", }, 400, ); return; } // приглашение истекло if ( invite.expirationDate && dayjs(invite.expirationDate).isBefore(dayjs()) ) { RouterUtils.validAndSendResponse( api.client.pe.POST_AcceptInvite.res, res, { code: "inviteExpired", }, 400, ); return; } // TODO: много лишних данных // участник уже в pe const peMembers = await cPeService.getPeMembersWithFields(invite.peId); const isFound = peMembers.find((m) => m.userId === user.userId); if (isFound) { RouterUtils.validAndSendResponse( api.client.pe.POST_AcceptInvite.res, res, { code: "peMemberAlreadyExists", }, 400, ); return; } // запрос уже отправлен const isPending = await cPeService.checkPeMemberInvite({ peInviteId: invite.peInviteId, userId: user.userId, }); if (isPending) { RouterUtils.validAndSendResponse( api.client.pe.POST_AcceptInvite.res, res, { code: "peMemberInviteAlreadyExists", }, 400, ); return; } await updPool.transaction(async (t) => { const id = v7(); await t.query(sql.unsafe` insert into act.pe_members_requests (pe_member_request_id, pe_invite_id, user_id, status) values (${id}, ${invite.peInviteId}, ${user.userId}, 'PENDING') `); await t.query(sql.unsafe` update act.pe_invites set count_val = count_val + 1 where pe_id = ${invite.peId} `); }); RouterUtils.validAndSendResponse(api.client.pe.POST_AcceptInvite.res, res, { code: "success", peId: invite.peId, }); } async patchPeMembersRequests(req: Request, res: Response) { const requests = api.client.pe.PATCH_PeMembersRequests.req.body.parse( req.body, ); const user = sessionService.getUserFromReq(req); const peIdsToRecheck: Set = new Set(); await updPool.transaction(async (t) => { for (const r of requests) { const request = await cPeService.getPeMemberRequest( r.peMemberRequestId, ); if (!request) throw ApiError.BadRequest("requestNotFound", "Запрос не найден"); const pe = await cPeService.getPeForMember(request.peId); if (!pe) throw ApiError.BadRequest( "peNotFound", "Сущность участия не найдена", ); if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError(); // участник уже в pe const peMembers = await cPeService.getPeMembersWithFields(pe.peId); const isFound = peMembers.find((m) => m.userId === request.userId); if (isFound) throw ApiError.BadRequest( "memberAlreadyInPe", "Участник уже добавлен", ); peIdsToRecheck.add(pe.peId); await t.query(sql.unsafe` update act.pe_members_requests set status = ${r.status} where pe_member_request_id = ${r.peMemberRequestId} `); if (r.status === "ACCEPTED") { await t.query(sql.unsafe` insert into act.pe_members (pe_id, user_id) values (${pe.peId}, ${request.userId}) `); } } }); for (const peId of peIdsToRecheck) { await cPeService.updateAllActRegStatusByPe(peId); } RouterUtils.validAndSendResponse( api.client.pe.PATCH_PeMembersRequests.res, res, { code: "success", }, ); } 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( req.params, ); const peMember = await cPeService.getPeMember(peMemberId); if (!peMember) throw ApiError.BadRequest("peMemberNotFound", "Участник не найден"); const pe = await cPeService.getPeForMember(peMember.peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError(); await updPool.transaction(async (t) => { await cActService.refundByPeMemberId({ peMemberId, peId: pe.peId, userId: user.userId, }); await t.query(sql.unsafe` update act.pe_members set is_active = false where pe_member_id = ${peMemberId} `); }); RouterUtils.validAndSendResponse(api.client.pe.DELETE_PeMember.res, res, { code: "success", }); } async getRelatedActivityRegsToPe(req: Request, res: Response) { const user = sessionService.getUserFromReq(req); const { peId } = api.client.pe.GET_RelatedActivityRegsToPe.req.params.parse( req.params, ); const pe = await cPeService.getPeForMember(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); if (pe.ownerId !== user.userId) throw ApiError.ForbiddenError(); const activityRegs = await cActService.getActivityRegsByPeId(peId); RouterUtils.validAndSendResponse( api.client.pe.GET_RelatedActivityRegsToPe.res, res, { code: "success", activityRegs: [...activityRegs], }, ); } } export const clientPeController = new ClientPeController();