# ============================================================================= # Job·Ka — Groupe KA # Auteur : Simon-Pierre Boucher # Contact : contact@spboucher.ai # Fichier : jobka/quality.py # Rôle : Contrôle qualité avant publication — règles anti-aberrations, # quarantaine (offres douteuses non publiées, jamais supprimées) # Créé : 2026-08-19 Modifié : 2026-08-19 # ============================================================================= """Quarantaine qualité (patron hérité de Lou·Ka). Une offre qui viole une règle de plausibilité n'est PAS publiée « à peu près » : elle reste en base (active, resynchronisée normalement) avec la colonne ``quarantine`` = JSON des motifs, et les points d'accès publics l'excluent. Dès que la source corrige la donnée (ou qu'un correctif de parseur passe), le contrôle se ré-évalue au sync suivant et l'offre est réintégrée automatiquement. Règles (déterministes, bornes en CAD) : - salaire hors bornes : < 15 $/h ou > 200 $/h équivalent ; annuel < 25 k$ ou > 500 k$ ; - ville manifestement hors Québec ; - dates incohérentes : date limite antérieure à la publication ; - description absente ou < 50 caractères ; - titre vide. (Les titres tout-majuscules ne sont PAS quarantainés : la colonne ``title_clean`` corrige la casse à l'affichage.) """ from __future__ import annotations from .normalize import _NON_QC_CITIES, strip_accents # type: ignore MIN_HOURLY, MAX_HOURLY = 15.0, 200.0 MIN_YEARLY, MAX_YEARLY = 25000.0, 500000.0 MIN_DESCRIPTION = 50 def issues(*, title: str, description: str, city: str, date_posted: str | None, date_deadline: str | None, salary_hour_min: float | None, salary_hour_max: float | None, salary_year_min: float | None, salary_year_max: float | None, salary_unit: str | None = None) -> list[str]: """Motifs de quarantaine d'une offre (liste vide = publiable).""" out: list[str] = [] if not (title or "").strip(): out.append("titre_vide") if len((description or "").strip()) < MIN_DESCRIPTION: out.append("description_courte") key = " ".join(strip_accents((city or "").lower()).split()) if key and key in _NON_QC_CITIES: out.append("ville_hors_quebec") if date_posted and date_deadline and date_deadline < date_posted: out.append("dates_incoherentes") # bornes salariales : les bornes HORAIRES ne s'appliquent qu'aux salaires # affichés à l'heure (un annuel de 460 k$ n'est pas « > 200 $/h ») ; les # autres unités sont jugées sur l'équivalent annuel if salary_unit == "hour": for v in (salary_hour_min, salary_hour_max): if v is not None and not (MIN_HOURLY <= v <= MAX_HOURLY): out.append("salaire_hors_bornes") break else: for v in (salary_year_min, salary_year_max): if v is not None and not (MIN_YEARLY <= v <= MAX_YEARLY): out.append("salaire_hors_bornes") break return out def issues_for_posting(job) -> list[str]: """Motifs de quarantaine d'un JobPosting finalisé (utilisé au sync).""" return issues( title=job.title, description=job.description, city=job.city, date_posted=job.date_posted, date_deadline=job.date_deadline, salary_hour_min=job.salary_hour_min(), salary_hour_max=job.salary_hour_max(), salary_year_min=job.salary_year_min(), salary_year_max=job.salary_year_max(), salary_unit=job.salary_unit)