- get_scout_stats aggregator (app/engine/stats): assembles the REAL Summary metrics —
funnel + cohort engagement rank + active-window (activity timestamps by hour) from our DB,
accumulating salary band (avg of each deck's peak, ₹L), match/competition stats from the
feed, Momentum/QX + Q-Score trend (qscore-service), day streak (user-service). Honest:
unsourced cards return None so the UI omits/locks them, never faked. posted_date extractor.
- Activity tracking: viewed/saved flags + search_count → funnel (Matches→Viewed→Shortlisted→
Applied) + engagement percentile. matchesFound = all-time count.
- Per-board search cursors {board: page} (replaces the single cursor): only boards that truly
paginate (LinkedIn) get one; cursor = LAST page fetched (1st search of a new query → 1).
Resets on query change OR >24h (boards refresh ~daily). Dropped Naukri incremental/stateKey
(opaque, exhausting, cross-account dedup state) — dedup is the PER-USER seen-net only.
- tests: stats helpers (posting-age, histogram, active-window, engagement score).
59 lines
3.7 KiB
Python
59 lines
3.7 KiB
Python
"""ORM models. One table for now: the per-user cached feed (the last completed search)."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import JSON, Boolean, DateTime, Float, Integer, String, func
|
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
|
|
|
|
class Base(DeclarativeBase):
|
|
pass
|
|
|
|
|
|
class UserFeed(Base):
|
|
"""The user's last completed search — replayed on load so matches survive navigation.
|
|
Upserted on each `search_complete`; read by `get_feed`. One row per user (PK = user_id)."""
|
|
__tablename__ = "user_feed"
|
|
|
|
user_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
prefs: Mapped[dict] = mapped_column(JSON, default=dict) # the ScoutPrefs that produced it
|
|
opportunities: Mapped[list] = mapped_column(JSON, default=list) # the curated deck (with match blocks)
|
|
sources: Mapped[dict] = mapped_column(JSON, default=dict) # per-board counts
|
|
engine: Mapped[str] = mapped_column(String, default="") # "opus" | "fallback:…"
|
|
scanned: Mapped[int] = mapped_column(Integer, default=0)
|
|
# PER-BOARD search cursors — {board: LAST page fetched}. Next page = last + 1, so a NEW query's
|
|
# first search reads page 1 and stores 1. Resets to {} when the query signature changes (or >24h).
|
|
# Dedup is purely per-user (the seen-net below) — never cross-user, never opaque actor-side state.
|
|
query_sig: Mapped[str] = mapped_column(String, default="")
|
|
cursors: Mapped[dict] = mapped_column(JSON, default=dict)
|
|
search_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0") # total searches run (engagement)
|
|
# Salary band = AVERAGE of each deck's HIGHEST disclosed salary (₹L). Each deck contributes one
|
|
# peak; the band is the running mean of those peaks. Survives decks that disclose nothing.
|
|
salary_peak_sum: Mapped[float] = mapped_column(Float, default=0.0, server_default="0")
|
|
salary_peak_count: Mapped[int] = mapped_column(Integer, default=0, server_default="0")
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|
|
|
|
|
|
class OpportunityState(Base):
|
|
"""Per-(user, opportunity) state that SURVIVES deck refreshes. Persists: `applied`/`dismissed`
|
|
status, the resume-builder document ids already generated (never re-pay to re-tailor), and the
|
|
PROGRESSIVE activity flags that feed the dashboard funnel + engagement rank. A job can be viewed
|
|
AND saved AND applied at once, so viewed/saved are booleans (not the single-valued status)."""
|
|
__tablename__ = "opportunity_state"
|
|
|
|
user_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
opportunity_id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
status: Mapped[str | None] = mapped_column(String, nullable=True) # 'applied' | 'dismissed' | None
|
|
tailored_resume_id: Mapped[str | None] = mapped_column(String, nullable=True) # resume-builder resume id
|
|
tailored_version_id: Mapped[str | None] = mapped_column(String, nullable=True) # the tailored version
|
|
cover_letter_id: Mapped[str | None] = mapped_column(String, nullable=True) # the generated cover letter
|
|
seen: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # shown in a deck → exclude from new runs
|
|
viewed: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # opened the job → funnel "Viewed"
|
|
saved: Mapped[bool] = mapped_column(Boolean, default=False, server_default="false") # swiped right → funnel "Shortlisted"
|
|
updated_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
|
)
|