using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Criteria; using GB5Shared.DTO.Framework.Login; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using RecruitmentDAL.DTO.Reports; using RecruitmentDAL.Reports; namespace RecruitmentBLL.Reports { // HR/executive recruitment analytics rollup (Phase 5 of the Recruitment build-out plan) — // org-wide, no recruiter grouping: requisitions by RequisitionType/RequisitionStatus, planned // headcount vs. actual hires in the period, and average cost-per-hire. // // AvgCostPerHire is a deliberate v1 proxy (raw MJOBOFFER.CTC only, no recruiter/agency fees, // sourcing spend, or onboarding cost folded in) — see HrRecruitmentAnalyticsDTO.AvgCostPerHire's // doc comment. Not presented as a finance-grade figure. // // "Actual hires" reuses CandidateConversionBLL's exact structural signal for a genuine hire // (Terminal stage AND stage-history Outcome=Passed(2)) rather than diverging with a new // definition — see that class's doc comment for why Terminal alone is insufficient. // // Live transactional analytics — CacheKeyLevel.NOT_REQUIRED at the endpoint, never cached, per // CLAUDE.md's caching anti-pattern guidance. public class HrRecruitmentAnalyticsReportBLL : IHrRecruitmentAnalyticsReportBLL { private const int DEFAULT_MAX_RESULT_CAP = 500; private readonly IHrRecruitmentAnalyticsReportDAL _dal; public HrRecruitmentAnalyticsReportBLL(IHrRecruitmentAnalyticsReportDAL dal) { _dal = dal; } public async Task> GetHrRecruitmentAnalytics( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login, CancellationToken ct) { try { GB5Trace.Step("parse-criteria-hr-recruitment-analytics", new { firstNumber, maxResult }); var criteria = RecruitmentReportCriteriaParser.Parse(criteriaDTO); var cappedMaxResult = maxResult > 0 ? Math.Min(maxResult, DEFAULT_MAX_RESULT_CAP) : DEFAULT_MAX_RESULT_CAP; GB5Trace.Step("query-hr-recruitment-analytics", new { firstNumber, cappedMaxResult }); return await _dal.GetHrRecruitmentAnalytics(firstNumber, cappedMaxResult, criteria, login, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("hr-recruitment-analytics-failed", ex); throw; } } public async IAsyncEnumerable GetHrRecruitmentAnalyticsStream( int firstNumber, int maxResult, CriteriaDTO criteriaDTO, LoginDTO login, [EnumeratorCancellation] CancellationToken ct) { GB5Trace.Step("stream-hr-recruitment-analytics", new { firstNumber, maxResult }); var criteria = RecruitmentReportCriteriaParser.Parse(criteriaDTO); var cappedMaxResult = maxResult > 0 ? Math.Min(maxResult, DEFAULT_MAX_RESULT_CAP) : DEFAULT_MAX_RESULT_CAP; await foreach (var row in _dal.GetHrRecruitmentAnalyticsStream(firstNumber, cappedMaxResult, criteria, login, ct).ConfigureAwait(false)) yield return row; } } }