using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Validation; using Newtonsoft.Json; using System.ComponentModel.DataAnnotations; using TMSDAL.CustomCode.TrainingInstance; using TMSDAL.DTO.TrainingInstance; using TMSDAL.Enums; namespace TMSBLL.TrainingInstance { public class TrainingInstanceBLL : ITrainingInstanceBLL { private readonly ITrainingInstanceDAL _TrainingInstanceDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; public TrainingInstanceBLL(ITrainingInstanceDAL trainingInstanceDAL, AutoNumber autoNumber, IValidation validation) { _TrainingInstanceDAL = trainingInstanceDAL; _AutoNumber = autoNumber; _Validation = validation; } public async Task GetTrainingInstance(int instanceId, LoginDTO loginDTO) { try { var instance = await _TrainingInstanceDAL.GetTrainingInstance(instanceId, loginDTO); if (instance == null) return JsonConvert.SerializeObject(null); instance.Sessions = await _TrainingInstanceDAL.GetInstanceSessions(instanceId, loginDTO); instance.DeliveryConfig = await _TrainingInstanceDAL.GetDeliveryConfig(instanceId, loginDTO); return JsonConvert.SerializeObject(instance); } catch (Exception) { throw; } } public async Task GetTrainingInstancePageList(byte? instanceStatus, int? programmeId, int page, int size, LoginDTO loginDTO) { try { return await _TrainingInstanceDAL.GetTrainingInstancePageList(instanceStatus, programmeId, page, size, loginDTO); } catch (Exception) { throw; } } public async Task GetTrainingInstanceList(LoginDTO loginDTO) { try { return await _TrainingInstanceDAL.GetTrainingInstanceList(loginDTO); } catch (Exception) { throw; } } public async Task GetSelectList(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) => await _TrainingInstanceDAL.GetSelectList(criteriaDTO, login, ct); public async Task SaveTrainingInstance(TrainingInstanceDTO dto, LoginDTO loginDTO) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { await _Validation.NotEmpty(dto.InstanceCode, nameof(dto.InstanceCode)); if (dto.ProgrammeId == 0) throw new ValidationException("ProgrammeId is required."); if (dto.PlannedStartDate >= dto.PlannedEndDate) throw new ValidationException("PlannedStartDate must be before PlannedEndDate."); if (dto.MaxCapacity > dto.MaxCapacity) throw new ValidationException("MinParticipants cannot exceed MaxParticipants."); if (!Enum.IsDefined(typeof(InstanceStatus), dto.InstanceStatus)) throw new ValidationException($"Invalid InstanceStatus value: {dto.InstanceStatus}."); // New instances can only be Draft or Open if (dto.InstanceId == 0 && dto.InstanceStatus != (byte)InstanceStatus.Draft && dto.InstanceStatus != (byte)InstanceStatus.Open) throw new ValidationException("New instances must start with Draft or Open status."); dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; if (dto.InstanceId == 0) { autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "TRAININGINSTANCE", loginDTO); dto.InstanceId = autoNumberDTO.StartNumber; dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; await _TrainingInstanceDAL.SaveTrainingInstance(dto, loginDTO); await SaveChildRecords(dto, loginDTO); return string.Format(SuccessResponse.SaveSuccessWithCodeAndName, dto.InstanceCode, dto.InstanceTitle); } else { await _TrainingInstanceDAL.UpdateTrainingInstance(dto, loginDTO); // Replace sessions on update (delete + re-insert) await _TrainingInstanceDAL.DeleteInstanceSessions(dto.InstanceId, loginDTO); // Replace delivery config on update (delete + re-insert) — SaveChildRecords // below only INSERTs, so the old row must go first or the UNIQUE(INSTANCEID) // constraint rejects the re-save. if (dto.DeliveryConfig != null) await _TrainingInstanceDAL.DeleteDeliveryConfig(dto.InstanceId, loginDTO); await SaveChildRecords(dto, loginDTO); return string.Format(SuccessResponse.UpdateSuccessWithCodeAndName, dto.InstanceCode, dto.InstanceTitle); } } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception) { if (autoNumberDTO != null && dto.InstanceId > 0) await _AutoNumber.RollbackAutoNumber("TRAININGINSTANCE", dto.InstanceId, loginDTO); throw; } } public async Task DeleteTrainingInstance(int instanceId, LoginDTO loginDTO) { try { // Delete in FK-safe order: deepest children first before parents // ── Children of TTRAININGENROLLMENT (deepest first) ────────────── await _TrainingInstanceDAL.DeleteCertRenewalAlertsByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteCertificatesByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteAttemptResponsesByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteAssessmentAttemptsByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteEvaluationResponsesByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteEvaluationInstancesByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteAttendanceByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteSkillUpgradesByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteEnrollmentsByInstance(instanceId, loginDTO); // ── Direct children of TTRAININGINSTANCE ──────────────────────── await _TrainingInstanceDAL.DeleteCompletionsByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteEvaluationInsightsByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteLmsBridgeByInstance(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteInstanceSessions(instanceId, loginDTO); await _TrainingInstanceDAL.DeleteDeliveryConfig(instanceId, loginDTO); int result = await _TrainingInstanceDAL.DeleteTrainingInstance(instanceId, loginDTO); return result > 0 ? SuccessResponse.DeleteSuccessMessage : ErrorResponse.DeleteNotFoundMessage; } catch (Exception) { throw; } } private async Task SaveChildRecords(TrainingInstanceDTO dto, LoginDTO loginDTO) { if (dto.Sessions != null) { foreach (var session in dto.Sessions) { session.InstanceId = dto.InstanceId; session.TenantId = loginDTO.ClientId; // The FE always sends InstanceSessionId=0 for "the session as currently // configured", even when a row already exists for this // (InstanceId, ProgrammeSessionId) — e.g. one that already has attendance // recorded and so was NOT deleted by DeleteInstanceSessions above. Look up by // the stable natural key rather than trusting the payload's id, so an existing // session gets updated in place instead of inserted a second time under a new // AutoNumber id (which would violate FK_TTRAININGATTENDANCE_INSTANCESESSIONID's // referenced row disappearing, or just silently orphan attendance history). if (session.InstanceSessionId == 0 && session.ProgrammeSessionId.HasValue) { var existingId = await _TrainingInstanceDAL.GetExistingSessionId( dto.InstanceId, session.ProgrammeSessionId.Value, loginDTO); session.InstanceSessionId = existingId ?? 0; } if (session.InstanceSessionId == 0) { var sessionAuto = await _AutoNumber.GetAutoNumber(1, "INSTANCESESSION", loginDTO); session.InstanceSessionId = sessionAuto.StartNumber; await _TrainingInstanceDAL.SaveInstanceSession(session, loginDTO); } else { await _TrainingInstanceDAL.UpdateInstanceSession(session, loginDTO); } } } if (dto.DeliveryConfig != null && dto.InstanceId > 0) { dto.DeliveryConfig.InstanceId = dto.InstanceId; dto.DeliveryConfig.TenantId = loginDTO.ClientId; // TINSTANCEDELIVERYCFG.EXTERNALVENDORID/LMSCOURSEID/OJTBUDDYID/OJTSUPERVISORID are // NOT NULL DEFAULT -1 (sentinel "none", not a real NULL) — the DTO models them as // nullable for API ergonomics (caller can just omit an unused field), so normalize // here rather than pushing every caller to remember to send -1 explicitly. dto.DeliveryConfig.ExternalVendorId ??= -1; dto.DeliveryConfig.LMSCourseId ??= -1; dto.DeliveryConfig.OJTBuddyId ??= -1; dto.DeliveryConfig.OJTSupervisorId ??= -1; if (dto.DeliveryConfig.InstanceDeliveryConfigId == 0) { var configAuto = await _AutoNumber.GetAutoNumber(1, "INSTANCEDELIVERYCFG", loginDTO); dto.DeliveryConfig.InstanceDeliveryConfigId = configAuto.StartNumber; } await _TrainingInstanceDAL.SaveDeliveryConfig(dto.DeliveryConfig, loginDTO); } } } }