using CRMBLL.Contact; using CRMDAL.CustomCode.Call; using CRMDAL.DTO.Call; using CRMDAL.DTO.Contact; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using System; using System.Threading.Tasks; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace CRMBLL.Call { public class CallBLL : ICallBLL { private readonly ICallDAL _callDAL; private readonly IContactBLL _contactBLL; private readonly AutoNumber _autoNumber; private readonly VoucherNumberService _voucherNumberService; private readonly IQueryExecutor _queryExecutor; private readonly BaseEntityAppService _baseEntityAppService; public CallBLL( ICallDAL callDAL, IContactBLL contactBLL, AutoNumber autoNumber, VoucherNumberService voucherNumberService, IQueryExecutor queryExecutor, BaseEntityAppService baseEntityAppService) { _callDAL = callDAL; _contactBLL = contactBLL; _autoNumber = autoNumber; _voucherNumberService = voucherNumberService; _queryExecutor = queryExecutor; _baseEntityAppService = baseEntityAppService; } public async Task GetCall(int CallId, LoginDTO LoginDTO) { try { return await _callDAL.GetCall(CallId, LoginDTO); } catch (Exception) { throw; } } public async Task GetSelectListCall(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { var result = await _callDAL.GetSelectListCall(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); return result; } catch (Exception ex) { throw new Exception($"Error in CallBLL.GetSelectListCall: {ex.Message}", ex); } } public async Task SaveCall(CallDTO callDTO, LoginDTO loginDTO) { NormalizeDto(callDTO); // ── Step 1: Validation ──────────────────────────────────────────────────── await ValidateCallAsync(callDTO, loginDTO); // ── Step 2: Resolve ObjectTypeId / CallObjectId pairing ────────────────── // Copies the correct entity FK into CallObjectId based on ObjectTypeId sentinel. // Must run before DAL to ensure CallObjectId has the real value. ResolveObjectIdFromEntityType(callDTO); // ── Step 3: SMS / Email building — GB4 parity ──────────────────────────── await BuildSmsAndEmailFieldsAsync(callDTO, loginDTO); // ── Step 4: Time conversion — GB4 parity ───────────────────────────────── callDTO.CallProblemTime = ParseTimeToMinutes(callDTO.CallProblemTimeActual); callDTO.CallCompletedOnTime = ParseTimeToMinutes(callDTO.CallCompletedOnTimeActual); callDTO.CallClosedOnTime = ParseTimeToMinutes(callDTO.CallClosedOnTimeActual); // ── Step 5: Status label mapping — GB4 parity ──────────────────────────── MapStatusLabels(callDTO); // ── NO NORMALIZATION STEP ──────────────────────────────────────────────── // GB4 NHibernate with int properties sent the exact value to SQL. // -1 → SQL gets -1 // 0 → SQL gets 0 // 1234 → SQL gets 1234 // It NEVER converted -1→0 or 0→NULL. Only int? (nullable) properties sent NULL. // All FK fields in CallDTO are `int` (not int?), so pass POST values as-is. bool isNew = callDTO.CallId == 0; // ── Step 6: AutoNumber — GB4 parity ────────────────────────────────────── int defectCount = isNew ? callDTO.CallVsDefectsArray.Count : 0; AutoNumberDTO? autoDto = isNew ? await _autoNumber.GetNumberAsync(1 + defectCount, AUTONUMBERCONSTANT.CALL, loginDTO) : null; // ── Step 7: Voucher number — GB4 parity ────────────────────────────────── if (isNew) { string? vno = await _voucherNumberService.GetNextVoucherNumberAsync( callDTO.BIZTransactionTypeId, callDTO.CallDate, loginDTO, transaction: null); if (vno != null) callDTO.CallNumber = vno; } // ── Step 8: Begin Transaction ───────────────────────────────────────────── var trans = await _queryExecutor.BeginTransactionAsync(loginDTO); try { if (isNew) { callDTO.CallId = autoDto!.StartNumber; int defectOffset = 1; foreach (var defect in callDTO.CallVsDefectsArray) { if (defect.CallVsDefectsId == 0) { defect.CallVsDefectsId = autoDto.StartNumber + defectOffset; defectOffset++; } defect.CallId = callDTO.CallId; } } // ── Step 9: Contact save — GB4 parity ──────────────────────────────── if (isNew && callDTO.ContactId == 0 && !string.IsNullOrWhiteSpace(callDTO.TempContactName)) { var contactDto = new ContactDTO { ContactName = callDTO.TempContactName, ContactMobileNo = callDTO.TempContactMobile, ContactPhoneNo = callDTO.TempContactMobile, ContactFirstName = callDTO.TempContactName, ContactLinkType = 1, EntityId = -2147481759 }; callDTO.ContactId = await _contactBLL.SaveContact(contactDto, loginDTO, trans); } // ── Step 10: Audit fields — GB4 parity ─────────────────────────────── callDTO.CallModifiedById = loginDTO.UserId; callDTO.CallModifiedOn = DateTime.UtcNow; if (isNew) { callDTO.CallCreatedById = loginDTO.UserId; callDTO.CallCreatedOn = DateTime.UtcNow; callDTO.CallVersion = 1; callDTO.CallStatus = 1; } // WIP final approval: entity is being persisted for the first time as approved. // Force STATUS = 1 (Active) — the record was held in WIP, not the live table. // Mirrors TaskBLL.SaveSingleTask. if (loginDTO.WipApprovalId > 0) callDTO.CallStatus = 1; // ── Step 11: DAL Save or Update — GB4 parity ───────────────────────── if (callDTO.CallWorkFlowRequired == 1) { // Workflow approval not required for this call — persist directly, // no workflow pipeline. Mirrors TaskBLL.SaveSingleTask bypass branch. await PersistCallAsync(callDTO, loginDTO, trans, isNew); } else { await _baseEntityAppService.ExecuteSaveAsync( EntityConstant.CALL, EventTypeConstant.SAVECALLEVENTTYPEID, callDTO, loginDTO, async tx => { await PersistCallAsync(callDTO, loginDTO, tx, isNew); return callDTO.CallId; }, bizTransactionClassId: BIZTRANSACTIONCLASSCONSTANT.CALL, bizTransactionId: BizTransactionConstant.CALL, externalTransaction: trans, isNewEntity: isNew); } // ── Step 12: Status-specific field updates — GB4 parity ────────────── if (callDTO.CallCallOriginalStatus == 2) // Completed { var now = DateTime.UtcNow; int minutesSinceMidnight = (int)(now - now.Date).TotalMinutes; await _callDAL.UpdateCompletedStatusFields( callDTO.CallId, callDTO.CompletedById, minutesSinceMidnight, loginDTO, trans); } if (callDTO.CallCallOriginalStatus == 5) // Closed { var now = DateTime.UtcNow; int minutesSinceMidnight = (int)(now - now.Date).TotalMinutes; await _callDAL.UpdateClosedStatusFields( callDTO.CallId, callDTO.ClosedById, minutesSinceMidnight, loginDTO, trans); } // ── Step 13: Party Product — GB4 parity ────────────────────────────── if (callDTO.IsGenralCall == 0 && isNew && callDTO.CallOnObjectTypeId == -1399998989) { bool exists = await _callDAL.CheckPartyProductExists( callDTO.PartyId, callDTO.StandByItemId, loginDTO); if (!exists) await _callDAL.InsertPartyProduct(callDTO.CallId, loginDTO, trans); await _callDAL.UpdateCallAfterPartyProduct(callDTO.CallId, loginDTO, trans); } await _queryExecutor.CommitAsync(trans); // ── Step 14: Post-commit notifications — GB4 parity ────────────────── await BuildNotifyUsersAsync(callDTO, loginDTO); return isNew ? $"Details saved successfully with TicketNumber: {callDTO.CallNumber}" : $"Details updated successfully on TicketNumber: {callDTO.CallNumber}"; } catch (Exception) { await _queryExecutor.RollbackAsync(trans); throw; } } // ───────────────────────────────────────────────────────────────────────────── // PersistCallAsync — shared by both the workflow-bypass and // BaseEntityAppService.ExecuteSaveAsync persist-callback paths. // ───────────────────────────────────────────────────────────────────────────── private async System.Threading.Tasks.Task PersistCallAsync( CallDTO callDTO, LoginDTO loginDTO, System.Data.Common.DbTransaction trans, bool isNew) { if (isNew) { await _callDAL.SaveCall(callDTO, loginDTO, trans); var validDefects = callDTO.CallVsDefectsArray .Where(d => d.ProblemId > 0).ToList(); if (validDefects.Count > 0) await _callDAL.SaveCallVsDefects(validDefects, loginDTO, trans); } else { await _callDAL.DeleteCallVsDefects(callDTO.CallId, loginDTO, trans); callDTO.CallVersion = (short)(callDTO.CallVersion + 1); await _callDAL.UpdateCall(callDTO, loginDTO, trans); var validDefects = callDTO.CallVsDefectsArray .Where(d => d.ProblemId > 0).ToList(); if (validDefects.Count > 0) await _callDAL.SaveCallVsDefects(validDefects, loginDTO, trans); } } // ───────────────────────────────────────────────────────────────────────────── // ResolveObjectIdFromEntityType // ───────────────────────────────────────────────────────────────────────────── private static void ResolveObjectIdFromEntityType(CallDTO dto) { int resolved = dto.ObjectTypeId switch { -1899999858 => dto.PartyId, -1899999839 => dto.PartyBranchId, -2147481759 => dto.ContactId, -1399999905 => dto.EmployeeId, -1399999289 => dto.PersonId, _ => dto.CallObjectId }; // ✅ ONLY accept real DB IDs if (resolved > 0) dto.CallObjectId = resolved; else dto.CallObjectId = 0; // or NULL if DB allows } // ───────────────────────────────────────────────────────────────────────────── // ValidateCallAsync // ───────────────────────────────────────────────────────────────────────────── private async System.Threading.Tasks.Task ValidateCallAsync(CallDTO dto, LoginDTO login) { if (dto.CallCompletedOn < dto.CallClosedOn) throw new InvalidOperationException("Completed date cannot be before Closed date."); // FK_TCALL_BIZTRANSACTIONTYPEID_OUID_PERIODID requires the exact // (BIZTransactionTypeId, OUId, PeriodId) triplet to exist as one MBIZTRANSACTIONTYPE // row — that record is scoped to a specific OU + Period, not global. A stale Call // Type selection made under a different OU/Period (e.g. after the user switched work // OU without re-fetching the dropdown) fails this FK with a raw SQL 547 error. // Catching it here turns that into a clear, actionable message before the transaction // even opens. bool bizTransactionTypeValid = await _callDAL.BizTransactionTypeExists( dto.BIZTransactionTypeId, dto.OUId, dto.PeriodId, login); if (!bizTransactionTypeValid) throw new InvalidOperationException( $"The selected Call Type is not valid for the current Organization Unit/Period. " + $"Please reselect the Call Type and try again. " + $"(BIZTransactionTypeId={dto.BIZTransactionTypeId}, OUId={dto.OUId}, PeriodId={dto.PeriodId})"); if (dto.CallOnObjectTypeId == -1399998989 && dto.CallCallOnObjectId > 0) { var addr = await _callDAL.ValidatePartyProductAddress(dto.CallCallOnObjectId, login); if (addr != null) { if ((addr.Mobile ?? string.Empty).Length < 10) throw new InvalidOperationException( "Mobile is invalid in Party product. Please check."); if ((addr.ZipCode ?? string.Empty).Length < 6) throw new InvalidOperationException( "ZipCode is invalid in Party product. Please check."); } } } // ───────────────────────────────────────────────────────────────────────────── // BuildSmsAndEmailFieldsAsync // ───────────────────────────────────────────────────────────────────────────── private async System.Threading.Tasks.Task BuildSmsAndEmailFieldsAsync(CallDTO dto, LoginDTO login) { if (dto.CallId == 0) { var employee = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); AssetContactInfoDTO? asset = null; if (dto.CallOnObjectTypeId == -1200000000) { asset = await _callDAL.GetAssetContactInfo(dto.CallCallOnObjectId, login); if (asset != null) { dto.AssetInchargeMailIds = $"{asset.SalesInchargeEmail};{asset.InchargeEmail}"; dto.AssetInchargeMobileSmsNos = $"{asset.SalesInchargeMobile};{asset.InchargeMobile}"; } } string empMobile = employee?.PrimaryMobile ?? string.Empty; string empEmail = employee?.PrimaryEMailId ?? string.Empty; string repMobile = employee?.ReportingMobile ?? string.Empty; string repEmail = employee?.ReportingEmail ?? string.Empty; string assetMob = asset?.InchargeMobile ?? string.Empty; string assetMail = asset?.InchargeEmail ?? string.Empty; if (dto.ContactId != -1) { var contact = await _callDAL.GetContactInfo(dto.ContactId, login); dto.SMSNo = JoinNonEmpty(",", contact?.MobileNo, empMobile, repMobile, assetMob); dto.AllotedCompletedMailId = JoinNonEmpty(";", contact?.MailId, empEmail, repEmail, assetMail); } else { var user = await _callDAL.GetUserContactInfo(login.UserId, login); dto.SMSNo = JoinNonEmpty(",", user?.PrimaryMobile, empMobile, repMobile, assetMob); dto.AllotedCompletedMailId = JoinNonEmpty(";", user?.PrimaryMail, empEmail, repEmail, assetMail); } } else { switch (dto.CallCallOriginalStatus) { case 1: { var emp = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); dto.SMSNo = emp?.PrimaryMobile ?? string.Empty; dto.AllotedCompletedMailId = emp?.PrimaryEMailId ?? string.Empty; break; } case 2: { var emp = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); AssetContactInfoDTO? asset = null; if (dto.CallOnObjectTypeId == -1200000000) asset = await _callDAL.GetAssetContactInfo(dto.CallCallOnObjectId, login); string repMob = emp?.ReportingMobile ?? string.Empty; string repMail = emp?.ReportingEmail ?? string.Empty; string aInchMob = asset?.InchargeMobile ?? string.Empty; string aInchMail = asset?.InchargeEmail ?? string.Empty; string aBrMob = asset?.SalesInchargeMobile ?? string.Empty; string aBrMail = asset?.SalesInchargeEmail ?? string.Empty; if (dto.ContactId != -1) { var contact = await _callDAL.GetContactInfo(dto.ContactId, login); dto.SMSNo = JoinNonEmpty(",", contact?.MobileNo, repMob, aInchMob, aBrMob); dto.AllotedCompletedMailId = JoinNonEmpty(";", contact?.MailId, repMail, aInchMail, aBrMail); } else { int? createdById = await _callDAL.GetCallCreatedById(dto.CallId, login); UserContactInfoDTO? user = createdById.HasValue ? await _callDAL.GetUserContactInfo(createdById.Value, login) : null; dto.SMSNo = JoinNonEmpty(",", user?.PrimaryMobile, repMob, aInchMob, aBrMob); dto.AllotedCompletedMailId = JoinNonEmpty(";", user?.PrimaryMail, repMail, aInchMail, aBrMail); } break; } default: { var emp = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); if (!string.IsNullOrEmpty(emp?.PrimaryMobile)) { dto.SMSNo = emp.PrimaryMobile; dto.AllotedCompletedMailId = emp.PrimaryEMailId; } break; } } } } // ───────────────────────────────────────────────────────────────────────────── // BuildNotifyUsersAsync // ───────────────────────────────────────────────────────────────────────────── private async System.Threading.Tasks.Task BuildNotifyUsersAsync(CallDTO dto, LoginDTO login) { if (dto.CallCallOriginalStatus == 0 || dto.CallCallOriginalStatus == 1) { if (dto.AllotedToEmployeeId <= 0) return; var parts = new List { dto.AllotedToEmployeeId.ToString() }; var emp = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); if (emp?.ReportingEmployeeId > 0) parts.Add(emp.ReportingEmployeeId.ToString()); if (dto.CallOnObjectTypeId == -1200000000) { var asset = await _callDAL.GetAssetContactInfo(dto.CallCallOnObjectId, login); if (asset != null) { if (asset.InchargeId > 0) parts.Add(asset.InchargeId.ToString()); if (asset.SalesInchargeId > 0) parts.Add(asset.SalesInchargeId.ToString()); } } dto.NotifyUser = string.Join(",", parts); } else if (dto.CallCallOriginalStatus == 2) { var parts = new List(); var emp = await _callDAL.GetEmployeeInfo(dto.AllotedToEmployeeId, login); if (emp?.ReportingEmployeeId > 0) parts.Add(emp.ReportingEmployeeId.ToString()); if (dto.EmployeeId > 0) parts.Add(dto.EmployeeId.ToString()); if (dto.CallOnObjectTypeId == -1200000000) { var asset = await _callDAL.GetAssetContactInfo(dto.CallCallOnObjectId, login); if (asset != null) { if (asset.InchargeId > 0) parts.Add(asset.InchargeId.ToString()); if (asset.SalesInchargeId > 0) parts.Add(asset.SalesInchargeId.ToString()); } } dto.NotifyUser = string.Join(",", parts); } } // ───────────────────────────────────────────────────────────────────────────── // MapStatusLabels // ───────────────────────────────────────────────────────────────────────────── private static void MapStatusLabels(CallDTO dto) { dto.MailCallOriginalStatus = dto.CallCallOriginalStatus switch { 0 => "Pending", 1 => "Alloted", 2 => "Completed", 3 => "PostPoned", 4 => "CallBack", 5 => "Closed", 6 => "Cancel", 7 => "Request For Cancellation", 8 => "Waiting For Spares", 9 => "Pending For Approval", _ => "Closed" }; dto.MailCallTypeName = dto.ObjectTypeId switch { -1899999858 => "Party", -1899999839 => "Party Branch", -2147481759 => "Contact", -1399999905 => "Employee", -1399999289 => "Lead", _ => "Applicant" }; } // ───────────────────────────────────────────────────────────────────────────── // ParseTimeToMinutes // ───────────────────────────────────────────────────────────────────────────── private static short ParseTimeToMinutes(string timeActual) { if (string.IsNullOrWhiteSpace(timeActual) || timeActual == "NONE") return 0; if (!TimeSpan.TryParse(timeActual, out var ts)) return 0; int total = (int)ts.TotalMinutes; return total > short.MaxValue ? short.MaxValue : (short)total; } private static string JoinNonEmpty(string separator, params string?[] values) => string.Join(separator, values.Where(v => !string.IsNullOrEmpty(v))); private static int ToInt(object? value) { if (value == null) return 0; if (value is int i) return i; if (value is long l) return (int)l; if (int.TryParse(value.ToString(), out int result)) return result; return 0; } private static void NormalizeDto(CallDTO dto) { dto.ObjectTypeId = ToInt(dto.ObjectTypeId); dto.PartyBranchId = ToInt(dto.PartyBranchId); dto.ContactId = ToInt(dto.ContactId); dto.AllotedToContactId = ToInt(dto.AllotedToContactId); dto.AllotedToPartyId = ToInt(dto.AllotedToPartyId); dto.ProblemNatureId = ToInt(dto.ProblemNatureId); dto.StandByItemId = ToInt(dto.StandByItemId); dto.RootCauseId = ToInt(dto.RootCauseId); if (string.IsNullOrWhiteSpace(dto.CallCallOnName)) dto.CallCallOnName = "NONE"; // Add more fields if needed } } }