using System.Text.Json; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using PIEDAL.CustomCode.Estimation; using PIEDAL.DTO.Estimation; using static GB5Shared.GB5Constant.Constant; namespace PIEBLL.Estimation; public class PIEEstimationBLL : IPIEEstimationBLL { private readonly IPIEEstimationDAL _PIEEstimationDAL; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _RuleEntityAppService; private readonly BaseEntityAppService _SnapshotEntityAppService; public PIEEstimationBLL( IPIEEstimationDAL pieEstimationDAL, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService ruleEntityAppService, BaseEntityAppService snapshotEntityAppService) { _PIEEstimationDAL = pieEstimationDAL; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _RuleEntityAppService = ruleEntityAppService; _SnapshotEntityAppService = snapshotEntityAppService; } // ── Reads ───────────────────────────────────────────────────────────────── public async Task GetPIEEstimationRuleAsync( Guid ruleId, LoginDTO login, CancellationToken ct) => await _PIEEstimationDAL.GetPIEEstimationRuleAsync(ruleId, login, ct).ConfigureAwait(false); public async Task GetSnapshotByEngagementAsync( string engagementRef, LoginDTO login, CancellationToken ct) => await _PIEEstimationDAL.GetSnapshotByEngagementAsync(engagementRef, login, ct).ConfigureAwait(false); // ── Calculation engine ──────────────────────────────────────────────────── /// /// Calculates effort hours for a set of modules under the given engagement type and /// complexity level, persists an immutable snapshot, and returns the breakdown. /// Called by IDMS during engagement creation (integration endpoint). /// public async Task CalculatePIEEstimationAsync( PIEEstimationRequestDTO req, LoginDTO login, CancellationToken ct) { if (req is null) throw new ArgumentNullException(nameof(req)); if (req.ModuleIds.Count == 0) throw new ArgumentException("At least one ModuleId is required for estimation."); var rules = (await _PIEEstimationDAL.GetActiveRulesByModuleIdsAsync( req.ModuleIds.ToArray(), req.EngagementType, req.ComplexityLevel, login, ct).ConfigureAwait(false)) .ToList(); // Build per-module breakdown var breakdown = new List(); decimal totalBase = 0m; decimal confidenceSum = 0m; int confidenceCount = 0; foreach (var moduleId in req.ModuleIds) { var rule = rules.FirstOrDefault(r => r.ModuleId == moduleId); if (rule is null) continue; // No active rule for this module; skip var moduleHours = new PIEModuleEstimateDTO { ModuleId = moduleId, ModuleName = rule.ModuleName ?? string.Empty, BaseHours = rule.BaseHours, ConfigHours = rule.ConfigHours, TrainingHours = rule.TrainingHours, MigrationHours = rule.MigrationHours, UatHours = rule.UatHours, TotalHours = rule.BaseHours + rule.ConfigHours + rule.TrainingHours + rule.MigrationHours + rule.UatHours }; breakdown.Add(moduleHours); totalBase += moduleHours.TotalHours; confidenceSum += rule.ConfidenceScore; confidenceCount++; } decimal avgConfidence = confidenceCount > 0 ? confidenceSum / confidenceCount : 0m; decimal pmOverheadPct = rules.FirstOrDefault()?.PmOverheadPct ?? 15m; decimal totalWithPm = totalBase * (1m + pmOverheadPct / 100m); var snapshotId = Guid.NewGuid(); // Persist immutable snapshot var snapshot = new PIEEstimationSnapshotDTO { SnapshotId = snapshotId, EngagementRef = req.EngagementRef, SnapshotData = JsonSerializer.Serialize(breakdown), TotalHours = Math.Round(totalWithPm, 2), ConfidenceScore = Math.Round(avgConfidence, 2) }; var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _SnapshotEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEESTIMATIONRULE, EventTypeConstant.SAVEPIEESTIMATIONRULEEVENTTYPEID, snapshot, login, async tx => { await _PIEEstimationDAL.InsertSnapshotAsync(snapshot, login, tx, ct); return 0; // LPIEESTIMATIONSNAPSHOT uses a Guid PK — no int object ID to report back }, null, -1, -1, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("calculate-pie-estimation-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } return new PIEEstimationResultDTO { SnapshotId = snapshotId, TotalHours = snapshot.TotalHours, ConfidenceScore = snapshot.ConfidenceScore, ModuleBreakdown = breakdown }; } // ── Saves ───────────────────────────────────────────────────────────────── public async Task SavePIEEstimationRuleAsync( PIEEstimationRuleDTO dto, LoginDTO login, CancellationToken ct) { if (dto is null) throw new ArgumentNullException(nameof(dto)); bool isNew = dto.RuleId == Guid.Empty; if (isNew) dto.RuleId = Guid.NewGuid(); var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _RuleEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEESTIMATIONRULE, EventTypeConstant.SAVEPIEESTIMATIONRULEEVENTTYPEID, dto, login, async tx => { if (isNew) await _PIEEstimationDAL.SavePIEEstimationRuleAsync(dto, login, tx, ct); else await _PIEEstimationDAL.UpdatePIEEstimationRuleAsync(dto, login, tx, ct); return 0; // PIEEstimationRule uses a Guid PK — no int object ID to report back }, null, -1, -1, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( dto.RuleId, EntityConstant.OBJECTPIEESTIMATIONRULE, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return isNew ? SuccessResponse.SaveSuccess : SuccessResponse.UpdateSuccess; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("save-pie-estimation-rule-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } public async Task DeletePIEEstimationRuleAsync( Guid ruleId, LoginDTO login, CancellationToken ct) { var Trans = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { await _RuleEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTPIEESTIMATIONRULE, EventTypeConstant.SAVEPIEESTIMATIONRULEEVENTTYPEID, new PIEEstimationRuleDTO { RuleId = ruleId }, login, async tx => { await _PIEEstimationDAL.SoftDeletePIEEstimationRuleAsync(ruleId, login, tx, ct); return 0; }, null, -1, -1, Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( ruleId, EntityConstant.OBJECTPIEESTIMATIONRULE, CacheKeyLevel.CLIENT_LEVEL, login); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Shared.Telemetry.GB5Trace.MarkFailed("delete-pie-estimation-rule-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); throw; } } }