using AccountsDAL.CustomCode.Budget; using AccountsDAL.DTO.Budget; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.EventLogPublish; using GB5Shared.GenerateAutoNumber; using GB5Shared.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using static GB5Shared.GB5Constant.Constant; namespace AccountsBLL.Budget { public class BudgetBLL : IBudgetBLL { private readonly IBudgetDAL _BudgetDAL; private readonly AutoNumber _AutoNumber; private readonly IQueryExecutor _QueryExecutor; private readonly EventLogPublish _EventLogPublish; public BudgetBLL( IBudgetDAL BudgetDAL, AutoNumber AutoNumber, IQueryExecutor QueryExecutor, EventLogPublish EventLogPublish) { _BudgetDAL = BudgetDAL; _AutoNumber = AutoNumber; _QueryExecutor = QueryExecutor; _EventLogPublish = EventLogPublish; } public async Task GetSelectListBudget(int FirstNumber, int MaxResult, CriteriaDTO CriteriaDTO, LoginDTO LoginDTO) { try { var result = await _BudgetDAL.GetSelectListBudget(FirstNumber, MaxResult, CriteriaDTO, LoginDTO); return result; } catch (Exception ex) { throw new Exception($"Error in BudgetBLL.GetSelectListBudget: {ex.Message}", ex); } } // Minimal composite Save: one Budget header + its OU-level BudgetData rows + their // account/account-group BudgetDataDetail lines, all allocated and inserted inside a single // transaction. Per CLAUDE.md's mutate-method rule this is one DTO parameter; per the // approved plan this is a genuinely minimal CRUD (no workflow/qualifier engine), matching // GetSelectListBudget's own equally-minimal precedent in this file. public async Task SaveBudget(BudgetDTO dto, LoginDTO loginDTO, CancellationToken ct) { if (dto == null) throw new ArgumentNullException(nameof(dto)); if (string.IsNullOrWhiteSpace(dto.BudgetCode)) throw new ArgumentException("Budget code is required."); if (string.IsNullOrWhiteSpace(dto.BudgetName)) throw new ArgumentException("Budget name is required."); if (dto.BudgetDataArray == null || dto.BudgetDataArray.Count == 0) throw new ArgumentException("At least one organizational-unit budget data row is required."); foreach (var budgetData in dto.BudgetDataArray) { if (budgetData.OUId == 0) throw new ArgumentException("OUId is required for each budget data row."); if (budgetData.BudgetDataDetailArray == null || budgetData.BudgetDataDetailArray.Count == 0) throw new ArgumentException("At least one account/account-group line is required per organizational unit."); } GB5Trace.Step("validate-budget", new { dto.BudgetCode }); bool isNew = dto.BudgetId == 0; var tx = await _QueryExecutor.BeginTransactionAsync(loginDTO).ConfigureAwait(false); try { if (isNew) { var auto = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.BUDGET, loginDTO).ConfigureAwait(false); dto.BudgetId = auto.StartNumber; } dto.TenantId = loginDTO.ClientId; dto.BudgetCreatedById = loginDTO.UserId; dto.BudgetCreatedOn = DateTime.UtcNow; dto.BudgetModifiedById = loginDTO.UserId; dto.BudgetModifiedOn = DateTime.UtcNow; GB5Trace.Step("save-budget", new { dto.BudgetId, isNew }); await _BudgetDAL.SaveBudget(dto, loginDTO, tx, ct).ConfigureAwait(false); foreach (var budgetData in dto.BudgetDataArray) { var autoData = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.BUDGETDATA, loginDTO).ConfigureAwait(false); budgetData.BudgetDataId = autoData.StartNumber; budgetData.BudgetId = dto.BudgetId; budgetData.BudgetDataCreatedById = loginDTO.UserId; budgetData.BudgetDataCreatedOn = DateTime.UtcNow; budgetData.BudgetDataModifiedById = loginDTO.UserId; budgetData.BudgetDataModifiedOn = DateTime.UtcNow; await _BudgetDAL.SaveBudgetData(budgetData, loginDTO, tx, ct).ConfigureAwait(false); short slNo = 1; foreach (var detail in budgetData.BudgetDataDetailArray) { var autoDetail = await _AutoNumber.GetNumberAsync(1, AUTONUMBERCONSTANT.BUDGETDATADETAIL, loginDTO).ConfigureAwait(false); detail.BudgetDataDetailId = autoDetail.StartNumber; detail.BudgetDataId = budgetData.BudgetDataId; detail.BudgetDataDetailSlNo = slNo++; await _BudgetDAL.SaveBudgetDataDetail(detail, loginDTO, tx, ct).ConfigureAwait(false); } } await _QueryExecutor.CommitAsync(tx).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeConstant.SAVEBUDGETEVENTTYPEID }); await _EventLogPublish.PublishEventLogAsync( "Budget Saved", dto, EventTypeConstant.SAVEBUDGETEVENTTYPEID, dto.BudgetId, loginDTO, ct: ct) .ConfigureAwait(false); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {dto.BudgetId}" : SuccessResponse.UpdateSuccess; } catch (Exception ex) { await _QueryExecutor.RollbackAsync(tx).ConfigureAwait(false); GB5Trace.MarkFailed("save-budget-failed", ex); throw; } } } }