mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-21 06:08:12 +02:00
correction produit indisponible
This commit is contained in:
1 parent
f7059da02a
commit
7eb9448ae7
2 files changed
+115
-21
No files matched your search
@@ -17,6 +17,52 @@ scheduler = AsyncIOScheduler()
|
|||||||
|
|
||||||
PRICE_CHANGE_THRESHOLD_PERCENT = 20.0
|
PRICE_CHANGE_THRESHOLD_PERCENT = 20.0
|
||||||
LOW_CONFIDENCE_THRESHOLD = 0.7
|
LOW_CONFIDENCE_THRESHOLD = 0.7
|
||||||
|
TITLE_SIMILARITY_THRESHOLD = 0.4 # Below this, consider product changed
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_title(title: str) -> str:
|
||||||
|
"""Normalize title for comparison: lowercase, remove common suffixes, strip."""
|
||||||
|
if not title:
|
||||||
|
return ""
|
||||||
|
title = title.lower().strip()
|
||||||
|
# Remove common website suffixes
|
||||||
|
for suffix in [" - b&m", " | b&m", " - action", " | action", " - gifi", " | gifi", " - amazon"]:
|
||||||
|
if title.endswith(suffix):
|
||||||
|
title = title[: -len(suffix)]
|
||||||
|
return title.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _titles_match(stored_name: str, page_title: str) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the page title still matches the stored product name.
|
||||||
|
Returns True if they match (product still exists), False if different (product replaced).
|
||||||
|
"""
|
||||||
|
if not stored_name or not page_title:
|
||||||
|
return True # Can't compare, assume match
|
||||||
|
|
||||||
|
norm_stored = _normalize_title(stored_name)
|
||||||
|
norm_page = _normalize_title(page_title)
|
||||||
|
|
||||||
|
# If one is empty after normalization, assume match
|
||||||
|
if not norm_stored or not norm_page:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if stored name is contained in page title or vice versa
|
||||||
|
if norm_stored in norm_page or norm_page in norm_stored:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check word overlap ratio (Jaccard similarity)
|
||||||
|
stored_words = set(norm_stored.split())
|
||||||
|
page_words = set(norm_page.split())
|
||||||
|
|
||||||
|
if not stored_words or not page_words:
|
||||||
|
return True
|
||||||
|
|
||||||
|
intersection = stored_words & page_words
|
||||||
|
union = stored_words | page_words
|
||||||
|
similarity = len(intersection) / len(union) if union else 0
|
||||||
|
|
||||||
|
return similarity >= TITLE_SIMILARITY_THRESHOLD
|
||||||
|
|
||||||
|
|
||||||
def _get_thresholds():
|
def _get_thresholds():
|
||||||
@@ -58,10 +104,8 @@ def _update_db_result(
|
|||||||
item.current_price_confidence = p_conf
|
item.current_price_confidence = p_conf
|
||||||
|
|
||||||
# Auto-reset availability if price was successfully extracted
|
# Auto-reset availability if price was successfully extracted
|
||||||
# A valid price proves the product is still available
|
# This is handled elsewhere based on title matching
|
||||||
if item.is_available is False:
|
pass
|
||||||
item.is_available = True
|
|
||||||
logger.info(f"Item {item_id} marked as available again (price found: {price})")
|
|
||||||
|
|
||||||
session.add(
|
session.add(
|
||||||
models.PriceHistory(
|
models.PriceHistory(
|
||||||
@@ -201,6 +245,48 @@ async def process_item_check(item_id: int):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Detect product replacement by comparing page title with stored name
|
||||||
|
# If title has changed significantly, the original product was replaced
|
||||||
|
titles_match = _titles_match(item_data["name"], page_title)
|
||||||
|
|
||||||
|
if not titles_match:
|
||||||
|
logger.warning(
|
||||||
|
f"Product replacement detected for item {item_id}. "
|
||||||
|
f"Stored: '{item_data['name']}', Page title: '{page_title}'"
|
||||||
|
)
|
||||||
|
await loop.run_in_executor(
|
||||||
|
None,
|
||||||
|
_update_db_product_unavailable,
|
||||||
|
item_id,
|
||||||
|
f"Produit remplacé (titre: {page_title[:50]}...)"
|
||||||
|
if len(page_title) > 50
|
||||||
|
else f"Produit remplacé (titre: {page_title})",
|
||||||
|
screenshot_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send notification if configured
|
||||||
|
if item_data["notification_channel"]:
|
||||||
|
await NotificationService.send_notification(
|
||||||
|
item_data["notification_channel"],
|
||||||
|
title=f"⚠️ Produit remplacé : {item_data['name']}",
|
||||||
|
body=f"Le produit '{item_data['name']}' semble avoir été remplacé.\nNouveau titre: {page_title}\n{item_data['url']}",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If titles match and product was previously marked unavailable, reset it
|
||||||
|
if titles_match:
|
||||||
|
|
||||||
|
def _reset_availability(item_id: int):
|
||||||
|
with database.SessionLocal() as session:
|
||||||
|
if item := session.query(models.Item).filter(models.Item.id == item_id).first():
|
||||||
|
if item.is_available is False:
|
||||||
|
item.is_available = True
|
||||||
|
item.last_error = None
|
||||||
|
session.commit()
|
||||||
|
logger.info(f"Item {item_id} marked as available again (title matches)")
|
||||||
|
|
||||||
|
await loop.run_in_executor(None, _reset_availability, item_id)
|
||||||
|
|
||||||
# HYBRID EXTRACTION STRATEGY (Aligned with ImprovedSearchService)
|
# HYBRID EXTRACTION STRATEGY (Aligned with ImprovedSearchService)
|
||||||
# 1. Try specialized parser (if available) or JSON-LD
|
# 1. Try specialized parser (if available) or JSON-LD
|
||||||
price = None
|
price = None
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
# Correction Statut Disponibilité Produit B&M
|
# Correction Statut Disponibilité Produit
|
||||||
|
|
||||||
## Contexte
|
## Contexte
|
||||||
|
|
||||||
Le produit "Lot de 24 bougies blanches" sur bmstores.fr est affiché comme "Produit retiré du site" dans Priceflow alors qu'il est bien disponible sur le site (0,88€).
|
Le produit "Lot de 24 bougies blanches" sur bmstores.fr est affiché comme "Produit retiré du site" alors qu'il est bien disponible.
|
||||||
|
|
||||||
## Master Plan
|
## Logique Implémentée
|
||||||
|
|
||||||
- [x] Correction automatique du statut lors d'un refresh réussi
|
**Détection d'indisponibilité :**
|
||||||
- [x] Modifier `_update_db_result` dans `scheduler_service.py` pour remettre `is_available = True` quand un prix est détecté
|
|
||||||
- [x] Endpoint API pour correction manuelle
|
|
||||||
- [x] Ajouter un endpoint `PATCH /{item_id}/availability` dans `items.py` router
|
|
||||||
- [x] Permettre de modifier `is_available` via l'API
|
|
||||||
- [x] Interface utilisateur
|
|
||||||
- [x] Ajouter `onMarkAvailable` prop dans `ItemCard.jsx`
|
|
||||||
- [x] Ajouter le handler `handleMarkAvailable` dans `Dashboard.jsx`
|
|
||||||
- [x] Afficher un bouton "Marquer comme disponible" dans l'overlay des produits indisponibles
|
|
||||||
|
|
||||||
## Progress Log
|
- Si le **titre de la page a changé** significativement par rapport au nom stocké → le produit original a été remplacé → marquer comme "Produit remplacé"
|
||||||
|
- Exemple: URL `/produit/123-bougies-blanches` maintenant affiche "Bougies Multicolores" → produit indisponible
|
||||||
|
|
||||||
- Diagnostic effectué : produit B&M bien disponible (0,88€) mais marqué indisponible dans la DB
|
**Reset de disponibilité :**
|
||||||
- Modifié `scheduler_service.py` : reset auto de `is_available` quand prix détecté
|
|
||||||
- Ajouté endpoint API `PATCH /api/items/{id}/availability`
|
- Si le **titre correspond toujours** au nom stocké → le produit existe encore → remettre `is_available = true`
|
||||||
- Ajouté bouton dans l'overlay frontend pour correction manuelle
|
|
||||||
|
## Tâches Complétées
|
||||||
|
|
||||||
|
- [x] Fonction `_normalize_title()` - normalise les titres pour comparaison
|
||||||
|
- [x] Fonction `_titles_match()` - compare les titres avec similarité Jaccard
|
||||||
|
- [x] Détection de remplacement de produit dans `process_item_check()`
|
||||||
|
- [x] Reset automatique quand le titre correspond
|
||||||
|
- [x] Endpoint API `PATCH /{item_id}/availability` pour correction manuelle
|
||||||
|
- [x] Bouton "Marquer comme disponible" dans l'interface
|
||||||
|
|
||||||
|
## Fichiers Modifiés
|
||||||
|
|
||||||
|
- `app/services/scheduler_service.py` - logique de détection
|
||||||
|
- `app/routers/items.py` - endpoint API
|
||||||
|
- `frontend/src/pages/Dashboard.jsx` - handler
|
||||||
|
- `frontend/src/components/dashboard/ItemCard.jsx` - bouton UI
|
||||||
Reference in new issue
Block a user