using System.ComponentModel.DataAnnotations; using System.Text.Json; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using TMSDAL.CustomCode.AssessmentAssignment; using TMSDAL.DTO.AssessmentAssignment; using static GB5Shared.GB5Constant.Constant; namespace TMSBLL.AssessmentAssignment { public class AssessmentAssignmentBLL : IAssessmentAssignmentBLL { private readonly IAssessmentAssignmentDAL _AssessmentAssignmentDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly EventLogPublish _EventLog; public AssessmentAssignmentBLL(IAssessmentAssignmentDAL assessmentAssignmentDAL, AutoNumber autoNumber, IValidation validation, EventLogPublish eventLog) { _AssessmentAssignmentDAL = assessmentAssignmentDAL; _AutoNumber = autoNumber; _Validation = validation; _EventLog = eventLog; } public async Task GetAssessmentAssignment(int assessmentAssignmentId, LoginDTO loginDTO, CancellationToken ct) => await _AssessmentAssignmentDAL.GetAssessmentAssignment(assessmentAssignmentId, loginDTO, ct).ConfigureAwait(false); public async Task GetAssessmentAssignmentList(int employeeId, int assessmentId, LoginDTO loginDTO, CancellationToken ct) => await _AssessmentAssignmentDAL.GetAssessmentAssignmentList(employeeId, assessmentId, loginDTO, ct).ConfigureAwait(false); public async Task SaveAssessmentAssignment(AssessmentAssignmentDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-assessment-assignment", new { dto.AssessmentId, dto.EmployeeId }); await _Validation.NotEmpty(dto.AssessmentId.ToString(), nameof(dto.AssessmentId)); await _Validation.NotEmpty(dto.EmployeeId.ToString(), nameof(dto.EmployeeId)); if (dto.DueDate < DateTime.UtcNow.Date) throw new ValidationException("DueDate cannot be in the past."); dto.TenantId = loginDTO.ClientId; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; dto.AssignedById = loginDTO.UserId; dto.AssignedOn = DateTime.UtcNow; dto.AssignmentStatus = (byte)TMSDAL.Enums.AssignmentStatus.Assigned; dto.SourceType = 5; // CK_MASSESSMENTASSIGNMENT_SOURCETYPE requires valid source; override FE value // Normalize nullable date sentinels — empty string / MinValue from client must // not reach SQL Server (valid range starts 1753-01-01). if (dto.ResolvedOn.HasValue && dto.ResolvedOn.Value == DateTime.MinValue) dto.ResolvedOn = null; if (dto.EnrollmentId == 0) dto.EnrollmentId = -1; if (dto.ResolvedAttemptId == 0) dto.ResolvedAttemptId = -1; if (dto.AssignedById == 0) dto.AssignedById = -1; bool isNew = dto.AssessmentAssignmentId == 0; GB5Trace.Step("save-assessment-assignment", new { dto.AssessmentAssignmentId, isNew }); if (isNew) { autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "ASSESSMENTASSIGNMENT", loginDTO).ConfigureAwait(false); dto.AssessmentAssignmentId = autoNumberDTO.StartNumber; dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; } await _AssessmentAssignmentDAL.SaveAssessmentAssignment(dto, loginDTO, ct).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.TMSASSESSMENTASSIGNMENTSAVEDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "Assessment Assignment Saved", new { dto.AssessmentAssignmentId, dto.AssessmentId, dto.EmployeeId, dto.DueDate }, EventTypeConstant.TMSASSESSMENTASSIGNMENTSAVEDEVENTTYPEID, dto.AssessmentAssignmentId, loginDTO, ct: ct).ConfigureAwait(false); var savedJson = await _AssessmentAssignmentDAL.GetAssessmentAssignment(dto.AssessmentAssignmentId, loginDTO, ct).ConfigureAwait(false); var saved = JsonSerializer.Deserialize( savedJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return isNew ? string.Format(SuccessResponse.SaveSuccessWithCodeAndName, saved?.EmployeeCode, saved?.EmployeeName) : string.Format(SuccessResponse.UpdateSuccessWithCodeAndName, saved?.EmployeeCode, saved?.EmployeeName); } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-assessment-assignment-failed", ex); if (autoNumberDTO != null && dto.AssessmentAssignmentId > 0) await _AutoNumber.RollbackAutoNumber("ASSESSMENTASSIGNMENT", dto.AssessmentAssignmentId, loginDTO).ConfigureAwait(false); throw; } } public async Task SkipAssessmentAssignment(SkipAssignmentDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); try { GB5Trace.Step("validate-skip-assessment-assignment", new { dto.AssessmentAssignmentId }); await _Validation.NotEmpty(dto.SkipReason, nameof(dto.SkipReason)); int rows = await _AssessmentAssignmentDAL.SkipAssessmentAssignment(dto, loginDTO, ct).ConfigureAwait(false); if (rows == 0) return ErrorResponse.UpdateNotFoundMessage; GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.TMSASSESSMENTASSIGNMENTSKIPPEDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "Assessment Assignment Skipped", new { dto.AssessmentAssignmentId, dto.SkipReason }, EventTypeConstant.TMSASSESSMENTASSIGNMENTSKIPPEDEVENTTYPEID, dto.AssessmentAssignmentId, loginDTO, ct: ct).ConfigureAwait(false); return SuccessResponse.UpdateSuccessMessage; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("skip-assessment-assignment-failed", ex); throw; } } // Called from AssessmentAttemptBLL after Submit/Evaluate finalizes an attempt — non-fatal // by design (a no-op when no assignment exists for this attempt is the common case, not // an error), so this never throws outward; failures are logged via GB5Trace only. public async Task ResolveAssignmentOnAttemptAsync(int assessmentId, int enrollmentId, int assessmentAttemptId, LoginDTO loginDTO, CancellationToken ct) { try { await _AssessmentAssignmentDAL.ResolveAssignmentOnAttempt(assessmentId, enrollmentId, assessmentAttemptId, loginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("resolve-assessment-assignment-on-attempt-failed", ex); } } public async Task SweepMissedAssignmentsAsync(LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step("sweep-missed-assessment-assignments", new { loginDTO.ClientId }); int rows = await _AssessmentAssignmentDAL.SweepMissedAssignments(loginDTO, ct).ConfigureAwait(false); if (rows > 0) { GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.TMSASSESSMENTASSIGNMENTMISSEDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "Assessment Assignments Missed (Sweep)", new { RowsAffected = rows }, EventTypeConstant.TMSASSESSMENTASSIGNMENTMISSEDEVENTTYPEID, -1, loginDTO, ct: ct).ConfigureAwait(false); } return $"{rows} assignment(s) marked Missed."; } catch (Exception ex) { GB5Trace.MarkFailed("sweep-missed-assessment-assignments-failed", ex); throw; } } } }