""" Smart part kits — automatic cross-sell recommendations. When a customer adds a part to their quotation, suggest related parts that are typically needed together for a complete job. """ # Spanish keywords in part name → related parts to suggest (in Spanish) # These appear after a successful "cotizar" command. KIT_SUGGESTIONS = { "balata": ["disco de freno", "líquido de frenos", "balero de rueda"], "disco de freno": ["balata", "líquido de frenos"], "alternador": ["banda serpentina", "batería", "regulador de alternador"], "batería": ["alternador", "cable de bujía"], "marcha": ["batería", "solenoide de marcha"], "bujía": ["bobina de encendido", "filtro de aire", "filtro de gasolina"], "bobina": ["bujía", "cable de bujía"], "bomba de agua": ["termostato", "refrigerante", "manguera de radiador"], "radiador": ["manguera de radiador", "termostato", "tapón de radiador"], "termostato": ["refrigerante", "manguera de radiador"], "amortiguador": ["base de amortiguador", "goma de suspensión", "rótula"], "rótula": ["terminal de dirección", "brazo de suspensión", "bujes"], "terminal": ["rótula", "brazo de suspensión"], "filtro de aceite": ["filtro de aire", "filtro de gasolina", "filtro de habitáculo"], "filtro de aire": ["filtro de aceite", "filtro de gasolina", "bujía"], "filtro de gasolina": ["filtro de aire", "filtro de aceite", "inyector"], "clutch": ["collarín", "disco de clutch", "plato de presión"], "collarín": ["clutch", "disco de clutch"], "banda de distribución": ["bomba de agua", "tensor", "polea loca"], "banda serpentina": ["tensor de banda", "polea loca"], "foco": ["foco trasero", "cuarto"], "faro": ["foco trasero", "cuarto"], "aceite": ["filtro de aceite", "filtro de aire"], } def get_kit_suggestions(part_name: str) -> list: """Return related part names for a given part (Spanish).""" if not part_name: return [] name_lower = part_name.lower() for keyword, related in KIT_SUGGESTIONS.items(): if keyword in name_lower: return related return [] def build_kit_text(part_name: str) -> str: """Build a WhatsApp-friendly kit suggestion text. Returns empty string if no kit is found. """ suggestions = get_kit_suggestions(part_name) if not suggestions: return "" items = "\n".join(f" • {s.title()}" for s in suggestions[:3]) return ( "\n\n🔧 *¿Ya que estás en eso, checa si también necesitas:*\n" + items + '\n\n_Escribe la parte que te interese y la agregamos._' ) # ── Urgency detection ──────────────────────────────────────────────── URGENCY_KEYWORDS = [ "urgente", "urgencia", "emergencia", "ya", "ahora", "hoy", "lo necesito", "se me paro", "no arranca", "no jala", "rapido", "apúrate", "apurate", "prisa", "de volada", "para hoy", "para ahora", "lo mas pronto", "lo más pronto", "inmediato", "express", "exprés", ] def is_urgent(text: str) -> bool: """Detect if the customer message signals urgency.""" if not text: return False t = text.lower() return any(kw in t for kw in URGENCY_KEYWORDS) def urgency_note() -> str: return ( "\n\n⚡ NOTA DE URGENCIA: El cliente necesita la pieza lo antes posible. " "Prioriza stock local y ofrece entrega express (2-4 horas) o recolección inmediata en tienda. " "Si no hay stock exacto, ofrece alternativa disponible inmediatamente." ) # ── Abandoned quotation follow-up ──────────────────────────────────── FOLLOW_UP_MINUTES = 15 def should_send_followup(phone: str, tenant_conn) -> str: """Check if we should send a follow-up message for an abandoned quotation. Returns the follow-up text if yes, empty string if no. """ if not tenant_conn or not phone: return "" try: cur = tenant_conn.cursor() # 1. Check if there's an active quotation for this phone cur.execute(""" SELECT id FROM quotations WHERE notes LIKE %s AND status = 'active' ORDER BY created_at DESC LIMIT 1 """, (f'%WA:{phone}%',)) row = cur.fetchone() if not row: cur.close() return "" # 2. Check last bot message mentioning "cotización" or "cotizar" cur.execute(""" SELECT created_at, message_text FROM whatsapp_messages WHERE phone = %s AND direction = 'outgoing' AND (message_text ILIKE '%cotización%' OR message_text ILIKE '%cotizar%') ORDER BY created_at DESC LIMIT 1 """, (phone,)) last_quote_msg = cur.fetchone() cur.close() if not last_quote_msg: return "" from datetime import datetime, timezone last_time = last_quote_msg[0] now = datetime.now(timezone.utc) if last_time.tzinfo is None: last_time = last_time.replace(tzinfo=timezone.utc) minutes_since = (now - last_time).total_seconds() / 60 if minutes_since >= FOLLOW_UP_MINUTES: return ( "👋 *¿Todo bien?*\n\n" "Veo que estabas armando tu cotización. ¿Te falta algo más o quieres que te la envíe ahora?\n\n" "_Escribe *enviar cotización* para ver el total, o dime si necesitas otra parte._" ) except Exception as e: print(f"[WA-AI] Follow-up check failed: {e}") return "" # ── Customer purchase history awareness ────────────────────────────── def get_purchase_history(phone: str, tenant_conn, limit: int = 3) -> str: """Build a short text summary of recent confirmed quotations for this customer. Returns empty string if no history. """ if not tenant_conn or not phone: return "" try: cur = tenant_conn.cursor() cur.execute(""" SELECT q.id, q.created_at, q.total, ARRAY_AGG(qi.name ORDER BY qi.name) AS items FROM quotations q JOIN quotation_items qi ON qi.quotation_id = q.id WHERE q.notes LIKE %s AND q.status = 'converted' GROUP BY q.id, q.created_at, q.total ORDER BY q.created_at DESC LIMIT %s """, (f'%WA:{phone}%', limit)) rows = cur.fetchall() cur.close() if not rows: return "" from datetime import datetime, timezone now = datetime.now(timezone.utc) parts = [] for qid, created, total, items in rows: if created.tzinfo is None: created = created.replace(tzinfo=timezone.utc) months_ago = (now - created).days // 30 time_str = f"hace {months_ago} meses" if months_ago > 0 else "recientemente" item_list = ", ".join(items[:3]) parts.append(f"- {time_str}: {item_list} (total ${float(total):,.2f})") return "HISTORIAL DE COMPRAS DEL CLIENTE:\n" + "\n".join(parts) except Exception as e: print(f"[WA-AI] Purchase history failed: {e}") return ""