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 MarketingDAL.CustomCode.ICP; using MarketingDAL.DTO.ICP; using Microsoft.Extensions.Logging; using static GB5Shared.GB5Constant.Constant; namespace MarketingBLL.ICP; public class ICPBLL : IICPBLL { private readonly IICPDAL _ICPDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; public ICPBLL( IICPDAL icpDAL, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _ICPDAL = icpDAL; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } public async Task GetICP(int icpId, LoginDTO loginDTO, CancellationToken ct) { try { return await _ICPDAL.GetICP(icpId, loginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-icp-failed", ex); _Logger.LogError(ex, "GetICP failed for ICPId {ICPId}", icpId); throw; } } /// /// Deterministic ABCD quadrant rule (no AI/ML) — see plan §MarketMind AI, ICP scoring. /// avgEffort = average(LeadGenEffortScore, SalesConversionEffortScore), on a 1-10 scale. /// Threshold = midpoint of the scale (5): avgEffort <= 5 is "low" effort, > 5 is "high". /// Same threshold applied to ReturnScore alone: <= 5 is "low" return, > 5 is "high". /// low effort + high return -> Amazing (easy to win, profitable) /// high effort + high return -> Breadwinning (hard-won, but profitable) /// low effort + low return -> Convenience (easy, but low payoff) /// high effort + low return -> Dangerous (hard-won and low payoff — avoid) /// public static string ComputeABCDCategory(byte leadGenEffortScore, byte salesConversionEffortScore, byte returnScore) { double avgEffort = (leadGenEffortScore + salesConversionEffortScore) / 2.0; bool highEffort = avgEffort > 5; bool highReturn = returnScore > 5; return (highEffort, highReturn) switch { (false, true) => "Amazing", (true, true) => "Breadwinning", (false, false) => "Convenience", (true, false) => "Dangerous" }; } public async Task SaveICP(ICPDTO icpDTO, LoginDTO loginDTO, CancellationToken ct) { if (icpDTO == null) throw new ArgumentNullException(nameof(icpDTO)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-icp", new { icpDTO.ICPId }); await _Validation.NotEmpty(icpDTO.ICPCode, nameof(icpDTO.ICPCode)); await _Validation.NotEmpty(icpDTO.ICPName, nameof(icpDTO.ICPName)); await _Validation.Range(1, 3, icpDTO.BusinessType, nameof(icpDTO.BusinessType)); await _Validation.Range((byte)1, (byte)10, icpDTO.LeadGenEffortScore, nameof(icpDTO.LeadGenEffortScore)); await _Validation.Range((byte)1, (byte)10, icpDTO.SalesConversionEffortScore, nameof(icpDTO.SalesConversionEffortScore)); await _Validation.Range((byte)1, (byte)10, icpDTO.ReturnScore, nameof(icpDTO.ReturnScore)); icpDTO.ABCDCategory = ComputeABCDCategory(icpDTO.LeadGenEffortScore, icpDTO.SalesConversionEffortScore, icpDTO.ReturnScore); icpDTO.ModifiedById = loginDTO.UserId; icpDTO.ModifiedOn = DateTime.UtcNow; bool isNew = icpDTO.ICPId == 0; if (isNew) { autoNumberDTO = await _AutoNumber.GetNumberAsync(1, "ICP", loginDTO).ConfigureAwait(false); icpDTO.ICPId = autoNumberDTO.StartNumber; icpDTO.CreatedById = loginDTO.UserId; icpDTO.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-icp", new { icpDTO.ICPId, isNew, icpDTO.ABCDCategory }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTMARKETINGICP, isNew ? EventTypeConstant.SAVEMARKETINGICPEVENTTYPEID : EventTypeConstant.UPDATEMARKETINGICPEVENTTYPEID, icpDTO, loginDTO, async tx => { _ = isNew ? await _ICPDAL.SaveICP(icpDTO, loginDTO, tx, ct).ConfigureAwait(false) : await _ICPDAL.UpdateICP(icpDTO, loginDTO, tx, ct).ConfigureAwait(false); return icpDTO.ICPId; }, null, -1, -1, null, isNew).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVEMARKETINGICPEVENTTYPEID : EventTypeConstant.UPDATEMARKETINGICPEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( icpDTO.ICPId, EntityConstant.OBJECTMARKETINGICP, CacheKeyLevel.CLIENT_LEVEL, loginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("ICP {ICPId} {Action} by user {UserId}", icpDTO.ICPId, isNew ? "saved" : "updated", loginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {icpDTO.ICPId}" : $"{SuccessResponse.UpdateSuccessMessage} {icpDTO.ICPId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-icp-failed", vex); if (autoNumberDTO != null && icpDTO.ICPId > 0) await _AutoNumber.RollbackAutoNumber("ICP", icpDTO.ICPId, loginDTO).ConfigureAwait(false); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-icp-failed", ex); _Logger.LogError(ex, "SaveICP failed for ICPId {ICPId}", icpDTO.ICPId); if (autoNumberDTO != null && icpDTO.ICPId > 0) await _AutoNumber.RollbackAutoNumber("ICP", icpDTO.ICPId, loginDTO).ConfigureAwait(false); throw; } } public async Task GetSelectListICP(CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct) { try { return await _ICPDAL.GetSelectListICP(criteriaDTO, loginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-icp-failed", ex); _Logger.LogError(ex, "GetSelectListICP failed"); throw; } } public async Task DeleteICP(int icpId, LoginDTO loginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-icp", new { icpId }); var result = await _ICPDAL.DeleteICP(icpId, loginDTO, ct).ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( icpId, EntityConstant.OBJECTMARKETINGICP, CacheKeyLevel.CLIENT_LEVEL, loginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("ICP {ICPId} deleted by user {UserId}", icpId, loginDTO.UserId); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-icp-failed", ex); _Logger.LogError(ex, "DeleteICP failed for ICPId {ICPId}", icpId); throw; } } }