using System; using System.ComponentModel.DataAnnotations; using System.Threading; using System.Threading.Tasks; using RecruitmentDAL.DTO.JobProfileLibrary; 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.QueryExecutor; using GB5Shared.Resource.Response; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.Extensions.Logging; using RecruitmentDAL.CustomeCode.JobProfileLibrary; using static GB5Shared.GB5Constant.Constant; namespace RecruitmentBLL.JobProfileLibrary { public class JobProfileLibraryBLL : IJobProfileLibraryBLL { private readonly IJobProfileLibraryDAL _JobProfileLibraryDAL; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly IQueryExecutor _QueryExecutor; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; private const string AUTONUMBER_ENTITY_CODE = "JOBPROFILELIBRARY"; public JobProfileLibraryBLL( IJobProfileLibraryDAL jobProfileLibraryDAL, AutoNumber autoNumber, IValidation validation, IQueryExecutor queryExecutor, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _JobProfileLibraryDAL = jobProfileLibraryDAL; _AutoNumber = autoNumber; _Validation = validation; _QueryExecutor = queryExecutor; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } #region Get public async Task GetJobProfileLibrary(int JobProfileLibraryId, LoginDTO LoginDTO, CancellationToken ct) { try { return await _JobProfileLibraryDAL .GetJobProfileLibrary(JobProfileLibraryId, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-jobprofilelibrary-failed", ex); _Logger.LogError(ex, "GetJobProfileLibrary failed for JobProfileLibraryId {JobProfileLibraryId}", JobProfileLibraryId); throw; } } #endregion #region Save public async Task SaveJobProfileLibrary(JobProfileLibraryDTO JobProfileLibraryDTO, LoginDTO LoginDTO, CancellationToken ct) { if (JobProfileLibraryDTO == null) throw new ArgumentNullException(nameof(JobProfileLibraryDTO)); AutoNumberDTO? autoNumberDTO = null; bool isNew = JobProfileLibraryDTO.JobProfileLibraryId == 0; var Trans = await _QueryExecutor.BeginTransactionAsync(LoginDTO).ConfigureAwait(false); try { GB5Trace.Step("validate-jobprofilelibrary", new { JobProfileLibraryDTO.JobProfileLibraryId }); await _Validation.NotEmpty( JobProfileLibraryDTO.RoleTitle, nameof(JobProfileLibraryDTO.RoleTitle)).ConfigureAwait(false); JobProfileLibraryDTO.TenantId = LoginDTO.ClientId; JobProfileLibraryDTO.ModifiedById = LoginDTO.UserId; JobProfileLibraryDTO.ModifiedOn = DateTime.UtcNow; if (isNew) { // Plain AutoNumber INT PK (not IDENTITY) — matches JobPositionBLL's pattern, // unlike ProfileGenerator which reads back an IDENTITY value via // ExecuteIdentityAsync. autoNumberDTO = await _AutoNumber.GetNumberAsync( 1, AUTONUMBER_ENTITY_CODE, LoginDTO).ConfigureAwait(false); JobProfileLibraryDTO.JobProfileLibraryId = autoNumberDTO.StartNumber; JobProfileLibraryDTO.CreatedById = LoginDTO.UserId; JobProfileLibraryDTO.CreatedOn = DateTime.UtcNow; } int eventTypeId = isNew ? EventTypeConstant.SAVERECRUITMENTJOBPROFILELIBRARYEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTJOBPROFILELIBRARYEVENTTYPEID; GB5Trace.Step("save-jobprofilelibrary", new { JobProfileLibraryDTO.JobProfileLibraryId, isNew }); // Routes through the universal ExecuteSaveAsync pipeline (validation/workflow // hooks, Dapr event publish, outbox write) instead of a hand-rolled tx block. // The id is already known (AutoNumber-allocated above for inserts, or the // existing id for updates) — persistFunc just runs the INSERT/UPDATE and hands // that same id back. await _BaseEntityAppService.ExecuteSaveAsync( entityId: EntityConstant.OBJECTRECRUITMENTJOBPROFILELIBRARY, eventTypeId: eventTypeId, dto: JobProfileLibraryDTO, login: LoginDTO, persistFunc: async tx => { _ = isNew ? await _JobProfileLibraryDAL.SaveJobProfileLibrary(JobProfileLibraryDTO, LoginDTO, tx, ct).ConfigureAwait(false) : await _JobProfileLibraryDAL.UpdateJobProfileLibrary(JobProfileLibraryDTO, LoginDTO, tx, ct).ConfigureAwait(false); return JobProfileLibraryDTO.JobProfileLibraryId; }, externalTransaction: Trans, isNewEntity: isNew).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = eventTypeId }); var cacheKey = new CacheKeyGeneration().KeyGeneration( JobProfileLibraryDTO.JobProfileLibraryId, EntityConstant.OBJECTRECRUITMENTJOBPROFILELIBRARY, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation( "JobProfileLibrary {JobProfileLibraryId} {Action} by user {UserId}", JobProfileLibraryDTO.JobProfileLibraryId, isNew ? "created" : "updated", LoginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {JobProfileLibraryDTO.JobProfileLibraryId}" : $"{SuccessResponse.UpdateSuccessMessage} {JobProfileLibraryDTO.JobProfileLibraryId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-jobprofilelibrary-failed", vex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); if (autoNumberDTO != null && JobProfileLibraryDTO.JobProfileLibraryId > 0) await _AutoNumber.RollbackAutoNumber( AUTONUMBER_ENTITY_CODE, JobProfileLibraryDTO.JobProfileLibraryId, LoginDTO).ConfigureAwait(false); _Logger.LogWarning(vex, "SaveJobProfileLibrary validation failed for JobProfileLibraryId {JobProfileLibraryId}", JobProfileLibraryDTO.JobProfileLibraryId); // ArgumentException maps to HTTP 400 in BaseEndpoint — a validation failure is a // caller-input problem, not a server fault. throw new ArgumentException(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-jobprofilelibrary-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); if (autoNumberDTO != null && JobProfileLibraryDTO.JobProfileLibraryId > 0) await _AutoNumber.RollbackAutoNumber( AUTONUMBER_ENTITY_CODE, JobProfileLibraryDTO.JobProfileLibraryId, LoginDTO).ConfigureAwait(false); _Logger.LogError(ex, "SaveJobProfileLibrary failed for JobProfileLibraryId {JobProfileLibraryId}", JobProfileLibraryDTO.JobProfileLibraryId); throw; } } #endregion #region Select List public async Task GetSelectListJobProfileLibrary(CriteriaDTO criteriaDTO, LoginDTO LoginDTO, CancellationToken ct) { try { return await _JobProfileLibraryDAL .GetSelectListJobProfileLibrary(criteriaDTO, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-jobprofilelibrary-failed", ex); _Logger.LogError(ex, "GetSelectListJobProfileLibrary failed"); throw; } } #endregion #region Delete public async Task DeleteJobProfileLibrary(int JobProfileLibraryId, LoginDTO LoginDTO, CancellationToken ct) { var Trans = await _QueryExecutor.BeginTransactionAsync(LoginDTO).ConfigureAwait(false); int rowsAffected = 0; try { GB5Trace.Step("delete-jobprofilelibrary", new { JobProfileLibraryId }); await _BaseEntityAppService.ExecuteSaveAsync( entityId: EntityConstant.OBJECTRECRUITMENTJOBPROFILELIBRARY, eventTypeId: EventTypeConstant.DELETERECRUITMENTJOBPROFILELIBRARYEVENTTYPEID, dto: new JobProfileLibraryDTO { JobProfileLibraryId = JobProfileLibraryId }, login: LoginDTO, persistFunc: async tx => { rowsAffected = await _JobProfileLibraryDAL .DeleteJobProfileLibrary(JobProfileLibraryId, LoginDTO, tx, ct) .ConfigureAwait(false); return JobProfileLibraryId; }, externalTransaction: Trans).ConfigureAwait(false); await _QueryExecutor.CommitAsync(Trans).ConfigureAwait(false); if (rowsAffected <= 0) { _Logger.LogWarning( "DeleteJobProfileLibrary found no row for JobProfileLibraryId {JobProfileLibraryId}", JobProfileLibraryId); } GB5Trace.Step("event-publish", new { EventTypeId = EventTypeConstant.DELETERECRUITMENTJOBPROFILELIBRARYEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( JobProfileLibraryId, EntityConstant.OBJECTRECRUITMENTJOBPROFILELIBRARY, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation( "JobProfileLibrary {JobProfileLibraryId} deleted by user {UserId}", JobProfileLibraryId, LoginDTO.UserId); return SuccessResponse.DeleteSuccessMessage; } catch (Exception ex) { GB5Trace.MarkFailed("delete-jobprofilelibrary-failed", ex); await _QueryExecutor.RollbackAsync(Trans).ConfigureAwait(false); _Logger.LogError(ex, "DeleteJobProfileLibrary failed for JobProfileLibraryId {JobProfileLibraryId}", JobProfileLibraryId); throw; } } #endregion } }