using System; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using RecruitmentDAL.DTO.ManpowerBudget; 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 Microsoft.Extensions.Logging; using RecruitmentDAL.CustomeCode.ManpowerBudget; using static GB5Shared.GB5Constant.Constant; namespace RecruitmentBLL.ManpowerBudget { public class ManpowerBudgetBLL : IManpowerBudgetBLL { // FISCALQUARTER (TMANPOWERBUDGET, migration // 20260905_Recruitment_Phase6_ManpowerBudget_SqlServer): 0=whole-year 1..4=quarter. private const byte FISCALQUARTER_ANNUAL = 0; private const byte FISCALQUARTER_MAX = 4; private readonly IManpowerBudgetDAL _ManpowerBudgetDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; public ManpowerBudgetBLL( IManpowerBudgetDAL manpowerBudgetDAL, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _ManpowerBudgetDAL = manpowerBudgetDAL; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } public async Task GetManpowerBudget(int ManpowerBudgetId, LoginDTO LoginDTO, CancellationToken ct) { try { return await _ManpowerBudgetDAL.GetManpowerBudget(ManpowerBudgetId, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-manpowerbudget-failed", ex); _Logger.LogError(ex, "GetManpowerBudget failed for ManpowerBudgetId {ManpowerBudgetId}", ManpowerBudgetId); throw; } } public async Task SaveManpowerBudget(ManpowerBudgetDTO ManpowerBudgetDTO, LoginDTO LoginDTO, CancellationToken ct) { if (ManpowerBudgetDTO == null) throw new ArgumentNullException(nameof(ManpowerBudgetDTO)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-manpowerbudget", new { ManpowerBudgetDTO.ManpowerBudgetId }); if (ManpowerBudgetDTO.DepartmentId <= 0) { throw new ValidationException( $"{nameof(ManpowerBudgetDTO.DepartmentId)} must reference a valid MDEPARTMENT row."); } if (ManpowerBudgetDTO.FiscalYear <= 0) { throw new ValidationException( $"{nameof(ManpowerBudgetDTO.FiscalYear)} must be a valid year. Received: {ManpowerBudgetDTO.FiscalYear}."); } if (ManpowerBudgetDTO.FiscalQuarter > FISCALQUARTER_MAX) { throw new ValidationException( $"{nameof(ManpowerBudgetDTO.FiscalQuarter)} must be 0 (whole year) or 1-4 (quarter). Received: {ManpowerBudgetDTO.FiscalQuarter}."); } if (ManpowerBudgetDTO.ApprovedHeadcount < 0) { throw new ValidationException( $"{nameof(ManpowerBudgetDTO.ApprovedHeadcount)} cannot be negative."); } if (ManpowerBudgetDTO.ApprovedBudgetAmount < 0) { throw new ValidationException( $"{nameof(ManpowerBudgetDTO.ApprovedBudgetAmount)} cannot be negative."); } bool isNew = ManpowerBudgetDTO.ManpowerBudgetId == 0; // Consumed counters are only ever advanced by JobRequisitionBLL.ApproveJobRequisition's // IncrementManpowerBudgetConsumed follow-up call — never accepted from a caller-supplied // save payload, so a new budget row always starts at zero consumption and an update to // an existing row never lets a caller silently reset/inflate what's already been consumed. if (isNew) { ManpowerBudgetDTO.ConsumedHeadcount = 0; ManpowerBudgetDTO.ConsumedBudgetAmount = 0; } else { var existingJson = await _ManpowerBudgetDAL.GetManpowerBudget(ManpowerBudgetDTO.ManpowerBudgetId, LoginDTO, ct).ConfigureAwait(false); var existing = Newtonsoft.Json.JsonConvert.DeserializeObject(existingJson); ManpowerBudgetDTO.ConsumedHeadcount = existing?.ConsumedHeadcount ?? 0; ManpowerBudgetDTO.ConsumedBudgetAmount = existing?.ConsumedBudgetAmount ?? 0; } ManpowerBudgetDTO.ModifiedById = LoginDTO.UserId; ManpowerBudgetDTO.ModifiedOn = DateTime.UtcNow; ManpowerBudgetDTO.TenantId = LoginDTO.ClientId; if (isNew) { autoNumberDTO = await _AutoNumber.GetNumberAsync( 1, "MANPOWERBUDGET", LoginDTO).ConfigureAwait(false); ManpowerBudgetDTO.ManpowerBudgetId = autoNumberDTO.StartNumber; ManpowerBudgetDTO.CreatedById = LoginDTO.UserId; ManpowerBudgetDTO.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-manpowerbudget", new { ManpowerBudgetDTO.ManpowerBudgetId, isNew }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTRECRUITMENTMANPOWERBUDGET, isNew ? EventTypeConstant.SAVERECRUITMENTMANPOWERBUDGETEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTMANPOWERBUDGETEVENTTYPEID, ManpowerBudgetDTO, LoginDTO, async tx => { _ = isNew ? await _ManpowerBudgetDAL.SaveManpowerBudget(ManpowerBudgetDTO, LoginDTO, tx, ct).ConfigureAwait(false) : await _ManpowerBudgetDAL.UpdateManpowerBudget(ManpowerBudgetDTO, LoginDTO, tx, ct).ConfigureAwait(false); return ManpowerBudgetDTO.ManpowerBudgetId; }, null, -1, -1, null, isNew).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVERECRUITMENTMANPOWERBUDGETEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTMANPOWERBUDGETEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( ManpowerBudgetDTO.ManpowerBudgetId, EntityConstant.OBJECTRECRUITMENTMANPOWERBUDGET, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation( "ManpowerBudget {ManpowerBudgetId} {Action} by user {UserId}", ManpowerBudgetDTO.ManpowerBudgetId, isNew ? "saved" : "updated", LoginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {ManpowerBudgetDTO.ManpowerBudgetId}" : $"{SuccessResponse.UpdateSuccessMessage} {ManpowerBudgetDTO.ManpowerBudgetId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-manpowerbudget-failed", vex); if (autoNumberDTO != null && ManpowerBudgetDTO.ManpowerBudgetId > 0) await _AutoNumber.RollbackAutoNumber("MANPOWERBUDGET", ManpowerBudgetDTO.ManpowerBudgetId, LoginDTO).ConfigureAwait(false); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-manpowerbudget-failed", ex); _Logger.LogError(ex, "SaveManpowerBudget failed for ManpowerBudgetId {ManpowerBudgetId}", ManpowerBudgetDTO.ManpowerBudgetId); if (autoNumberDTO != null && ManpowerBudgetDTO.ManpowerBudgetId > 0) { await _AutoNumber.RollbackAutoNumber( "MANPOWERBUDGET", ManpowerBudgetDTO.ManpowerBudgetId, LoginDTO).ConfigureAwait(false); } throw; } } public async Task GetSelectListManpowerBudget(CriteriaDTO criteriaDTO, LoginDTO LoginDTO, CancellationToken ct) { try { return await _ManpowerBudgetDAL.GetSelectListManpowerBudget(criteriaDTO, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-manpowerbudget-failed", ex); _Logger.LogError(ex, "GetSelectListManpowerBudget failed"); throw; } } public async Task DeleteManpowerBudget(int ManpowerBudgetId, LoginDTO LoginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-manpowerbudget", new { ManpowerBudgetId }); var result = await _ManpowerBudgetDAL.DeleteManpowerBudget(ManpowerBudgetId, LoginDTO, ct) .ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( ManpowerBudgetId, EntityConstant.OBJECTRECRUITMENTMANPOWERBUDGET, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("ManpowerBudget {ManpowerBudgetId} deleted by user {UserId}", ManpowerBudgetId, LoginDTO.UserId); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-manpowerbudget-failed", ex); _Logger.LogError(ex, "DeleteManpowerBudget failed for ManpowerBudgetId {ManpowerBudgetId}", ManpowerBudgetId); throw; } } } }