// db import { selPool, updPool } from "#db"; import { DbSchema } from "#db-schema"; import { sql } from "slonik"; // api import { api } from "#api"; // other import { z } from "zod"; import { RouterUtils } from "#utils/router-utils.js"; import { Request, Response } from "express"; import sessionService from "#modules/client/users/auth/services/session-service.js"; import { apiTypes } from "#api/current-api.js"; import { validateActValidators } from "./validators/act-validators.js"; import { ApiError } from "#exceptions/api-error.js"; import { cCustomFieldsValidateService } from "../custom-fields/c-cf-validate-service.js"; 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"; import { PeMemberWithIdentityShema } from "#api/v_0.1.0/types/pe-types.js"; class ClientActivitiesController { async getEventActivities( req: Request, res: Response, // next: NextFunction ) { const event = await sessionService.getCurrentEventFromReq(req); const categories = await selPool.any(sql.type( apiTypes.activities.ActCategory, )` select c.category_id "categoryId" , c.code, c."name", c.parent_id "parentId", p.code "parentCode" from act.activity_categories c left join act.activity_categories p on c.parent_id = p.category_id where c.event_inst_id = ${event.eventInstId} and c.parent_id is null `); const activities = await selPool.any(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, categoryCode: DbSchema.act.activityCategories.code, validators: z.array(apiTypes.activities.ActValidator), peTypes: z.array( z.object({ peTypeId: DbSchema.act.peTypes.peTypeId, code: DbSchema.act.peTypes.code, name: DbSchema.act.peTypes.name, }), ), isUserReg: DbSchema.act.activities.isUserReg, }), )` select a.activity_id "activityId", a.code, a.public_name "publicName", a.event_inst_id "eventInstId", a.category_id "categoryId", a.category_code "categoryCode", a.validators, a.pe_types "peTypes", a.is_user_reg "isUserReg" from act.act_with_validators a where a.event_inst_id = ${event.eventInstId} and a.category_id is null `); 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 { 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, }; }), ); RouterUtils.validAndSendResponse( api.client.activities.GET_EventActivities.res, res, { code: "success", categories: [...categories], activities: [...validatedActivities], }, ); } async getCategory(req: Request, res: Response) { const { categoryCode } = api.client.activities.GET_Category.req.params.parse(req.params); const categories = await selPool.any(sql.type( apiTypes.activities.ActCategory, )` select c.category_id "categoryId" , c.code, c."name", c.parent_id "parentId", p.code "parentCode" from act.activity_categories c left join act.activity_categories p on c.parent_id = p.category_id where p.code = ${categoryCode} `); const activities = await selPool.any(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, categoryCode: DbSchema.act.activityCategories.code, validators: z.array(apiTypes.activities.ActValidator), peTypes: z.array( z.object({ peTypeId: DbSchema.act.peTypes.peTypeId, code: DbSchema.act.peTypes.code, name: DbSchema.act.peTypes.name, }), ), isUserReg: DbSchema.act.activities.isUserReg, }), )` select a.activity_id "activityId", a.code, a.public_name "publicName", a.event_inst_id "eventInstId", a.category_id "categoryId", a.category_code "categoryCode", a.validators, a.pe_types "peTypes", a.is_user_reg "isUserReg" from act.act_with_validators a where a.category_code = ${categoryCode} `); 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 { 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, }; }), ); RouterUtils.validAndSendResponse( api.client.activities.GET_Category.res, res, { code: "success", categories: [...categories], activities: [...validatedActivities], }, ); } async getActRegData(req: Request, res: Response) { const event = await sessionService.getCurrentEventFromReq(req); const user = sessionService.getUserFromReq(req); const { activityCode } = api.client.activities.GET_ActRegData.req.params.parse(req.params); const actRegData = await cActService.getActRegDataWithUserCopyValuesAndActValidators( user.userId, event.eventId, activityCode, ); // TODO: заменить все BadRequest if (!actRegData) throw ApiError.BadRequest( "actRegDataNotFound", "Данные для регистрации на мероприятии не найдены", ); // 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: { 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, }, }, ); } async registerToAct(req: Request, res: Response) { const { fields, peId } = api.client.activities.POST_RegisterToAct.req.body.parse( JSON.parse(req.body.body), ); const { activityCode } = api.client.activities.POST_RegisterToAct.req.params.parse(req.params); const files = req.files; const user = sessionService.getUserFromReq(req); const actRegData = await cActService.getActRegDataWithFieldsAndValidatorsAndActValidators( activityCode, ); if (!actRegData) throw ApiError.BadRequest( "actRegDataNotFound", "Данные для регистрации на мероприятии не найдены", ); // // проверка доступа if (!actRegData.isUserReg) { if (!peId) throw ApiError.BadRequest( "peIdNotFound", "ID сущности участия не передан", ); const isOwner = await cPeService.checkPeOwner(user.userId, peId); if (!isOwner) throw ApiError.ForbiddenError(); } // // -- ВАЛИДАЦИЯ -- // валидация активности 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(", "), ); // // валидация pe if (!actRegData.isUserReg) { if (!peId) throw ApiError.BadRequest( "peIdNotFound", "ID сущности участия не передан", ); const pe = await cPeService.getPeWithValues(peId); const members = await cPeService.getPeMembersWithFields(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); const fullPe = { ...pe, members: [...members], }; const validationResult = await validatePeForAct(actRegData, fullPe); if (!validationResult.isValid) throw ApiError.BadRequest( "peValidatorFailed", validationResult.messages.join(", "), ); } // // валидация формы const refFields = actRegData.fields.map((f) => ({ ...f, idKey: "arffId", })); const validationResult = await cCustomFieldsValidateService.processAndValidateFields({ inputFields: fields, referenceFields: refFields, files, idKey: "arffId", addOldValue: false, }); if (!validationResult.isValid) throw ApiError.BadRequest( "fieldsValidationFailed", JSON.stringify(validationResult.messages), ); const validatedFields = validationResult.checkedfields; // // // вставляем в базу и сохраняем файлы const activityRegId = v7(); let activityRegNumber = generateRandomNumber(); while (await cActService.checkActivityRegNumber(activityRegNumber)) { activityRegNumber = generateRandomNumber(); } await updPool.transaction(async (tr) => { if (actRegData.isUserReg) { // для user // сама регистрация await tr.query(sql.unsafe` insert into act.activity_regs (activity_reg_id, activity_id, user_id, number) values (${activityRegId}, ${actRegData.activityId}, ${user.userId}, ${activityRegNumber}) `); } else { // для pe if (!peId) throw ApiError.BadRequest( "peIdNotFound", "ID сущности участия не передан", ); // сама регистрация await tr.query(sql.unsafe` insert into act.activity_regs (activity_reg_id, activity_id, pe_id, number) values (${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, action: "activityPeReg", inputFields: validatedFields, files, isDeleteBefore: false, }); // TODO: отправка уведомления }); RouterUtils.validAndSendResponse( api.client.activities.POST_RegisterToAct.res, res, { code: "success", activityRegId, }, ); } 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.getActRegWithValues(activityRegId); if (!r) throw ApiError.BadRequest( "actRegNotFound", "Регистрация на мероприятии не найдена", ); let peMembers: | (z.infer & { isPaid: boolean })[] | undefined = undefined; if (r.peId) { const m = [...(await cPeService.getPeMembersWithIdentity(r.peId))]; peMembers = await Promise.all( m.map(async (m) => ({ ...m, // TODO: слишком много операций, проще одним запросом вместо getPeMembersWithIdentity isPaid: await cActService.checkPeMemberActivityRegPayment({ peMemberId: m.peMemberId, activityRegId: activityRegId, }), })), ); } actReg = { ...r, peMembers, 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, }, ); } async getActRegForPatch(req: Request, res: Response) { const { activityRegId } = api.client.activities.GET_ActRegForPatch.req.params.parse(req.params); const actReg = await cActService.getActRegWithFieldsAndValuesWithValidators( activityRegId, ); if (!actReg) throw ApiError.BadRequest( "actRegNotFound", "Регистрация на мероприятии не найдена", ); const user = sessionService.getUserFromReq(req); if (actReg.userId !== user.userId && actReg.peOwnerId !== user.userId) throw ApiError.ForbiddenError(); RouterUtils.validAndSendResponse( api.client.activities.GET_ActRegForPatch.res, res, { code: "success", actReg, }, ); } async patchActReg(req: Request, res: Response) { const { activityRegId } = api.client.activities.PATCH_ActReg.req.params.parse(req.params); const { peId, fields } = api.client.activities.PATCH_ActReg.req.formData.body.parse( JSON.parse(req.body.body), ); const files = req.files; const user = sessionService.getUserFromReq(req); const actReg = await cActService.getActRegForPeMember(activityRegId); if (!actReg) throw ApiError.BadRequest( "actRegNotFound", "Регистрация на мероприятии не найдена", ); if (actReg.userId !== user.userId && actReg.peOwnerId !== user.userId) throw ApiError.ForbiddenError(); const actRegData = await cActService.getActRegDataWithFieldsAndValidatorsAndActValidators( actReg.activityCode, ); const oldActRegWithValues = await cActService.getActRegWithValues( actReg.activityRegId, ); if (!actRegData || !oldActRegWithValues) throw ApiError.BadRequest( "actRegDataNotFound", "Данные для регистрации на мероприятии не найдены", ); // // // -- ВАЛИДАЦИЯ -- 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(", "), ); // // валидация pe if ( !actRegData.isUserReg && peId // может не быть потому что patch ) { const pe = await cPeService.getPeWithValues(peId); const members = await cPeService.getPeMembersWithFields(peId); if (!pe) throw ApiError.BadRequest("peNotFound", "Сущность участия не найдена"); const fullPe = { ...pe, members: [...members], }; const validationResult = await validatePeForAct(actRegData, fullPe); if (!validationResult.isValid) throw ApiError.BadRequest( "peValidatorFailed", validationResult.messages.join(", "), ); } // // валидация формы const refFields = actRegData.fields .map((f) => ({ ...f, idKey: "arffId", value: oldActRegWithValues.fields.find((v) => v.arffId === f.arffId) ?.value || null, isChangeResetStatus: oldActRegWithValues.fields.find((v) => v.arffId === f.arffId) ?.isChangeResetStatus || false, })) // только изменяемые .filter((f) => fields.some((ff) => ff.arffId === f.arffId)); const validationResult = await cCustomFieldsValidateService.processAndValidateFields({ inputFields: fields, referenceFields: refFields, files, idKey: "arffId", addOldValue: true, }); if (!validationResult.isValid) throw ApiError.BadRequest( "fieldsValidationFailed", JSON.stringify(validationResult.messages), ); const validatedFields = validationResult.checkedfields; // const isNeedReset = refFields.some((f) => f.isChangeResetStatus); // // вставляем в базу и сохраняем файлы await updPool.transaction(async (tr) => { if (!actRegData.isUserReg && peId) { await tr.query(sql.unsafe` update act.activity_regs set pe_id = ${peId} where activity_reg_id = ${activityRegId} `); } const initialRegStatusId = await cActService.getInitialRegStatusId( actRegData.activityId, ); // устанавливаем начальный статус если были измены поля сбрасывающие регистрацию if (isNeedReset) { 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, action: "activityPeReg", inputFields: validatedFields, files, isDeleteBefore: true, }); }); RouterUtils.validAndSendResponse( api.client.activities.PATCH_ActReg.res, res, { code: "success", }, ); } async cancelActivityReg(req: Request, res: Response) { const { activityRegId } = api.client.activities.DELETE_ActivityReg.req.params.parse(req.params); const user = sessionService.getUserFromReq(req); const role = await cActService.checkActRegOwner(user.userId, activityRegId); if (role !== "owner") throw ApiError.ForbiddenError(); await updPool.transaction(async (tr) => { await tr.query(sql.unsafe` update act.activity_regs set is_canceled = true where activity_reg_id = ${activityRegId} `); 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}, 'e324fddb-81fe-45f4-90fe-074c0beaae45', 'Заявка отменена') `); }); // возвраты await cActService.refundByActivityRegId(activityRegId); RouterUtils.validAndSendResponse( api.client.activities.DELETE_ActivityReg.res, res, { code: "success", }, ); } } export const clientActController = new ClientActivitiesController();