using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using RecruitmentDAL.DTO.BgVerification; using GB5Shared.DaprCache; using GB5Shared.DTO.Framework.AutoNumber; 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 Newtonsoft.Json; using RecruitmentBLL.Integration; using RecruitmentDAL.CustomeCode.BgVerification; using static GB5Shared.GB5Constant.Constant; namespace RecruitmentBLL.BgVerification { // Entity CRUD for TBGVERIFICATION — RecruitmentFlsActionBLL-style single-entity save pipeline // (ExecuteSaveAsync, GB5Trace, ILogger, i18n, tenant filtering), per CLAUDE.md. See // BgVerificationDTO's class doc comment for why this is its own child table (one Application // can have several concurrent checks) rather than a field on ApplicationDTO. public class BgVerificationBLL : IBgVerificationBLL { private readonly IBgVerificationDAL _BgVerificationDAL; private readonly IBgVerificationVendorService _VendorService; private readonly AutoNumber _AutoNumber; private readonly IValidation _Validation; private readonly KeyInvalidate _KeyInvalidate; private readonly BaseEntityAppService _BaseEntityAppService; private readonly ILogger _Logger; public BgVerificationBLL( IBgVerificationDAL bgVerificationDAL, IBgVerificationVendorService vendorService, AutoNumber autoNumber, IValidation validation, KeyInvalidate keyInvalidate, BaseEntityAppService baseEntityAppService, ILogger logger) { _BgVerificationDAL = bgVerificationDAL; _VendorService = vendorService; _AutoNumber = autoNumber; _Validation = validation; _KeyInvalidate = keyInvalidate; _BaseEntityAppService = baseEntityAppService; _Logger = logger; } public async Task GetBgVerification(int BgVerificationId, LoginDTO LoginDTO, CancellationToken ct) { try { return await _BgVerificationDAL.GetBgVerification(BgVerificationId, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-bgverification-failed", ex); _Logger.LogError(ex, "GetBgVerification failed for BgVerificationId {BgVerificationId}", BgVerificationId); throw; } } public async Task GetSelectListBgVerification(int ApplicationId, LoginDTO LoginDTO, CancellationToken ct) { try { return await _BgVerificationDAL.GetSelectListBgVerification(ApplicationId, LoginDTO, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("get-selectlist-bgverification-failed", ex); _Logger.LogError(ex, "GetSelectListBgVerification failed for ApplicationId {ApplicationId}", ApplicationId); throw; } } public async Task SaveBgVerification(BgVerificationDTO BgVerificationDTO, LoginDTO LoginDTO, CancellationToken ct) { if (BgVerificationDTO == null) throw new ArgumentNullException(nameof(BgVerificationDTO)); AutoNumberDTO? autoNumberDTO = null; try { GB5Trace.Step("validate-bgverification", new { BgVerificationDTO.BgVerificationId }); if (BgVerificationDTO.ApplicationId <= 0) throw new ValidationException("ApplicationId is required for a BG verification check."); if (BgVerificationDTO.CheckType < BgVerificationCheckType.Education || BgVerificationDTO.CheckType > BgVerificationCheckType.Reference) throw new ValidationException($"CheckType must be 1-4. Received: {BgVerificationDTO.CheckType}."); if (BgVerificationDTO.CheckStatus < BgVerificationCheckStatus.Pending || BgVerificationDTO.CheckStatus > BgVerificationCheckStatus.Delayed) throw new ValidationException($"CheckStatus must be 1-5. Received: {BgVerificationDTO.CheckStatus}."); BgVerificationDTO.TenantId = LoginDTO.ClientId; BgVerificationDTO.ModifiedById = LoginDTO.UserId; BgVerificationDTO.ModifiedOn = DateTime.UtcNow; bool isNew = BgVerificationDTO.BgVerificationId == 0; if (isNew) { autoNumberDTO = await _AutoNumber.GetNumberAsync( 1, "BGVERIFICATION", LoginDTO).ConfigureAwait(false); BgVerificationDTO.BgVerificationId = autoNumberDTO.StartNumber; BgVerificationDTO.CreatedById = LoginDTO.UserId; BgVerificationDTO.CreatedOn = DateTime.UtcNow; if (BgVerificationDTO.InitiatedOn == null) BgVerificationDTO.InitiatedOn = DateTime.UtcNow; } GB5Trace.Step("save-bgverification", new { BgVerificationDTO.BgVerificationId, isNew }); await _BaseEntityAppService.ExecuteSaveAsync( EntityConstant.OBJECTRECRUITMENTBGVERIFICATION, isNew ? EventTypeConstant.SAVERECRUITMENTBGVERIFICATIONEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTBGVERIFICATIONEVENTTYPEID, BgVerificationDTO, LoginDTO, async tx => { _ = isNew ? await _BgVerificationDAL.SaveBgVerification(BgVerificationDTO, LoginDTO, tx, ct).ConfigureAwait(false) : await _BgVerificationDAL.UpdateBgVerification(BgVerificationDTO, LoginDTO, tx, ct).ConfigureAwait(false); return BgVerificationDTO.BgVerificationId; }, null, -1, -1, null, isNew).ConfigureAwait(false); GB5Trace.Step("event-publish", new { EventTypeId = isNew ? EventTypeConstant.SAVERECRUITMENTBGVERIFICATIONEVENTTYPEID : EventTypeConstant.UPDATERECRUITMENTBGVERIFICATIONEVENTTYPEID }); var cacheKey = new CacheKeyGeneration().KeyGeneration( BgVerificationDTO.BgVerificationId, EntityConstant.OBJECTRECRUITMENTBGVERIFICATION, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation( "BgVerification {BgVerificationId} {Action} by user {UserId}", BgVerificationDTO.BgVerificationId, isNew ? "saved" : "updated", LoginDTO.UserId); return isNew ? $"{SuccessResponse.SaveSuccessMessage} {BgVerificationDTO.BgVerificationId}" : $"{SuccessResponse.UpdateSuccessMessage} {BgVerificationDTO.BgVerificationId}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("save-bgverification-failed", vex); if (autoNumberDTO != null && BgVerificationDTO.BgVerificationId > 0) await _AutoNumber.RollbackAutoNumber("BGVERIFICATION", BgVerificationDTO.BgVerificationId, LoginDTO).ConfigureAwait(false); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("save-bgverification-failed", ex); _Logger.LogError(ex, "SaveBgVerification failed for BgVerificationId {BgVerificationId}", BgVerificationDTO.BgVerificationId); if (autoNumberDTO != null && BgVerificationDTO.BgVerificationId > 0) { await _AutoNumber.RollbackAutoNumber( "BGVERIFICATION", BgVerificationDTO.BgVerificationId, LoginDTO).ConfigureAwait(false); } throw; } } public async Task DeleteBgVerification(int BgVerificationId, LoginDTO LoginDTO, CancellationToken ct) { try { GB5Trace.Step("delete-bgverification", new { BgVerificationId }); var result = await _BgVerificationDAL.DeleteBgVerification(BgVerificationId, LoginDTO, ct) .ConfigureAwait(false); var cacheKey = new CacheKeyGeneration().KeyGeneration( BgVerificationId, EntityConstant.OBJECTRECRUITMENTBGVERIFICATION, CacheKeyLevel.CLIENT_LEVEL, LoginDTO); await _KeyInvalidate.AllInvalidateCache(cacheKey).ConfigureAwait(false); _Logger.LogInformation("BgVerification {BgVerificationId} deleted by user {UserId}", BgVerificationId, LoginDTO.UserId); return result; } catch (Exception ex) { GB5Trace.MarkFailed("delete-bgverification-failed", ex); _Logger.LogError(ex, "DeleteBgVerification failed for BgVerificationId {BgVerificationId}", BgVerificationId); throw; } } // Opens one or more concurrent BG checks for an Application. See IBgVerificationBLL's // doc comment: Education/Employment/Criminal are best-effort routed through // IBgVerificationVendorService (the GOP Target Operation placeholder — never blocks or // fails this method if the vendor call itself fails); Reference is left Pending for a // recruiter to dispatch separately via the existing FLS bridge once a referee is known. public async Task InitiateBgVerification(InitiateBgVerificationDTO InitiateBgVerificationDTO, LoginDTO LoginDTO, CancellationToken ct) { if (InitiateBgVerificationDTO == null) throw new ArgumentNullException(nameof(InitiateBgVerificationDTO)); try { GB5Trace.Step("validate-initiate-bgverification", new { InitiateBgVerificationDTO.ApplicationId }); if (InitiateBgVerificationDTO.ApplicationId <= 0) throw new ValidationException("ApplicationId is required to initiate BG verification."); var requestedTypes = (InitiateBgVerificationDTO.CheckTypes != null && InitiateBgVerificationDTO.CheckTypes.Count > 0) ? InitiateBgVerificationDTO.CheckTypes.Distinct().ToList() : new List { BgVerificationCheckType.Education, BgVerificationCheckType.Employment, BgVerificationCheckType.Criminal, BgVerificationCheckType.Reference }; var createdIds = new List(); foreach (var checkType in requestedTypes) { if (checkType < BgVerificationCheckType.Education || checkType > BgVerificationCheckType.Reference) { _Logger.LogWarning( "InitiateBgVerification skipped unknown CheckType {CheckType} for Application {ApplicationId}", checkType, InitiateBgVerificationDTO.ApplicationId); continue; } var bgDto = new BgVerificationDTO { ApplicationId = InitiateBgVerificationDTO.ApplicationId, CheckType = checkType, CheckStatus = BgVerificationCheckStatus.Pending, InitiatedOn = DateTime.UtcNow }; await SaveBgVerification(bgDto, LoginDTO, ct).ConfigureAwait(false); createdIds.Add(bgDto.BgVerificationId); // Education/Employment/Criminal: best-effort vendor dispatch. Reference stays // Pending — see method doc comment. if (checkType != BgVerificationCheckType.Reference) { try { var outcome = await _VendorService.InitiateVendorCheckAsync(bgDto, LoginDTO, ct).ConfigureAwait(false); if (outcome.Success) { await UpdateCheckStatusAsync(new UpdateBgVerificationStatusDTO { BgVerificationId = bgDto.BgVerificationId, CheckStatus = BgVerificationCheckStatus.InProgress, VendorReference = outcome.VendorReference }, LoginDTO, ct).ConfigureAwait(false); } } catch (Exception vendorEx) { // Non-fatal — the check stays Pending; a real vendor integration would // add its own retry/backfill job, out of scope for this placeholder. GB5Trace.MarkFailed("bg-verification-vendor-initiate-failed", vendorEx); _Logger.LogError(vendorEx, "Vendor check initiation failed for BgVerificationId {BgVerificationId}", bgDto.BgVerificationId); } } } _Logger.LogInformation( "BG verification initiated for Application {ApplicationId}: {Count} check(s) opened by user {UserId}", InitiateBgVerificationDTO.ApplicationId, createdIds.Count, LoginDTO.UserId); return $"{SuccessResponse.SaveSuccessMessage} {string.Join(",", createdIds)}"; } catch (ValidationException vex) { GB5Trace.MarkFailed("initiate-bgverification-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("initiate-bgverification-failed", ex); _Logger.LogError(ex, "InitiateBgVerification failed for ApplicationId {ApplicationId}", InitiateBgVerificationDTO.ApplicationId); throw; } } // Single-DTO-param status update — see IBgVerificationBLL's doc comment for callers. // Silently no-ops when the row can't be resolved (e.g. a plain, non-BG-verification // ReferenceCheck FLS submission with BgVerificationId left at its -1 sentinel). public async Task UpdateCheckStatusAsync(UpdateBgVerificationStatusDTO UpdateBgVerificationStatusDTO, LoginDTO LoginDTO, CancellationToken ct) { if (UpdateBgVerificationStatusDTO == null || UpdateBgVerificationStatusDTO.BgVerificationId <= 0) return; try { var json = await GetBgVerification(UpdateBgVerificationStatusDTO.BgVerificationId, LoginDTO, ct).ConfigureAwait(false); var existing = string.IsNullOrWhiteSpace(json) ? null : JsonConvert.DeserializeObject(json); if (existing == null || existing.BgVerificationId == 0) { _Logger.LogWarning( "UpdateCheckStatusAsync: BgVerificationId {BgVerificationId} not found — no-op", UpdateBgVerificationStatusDTO.BgVerificationId); return; } existing.CheckStatus = UpdateBgVerificationStatusDTO.CheckStatus; if (UpdateBgVerificationStatusDTO.VendorReference != null) existing.VendorReference = UpdateBgVerificationStatusDTO.VendorReference; if (UpdateBgVerificationStatusDTO.ReportAttachmentId.HasValue) existing.ReportAttachmentId = UpdateBgVerificationStatusDTO.ReportAttachmentId.Value; if (UpdateBgVerificationStatusDTO.CheckStatus == BgVerificationCheckStatus.Passed || UpdateBgVerificationStatusDTO.CheckStatus == BgVerificationCheckStatus.Failed) existing.CompletedOn = DateTime.UtcNow; await SaveBgVerification(existing, LoginDTO, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("update-bgverification-status-failed", ex); _Logger.LogError(ex, "UpdateCheckStatusAsync failed for BgVerificationId {BgVerificationId}", UpdateBgVerificationStatusDTO.BgVerificationId); throw; } } } }