using Dapr.Client; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using System.ComponentModel.DataAnnotations; using TMSDAL.CustomCode.TxSkillUpgrade; using TMSDAL.DTO.TxSkillUpgrade; using static GB5Shared.GB5Constant.Constant; using TmsUpgradeStatus = TMSDAL.Enums.UpgradeStatus; namespace TMSBLL.TxSkillUpgrade { public class TxSkillUpgradeBLL : ITxSkillUpgradeBLL { private readonly ITxSkillUpgradeDAL _TxSkillUpgradeDAL; private readonly TMSDAL.CustomCode.TrainingProgramme.ITrainingProgrammeDAL _TrainingProgrammeDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly EventLogPublish _EventLog; private readonly DaprClient _DaprClient; private readonly ILogger _Logger; public TxSkillUpgradeBLL( ITxSkillUpgradeDAL txSkillUpgradeDAL, TMSDAL.CustomCode.TrainingProgramme.ITrainingProgrammeDAL trainingProgrammeDAL, AutoNumber autoNumber, IValidation validation, EventLogPublish eventLog, DaprClient daprClient, ILogger logger) { _TxSkillUpgradeDAL = txSkillUpgradeDAL; _TrainingProgrammeDAL = trainingProgrammeDAL; _AutoNumber = autoNumber; _Validation = validation; _EventLog = eventLog; _DaprClient = daprClient; _Logger = logger; } public async Task GetTxSkillUpgrade(int txSkillUpgradeId, LoginDTO loginDTO, CancellationToken ct) => await _TxSkillUpgradeDAL.GetTxSkillUpgrade(txSkillUpgradeId, loginDTO, ct).ConfigureAwait(false); public async Task GetTxSkillUpgradeList(int upgradeStatus, LoginDTO loginDTO, CancellationToken ct) => await _TxSkillUpgradeDAL.GetTxSkillUpgradeList(upgradeStatus, loginDTO, ct).ConfigureAwait(false); public async Task GetSelectListTxSkillUpgrade(CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct) => await _TxSkillUpgradeDAL.GetSelectListTxSkillUpgrade(criteriaDTO, loginDTO, ct).ConfigureAwait(false); public async Task ApplySkillMappingAsync(int programmeId, int assessmentId, int enrollmentId, LoginDTO loginDTO, CancellationToken ct) { // Non-fatal by design: a failure here must never roll back the training-completion // or assessment-submission write that triggered it. try { var mappings = await _TrainingProgrammeDAL.GetProgrammeSkillMapsForTrigger(programmeId, assessmentId, loginDTO, ct).ConfigureAwait(false); if (mappings == null || mappings.Count == 0) return; // Most-specific-match-wins: prefer a row scoped to this exact AssessmentId over // a whole-programme (-1) row for the same (SkillId, ProductFamilyId, ModelId). var resolved = mappings .GroupBy(m => (m.SkillId, m.ProductFamilyId, m.ModelId)) .Select(g => g.OrderByDescending(m => m.AssessmentId != -1).First()) .ToList(); if (resolved.Count == 0) return; int employeeId = await _TxSkillUpgradeDAL.GetEmployeeIdByEnrollment(enrollmentId, loginDTO, ct).ConfigureAwait(false); foreach (var mapping in resolved) { GB5Trace.Step("propose-skill-upgrade", new { enrollmentId, mapping.SkillId, mapping.IsAutoPropose }); bool isAuto = mapping.IsAutoPropose == 1; var autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "TXSKILLUPGRADE", loginDTO).ConfigureAwait(false); var dto = new TxSkillUpgradeDTO { TxSkillUpgradeId = autoNumberDTO.StartNumber, EnrollmentId = enrollmentId, SkillId = mapping.SkillId, ProductFamilyId = mapping.ProductFamilyId, ModelId = mapping.ModelId, PreviousLevelNo = 0, // unknown here — SkillManagement owns the current level and performs the never-downgrade check ProposedLevelNo = mapping.QualifiesForLevelNo, ProposedOn = DateTime.UtcNow, UpgradeStatus = (byte)(isAuto ? TmsUpgradeStatus.Approved : TmsUpgradeStatus.Proposed), ReviewedById = -1, ReviewedOn = isAuto ? DateTime.UtcNow : null, Remarks = isAuto ? "Auto-applied via ProgrammeSkillMap — no manual approval required." : null, CreatedById = loginDTO.UserId, CreatedOn = DateTime.UtcNow, ModifiedById = loginDTO.UserId, ModifiedOn = DateTime.UtcNow, TenantId = loginDTO.ClientId }; await _TxSkillUpgradeDAL.SaveTxSkillUpgrade(dto, loginDTO, ct).ConfigureAwait(false); var eventTypeId = isAuto ? EventTypeConstant.TMSSKILLUPGRADEAPPROVEDEVENTTYPEID : EventTypeConstant.TMSSKILLUPGRADEPROPOSEDEVENTTYPEID; GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); await _EventLog.PublishEventLogAsync( isAuto ? "Skill Upgrade Auto-Approved" : "Skill Upgrade Proposed", new { dto.TxSkillUpgradeId, employeeId, mapping.SkillId, dto.ProposedLevelNo }, eventTypeId, dto.TxSkillUpgradeId, loginDTO, ct: ct).ConfigureAwait(false); if (isAuto) await PublishSkillGrantConfirmedAsync(dto, employeeId, loginDTO, ct).ConfigureAwait(false); } } catch (Exception ex) { GB5Trace.MarkFailed("apply-skill-mapping-failed", ex); _Logger.LogWarning(ex, "ApplySkillMappingAsync failed for EnrollmentId {EnrollmentId} — training/assessment write remains complete", enrollmentId); } } public async Task SaveTxSkillUpgrade(TxSkillUpgradeDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-save-skill-upgrade", new { dto.TxSkillUpgradeId, dto.EnrollmentId, dto.SkillId }); if (dto.EnrollmentId == 0) throw new ValidationException($"{nameof(dto.EnrollmentId)} is required."); if (dto.SkillId == 0) throw new ValidationException($"{nameof(dto.SkillId)} is required."); dto.ProductFamilyId = dto.ProductFamilyId == 0 ? -1 : dto.ProductFamilyId; dto.ModelId = dto.ModelId == 0 ? -1 : dto.ModelId; dto.ReviewedById = dto.ReviewedById == 0 ? -1 : dto.ReviewedById; dto.UpdatedProfileId = dto.UpdatedProfileId == 0 ? -1 : dto.UpdatedProfileId; if (dto.ProposedOn == default) dto.ProposedOn = DateTime.UtcNow; dto.ModifiedById = loginDTO.UserId; dto.ModifiedOn = DateTime.UtcNow; dto.TenantId = loginDTO.ClientId; bool isNew = dto.TxSkillUpgradeId == 0; GB5Trace.Step("save-skill-upgrade", new { dto.TxSkillUpgradeId, isNew }); if (isNew) { autoNumberDTO = await _AutoNumber.GetAutoNumber(1, "TXSKILLUPGRADE", loginDTO).ConfigureAwait(false); dto.TxSkillUpgradeId = autoNumberDTO.StartNumber; dto.CreatedById = loginDTO.UserId; dto.CreatedOn = DateTime.UtcNow; dto.Version = 1; await _TxSkillUpgradeDAL.SaveTxSkillUpgrade(dto, loginDTO, ct).ConfigureAwait(false); } else { await _TxSkillUpgradeDAL.UpdateTxSkillUpgrade(dto, loginDTO, ct).ConfigureAwait(false); } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.TMSSKILLUPGRADEPROPOSEDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( isNew ? "Skill Upgrade Manually Proposed" : "Skill Upgrade Updated", new { dto.TxSkillUpgradeId, dto.EnrollmentId, dto.SkillId, dto.ProposedLevelNo }, EventTypeConstant.TMSSKILLUPGRADEPROPOSEDEVENTTYPEID, dto.TxSkillUpgradeId, loginDTO, ct: ct).ConfigureAwait(false); var savedJson = await _TxSkillUpgradeDAL.GetTxSkillUpgrade(dto.TxSkillUpgradeId, loginDTO, ct).ConfigureAwait(false); var saved = Newtonsoft.Json.JsonConvert.DeserializeObject(savedJson); return isNew ? string.Format(SuccessResponse.SaveSuccessWithCodeAndName, saved?.SkillCode, saved?.SkillName) : string.Format(SuccessResponse.UpdateSuccessWithCodeAndName, saved?.SkillCode, saved?.SkillName); } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-skill-upgrade-failed", ex); if (autoNumberDTO != null && dto.TxSkillUpgradeId > 0) await _AutoNumber.RollbackAutoNumber("TXSKILLUPGRADE", dto.TxSkillUpgradeId, loginDTO).ConfigureAwait(false); throw; } } public async Task DeleteTxSkillUpgrade(int txSkillUpgradeId, LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-skill-upgrade", new { txSkillUpgradeId }); int rows = await _TxSkillUpgradeDAL.DeleteTxSkillUpgrade(txSkillUpgradeId, loginDTO, ct).ConfigureAwait(false); if (rows == 0) return ErrorResponse.DeleteNotFoundMessage; GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.TMSSKILLUPGRADEREJECTEDEVENTTYPEID }); await _EventLog.PublishEventLogAsync( "Skill Upgrade Deleted", new { TxSkillUpgradeId = txSkillUpgradeId }, EventTypeConstant.TMSSKILLUPGRADEREJECTEDEVENTTYPEID, txSkillUpgradeId, loginDTO, ct: ct).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-skill-upgrade-failed", ex); throw; } } public async Task ReviewTxSkillUpgrade(ReviewSkillUpgradeDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); try { GB5Trace.Step("validate-review-skill-upgrade", new { dto.TxSkillUpgradeId, dto.Decision }); await _Validation.NotEmpty(dto.Decision, nameof(dto.Decision)); if (!string.Equals(dto.Decision, "Approve", StringComparison.OrdinalIgnoreCase) && !string.Equals(dto.Decision, "Reject", StringComparison.OrdinalIgnoreCase)) throw new ValidationException("Decision must be 'Approve' or 'Reject'."); bool approve = string.Equals(dto.Decision, "Approve", StringComparison.OrdinalIgnoreCase); var upgradeDto = new TxSkillUpgradeDTO { TxSkillUpgradeId = dto.TxSkillUpgradeId, UpgradeStatus = (byte)(approve ? TmsUpgradeStatus.Approved : TmsUpgradeStatus.Rejected), ReviewedById = loginDTO.UserId, ReviewedOn = DateTime.UtcNow, RejectionReason = approve ? null : dto.ReviewRemarks, Remarks = dto.ReviewRemarks, ModifiedById = loginDTO.UserId, ModifiedOn = DateTime.UtcNow, TenantId = loginDTO.ClientId }; int rows = await _TxSkillUpgradeDAL.ReviewTxSkillUpgrade(upgradeDto, loginDTO, ct).ConfigureAwait(false); if (rows == 0) return ErrorResponse.UpdateNotFoundMessage; var eventTypeId = approve ? EventTypeConstant.TMSSKILLUPGRADEAPPROVEDEVENTTYPEID : EventTypeConstant.TMSSKILLUPGRADEREJECTEDEVENTTYPEID; GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); await _EventLog.PublishEventLogAsync( approve ? "Skill Upgrade Approved" : "Skill Upgrade Rejected", new { dto.TxSkillUpgradeId, dto.ReviewRemarks }, eventTypeId, dto.TxSkillUpgradeId, loginDTO, ct: ct).ConfigureAwait(false); if (approve) { // Re-read the full record so PublishSkillGrantConfirmedAsync has SkillId/ProductFamilyId/ // ModelId/ProposedLevelNo/EmployeeId — ReviewTxSkillUpgrade's DTO above only carried // review fields. var fullJson = await _TxSkillUpgradeDAL.GetTxSkillUpgrade(dto.TxSkillUpgradeId, loginDTO, ct).ConfigureAwait(false); var full = Newtonsoft.Json.JsonConvert.DeserializeObject(fullJson); if (full != null) await PublishSkillGrantConfirmedAsync(full, full.EmployeeId, loginDTO, ct).ConfigureAwait(false); } return SuccessResponse.UpdateSuccessMessage; } catch (ValidationException vex) { throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("review-skill-upgrade-failed", ex); throw; } } // Non-fatal: SkillManagement applying the grant is a downstream concern; a Dapr outage // must never roll back the (already-committed) TTXSKILLUPGRADE approval. private async Task PublishSkillGrantConfirmedAsync(TxSkillUpgradeDTO dto, int employeeId, LoginDTO loginDTO, CancellationToken ct) { try { var payload = new { TxSkillUpgradeId = dto.TxSkillUpgradeId, EmployeeId = employeeId, SkillId = dto.SkillId, ProductFamilyId = dto.ProductFamilyId, ModelId = dto.ModelId, LevelNo = dto.ProposedLevelNo, TenantId = loginDTO.ClientId, DatabaseName = loginDTO.DatabaseName, ClientId = loginDTO.ClientId, UserId = loginDTO.UserId }; await _DaprClient.PublishEventAsync("pubsub", "tms.skillgrant.confirmed", payload, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("skillgrant-confirmed-dapr-publish-failed", ex); _Logger.LogWarning(ex, "Dapr publish tms.skillgrant.confirmed failed for TxSkillUpgradeId {Id}", dto.TxSkillUpgradeId); } } } }