using GB5Shared.DTO.Framework.Login;
namespace RecruitmentBLL.Integration
{
/// Outcome of a call that mutates something in OKR — same {Success, Error} shape as
/// EcpCallOutcome, kept as its own type so OKR integration stays self-contained and doesn't
/// couple to ECP's naming. Never throws out to the caller; a failed OKR call degrades to
/// Success=false with an Error, so an OKR outage (or a missing Goal) never breaks the
/// Recruitment save/transition that triggered the push.
public class OkrCallOutcome
{
public bool Success { get; set; }
public string? Error { get; set; }
}
/// A Goal found for a recruiter, carrying just enough of TGOAL's denormalized state
/// (CurrentValue/CurrentProgress/TargetValue) for the caller to compute an incremented value
/// and a new progress percentage before pushing it back via PushGoalProgressAsync.
public class OkrGoalLookupResult
{
public int GoalId { get; set; }
public string Name { get; set; } = string.Empty;
public string KRAName { get; set; } = string.Empty;
public decimal CurrentValue { get; set; }
public decimal CurrentProgress { get; set; }
public decimal TargetValue { get; set; }
}
///
/// Cross-host HTTP integration with OKR (hosted by EngagementHost, a different process than
/// Recruitment's HRFinanceHost — no ProjectReference/DI injection of OKR's BLL is possible
/// across hosts), following the exact IHttpClientFactory + "Login" header pattern used by
/// IEcpIntegrationService/EcpIntegrationService — read that class first, it is the template
/// for this one.
///
/// Feeds recruitment team metrics (time-to-fill, offer-acceptance, requisitions closed) into
/// OKR's existing PERM/OKR KRA-tracking mechanism (Phase 5 of the Recruitment build-out plan)
/// instead of a recruitment-only scorecard — see GoalBLL.UpdateGoalProgressAsync (the real,
/// generic, programmatic progress-update mechanism) and its OKRSL wrapper endpoint
/// POST /Goal/UpdateGoalProgress.
///
/// A Goal row must already exist (linking a recruiter Employee to a KRA for an active
/// OKRCycle) before a push can target it — Recruitment cannot create Goals itself, that is
/// PERM/OKR admin setup, out of scope here. Most recruiters will NOT have a
/// recruitment-specific Goal provisioned yet; FindGoalForRecruiterAsync returning null is the
/// expected, common case, not a failure — callers must log at Info and continue, never treat
/// it as a warning.
///
/// Every method here degrades gracefully: a non-2xx response, an unreachable OKR process, or
/// any thrown exception is logged and returned as a failure/null result — it never throws
/// back into JobRequisitionBLL/JobOfferBLL/CandidateConversionBLL, so an OKR outage never
/// blocks the underlying Recruitment action that triggered the push.
///
/// NOTE for whoever next touches this: as of this writing OKRSL is NOT wired into
/// EngagementHost's ProjectReferences (see EngagementHost.csproj's own comment: "OKRSL
/// deferred: GoalBLL depends on concrete ObjectiveBLL, not IObjectiveBLL; DI resolution fails
/// at startup") — so in the current multi-host deployment topology, calls through this
/// service will fail with a connection error until either that DI bug is fixed and OKRSL is
/// added back to EngagementHost, or OKRSL is run standalone on its own configured port. This
/// is exactly the "OKR outage" case the graceful-degradation contract above exists for; no
/// code here assumes OKR is reachable.
///
public interface IOkrIntegrationService
{
/// Looks up an existing Goal for the given recruiter (TGOAL.EMPLOYEEID —
/// same "EmployeeId is actually populated with UserId" convention GoalBLL.GetMyGoalsAsync
/// itself relies on for ESS) that loosely matches against
/// the Goal's KRAName or Name (case-insensitive substring match).
///
/// There is no single "goals for employee across all cycles" endpoint — GetGoalList
/// requires a real OKRCycleId — so this first resolves every currently-Active OKRCycle
/// (GET /Cycle/GetOKRCycleList?Status=1) and searches each one's goal list
/// (GET /Goal/GetGoalList?OKRCycleId=..&EmployeeId=..) for a match.
///
/// Returns null — not an exception — when OKR is unreachable, when the recruiter has no
/// Active-cycle goals at all, or when none of their goals match the hint. All three are
/// expected, normal outcomes for most recruiters; log at Info, not Warning.
Task FindGoalForRecruiterAsync(
int recruiterUserId, string kraNameHint, LoginDTO login, CancellationToken ct);
/// Thin wrapper over POST /Goal/UpdateGoalProgress (OKRSL.Endpoints.Goal
/// .UpdateGoalProgress) — that endpoint binds every field as a query parameter, not a
/// [FromBody] payload (see its Params record), so this posts a query string with an empty
/// body. ConfidenceLevel is left at the endpoint's own default (1). Never throws; a
/// non-2xx response or transport failure returns Success=false with a logged
/// warning.
Task PushGoalProgressAsync(
int goalId, decimal currentValue, decimal progressPercent, string remarks,
LoginDTO login, CancellationToken ct);
}
}