using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using OKRBLL.Interfaces; using OKRDAL.DTOs; using OKRDAL.Interfaces; using System.Text.Json; using static GB5Shared.GB5Constant.Constant; namespace OKRBLL.Implementations; public class GoalBLL : IGoalBLL { private readonly IGoalDAL _dal; private readonly IGoalTrackDAL _trackDal; private readonly IObjectiveDAL _objectiveDal; private readonly IObjectiveBLL _objectiveBll; private readonly IValidation _validation; private readonly KeyInvalidate _keyInvalidate; private readonly EventLogPublish _eventLog; private readonly ILogger _logger; public GoalBLL( IGoalDAL dal, IGoalTrackDAL trackDal, IObjectiveDAL objectiveDal, IObjectiveBLL objectiveBll, IValidation validation, KeyInvalidate keyInvalidate, EventLogPublish eventLog, ILogger logger) { _dal = dal; _trackDal = trackDal; _objectiveDal = objectiveDal; _objectiveBll = objectiveBll; _validation = validation; _keyInvalidate = keyInvalidate; _eventLog = eventLog; _logger = logger; } public async Task GetGoalAsync(int goalId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-goal", new { goalId }); var dto = await _dal.GetGoalAsync(goalId, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(dto); } catch (Exception ex) { GB5Trace.MarkFailed("get-goal-failed", ex); _logger.LogError(ex, "GetGoal failed for GoalId {GoalId}", goalId); throw; } } public async Task GetGoalListAsync(int okrCycleId, int objectiveId, int employeeId, int status, int page, int pageSize, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-goal-list", new { okrCycleId, objectiveId, employeeId }); var list = await _dal.GetGoalListAsync(okrCycleId, objectiveId, employeeId, status, page, pageSize, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } catch (Exception ex) { GB5Trace.MarkFailed("get-goal-list-failed", ex); _logger.LogError(ex, "GetGoalList failed for OKRCycleId {OKRCycleId}", okrCycleId); throw; } } public async Task GetMyGoalsAsync(int okrCycleId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("get-my-goals", new { okrCycleId, login.UserId }); // ESS — EmployeeId always from login.UserId, never from request var list = await _dal.GetGoalListAsync(okrCycleId, 0, login.UserId, -1, 1, 100, login, ct).ConfigureAwait(false); return JsonSerializer.Serialize(list); } catch (Exception ex) { GB5Trace.MarkFailed("get-my-goals-failed", ex); _logger.LogError(ex, "GetMyGoals failed for UserId {UserId}", login.UserId); throw; } } public async Task SaveGoalAsync(GoalDTO dto, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-goal", new { dto.GoalId }); await _validation.NotEmpty(dto.Name, nameof(dto.Name)).ConfigureAwait(false); bool isNew = dto.GoalId == 0; GB5Trace.Step("save-goal", new { dto.GoalId, isNew }); int goalId = await _dal.SaveGoalAsync(dto, login, ct).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.SAVEOKRGOALEVENTTYPEID }); await _eventLog.PublishEventLogAsync( isNew ? "Goal Created" : "Goal Updated", new { dto.Name, dto.ObjectiveId }, EventTypeConstant.SAVEOKRGOALEVENTTYPEID, goalId, login, ct: ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(goalId, EntityConstant.OBJECTGOAL, CacheKeyLevel.USER_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {goalId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("save-goal-failed", ex); _logger.LogError(ex, "SaveGoal failed for Name {Name}", dto.Name); throw; } } public async Task DeleteGoalAsync(int goalId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("delete-goal", new { goalId }); var current = await _dal.GetGoalStatusAsync(goalId, login, ct).ConfigureAwait(false); if (current == 4) return "Cannot delete a Completed goal."; await _dal.DeleteGoalAsync(goalId, login, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(goalId, EntityConstant.OBJECTGOAL, CacheKeyLevel.USER_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-goal-failed", ex); _logger.LogError(ex, "DeleteGoal failed for GoalId {GoalId}", goalId); throw; } } public async Task UpdateGoalProgressAsync( int goalId, decimal progressPercent, decimal currentValue, string remarks, short confidenceLevel, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("validate-goal-progress", new { goalId, progressPercent }); await _validation.NotEmpty(remarks, nameof(remarks)).ConfigureAwait(false); var currentStatus = await _dal.GetGoalStatusAsync(goalId, login, ct).ConfigureAwait(false); if (currentStatus == 5) return "Cannot update progress on a Cancelled goal."; // Derive new status from progress short newStatus = progressPercent switch { 0 => 0, // NotStarted >= 100 => 4, // Completed >= 25 => 2, // OnTrack _ => 3 // AtRisk }; GB5Trace.Step("save-goal-progress", new { goalId, progressPercent, newStatus }); // INSERT track history (append-only) await _trackDal.InsertTrackAsync(new GoalTrackDTO { GoalId = goalId, ProgressPercent = progressPercent, CurrentValue = currentValue, Remarks = remarks, TrackedById = login.UserId, ConfidenceLevel = confidenceLevel }, login, ct).ConfigureAwait(false); // Update denormalized fields on TGOAL await _dal.UpdateGoalProgressAsync(goalId, progressPercent, currentValue, confidenceLevel, newStatus, login, ct).ConfigureAwait(false); // Rollup to parent objective int objectiveId = await _dal.GetGoalObjectiveIdAsync(goalId, login, ct).ConfigureAwait(false); await _objectiveBll.UpdateObjectiveOverallProgressAsync(objectiveId, login, ct).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.UPDATEOKRGOALPROGRESSEVENTTYPEID }); await _eventLog.PublishEventLogAsync( "Goal Progress Updated", new { GoalId = goalId, progressPercent }, EventTypeConstant.UPDATEOKRGOALPROGRESSEVENTTYPEID, goalId, login, ct: ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(goalId, EntityConstant.OBJECTGOAL, CacheKeyLevel.USER_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("update-goal-progress-failed", ex); _logger.LogError(ex, "UpdateGoalProgress failed for GoalId {GoalId}", goalId); throw; } } public async Task UpdateGoalConfidenceAsync(int goalId, short confidence, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("update-goal-confidence", new { goalId, confidence }); await _dal.UpdateGoalConfidenceAsync(goalId, confidence, login, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration(goalId, EntityConstant.OBJECTGOAL, CacheKeyLevel.USER_LEVEL, login); await _keyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("update-goal-confidence-failed", ex); _logger.LogError(ex, "UpdateGoalConfidence failed for GoalId {GoalId}", goalId); throw; } } public async Task LinkGoalToTaskAsync(int goalId, int taskId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("link-goal-task", new { goalId, taskId }); await _dal.LinkGoalToTaskAsync(goalId, taskId, login, ct).ConfigureAwait(false); return SuccessResponse.SaveSuccess; } catch (Exception ex) { GB5Trace.MarkFailed("link-goal-task-failed", ex); _logger.LogError(ex, "LinkGoalToTask failed for GoalId {GoalId} TaskId {TaskId}", goalId, taskId); throw; } } public async Task UnlinkGoalFromTaskAsync(int goalId, int taskId, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("unlink-goal-task", new { goalId, taskId }); await _dal.UnlinkGoalFromTaskAsync(goalId, taskId, login, ct).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("unlink-goal-task-failed", ex); _logger.LogError(ex, "UnlinkGoalFromTask failed for GoalId {GoalId} TaskId {TaskId}", goalId, taskId); throw; } } }