Files
matchmaking-v2/app/db/models.py
raulgupta 75b2e538ef Manual-apply backend: per-opportunity state, unique-decks engine, offsite-first + logos
- OpportunityState model + repo: persist applied / dismissed / restored status and the
  generated resume-builder doc ids (never re-pay to re-tailor). annotate_and_filter drops
  dismissed + annotates applied/docs on read; handlers for dismiss/restore/mark_applied/
  save_apply_docs.
- Unique decks per run (spend only for NEW jobs, no cache, no re-fetch+dedup band-aid):
  per-(user,query) search cursor → LinkedIn page++ and Naukri incremental+stateKey (async
  run path in apify_client, since the actor's crawl exceeds the run-sync window); seen-net
  excludes already-shown ids (covers Foundit, which can't paginate).
- normalize: every board offsite-first (apply_url prefers the employer/ATS redirect over the
  board listing) + offsite_apply flag; real company logos (_logo_url across Naukri logoPath /
  LinkedIn company.logo / etc.).
2026-06-20 17:52:06 +05:30

50 lines
2.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, 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)
# Search cursor — advances per re-run of the SAME query so each run fetches NEW jobs (LinkedIn
# page++, Naukri incremental stateKey). Resets when the query signature changes.
query_sig: Mapped[str] = mapped_column(String, default="")
cursor_page: Mapped[int] = mapped_column(Integer, default=1)
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 three things:
`applied`/`dismissed` status, and the resume-builder document ids already generated for the
role (so we never re-tailor / re-generate a paid document twice). Keyed by (user, opportunity)."""
__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
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)