using GB5Shared.DTO.Framework.AutoNumber; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using PayRollDAL.CustomeCode.Relieving; using PayRollDAL.DTO.Relieving; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace PayRollBLL.Relieving { public class RelievingBLL : IRelievingBLL { private readonly IRelievingDAL _RelievingDAL; private readonly AutoNumber _AutoNumber; private readonly VoucherNumberService _VoucherNumberService; private readonly IQueryExecutor _QueryExecutor; public RelievingBLL( IRelievingDAL relievingDAL, AutoNumber autoNumber, VoucherNumberService voucherNumberService, IQueryExecutor queryExecutor) { _RelievingDAL = relievingDAL; _AutoNumber = autoNumber; _VoucherNumberService = voucherNumberService; _QueryExecutor = queryExecutor; } // ── Get single record ───────────────────────────────────────────── public async Task GetRelieving(int RelievingId, LoginDTO LoginDTO) { return await _RelievingDAL.GetRelieving(RelievingId, LoginDTO).ConfigureAwait(false); } // ── Save (insert or update) ─────────────────────────────────────── // GB4 parity: RelievingBLL.SaveRelieving — autonumber block sized for // header + all detail rows (GenericDal.GetNumber(Count,"RELIEVING",...)), // VNO generated only on insert (VnoGenerationDLL.GetNextVno when // BIZTransactionType.NOGenerationType==0 — VoucherNumberService returns // null for manual mode, so the number is left as supplied by the caller). // Transaction shared across autonumber-consuming inserts so a rollback // reverts both the counter and the entity rows, matching GB4 semantics. public async Task SaveRelieving(RelievingDTO RelievingDTO, LoginDTO LoginDTO) { if (RelievingDTO == null) throw new ArgumentNullException(nameof(RelievingDTO)); bool isNew = RelievingDTO.RelievingId == 0; AutoNumberDTO? autoNumberDTO = null; // An omitted/unparseable reference date (e.g. "0") deserializes to // default(DateTime) = 0001-01-01, which is below SQL Server's DATETIME // floor (1/1/1753) and throws SqlDateTime overflow on save. if (RelievingDTO.RelievingReferenceDate == default) RelievingDTO.RelievingReferenceDate = RelievingDTO.RelievingRelievingDate; var trans = await _QueryExecutor.BeginTransactionAsync(LoginDTO).ConfigureAwait(false); try { if (isNew) { int count = 1 + (RelievingDTO.RelievingDetailArray?.Count ?? 0); autoNumberDTO = await _AutoNumber.GetNumberAsync(count, "RELIEVING", LoginDTO) .ConfigureAwait(false); RelievingDTO.RelievingId = autoNumberDTO.StartNumber; RelievingDTO.RelievingCreatedById = LoginDTO.UserId; RelievingDTO.RelievingCreatedOn = DateTime.UtcNow; if (RelievingDTO.RelievingDetailArray != null) { int tmpCount = 1; foreach (var detail in RelievingDTO.RelievingDetailArray) { detail.RelievingDetailId = autoNumberDTO.StartNumber + tmpCount; tmpCount++; } } string? generatedNumber = await _VoucherNumberService .GetNextVoucherNumberAsync( RelievingDTO.BIZTransactionTypeId, RelievingDTO.RelievingRelievingDate, LoginDTO, transaction: trans) .ConfigureAwait(false); if (generatedNumber != null) RelievingDTO.RelievingRelievingNumber = generatedNumber; } RelievingDTO.RelievingModifiedById = LoginDTO.UserId; RelievingDTO.RelievingModifiedOn = DateTime.UtcNow; int rows = isNew ? await _RelievingDAL.SaveRelieving(RelievingDTO, LoginDTO, trans).ConfigureAwait(false) : await _RelievingDAL.UpdateRelieving(RelievingDTO, LoginDTO, trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); if (rows <= 0) return isNew ? "Failed to save separation." : "Failed to update separation."; return isNew ? "Details Saved Successfully" : "Details Updated Successfully"; } catch (Exception) { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); if (autoNumberDTO != null) await _AutoNumber.RollbackAutoNumber("RELIEVING", autoNumberDTO.StartNumber, LoginDTO) .ConfigureAwait(false); throw; } } // ── Delete ──────────────────────────────────────────────────────── public async Task DeleteRelieving(int RelievingId, LoginDTO LoginDTO) { return await _RelievingDAL.DeleteRelieving(RelievingId, LoginDTO).ConfigureAwait(false); } // ── Picklist ────────────────────────────────────────────────────── public async Task GetSelectListRelieving(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { return await _RelievingDAL .GetSelectListRelieving(FirstNumber, MaxResult, CriteriaDTO, LoginDTO) .ConfigureAwait(false); } // ── Save Relieving Settlement (separate settlement grid) ──────────── // GB4 parity: RelievingBLL.SaveRelievingSettlement. // 1. Sums Add (OperationType==0) / Sub (OperationType==1) amounts across the // whole incoming list, and pushes SETTLEMENTAMOUNT = Add - Sub onto the // parent Relieving header (GB4's "Update Releiving Table" block). // 2. New rows (Id==0) get autonumbers and are inserted. // 3. Existing rows (Id!=0) are update-path: GB4 deleted ALL settlement rows // for the row's RelievingId and re-inserted, ONCE PER DTO IN THE LOOP — so // N update-rows sharing a RelievingId caused N delete+reinsert cycles. // Fixed here: group update-rows by RelievingId and delete-once per distinct // RelievingId, then write all of that group's rows — same end result, no // redundant churn. GB4's raw string-concatenated delete // ("...WHERE RELIEVINGID =" + id) is also replaced with the parameterized // DELETE_RELIEVINGSETTLEMENT_BY_RELIEVINGID query. // Whole batch (new-row inserts, update-row delete+reinsert, header amount update) // is wrapped in one transaction; autonumber allocation happens before the // transaction per the established SaveFoo/SaveTLeave pattern. public async Task SaveRelievingSettlement(List RelievingSettlementDTOs, LoginDTO LoginDTO) { if (RelievingSettlementDTOs == null || RelievingSettlementDTOs.Count == 0) return "No settlement rows supplied."; double add = 0; double sub = 0; foreach (var dto in RelievingSettlementDTOs) { if (dto.RelievingSettlementOperationType == 0) // Add add += dto.RelievingSettlementAmount; else if (dto.RelievingSettlementOperationType == 1) // Sub sub += dto.RelievingSettlementAmount; } var newRows = RelievingSettlementDTOs.Where(d => d.RelievingSettlementId == 0).ToList(); var updateRows = RelievingSettlementDTOs.Where(d => d.RelievingSettlementId != 0).ToList(); AutoNumberDTO? autoNumberDTO = null; if (newRows.Count > 0) autoNumberDTO = await _AutoNumber.GetNumberAsync(newRows.Count, "RELIEVINGSETTLEMENT", LoginDTO) .ConfigureAwait(false); var trans = await _QueryExecutor.BeginTransactionAsync(LoginDTO).ConfigureAwait(false); try { int t = 0; foreach (var dto in newRows) { dto.RelievingSettlementId = autoNumberDTO!.StartNumber + t; t++; await _RelievingDAL.SaveRelievingSettlementDetail(dto, LoginDTO, trans).ConfigureAwait(false); } // Group update-path rows by RelievingId so each distinct header's existing // settlement rows are deleted exactly once, not once per DTO (see note above). foreach (var group in updateRows.GroupBy(d => d.RelievingId)) { await _RelievingDAL.DeleteRelievingSettlementsByRelievingId(group.Key, LoginDTO, trans) .ConfigureAwait(false); foreach (var dto in group) await _RelievingDAL.SaveRelievingSettlementDetail(dto, LoginDTO, trans).ConfigureAwait(false); } decimal settlementAmount = (decimal)(add - sub); int headerRelievingId = RelievingSettlementDTOs[0].RelievingId; await _RelievingDAL.UpdateRelievingSettlementAmount(headerRelievingId, settlementAmount, LoginDTO, trans) .ConfigureAwait(false); await _QueryExecutor.CommitAsync(trans).ConfigureAwait(false); return "Details Saved Successfully"; } catch (Exception) { await _QueryExecutor.RollbackAsync(trans).ConfigureAwait(false); if (autoNumberDTO != null) await _AutoNumber.RollbackAutoNumber("RELIEVINGSETTLEMENT", autoNumberDTO.StartNumber, LoginDTO) .ConfigureAwait(false); throw; } } } }