"""Flags sourced LinkedIn profiles that are already applicants in the ATS. A sourced profile and a CV describe the same person when they carry the same /in/. The slug is persisted on application rows as the CV is processed (inbox_messages.linkedin_slug, manual_upload_candidate.linkedin_slug); rows that predate those columns are backfilled lazily here in bounded batches, so the matching converges over normal use without a migration script. The annotation rides on the profile list/detail payloads as `already_applied`: {"source": "inbox"|"manual", "status", "job_post_id", "candidate", "applied_at", "same_job": bool, "applications": N} # or null When the person applied to several jobs, the application for the profile's own job wins the summary slot and `same_job` says which case the UI is looking at. """ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from inbox.models import Inbox_Messages from job.candidate.models import Manual_UPLOAD_CANDIDATE from linkedin_utils import primary_slug_from_text, slug_from_url BACKFILL_BATCH = 200 async def _backfill_slugs(session: AsyncSession) -> None: """Scan a bounded batch of never-scanned CVs (linkedin_slug IS NULL).""" changed = False inbox_q = ( select(Inbox_Messages) .where( Inbox_Messages.linkedin_slug.is_(None), Inbox_Messages.resume_text.is_not(None), Inbox_Messages.resume_text != "", ) .limit(BACKFILL_BATCH) ) for row in (await session.execute(inbox_q)).scalars().all(): row.linkedin_slug = primary_slug_from_text(row.resume_text) session.add(row) changed = True manual_q = ( select(Manual_UPLOAD_CANDIDATE) .where( Manual_UPLOAD_CANDIDATE.linkedin_slug.is_(None), Manual_UPLOAD_CANDIDATE.full_text != "", ) .limit(BACKFILL_BATCH) ) for row in (await session.execute(manual_q)).scalars().all(): row.linkedin_slug = primary_slug_from_text(row.full_text) session.add(row) changed = True if changed: await session.commit() async def annotate_applications(session: AsyncSession, profiles: list[dict]) -> list[dict]: """Attach `already_applied` to serialized profile dicts, matched by slug.""" for profile in profiles: profile["already_applied"] = None slug_map: dict[str, list[dict]] = {} for profile in profiles: slug = slug_from_url(profile.get("linkedin_url")) if slug: slug_map.setdefault(slug, []).append(profile) if not slug_map: return profiles await _backfill_slugs(session) matches: dict[str, list[dict]] = {} inbox_q = select( Inbox_Messages.linkedin_slug, Inbox_Messages.application_status, Inbox_Messages.assigned_job_post_id, Inbox_Messages.message_from, Inbox_Messages.created_at, ).where(Inbox_Messages.linkedin_slug.in_(list(slug_map))) for slug, status, job_id, sender, created in (await session.execute(inbox_q)).all(): matches.setdefault(slug, []).append({ "source": "inbox", "status": (getattr(status, "value", status) or None), "job_post_id": str(job_id) if job_id else None, "candidate": sender or None, "applied_at": created.isoformat() if created else None, }) manual_q = select( Manual_UPLOAD_CANDIDATE.linkedin_slug, Manual_UPLOAD_CANDIDATE.status, Manual_UPLOAD_CANDIDATE.job_post_id, Manual_UPLOAD_CANDIDATE.candidate_name, Manual_UPLOAD_CANDIDATE.created_at, ).where(Manual_UPLOAD_CANDIDATE.linkedin_slug.in_(list(slug_map))) for slug, status, job_id, name, created in (await session.execute(manual_q)).all(): matches.setdefault(slug, []).append({ "source": "manual", "status": (status or "").strip() or None, "job_post_id": str(job_id) if job_id else None, "candidate": (name or "").strip() or None, "applied_at": created.isoformat() if created else None, }) for slug, slug_profiles in slug_map.items(): found = matches.get(slug) if not found: continue for profile in slug_profiles: job_id = profile.get("job_post_id") same = [m for m in found if m["job_post_id"] and m["job_post_id"] == job_id] best = same[0] if same else found[0] profile["already_applied"] = { **best, "same_job": bool(same), "applications": len(found), } return profiles