using System; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using RecruitmentDAL.DTO.FlsBridge; 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.FlsBridge; using static GB5Shared.GB5Constant.Constant; namespace RecruitmentBLL.FlsAction { // Entity CRUD for TRECRUITMENTFLSACTION — JobPosition-style single-entity save pipeline // (ExecuteSaveAsync, GB5Trace, ILogger, i18n, tenant filtering), per CLAUDE.md. In practice // most writes to this entity come from RecruitmentFlsBridgeBLL (Prepare/Submit) rather than // a direct FE call — this class exists so those bridge calls, and any future admin/audit // screen, route through the exact same CLAUDE.md-compliant save pipeline as every other // Recruitment entity, instead of the bridge hand-rolling its own persistence. public class RecruitmentFlsActionBLL : IRecruitmentFlsActionBLL { private readonly IRecruitmentFlsActionDAL _RecruitmentFlsActionDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; public RecruitmentFlsActionBLL( IRecruitmentFlsActionDAL recruitmentFlsActionDAL, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _RecruitmentFlsActionDAL = recruitmentFlsActionDAL; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } public async Task GetRecruitmentFlsAction(int RecruitmentFlsActionId, LoginDTO LoginDTO, CancellationToken ct) { try { return await _RecruitmentFlsActionDAL.GetRecruitmentFlsAction(RecruitmentFlsActionId, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-recruitmentflsaction-failed", ex); _Logger.LogError(ex, "GetRecruitmentFlsAction failed for RecruitmentFlsActionId {RecruitmentFlsActionId}", RecruitmentFlsActionId); throw; } } public async Task SaveRecruitmentFlsAction(RecruitmentFlsActionDTO RecruitmentFlsActionDTO, LoginDTO LoginDTO, CancellationToken ct) { if (RecruitmentFlsActionDTO == null) throw new ArgumentNullException(nameof(RecruitmentFlsActionDTO)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-recruitmentflsaction", new { RecruitmentFlsActionDTO.RecruitmentFlsActionId }); if (RecruitmentFlsActionDTO.ActionType < RecruitmentFlsActionType.DetailedApplication || RecruitmentFlsActionDTO.ActionType > RecruitmentFlsActionType.VoluntarySelfId) throw new ValidationException($"ActionType must be 1-6. Received: {RecruitmentFlsActionDTO.ActionType}."); if (RecruitmentFlsActionDTO.ApplicationId <= 0) throw new ValidationException("ApplicationId is required for every Recruitment FLS action."); RecruitmentFlsActionDTO.TenantId = LoginDTO.ClientId; RecruitmentFlsActionDTO.ModifiedById = LoginDTO.UserId; RecruitmentFlsActionDTO.ModifiedOn = DateTime.UtcNow; bool isNew = RecruitmentFlsActionDTO.RecruitmentFlsActionId == 0; if (isNew) { autoNumberDTO = await _AutoNumber.GetNumberAsync( 1, "RECRUITMENTFLSACTION", LoginDTO).ConfigureAwait(false); RecruitmentFlsActionDTO.RecruitmentFlsActionId = autoNumberDTO.StartNumber; RecruitmentFlsActionDTO.CreatedById = LoginDTO.UserId; RecruitmentFlsActionDTO.CreatedOn = DateTime.UtcNow; } GB5Trace.Step("save-recruitmentflsaction", new { RecruitmentFlsActionDTO.RecruitmentFlsActionId, isNew }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTRECRUITMENTFLSACTION, isNew ? EventTypeConstant.SAVERECRUITMENTFLSACTIONEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTFLSACTIONEVENTTYPEID, RecruitmentFlsActionDTO, LoginDTO, async tx => { _ = isNew ? await _RecruitmentFlsActionDAL.SaveRecruitmentFlsAction(RecruitmentFlsActionDTO, LoginDTO, tx, ct).ConfigureAwait(false) : await _RecruitmentFlsActionDAL.UpdateRecruitmentFlsAction(RecruitmentFlsActionDTO, LoginDTO, tx, ct).ConfigureAwait(false); return RecruitmentFlsActionDTO.RecruitmentFlsActionId; }, null, -1, -1, null, isNew).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVERECRUITMENTFLSACTIONEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTFLSACTIONEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( RecruitmentFlsActionDTO.RecruitmentFlsActionId, EntityConstant.OBJECTRECRUITMENTFLSACTION, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation( "RecruitmentFlsAction {RecruitmentFlsActionId} {Action} by user {UserId}", RecruitmentFlsActionDTO.RecruitmentFlsActionId, isNew ? "saved" : "updated", LoginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {RecruitmentFlsActionDTO.RecruitmentFlsActionId}" : $"{SuccessResponse.UpdateSuccessMessage} {RecruitmentFlsActionDTO.RecruitmentFlsActionId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-recruitmentflsaction-failed", vex); if (autoNumberDTO != null && RecruitmentFlsActionDTO.RecruitmentFlsActionId > 0) await _AutoNumber.RollbackAutoNumber("RECRUITMENTFLSACTION", RecruitmentFlsActionDTO.RecruitmentFlsActionId, LoginDTO).ConfigureAwait(false); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-recruitmentflsaction-failed", ex); _Logger.LogError(ex, "SaveRecruitmentFlsAction failed for RecruitmentFlsActionId {RecruitmentFlsActionId}", RecruitmentFlsActionDTO.RecruitmentFlsActionId); if (autoNumberDTO != null && RecruitmentFlsActionDTO.RecruitmentFlsActionId > 0) { await _AutoNumber.RollbackAutoNumber( "RECRUITMENTFLSACTION", RecruitmentFlsActionDTO.RecruitmentFlsActionId, LoginDTO).ConfigureAwait(false); } throw; } } public async Task GetSelectListRecruitmentFlsAction(CriteriaDTO criteriaDTO, LoginDTO LoginDTO, CancellationToken ct) { try { return await _RecruitmentFlsActionDAL.GetSelectListRecruitmentFlsAction(criteriaDTO, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-recruitmentflsaction-failed", ex); _Logger.LogError(ex, "GetSelectListRecruitmentFlsAction failed"); throw; } } public async Task DeleteRecruitmentFlsAction(int RecruitmentFlsActionId, LoginDTO LoginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-recruitmentflsaction", new { RecruitmentFlsActionId }); var result = await _RecruitmentFlsActionDAL.DeleteRecruitmentFlsAction(RecruitmentFlsActionId, LoginDTO, ct) .ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( RecruitmentFlsActionId, EntityConstant.OBJECTRECRUITMENTFLSACTION, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("RecruitmentFlsAction {RecruitmentFlsActionId} deleted by user {UserId}", RecruitmentFlsActionId, LoginDTO.UserId); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-recruitmentflsaction-failed", ex); _Logger.LogError(ex, "DeleteRecruitmentFlsAction failed for RecruitmentFlsActionId {RecruitmentFlsActionId}", RecruitmentFlsActionId); throw; } } } }