using System.ComponentModel.DataAnnotations; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using CRMSalesDAL.CustomCode.Deal; using CRMSalesDAL.DTO.Deal; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace CRMSalesBLL.Deal; public class DealBLL : IDealBLL { private readonly IDealDAL _DealDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; public DealBLL( IDealDAL dealDAL, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _DealDAL = dealDAL; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } public async Task GetDeal(int dealId, LoginDTO loginDTO, CancellationToken ct) { try { return await _DealDAL.GetDeal(dealId, loginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-deal-failed", ex); _Logger.LogError(ex, "GetDeal failed for DealId {DealId}", dealId); throw; } } /// /// Deterministic Temperature rule (no AI/ML) — see plan §SalesMind AI, Pipeline & Lead /// Intelligence. MAN fields are 1=Yes, 2=Partial, 3=No; this counts how many of the 3 are "Yes": /// all 3 "Yes" -> Hot (1) /// exactly 2 "Yes" -> Warm (2) /// 0 or 1 "Yes" -> Cold (3) /// public static byte ComputeTemperature(byte manMoney, byte manAuthority, byte manNeed) { const byte yes = 1; int yesCount = (manMoney == yes ? 1 : 0) + (manAuthority == yes ? 1 : 0) + (manNeed == yes ? 1 : 0); return yesCount switch { 3 => (byte)1, // Hot 2 => (byte)2, // Warm _ => (byte)3 // Cold (0 or 1 "Yes") }; } public async Task SaveDeal(DealDTO dealDTO, LoginDTO loginDTO, CancellationToken ct) { if (dealDTO == null) throw new ArgumentNullException(nameof(dealDTO)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-deal", new { dealDTO.DealId }); await _Validation.NotEmpty(dealDTO.DealCode, nameof(dealDTO.DealCode)); await _Validation.NotEmpty(dealDTO.DealName, nameof(dealDTO.DealName)); await _Validation.Range(1, int.MaxValue, dealDTO.LeadId, nameof(dealDTO.LeadId)); await _Validation.Range((byte)1, (byte)6, dealDTO.Stage, nameof(dealDTO.Stage)); await _Validation.Range((byte)1, (byte)3, dealDTO.MANMoney, nameof(dealDTO.MANMoney)); await _Validation.Range((byte)1, (byte)3, dealDTO.MANAuthority, nameof(dealDTO.MANAuthority)); await _Validation.Range((byte)1, (byte)3, dealDTO.MANNeed, nameof(dealDTO.MANNeed)); bool isNew = dealDTO.DealId == 0; // Recompute Temperature from the MAN fields on every save — never trust a // client-supplied value (same pattern as ICPBLL.ComputeABCDCategory). dealDTO.Temperature = ComputeTemperature(dealDTO.MANMoney, dealDTO.MANAuthority, dealDTO.MANNeed); // StageChangedOn: stamp UtcNow only when Stage actually differs from the previously- // persisted value. New Deals always stamp (first stage is, by definition, a change). if (isNew) { dealDTO.StageChangedOn = DateTime.UtcNow; } else { var previousStage = await _DealDAL.GetDealStage(dealDTO.DealId, loginDTO, ct).ConfigureAwait(false); if (previousStage == null || previousStage.Value != dealDTO.Stage) dealDTO.StageChangedOn = DateTime.UtcNow; // else: leave the caller-supplied StageChangedOn (round-tripped from GetDeal) as-is. } dealDTO.ModifiedById = loginDTO.UserId; dealDTO.ModifiedOn = DateTime.UtcNow; if (isNew) { autoNumberDTO = await _AutoNumber.GetNumberAsync(1, "DEAL", loginDTO).ConfigureAwait(false); dealDTO.DealId = autoNumberDTO.StartNumber; dealDTO.CreatedById = loginDTO.UserId; dealDTO.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-deal", new { dealDTO.DealId, isNew, dealDTO.Stage, dealDTO.Temperature }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTCRMSALESDEAL, isNew ? EventTypeConstant.SAVECRMSALESDEALEVENTTYPEID : EventTypeConstant.UPDATECRMSALESDEALEVENTTYPEID, dealDTO, loginDTO, async tx => { _ = isNew ? await _DealDAL.SaveDeal(dealDTO, loginDTO, tx, ct).ConfigureAwait(false) : await _DealDAL.UpdateDeal(dealDTO, loginDTO, tx, ct).ConfigureAwait(false); return dealDTO.DealId; }, null, -1, -1, null, isNew).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVECRMSALESDEALEVENTTYPEID : EventTypeConstant.UPDATECRMSALESDEALEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( dealDTO.DealId, EntityConstant.OBJECTCRMSALESDEAL, CacheKeyLevel.CLIENT_LEVEL, loginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("Deal {DealId} {Action} by user {UserId}", dealDTO.DealId, isNew ? "saved" : "updated", loginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dealDTO.DealId}" : $"{SuccessResponse.UpdateSuccessMessage} {dealDTO.DealId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-deal-failed", vex); if (autoNumberDTO != null && dealDTO.DealId > 0) await _AutoNumber.RollbackAutoNumber("DEAL", dealDTO.DealId, loginDTO).ConfigureAwait(false); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-deal-failed", ex); _Logger.LogError(ex, "SaveDeal failed for DealId {DealId}", dealDTO.DealId); if (autoNumberDTO != null && dealDTO.DealId > 0) await _AutoNumber.RollbackAutoNumber("DEAL", dealDTO.DealId, loginDTO).ConfigureAwait(false); throw; } } public async Task GetSelectListDeal(CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct) { try { return await _DealDAL.GetSelectListDeal(criteriaDTO, loginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-deal-failed", ex); _Logger.LogError(ex, "GetSelectListDeal failed"); throw; } } public async Task DeleteDeal(int dealId, LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-deal", new { dealId }); var result = await _DealDAL.DeleteDeal(dealId, loginDTO, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( dealId, EntityConstant.OBJECTCRMSALESDEAL, CacheKeyLevel.CLIENT_LEVEL, loginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("Deal {DealId} deleted by user {UserId}", dealId, loginDTO.UserId); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-deal-failed", ex); _Logger.LogError(ex, "DeleteDeal failed for DealId {DealId}", dealId); throw; } } }