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;
}
}
}