using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using CRMSalesDAL.CustomCode.LeadAllocation; using CRMSalesDAL.DTO.LeadAllocation; namespace CRMSalesBLL.LeadAllocation; /// /// Smart Lead Allocation v1 — CAPACITY-BASED ONLY. The reference material's full vision (rep /// skill-profile match, historical win-rate for similar segments, learning-opportunity stretch /// assignments) depends on Team Development and Conversion Intelligence data that don't exist yet /// in this codebase — deliberately NOT fabricated here. See /// for exactly what this v1 does instead. /// public class LeadAllocationBLL : ILeadAllocationBLL { private readonly ILeadAllocationDAL _LeadAllocationDAL; private readonly ILogger _Logger; public LeadAllocationBLL(ILeadAllocationDAL leadAllocationDAL, ILogger logger) { _LeadAllocationDAL = leadAllocationDAL; _Logger = logger; } /// /// Capacity-based allocation suggestion for one Lead: /// /// Queries every active sales rep's current OPEN deal count (TDEAL.Stage NOT IN /// (5,6) ClosedWon/ClosedLost) — reps with zero current deals are still considered, via a /// LEFT JOIN against the full MEMPLOYEE roster (see LeadAllocationQB.GET_REP_CAPACITY and its /// doc comment for the discovered, not live-verified, ISSALES/EMPLOYEESTATUS filter /// convention). /// Suggests the rep(s) with the LOWEST current open-deal count, ties broken by lowest /// RepId for determinism. /// Returns the suggestion WITHOUT auto-assigning — confirming it is a normal Deal save /// (setting DealDTO.RepId to the suggested rep), matching the "AI assists, human decides" /// principle already established this week for GEF. No Deal row is written by this method. /// /// Skill-match/win-rate refinement is deferred pending the Team Development and Conversion /// Intelligence passes described in the wider SalesMind AI plan — this method must not be /// extended to fabricate either signal ahead of those passes actually landing. /// public async Task SuggestAllocationAsync(int leadId, LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step("suggest-lead-allocation", new { leadId }); var capacities = (await _LeadAllocationDAL.GetRepCapacities(loginDTO, ct).ConfigureAwait(false)).ToList(); if (capacities.Count == 0) { _Logger.LogWarning("SuggestAllocationAsync found no active sales reps for LeadId {LeadId}", leadId); throw new Exception("No active sales reps were found to suggest an allocation for this Lead."); } int minOpenDealCount = capacities.Min(c => c.OpenDealCount); var tiedCandidates = capacities .Where(c => c.OpenDealCount == minOpenDealCount) .OrderBy(c => c.RepId) // deterministic tie-break — lowest RepId wins .ToList(); var winner = tiedCandidates[0]; var suggestion = new LeadAllocationSuggestionDTO { LeadId = leadId, SuggestedRepId = winner.RepId, SuggestedRepCode = winner.RepCode, SuggestedRepName = winner.RepName, SuggestedRepOpenDealCount = winner.OpenDealCount, Candidates = tiedCandidates, GeneratedOn = DateTime.UtcNow }; _Logger.LogInformation( "Lead {LeadId} allocation suggested: RepId {RepId} ({OpenDealCount} open deals, {CandidateCount} tied candidates)", leadId, winner.RepId, winner.OpenDealCount, tiedCandidates.Count); return JsonConvert.SerializeObject(suggestion); } catch (Exception ex) { GB5Trace.MarkFailed("suggest-lead-allocation-failed", ex); _Logger.LogError(ex, "SuggestAllocationAsync failed for LeadId {LeadId}", leadId); throw; } } }