From 98d0ad0a3def6a2d18291b8b8a96708575aabb19 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Thu, 7 May 2026 21:58:34 +0200 Subject: [PATCH] fix(pct/cc): parse et match email SANEF strict (prenom.nom@%) Cas reel observe: contacts.name = juste le NOM de famille (DUFOUR, DELCOUR), avec plusieurs entrees par nom (Antoine vs Jean-Charles DUFOUR par ex). Le match precedent '%nom%' renvoyait des faux positifs. Solution: - Nettoie suffixes parasites: '(externe)', '- PCT', '- DBA' en fin - Identifie nom de famille = token UPPERCASE longueur >= 3 (DUFOUR, DELCOUR, etc.) - Prenom = 1er token qui n'est pas le nom - Match SQL: LOWER(email) LIKE 'prenom.nom@%' insensible casse - Pas de FROM nom de famille SEUL (qui matcherait des homonymes) Pour 'Laurent DELCOUR' -> match strict laurent.delcour@% Pour 'DUFOUR Antoine - PCT' -> nettoye en 'DUFOUR Antoine' -> match antoine.dufour@% --- app/routers/planning_import.py | 69 +++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/app/routers/planning_import.py b/app/routers/planning_import.py index cc06b8d..5418904 100644 --- a/app/routers/planning_import.py +++ b/app/routers/planning_import.py @@ -1178,29 +1178,54 @@ def _fetch_pct_cc_emails(db, row_ids): if len(part) >= 4 and re.search(r"[A-Za-zÀ-ÿ]{3}", part): legacy_names.add(part) - # 4) Match par nom dans la table contacts pour les responsables/référents - # qui n'ont pas d'email côté servers. Match insensible casse + accents. + # 4) Match par parsing du nom complet → email SANEF (format prenom.nom@sanef.com) + # Le contacts.name en BDD est juste le NOM DE FAMILLE (ex 'DELCOUR'), + # et il y a souvent plusieurs DELCOUR — il faut matcher ET le nom ET le prénom. if legacy_names: - # On construit une condition ILIKE pour chaque nom + ses variantes simples - names_list = list(legacy_names)[:50] # limite de sécurité - params = {} - ors = [] - for i, n in enumerate(names_list): - params[f"n{i}"] = n - # Match exact (case insensible) ou nom contenu (utile pour 'DUFOUR Antoine' vs 'Antoine Dufour') - ors.append(f"LOWER(c.name) = LOWER(:n{i})") - ors.append(f"LOWER(c.name) LIKE LOWER('%' || :n{i} || '%')") - sql = ( - "SELECT DISTINCT c.name, c.email FROM contacts c " - "WHERE c.email IS NOT NULL AND c.email <> '' " - f"AND ({' OR '.join(ors)})" - ) - try: - matched = db.execute(text(sql), params).fetchall() - for c in matched: - _add(c.name, c.email) - except Exception as e: - print(f"[pct_cc] match par nom failed: {e}") + pairs = [] # liste de (prenom, nom) + for raw in legacy_names: + # Nettoie suffixes parasites courants : "- PCT", "- DBA", "(externe)", etc. + s = re.sub(r"\s*[\(\[].*?[\)\]]\s*", " ", raw) + s = re.sub(r"\s*-\s*[A-Z]{2,5}\s*$", "", s).strip() # "- PCT" en fin + if not s: + continue + tokens = re.split(r"\s+", s) + tokens = [t for t in tokens if t and t not in ("PCT", "DBA", "ext", "Ext")] + if len(tokens) < 2: + continue + # Identifie le nom de famille = token UPPERCASE de longueur >= 3 + nom = None + for t in tokens: + # Match nom de famille : uniquement majuscules ASCII, longueur >= 3 + # (gère 'DUFOUR', 'DELCOUR', 'GRAFFAGNINO' ; pas 'PCT' qui est trop court) + tt = t.replace("-", "").replace("'", "") + if len(tt) >= 3 and tt.isupper(): + nom = t + break + if nom: + # Prénom = 1er token qui n'est pas le nom (souvent le 1er token) + prenom = next((t for t in tokens if t != nom), None) + else: + # Pas de UPPERCASE détecté : on suppose et prend dernier token + prenom = tokens[0] + nom = tokens[-1] + if prenom and nom and prenom != nom: + pairs.append((prenom, nom)) + + # Pour chaque (prenom, nom), match strict sur email SANEF + for prenom, nom in pairs[:50]: # limite de sécurité + # email format SANEF : prenom.nom@... (insensible casse, accents non gérés en BDD) + email_pattern = f"{prenom.lower()}.{nom.lower()}@%" + try: + matched = db.execute(text(""" + SELECT name, email FROM contacts + WHERE email IS NOT NULL AND email <> '' + AND LOWER(email) LIKE :pat + """), {"pat": email_pattern}).fetchall() + for c in matched: + _add(f"{prenom} {nom}", c.email) + except Exception as e: + print(f"[pct_cc] match {prenom}.{nom} failed: {e}") out.sort(key=lambda c: c["name"].lower()) return out