using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using RecruitmentBLL.Application; using RecruitmentBLL.CandidateConversion; using RecruitmentBLL.SelectionProcess; using RecruitmentDAL.CustomeCode.Career; using RecruitmentDAL.DTO.Application; using RecruitmentDAL.DTO.Candidate; using RecruitmentDAL.DTO.InternalMobility; using RecruitmentDAL.DTO.SelectionProcess; namespace RecruitmentBLL.InternalMobility { // Internal mobility apply flow (Phase 6 of the Recruitment build-out plan, see // /Users/venkatv/.claude/plans/can-you-check-the-precious-sunbeam.md) — lets an existing // employee apply to a different internal opening through the SAME Candidate/Application // pipeline the public career site and every other intake channel already use, instead of a // parallel system. This is the internally-authenticated-context counterpart to // RecruitmentBLL.Career.CareerBLL.SubmitApplication — same composition shape (reuse-or-create // the Candidate, resolve the requisition's first pipeline stage, create the Application), but // fronted by a distinct route (POST /InternalMobility/SubmitInternalApplication) reachable // only by whatever calls it internally, never by the public career site. // // Deliberately does NOT own an entity save pipeline of its own — Candidate resolution routes // through IEmployeeToInternalCandidateBLL (which itself routes through the existing, // already-instrumented ICandidateBLL.SaveCandidate), and Application creation routes through // the existing IApplicationBLL.SaveApplication — both already handle ExecuteSaveAsync, // AutoNumber, event-publish and cache invalidation, so none of that is duplicated here. // // Out of scope for this pass (per the task): manager-approval-chain notification logic. This // creates the Application at the pipeline's first stage exactly like the public apply flow; // any approval-routing on top of that is a separate, later concern. public class InternalMobilityBLL : IInternalMobilityBLL { private readonly IEmployeeToInternalCandidateBLL _EmployeeToInternalCandidateBLL; private readonly ICareerDAL _CareerDAL; private readonly IApplicationBLL _ApplicationBLL; private readonly ISelectionProcessBLL _SelectionProcessBLL; private readonly ILogger _Logger; // TAPPLICATION.APPLICATIONSOURCE — mirrors MCANDIDATE.SOURCE's pre-existing 5=Internal // value (see ApplicationDTO's doc comment); not a new enum value. private const byte APPLICATIONSOURCE_INTERNAL = 5; public InternalMobilityBLL( IEmployeeToInternalCandidateBLL employeeToInternalCandidateBLL, ICareerDAL careerDAL, IApplicationBLL applicationBLL, ISelectionProcessBLL selectionProcessBLL, ILogger logger) { _EmployeeToInternalCandidateBLL = employeeToInternalCandidateBLL; _CareerDAL = careerDAL; _ApplicationBLL = applicationBLL; _SelectionProcessBLL = selectionProcessBLL; _Logger = logger; } public async Task SubmitInternalApplication(InternalApplicationSubmissionDTO Submission, LoginDTO LoginDTO, CancellationToken ct) { if (Submission == null) throw new ArgumentNullException(nameof(Submission)); try { GB5Trace.Step("validate-internal-application", new { Submission.EmployeeId, Submission.JobRequisitionId }); if (Submission.EmployeeId <= 0) throw new ValidationException($"{nameof(Submission.EmployeeId)} is required."); if (Submission.JobRequisitionId <= 0) throw new ValidationException($"{nameof(Submission.JobRequisitionId)} is required."); // ── Confirm the target requisition is a genuine, currently Published+Active // apply target for this tenant — same eligibility check as the public // career-site flow; internal mobility does not get its own parallel // requisition-eligibility rule for this pass. ──────────────────────────────── var requisition = await _CareerDAL .GetPublishedRequisitionForApply(Submission.JobRequisitionId, LoginDTO, ct) .ConfigureAwait(false); if (requisition == null) throw new ValidationException("This job opening is not currently accepting applications."); // ── Resolve/seed the applying employee's InternalEmployee-type Candidate ─────── GB5Trace.Step("resolve-internal-candidate", new { Submission.EmployeeId }); var candidateJson = await _EmployeeToInternalCandidateBLL .ConvertEmployeeToInternalCandidate(Submission.EmployeeId, LoginDTO, ct) .ConfigureAwait(false); var candidate = JsonConvert.DeserializeObject(candidateJson ?? "null"); if (candidate is null || candidate.CandidateId <= 0) throw new Exception("IEmployeeToInternalCandidateBLL.ConvertEmployeeToInternalCandidate did not return a Candidate."); // ── Resolve the pipeline's first stage (StageOrder = 1) from the requisition's // SelectionProcessTemplate — same as CareerBLL.SubmitApplication. ───────────── int firstStageId = -1; var stagesJson = await _SelectionProcessBLL .GetSelectionProcessStage(requisition.SelectionProcessTemplateId, LoginDTO, ct) .ConfigureAwait(false); var stages = JsonConvert.DeserializeObject>(stagesJson ?? "[]"); var firstStage = stages?.OrderBy(s => s.StageOrder).FirstOrDefault(); if (firstStage != null) firstStageId = firstStage.SelectionProcessStageId; // ── Create the Application, linking Candidate x JobRequisition ──────────────── var applicationDto = new ApplicationDTO { CandidateId = candidate.CandidateId, JobRequisitionId = Submission.JobRequisitionId, CurrentStageId = firstStageId, ApplicationSource = APPLICATIONSOURCE_INTERNAL, AppliedOn = DateTime.UtcNow }; GB5Trace.Step("save-internal-application", new { candidate.CandidateId, Submission.JobRequisitionId }); // Routes through ApplicationBLL.SaveApplication — ExecuteSaveAsync, AutoNumber, // event-publish and cache invalidation all already happen inside that call. var applicationResult = await _ApplicationBLL .SaveApplication(applicationDto, LoginDTO, ct) .ConfigureAwait(false); GB5Trace.Step("event-publish", new { candidate.CandidateId, Submission.JobRequisitionId }); _Logger.LogInformation( "Internal mobility: Application submitted for Employee {EmployeeId} (Candidate {CandidateId}) against JobRequisition {JobRequisitionId}", Submission.EmployeeId, candidate.CandidateId, Submission.JobRequisitionId); return applicationResult; } catch (ValidationException vex) { GB5Trace.MarkFailed("submit-internal-application-failed", vex); throw new Exception(vex.Message); } catch (Exception ex) { GB5Trace.MarkFailed("submit-internal-application-failed", ex); _Logger.LogError(ex, "SubmitInternalApplication failed for EmployeeId {EmployeeId} JobRequisitionId {JobRequisitionId}", Submission?.EmployeeId, Submission?.JobRequisitionId); throw; } } } }