fix scout stats applicant counts

This commit is contained in:
-Puter
2026-07-17 19:47:44 +05:30
parent 19d7e8e04f
commit e30d24dbe1
2 changed files with 130 additions and 25 deletions

View File

@@ -9,9 +9,11 @@ Honest sources (SCOUT_UI_SPEC):
Anything without a real source is omitted — the UI cuts or locks it, never fakes it.
"""
from __future__ import annotations
import asyncio
import math
import logging
from datetime import datetime, timezone
@@ -30,7 +32,9 @@ def _match_scores(opps: list[dict]) -> list[int]:
return out
def _histogram(scores: list[int], lo: int = 60, hi: int = 100, bins: int = 11) -> list[int]:
def _histogram(
scores: list[int], lo: int = 60, hi: int = 100, bins: int = 11
) -> list[int]:
h = [0] * bins
span = (hi - lo) / bins
for s in scores:
@@ -57,9 +61,14 @@ def _apply_window(opps: list[dict]) -> dict:
return {"median_age_days": None, "fresh_share": None, "urgency": None}
ages.sort()
median = ages[len(ages) // 2]
fresh = sum(1 for a in ages if a <= 3) / len(ages) # posted within 3 days
fresh = sum(1 for a in ages if a <= 3) / len(ages) # posted within 3 days
urgency = "high" if median <= 3 else "medium" if median <= 7 else "low"
return {"median_age_days": median, "fresh_share": round(fresh, 2), "urgency": urgency, "dated": len(ages)}
return {
"median_age_days": median,
"fresh_share": round(fresh, 2),
"urgency": urgency,
"dated": len(ages),
}
async def build_scout_stats(user_id: str, *, user_uuid: str | None = None) -> dict:
@@ -78,7 +87,17 @@ async def build_scout_stats(user_id: str, *, user_uuid: str | None = None) -> di
opps = feed.get("opportunities") or []
scores = _match_scores(opps)
apps = [j.get("applicants") for j in opps if j.get("applicants")]
apps = []
for opportunity in opps:
raw_applicants = opportunity.get("applicants")
if isinstance(raw_applicants, bool):
continue
try:
applicants = float(raw_applicants)
except (TypeError, ValueError):
continue
if math.isfinite(applicants) and applicants >= 0:
apps.append(applicants)
matches = len(opps)
return {
@@ -93,22 +112,34 @@ async def build_scout_stats(user_id: str, *, user_uuid: str | None = None) -> di
# salary band = average of each deck's peak salary, accumulated across decks (₹L)
"salaryMaxL": salary_band,
"avgApplicants": round(sum(apps) / len(apps)) if apps else None,
"activeWindow": active if active.get("samples") else None, # WHEN the user works their search
"activeWindow": active
if active.get("samples")
else None, # WHEN the user works their search
# ── activity-derived (real, this DB) — funnel is all-time so the stages are consistent ──
"funnel": [
{"label": "Matches", "value": max(act["matches"], matches), "tracked": True},
{
"label": "Matches",
"value": max(act["matches"], matches),
"tracked": True,
},
{"label": "Viewed", "value": act["viewed"], "tracked": True},
{"label": "Shortlisted", "value": act["saved"], "tracked": True},
{"label": "Applied", "value": act["applied"], "tracked": True},
],
"tailoredResumes": act["tailored"],
"searches": act["searches"],
"percentile": act["percentile"], # cohort engagement percentile (top 100-percentile %)
"percentile": act[
"percentile"
], # cohort engagement percentile (top 100-percentile %)
"cohortSize": act["cohort_size"],
"searchStage": "Applying" if act["applied"] else "Reviewing" if act["saved"] else "Scanning",
"searchStage": "Applying"
if act["applied"]
else "Reviewing"
if act["saved"]
else "Scanning",
# ── cross-service (real; None → the UI omits the card, never fakes it) ──
"qxNow": (qx or {}).get("qx"),
"quotients": (qx or {}).get("quotients"),
"qxTrend": qx_trend, # RQ Score series (qscore-service) → Readiness-trend sparkline
"qxTrend": qx_trend, # RQ Score series (qscore-service) → Readiness-trend sparkline
"dayStreak": streak,
}