"""WhatsApp service via Evolution API (self-hosted, free). Evolution API connects to WhatsApp Web via QR code scan. Docs: https://doc.evolution-api.com/ """ import logging import requests from config import EVOLUTION_API_URL, EVOLUTION_API_KEY logger = logging.getLogger(__name__) HEADERS = { 'apikey': EVOLUTION_API_KEY, 'Content-Type': 'application/json' } def is_configured(): """Return True if Evolution API credentials are set.""" return bool(EVOLUTION_API_URL and EVOLUTION_API_KEY) # -- Instance management ----------------------------------------------------- def create_instance(instance_name): """Create a WhatsApp instance (one per tenant/phone number).""" try: resp = requests.post( f'{EVOLUTION_API_URL}/instance/create', headers=HEADERS, json={ 'instanceName': instance_name, 'qrcode': True, 'integration': 'WHATSAPP-BAILEYS' }, timeout=15 ) return resp.json() except Exception as e: logger.exception("Failed to create Evolution instance") return {'error': str(e)} def get_qr_code(instance_name): """Get QR code to connect WhatsApp. Returns dict with 'base64' key containing data:image/png;base64,... string. """ try: resp = requests.get( f'{EVOLUTION_API_URL}/instance/connect/{instance_name}', headers=HEADERS, timeout=15 ) return resp.json() except Exception as e: logger.exception("Failed to get QR code") return {'error': str(e)} def get_instance_status(instance_name): """Check if instance is connected. Returns dict with 'state' key: 'open' | 'close' | 'connecting'. """ try: resp = requests.get( f'{EVOLUTION_API_URL}/instance/connectionState/{instance_name}', headers=HEADERS, timeout=10 ) return resp.json() except Exception as e: logger.exception("Failed to get instance status") return {'error': str(e), 'state': 'close'} def logout_instance(instance_name): """Disconnect WhatsApp instance.""" try: resp = requests.delete( f'{EVOLUTION_API_URL}/instance/logout/{instance_name}', headers=HEADERS, timeout=10 ) return resp.json() except Exception as e: logger.exception("Failed to logout instance") return {'error': str(e)} def delete_instance(instance_name): """Delete instance completely.""" try: resp = requests.delete( f'{EVOLUTION_API_URL}/instance/delete/{instance_name}', headers=HEADERS, timeout=10 ) return resp.json() except Exception as e: logger.exception("Failed to delete instance") return {'error': str(e)} # -- Sending messages --------------------------------------------------------- def send_message(instance_name, to_phone, message_text): """Send text message. Args: instance_name: Evolution instance name (tenant identifier) to_phone: phone in format 5214421234567 (country code + number, no +) message_text: text content Returns: dict with response or 'error' key on failure. """ if not is_configured(): return {'error': 'Evolution API not configured'} try: resp = requests.post( f'{EVOLUTION_API_URL}/message/sendText/{instance_name}', headers=HEADERS, json={'number': to_phone, 'text': message_text}, timeout=15 ) data = resp.json() if resp.status_code in (200, 201): return data else: err = data.get('message', data.get('error', resp.text)) logger.error("Evolution send failed: %s", err) return {'error': err} except Exception as e: logger.exception("Evolution send exception") return {'error': str(e)} def send_image(instance_name, to_phone, image_url, caption=''): """Send image message.""" try: resp = requests.post( f'{EVOLUTION_API_URL}/message/sendMedia/{instance_name}', headers=HEADERS, json={ 'number': to_phone, 'mediatype': 'image', 'media': image_url, 'caption': caption }, timeout=30 ) return resp.json() except Exception as e: logger.exception("Evolution image send exception") return {'error': str(e)} def send_document(instance_name, to_phone, doc_url, filename, caption=''): """Send document (PDF, etc).""" try: resp = requests.post( f'{EVOLUTION_API_URL}/message/sendMedia/{instance_name}', headers=HEADERS, json={ 'number': to_phone, 'mediatype': 'document', 'media': doc_url, 'fileName': filename, 'caption': caption }, timeout=30 ) return resp.json() except Exception as e: logger.exception("Evolution document send exception") return {'error': str(e)} def send_quote(instance_name, to_phone, quote_data): """Send a formatted quotation to a customer.""" biz = quote_data.get("business_name", "Nexus Autoparts") qnum = quote_data.get("quote_number", "--") items = quote_data.get("items", []) subtotal = quote_data.get("subtotal", 0) tax = quote_data.get("tax", 0) total = quote_data.get("total", 0) validity = quote_data.get("validity_days", 7) lines = [ f"*{biz}*", f"Cotizacion #{qnum}", "---", ] for it in items: name = it.get("name", "Articulo") qty = it.get("quantity", 1) price = it.get("unit_price", 0) lines.append(f" {qty}x {name} -- ${price:,.2f}") lines.append("---") lines.append(f"Subtotal: ${subtotal:,.2f}") lines.append(f"IVA: ${tax:,.2f}") lines.append(f"*Total: ${total:,.2f}*") lines.append(f"\nVigencia: {validity} dias") notes = quote_data.get("notes") if notes: lines.append(f"Nota: {notes}") return send_message(instance_name, to_phone, "\n".join(lines)) def send_order_confirmation(instance_name, to_phone, sale_data): """Send order / sale confirmation.""" biz = sale_data.get("business_name", "Nexus Autoparts") folio = sale_data.get("folio", "--") total = sale_data.get("total", 0) method = sale_data.get("payment_method", "efectivo") lines = [ f"*{biz}*", f"Confirmacion de venta #{folio}", "---", ] for it in sale_data.get("items", []): name = it.get("name", "Articulo") qty = it.get("quantity", 1) lines.append(f" {qty}x {name}") lines.append("---") lines.append(f"*Total: ${total:,.2f}*") lines.append(f"Pago: {method}") lines.append("\nGracias por su compra!") return send_message(instance_name, to_phone, "\n".join(lines)) def send_stock_alert(instance_name, to_phone, alert_data): """Send stock alert to owner/manager.""" lines = [ "*ALERTA DE INVENTARIO*", "Los siguientes articulos estan bajos en stock:", "", ] for it in alert_data.get("items", []): name = it.get("name", "?") current = it.get("current_stock", 0) minimum = it.get("min_stock", 0) lines.append(f" {name}: {current} uds (min: {minimum})") lines.append("\nRevisa el inventario en tu POS.") return send_message(instance_name, to_phone, "\n".join(lines)) # -- Incoming webhook processing ---------------------------------------------- def process_incoming(webhook_data): """Process incoming Evolution API webhook. Evolution sends a different format than Meta Cloud API. Returns: dict with parsed message info. """ result = {'handled': False} try: data = webhook_data.get('data', {}) key = data.get('key', {}) message = data.get('message', {}) from_me = key.get('fromMe', False) phone = key.get('remoteJid', '').replace('@s.whatsapp.net', '') if not phone: return result # Extract text from different message types text = '' msg_type = 'text' if 'conversation' in message: text = message['conversation'] elif 'extendedTextMessage' in message: text = message['extendedTextMessage'].get('text', '') elif 'imageMessage' in message: text = message['imageMessage'].get('caption', '[Imagen]') msg_type = 'image' elif 'documentMessage' in message: text = message['documentMessage'].get('caption', '[Documento]') msg_type = 'document' elif 'audioMessage' in message: text = '[Audio]' msg_type = 'audio' elif 'videoMessage' in message: text = message['videoMessage'].get('caption', '[Video]') msg_type = 'video' elif 'contactMessage' in message: text = '[Contacto]' msg_type = 'contact' elif 'locationMessage' in message: text = '[Ubicacion]' msg_type = 'location' # Contact info from pushName contact_name = data.get('pushName', '') result = { 'type': 'message', 'phone': phone, 'contact_name': contact_name, 'text': text, 'message_type': msg_type, 'from_me': from_me, 'message_id': key.get('id', ''), 'timestamp': data.get('messageTimestamp', 0), 'handled': True, } # Skip auto-reply for our own outgoing messages if from_me: result['type'] = 'outgoing' return result # Attempt AI auto-response if the ai_chat service is available try: from services.ai_chat import chat as ai_chat_fn ai_result = ai_chat_fn(text, []) ai_reply = ai_result.get("message", "") if ai_reply: result["auto_reply"] = ai_reply except Exception: logger.debug("AI auto-reply not available, message queued for employee") return result except Exception as e: logger.exception("Error processing incoming Evolution webhook") return {'error': str(e), 'handled': False}