using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using Microsoft.Extensions.Logging; using PayRollBLL.PayProcess; using PayRollDAL.CustomeCode.Adjustment; using PayRollDAL.CustomeCode.DailyAttendance; using PayRollDAL.CustomeCode.PayProcess; using PayRollDAL.DTO.Adjustment; using PayRollDAL.DTO.DailyAttendance; using PayrollDAL.DTO.Adjustment; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace PayRollBLL.Adjustment { public class AdjustmentBLL : IAdjustmentBLL { private IAdjustmentDAL _AdjustmentDAL; private readonly IQueryExecutor _QueryExecutor; private readonly AutoNumber _AutoNumber; private readonly BaseEntityAppService _baseEntityAppService; private readonly ILogger _Logger; private readonly IDailyAttendanceDAL _DailyAttendanceDAL; private readonly EventLogPublish _EventLogPublish; public AdjustmentBLL( IAdjustmentDAL adjustmentDAL, IQueryExecutor queryExecutor, AutoNumber autoNumber, BaseEntityAppService baseEntityAppService, ILogger logger, IDailyAttendanceDAL dailyAttendanceDAL, EventLogPublish eventLogPublish) { _AdjustmentDAL = adjustmentDAL; _QueryExecutor = queryExecutor; _AutoNumber = autoNumber; _baseEntityAppService = baseEntityAppService; _Logger = logger; _DailyAttendanceDAL = dailyAttendanceDAL; _EventLogPublish = eventLogPublish; } public async Task> GetAdjustmentReportAsync( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO, bool isCount = false) { try { var adjustmentReports = await _AdjustmentDAL.GetAdjustmentReportAsync( firstNumber, maxResult, criteriaDTO, loginDTO, isCount); return adjustmentReports?.ToList() ?? new List(); } catch (Exception) { throw; } } public async Task> AdjustmentNumberSelectlist( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO loginDTO, bool isCount = false) { try { if (firstNumber == 0 && maxResult == 0) { firstNumber = 0; maxResult = 10; } var result = await _AdjustmentDAL.AdjustmentNumberSelectlist(firstNumber, maxResult, criteriaDTO, loginDTO, isCount); return result?.ToList() ?? new List(); } catch (Exception) { throw; } } // GB4 parity: AdjustmentBLL.SaveAdjustment (BLL/PayrollBLL/Adjustment/AdjustmentBLL.cs:32). // Scope note: GB4's VnoGenerationDLL.GetNextVno (year/month/day-scoped, prefix/suffix, // per-BizTransactionType numbering engine used across many GB4 document types) is not // ported here — GB5 keeps its existing AutoNumber-based AdjustmentId/AdjustmentNumber // generation. Porting VnoGenerationDLL as a reusable cross-module engine is tracked // separately from this fix. public async Task SaveAdjustment(AdjustmentDTO adjustmentDTO, LoginDTO loginDTO, CancellationToken ct) { if (adjustmentDTO == null) throw new ArgumentNullException(nameof(adjustmentDTO)); if (adjustmentDTO.AdjustmentDetailArray == null || adjustmentDTO.AdjustmentDetailArray.Count == 0) throw new ArgumentException("AdjustmentDetail Grid is passing empty. Please check"); bool isNew = adjustmentDTO.AdjustmentId == 0; var periodRange = await _AdjustmentDAL.GetPeriodRange(adjustmentDTO.PeriodId, loginDTO).ConfigureAwait(false); if (periodRange != null) { var firstDetailDate = adjustmentDTO.AdjustmentDetailArray[0].AdjustmentDetailAttendanceDate; if (firstDetailDate < periodRange.Value.FromDate || firstDetailDate > periodRange.Value.ToDate) throw new InvalidOperationException("Attendance Date is Not in Financial Range"); } // GB4 parity: PayProcess.IsUpdatePossible guard runs once PER detail row, not once overall. foreach (var detail in adjustmentDTO.AdjustmentDetailArray) { bool payProcessDone = await _DailyAttendanceDAL.IsPayProcessDoneFromDate( detail.EmployeeId, detail.AdjustmentDetailAttendanceDate, loginDTO, ct).ConfigureAwait(false); if (payProcessDone) throw new InvalidOperationException( $"Pay process already done for employee {detail.EmployeeId} covering {detail.AdjustmentDetailAttendanceDate:yyyy-MM-dd}. Adjustment cannot be saved."); } // GB4 parity: time-string ("HH:mm") → minutes conversion for the four Cal* fields. foreach (var detail in adjustmentDTO.AdjustmentDetailArray) { if (!string.IsNullOrWhiteSpace(detail.AdjustmentDetailCalInTime)) detail.AdjustmentDetailInTime = (short)TimeSpan.Parse(detail.AdjustmentDetailCalInTime).TotalMinutes; if (!string.IsNullOrWhiteSpace(detail.AdjustmentDetailCalOutTime)) detail.AdjustmentDetailOutTime = (short)TimeSpan.Parse(detail.AdjustmentDetailCalOutTime).TotalMinutes; if (!string.IsNullOrWhiteSpace(detail.AdjustmentDetailPreviousCalInTime)) detail.AdjustmentDetailPreviousInTime = (short)TimeSpan.Parse(detail.AdjustmentDetailPreviousCalInTime).TotalMinutes; if (!string.IsNullOrWhiteSpace(detail.AdjustmentDetailPreviousCalOutTime)) detail.AdjustmentDetailPreviousOutTime = (short)TimeSpan.Parse(detail.AdjustmentDetailPreviousCalOutTime).TotalMinutes; } var firstEmployeeId = adjustmentDTO.AdjustmentDetailArray[0].EmployeeId; var employeeInfo = await _AdjustmentDAL.GetEmployeeReportingInfo(firstEmployeeId, loginDTO).ConfigureAwait(false); if (employeeInfo == null) throw new InvalidOperationException("User is not an employee..Record can not save.."); // GB4 parity: workflow reporting-to-mail enrichment (single vs multi-employee branch). var distinctEmployeeIds = adjustmentDTO.AdjustmentDetailArray .Select(d => d.EmployeeId).Distinct().ToList(); AdjustmentEmployeeReportingInfoDTO? workflowEmployeeInfo = employeeInfo; if (distinctEmployeeIds.Count > 1) { var loginEmployeeInfo = await _AdjustmentDAL.GetEmployeeReportingInfo(loginDTO.UserId, loginDTO).ConfigureAwait(false); if (loginEmployeeInfo != null) workflowEmployeeInfo = await _AdjustmentDAL.GetEmployeeReportingInfo(loginEmployeeInfo.ReportInttoEmployeeId, loginDTO).ConfigureAwait(false) ?? loginEmployeeInfo; } if (workflowEmployeeInfo != null) { adjustmentDTO.LoginMailId = workflowEmployeeInfo.ReportInttoEmployeeMailId; adjustmentDTO.LoginMobileNumber = workflowEmployeeInfo.ReportInttoEmployeeMobile; } string? reasonName = await _AdjustmentDAL.GetReasonName(adjustmentDTO.ReasonId, loginDTO, ct).ConfigureAwait(false); var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO).ConfigureAwait(false); try { if (isNew) { int idsNeeded = 1 + adjustmentDTO.AdjustmentDetailArray.Count; var autoNumber = await _AutoNumber.GetNumberAsync(idsNeeded, AUTONUMBERCONSTANT.ADJUSTMENT, loginDTO).ConfigureAwait(false); adjustmentDTO.AdjustmentId = autoNumber.StartNumber; adjustmentDTO.AdjustmentAdjustmentNumber = adjustmentDTO.AdjustmentId.ToString(); for (int d = 0; d < adjustmentDTO.AdjustmentDetailArray.Count; d++) adjustmentDTO.AdjustmentDetailArray[d].AdjustmentDetailSlNo = (short)(d + 1); } adjustmentDTO.AdjustmentCreatedOn = isNew ? DateTime.UtcNow : adjustmentDTO.AdjustmentCreatedOn; adjustmentDTO.AdjustmentModifiedOn = DateTime.UtcNow; adjustmentDTO.AdjustmentModifiedById = loginDTO.UserId; if (isNew) adjustmentDTO.AdjustmentCreatedById = loginDTO.UserId; foreach (var detail in adjustmentDTO.AdjustmentDetailArray) detail.AdjustmentId = adjustmentDTO.AdjustmentId; int eventTypeId = isNew ? EventTypeConstant.SAVEADJUSTMENTEVENTTYPEID : EventTypeConstant.UPDATEADJUSTMENTEVENTTYPEID; await _baseEntityAppService.ExecuteSaveAsync( adjustmentDTO.AdjustmentId, eventTypeId, adjustmentDTO, loginDTO, persistFunc: async tx => { if (isNew) { await _AdjustmentDAL.SaveAdjustment(adjustmentDTO, loginDTO, tx).ConfigureAwait(false); } else { await _AdjustmentDAL.DeleteAdjustmentDetails(adjustmentDTO.AdjustmentId, loginDTO, tx).ConfigureAwait(false); await _AdjustmentDAL.UpdateAdjustment(adjustmentDTO, loginDTO, tx).ConfigureAwait(false); } foreach (var detail in adjustmentDTO.AdjustmentDetailArray) await _AdjustmentDAL.SaveAdjustmentDetail(detail, loginDTO, tx).ConfigureAwait(false); return adjustmentDTO.AdjustmentId; }, null, BIZTRANSACTIONCLASSCONSTANT.ADJUSTMENT, -1, trans, isNewEntity: isNew).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); _Logger.LogInformation( "Adjustment {Action} — Id: {AdjustmentId}, Employee: {EmployeeCode} {EmployeeName}", isNew ? "Created" : "Updated", adjustmentDTO.AdjustmentId, employeeInfo.EmployeeCode, employeeInfo.EmployeeName); // GB4 parity: EventLog audit-trail entry with formatted in/out times and reason name. var firstDetail = adjustmentDTO.AdjustmentDetailArray[0]; string eventText = $"Attendance Adjustment Request/{employeeInfo.EmployeeCode}/{employeeInfo.EmployeeName}/" + $"{firstDetail.AdjustmentDetailAttendanceDate:dd-MMM-yyyy}/" + $"In: {MinutesIntoTime(firstDetail.AdjustmentDetailInTime)}/Out: {MinutesIntoTime(firstDetail.AdjustmentDetailOutTime)}/" + $"{reasonName}/{adjustmentDTO.AdjustmentRemarks}"; await _EventLogPublish.PublishEventLogAsync( eventText, adjustmentDTO, eventTypeId, adjustmentDTO.AdjustmentId, loginDTO, ct: ct).ConfigureAwait(false); // GB4 parity: post-save TDAILYATTENDANCE re-sync across the adjusted date range. var fromDate = adjustmentDTO.AdjustmentDetailArray.Min(d => d.AdjustmentDetailAttendanceDate); var toDate = adjustmentDTO.AdjustmentDetailArray.Max(d => d.AdjustmentDetailAttendanceDate); var resyncFilter = new DailyAttendanceFilterDTO { OuId = loginDTO.WorkOUId, EmployeeId = adjustmentDTO.AdjustmentDetailArray.Min(d => d.EmployeeId) }; await _DailyAttendanceDAL.UpdateDailyattendanceforSameAndAcrossDateFineTuned( fromDate, toDate, resyncFilter, loginDTO).ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {adjustmentDTO.AdjustmentId}" : SuccessResponse.UpdateSuccess; } catch (Exception) { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); throw; } } private static string MinutesIntoTime(int minutes) { if (minutes <= 0) return "00:00"; int hours = minutes / 60; int remaining = minutes % 60; return $"{hours:D2}:{remaining:D2}"; } // GB4 parity: AdjustmentBLL.GetAdjustment (AdjustmentBLL.cs:290). public async Task GetAdjustment(int adjustmentId, LoginDTO loginDTO, CancellationToken ct) { var header = await _AdjustmentDAL.GetAdjustment(adjustmentId, loginDTO, ct).ConfigureAwait(false); if (header == null) throw new KeyNotFoundException($"Adjustment with ID {adjustmentId} not found."); header.AdjustmentDetailArray = await _AdjustmentDAL.GetAdjustmentDetailsByHeader(adjustmentId, loginDTO, ct).ConfigureAwait(false); return header; } // GB4 parity: WCF Delete op handled by gRestServiceHandler.DeleteHandler(OBJECTADJUSTMENT,...) // with an EventLogDTO built via FillEventLog("Delete Claim>>{AdjustmentId}", EventTypeId // -1399999668, ...). GB5's custom delete now publishes the equivalent audit-trail entry. public async Task DeleteAdjustment(int adjustmentId, LoginDTO loginDTO, CancellationToken ct) { var trans = await _QueryExecutor.BeginTransactionAsync(loginDTO).ConfigureAwait(false); try { await _AdjustmentDAL.DeleteAdjustmentDetails(adjustmentId, loginDTO, trans).ConfigureAwait(false); await _AdjustmentDAL.DeleteAdjustment(adjustmentId, loginDTO, trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); await _EventLogPublish.PublishEventLogAsync( $"Delete Claim>>{adjustmentId}", new { AdjustmentId = adjustmentId }, EventTypeConstant.DELETEADJUSTMENTEVENTTYPEID, adjustmentId, loginDTO, ct: ct).ConfigureAwait(false); return SuccessResponse.DeleteSuccessMessage; } catch (Exception) { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); throw; } } // GB4 parity: AdjustmentBLL.GetAdjustmentForImport (AdjustmentBLL.cs:377). GB4 returned // CSV text from a DataTable; GB5 returns the typed list, letting the caller export/serialize. public async Task> GetAdjustmentForImport(CriteriaDTO criteriaDTO, LoginDTO loginDTO, CancellationToken ct) { return await _AdjustmentDAL.GetAdjustmentForImport(criteriaDTO, loginDTO, ct).ConfigureAwait(false); } } }