"""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() )