feat: LeadHunter CRUD API + auth fixes + blueprint registrations (HRT-136)

- leadhunter_crm.py: add update_lead(), delete_lead(); expand VALID_STATUSES to 7-step Kanban with legacy migration map
- leadhunter_api.py: add GET/PUT/DELETE /api/leads/<id> endpoints; import update_lead, delete_lead
- portal_server.py: add routes for /leadhunter/clients/le-big-ben/ and /formation/ai102
- saas_api_v1.py: register user blueprint (HRT-79/80) and history blueprint (HRT-81)
- api_v1/routes/user.py: switch auth import to saas_auth.require_auth
- api_v1/routes/history.py: fix auth import + request.current_user fallback
- api_v1/routes/ml_feedback.py: fix auth import + request.current_user fallback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO H3R7Tech
2026-05-10 08:29:44 +02:00
parent a126941f7f
commit 1ccf9f5cb8
7 changed files with 213 additions and 8 deletions

View File

@@ -52,8 +52,24 @@ if not logger.handlers:
# ─── Chemin DB ───────────────────────────────────────────────────────────────
DB_PATH = "/home/h3r7/leadhunter.db"
# Statuts valides pour un lead
VALID_STATUSES = {"new", "contacted", "closed", "rejected"}
# Statuts valides pour un lead (7 etapes Kanban)
VALID_STATUSES = {
"nouveau", # NOUVEAU
"contacte", # CONTACTÉ
"interesse", # INTÉRESSÉ
"demo_planifiee", # DÉMO PLANIFIÉE
"proposition_envoyee", # PROPOSITION ENVOYÉE
"negotiation", # NÉGOCIATION
"signe_ou_refuse", # SIGNÉ / REFUSÉ
}
# Mapping des anciens statuts vers les nouveaux (pour migration)
LEGACY_STATUS_MAP = {
"new": "nouveau",
"contacted": "contacte",
"closed": "signe_ou_refuse",
"rejected": "signe_ou_refuse",
}
# ─── Initialisation ──────────────────────────────────────────────────────────
@@ -212,6 +228,77 @@ def get_lead_by_id(lead_id: int, db_path: str = DB_PATH) -> Optional[dict]:
return None
def update_lead(lead_id: int, data: dict, db_path: str = DB_PATH) -> bool:
"""
Met à jour un lead avec les champs fournis.
Args:
lead_id: id du lead.
data: dict avec les champs a mettre a jour (name, address, phone, etc.)
Returns:
True si mise a jour reussie, False sinon.
"""
allowed_fields = {
"name",
"address",
"phone",
"rating",
"reviews_count",
"website",
"score",
"rgpd_ok",
"status",
}
fields_to_update = {k: v for k, v in data.items() if k in allowed_fields}
if not fields_to_update:
logger.warning(
f"update_lead : aucun champ valide fourni pour lead_id={lead_id}"
)
return False
if (
"status" in fields_to_update
and fields_to_update["status"] not in VALID_STATUSES
):
logger.warning(f"update_lead : statut invalide '{fields_to_update['status']}'")
return False
try:
with _get_conn(db_path) as conn:
set_clause = ", ".join([f"{k} = ?" for k in fields_to_update])
values = list(fields_to_update.values()) + [lead_id]
conn.execute(f"UPDATE leads SET {set_clause} WHERE id = ?", values)
logger.info(
f"Lead id={lead_id} mis a jour : {list(fields_to_update.keys())}"
)
return True
except Exception as e:
logger.warning(f"update_lead error : {e}")
return False
def delete_lead(lead_id: int, db_path: str = DB_PATH) -> bool:
"""
Supprime un lead physiquement.
Args:
lead_id: id du lead a supprimer.
Returns:
True si suppression reussie, False sinon.
"""
try:
with _get_conn(db_path) as conn:
conn.execute("DELETE FROM leads WHERE id = ?", (lead_id,))
logger.info(f"Lead id={lead_id} supprime")
return True
except Exception as e:
logger.warning(f"delete_lead error : {e}")
return False
def update_lead_status(lead_id: int, status: str, db_path: str = DB_PATH) -> bool:
"""
Met à jour le statut d'un lead.