using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using static GB5Shared.GB5Constant.Constant; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using PERMBLL.Interfaces; using PERMDAL.DTOs; using PERMDAL.Interfaces; using System.Text.Json; namespace PERMBLL.Implementations; public class AppraisalTypeBLL : IAppraisalTypeBLL { private readonly IAppraisalTypeDAL _dal; private readonly IValidation _validation; private readonly KeyInvalidate _keyInvalidate; private readonly EventLogPublish _eventLog; private readonly ILogger _logger; public AppraisalTypeBLL( IAppraisalTypeDAL dal, IValidation validation, KeyInvalidate keyInvalidate, EventLogPublish eventLog, ILogger logger) { _dal = dal; _validation = validation; _keyInvalidate = keyInvalidate; _eventLog = eventLog; _logger = logger; } public async Task GetAppraisalTypeAsync(int id, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-appraisaltype", new { id }); var dto = await _dal.GetAppraisalTypeAsync(id, login, ct).ConfigureAwait(false); if (dto is null) return JsonSerializer.Serialize((object?)null); var kras = (await _dal.GetAppraisalTypeKRAsAsync(id, login, ct).ConfigureAwait(false)).ToList(); if (kras.Count > 0) { var kraIds = kras.Select(k => k.AppraisalTypeKRAId).ToList(); var measures = await _dal.GetMeasuresForKRAsAsync(kraIds, login, ct).ConfigureAwait(false); var measuresLookup = measures.ToLookup(m => m.AppraisalTypeKRAId); foreach (var kra in kras) kra.Measures = measuresLookup[kra.AppraisalTypeKRAId].ToList(); } dto.KRAs = kras; return JsonSerializer.Serialize(dto); } catch (Exception ex) { GB5Trace.MarkFailed("get-appraisaltype-failed", ex); _logger.LogError(ex, "GetAppraisalType failed for Id {Id}", id); throw; } } public async Task GetAppraisalTypeListAsync( short? status, int page, int pageSize, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-appraisaltype-list", new { status, page }); var list = await _dal.GetAppraisalTypeListAsync(status, page, pageSize, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } catch (Exception ex) { GB5Trace.MarkFailed("get-appraisaltype-list-failed", ex); _logger.LogError(ex, "GetAppraisalTypeList failed"); throw; } } public async Task GetAppraisalTypeSelectListAsync(CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) { try { var list = await _dal.GetAppraisalTypeSelectListAsync(criteriaDTO, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } catch (Exception ex) { GB5Trace.MarkFailed("get-appraisaltype-selectlist-failed", ex); _logger.LogError(ex, "GetAppraisalTypeSelectList failed"); throw; } } public async Task SaveAppraisalTypeAsync(AppraisalTypeDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-appraisaltype", new { dto.AppraisalTypeId }); await _validation.NotEmpty(dto.AppraisalTypeCode, nameof(dto.AppraisalTypeCode)).ConfigureAwait(false); await _validation.NotEmpty(dto.AppraisalTypeName, nameof(dto.AppraisalTypeName)).ConfigureAwait(false); if (dto.KRAs.Count == 0) return "At least one KRA is required in an Appraisal Type."; decimal totalKRAWeight = dto.KRAs.Sum(k => k.KRAMeasureWeightage + k.KRAGoalWeightage); if (totalKRAWeight != 100m) return $"Total KRA weightages (Measure + Goal) must sum to 100. Current total: {totalKRAWeight}."; foreach (var kra in dto.KRAs.Where(k => k.Measures.Count > 0)) { decimal measureTotal = kra.Measures.Sum(m => m.Weightage); if (measureTotal != 100m) return $"Measure weightages for KRA {kra.KRAId} must sum to 100. Current total: {measureTotal}."; } bool duplicate = await _dal.CodeExistsAsync(dto.AppraisalTypeCode, dto.AppraisalTypeId, login, ct).ConfigureAwait(false); if (duplicate) return $"Appraisal type code '{dto.AppraisalTypeCode}' already exists."; bool isNew = dto.AppraisalTypeId == 0; GB5Trace.Step("save-appraisaltype", new { dto.AppraisalTypeId, isNew }); int appraisalTypeId; if (isNew) { dto.TenantId = login.ClientId; dto.Status = 1; appraisalTypeId = await _dal.InsertAppraisalTypeAsync(dto, login, ct).ConfigureAwait(false); } else { appraisalTypeId = dto.AppraisalTypeId; await _dal.UpdateAppraisalTypeAsync(dto, login, ct).ConfigureAwait(false); // Replace all KRAs and measures await _dal.DeleteAppraisalTypeKRAsAndMeasuresAsync(appraisalTypeId, login, ct).ConfigureAwait(false); } foreach (var kra in dto.KRAs) { int kraId = await _dal.InsertKRAAsync(kra, appraisalTypeId, login, ct).ConfigureAwait(false); foreach (var measure in kra.Measures) await _dal.InsertMeasureAsync(measure, kraId, login, ct).ConfigureAwait(false); } GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVEPERMAPPRAISALTYPEEVENTTYPEID : EventTypeConstant.UPDATEPERMAPPRAISALTYPEEVENTTYPEID }); await _eventLog.PublishEventLogAsync( isNew ? "Appraisal Type Created" : "Appraisal Type Updated", new { dto.AppraisalTypeCode, dto.AppraisalTypeName, KRACount = dto.KRAs.Count }, isNew ? EventTypeConstant.SAVEPERMAPPRAISALTYPEEVENTTYPEID : EventTypeConstant.UPDATEPERMAPPRAISALTYPEEVENTTYPEID, appraisalTypeId, login, ct: ct).ConfigureAwait(false); var saveCacheKey = new CacheKeyGeneration().KeyGeneration(appraisalTypeId, EntityConstant.OBJECTPERMAPPRAISALTYPE, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(saveCacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {appraisalTypeId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-appraisaltype-failed", ex); _logger.LogError(ex, "SaveAppraisalType failed for Code {Code}", dto.AppraisalTypeCode); throw; } } public async Task DeleteAppraisalTypeAsync(int id, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-appraisaltype", new { id }); await _dal.DeleteAppraisalTypeKRAsAndMeasuresAsync(id, login, ct).ConfigureAwait(false); int rows = await _dal.DeleteAppraisalTypeAsync(id, login, ct).ConfigureAwait(false); if (rows == 0) return "Appraisal Type not found or already deleted."; GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETEPERMAPPRAISALTYPEEVENTTYPEID }); await _eventLog.PublishEventLogAsync( "Appraisal Type Deleted", new { AppraisalTypeId = id }, EventTypeConstant.DELETEPERMAPPRAISALTYPEEVENTTYPEID, id, login, ct: ct).ConfigureAwait(false); var deleteCacheKey = new CacheKeyGeneration().KeyGeneration(id, EntityConstant.OBJECTPERMAPPRAISALTYPE, CacheKeyLevel.CLIENT_LEVEL, login); await _keyInvalidate.AllInvalidateCache(deleteCacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-appraisaltype-failed", ex); _logger.LogError(ex, "DeleteAppraisalType failed for Id {Id}", id); throw; } } }