Files
Autoparts-DB/pos/services/whatsapp_service.py
consultoria-as e95f7cf684 feat: complete session — catalog, marketplace, WhatsApp, peer-to-peer, install scripts
Major features:
- Pixel-Perfect glassmorphism design (landing + POS + public catalog)
- OEM/Local catalog toggle with Nexpart taxonomy (14 groups, 108 subgroups, 558 part types)
- Marketplace B2B Phase 1 (bodegas, POs, status machine, WA+email notifications)
- Peer-to-peer inventory (multi-instance, LAN discovery)
- WhatsApp: photo→Vision AI, voice→Whisper, conversational quotations
- Smart unified search (VIN/plate/part_number/keyword auto-detect)
- Shop Supplies tab (vehicle-independent parts)
- Chatbot AI fallback chain (5 models) + response cache
- CSV inventory import tool + setup_instance.sh installer
- Tablet-responsive CSS + sidebar toggle
- Filters, export CSV, employee edit, business data save
- Quotation system (WA→POS) with auto-print on confirmation
- Live stats on landing page

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 05:35:53 +00:00

118 lines
4.1 KiB
Python

"""WhatsApp service via Baileys Bridge (self-hosted, free).
Simple REST bridge at localhost:21465 that wraps WhatsApp Web via Baileys.
"""
import requests
from config import WHATSAPP_BRIDGE_URL
HEADERS = {'Content-Type': 'application/json'}
def get_status():
try:
return requests.get(f'{WHATSAPP_BRIDGE_URL}/status', timeout=5).json()
except Exception as e:
return {'state': 'error', 'error': str(e)}
def get_qr():
try:
return requests.get(f'{WHATSAPP_BRIDGE_URL}/qr', timeout=5).json()
except Exception as e:
return {'state': 'error', 'error': str(e)}
def connect():
try:
return requests.post(f'{WHATSAPP_BRIDGE_URL}/connect', headers=HEADERS, timeout=5).json()
except Exception as e:
return {'state': 'error', 'error': str(e)}
def send_message(phone, text):
try:
return requests.post(f'{WHATSAPP_BRIDGE_URL}/send', headers=HEADERS, json={'phone': phone, 'message': text}, timeout=15).json()
except Exception as e:
return {'error': str(e)}
def send_quote(phone, quote_data):
lines = [f"*Cotizacion #{quote_data.get('id', '')}*", ""]
for item in quote_data.get('items', []):
lines.append(f"- {item.get('quantity', 1)}x {item.get('name', '')} ${item.get('subtotal', 0):,.2f}")
lines.append(f"\nSubtotal: ${quote_data.get('subtotal', 0):,.2f}")
lines.append(f"IVA: ${quote_data.get('tax_total', 0):,.2f}")
lines.append(f"*Total: ${quote_data.get('total', 0):,.2f}*")
return send_message(phone, "\n".join(lines))
def logout():
try:
return requests.post(f'{WHATSAPP_BRIDGE_URL}/logout', headers=HEADERS, timeout=5).json()
except Exception as e:
return {'error': str(e)}
def process_incoming(webhook_data):
"""Extract a normalized dict from a Baileys webhook payload.
Supports text messages, image messages, audio (voice notes), and video.
Media content comes pre-downloaded as base64 from the bridge so Python
doesn't have to re-authenticate with WhatsApp servers.
Returns:
dict with keys:
phone — numeric phone (no JID suffix)
jid — full remote JID (may be @s.whatsapp.net or @lid)
text — text content (plain text or media caption)
from_me — bool, True if we sent the message
message_id — WhatsApp message ID
media_kind — 'text' | 'image' | 'audio' | 'video'
media_base64 — base64 string if media, else None
media_mimetype — e.g. 'image/jpeg', 'audio/ogg'
is_voice_note — True for WhatsApp voice notes (audioMessage ptt)
"""
data = webhook_data.get('data', {})
key = data.get('key', {})
message = data.get('message', {})
# remoteJid can be phone@s.whatsapp.net or LID@lid
remote_jid = key.get('remoteJid', '')
phone = remote_jid.replace('@s.whatsapp.net', '').replace('@lid', '')
# The bridge now classifies and passes these extra fields. Fall back to
# the old parsing if they're missing (older bridge version).
media_kind = data.get('media_kind', 'text')
media_base64 = data.get('media_base64')
media_mimetype = data.get('media_mimetype')
media_caption = data.get('media_caption') or ''
is_voice_note = bool(data.get('media_ptt'))
push_name = data.get('push_name') or ''
# Text content:
# - For 'text' messages → conversation or extendedTextMessage
# - For 'image'/'video' → the caption (may be empty)
# - For 'audio' → empty (filled in later by Whisper transcription)
if media_kind == 'text':
text = (
message.get('conversation', '')
or message.get('extendedTextMessage', {}).get('text', '')
or ''
)
else:
text = media_caption
return {
'phone': phone,
'jid': remote_jid,
'text': text,
'from_me': key.get('fromMe', False),
'message_id': key.get('id', ''),
'media_kind': media_kind,
'media_base64': media_base64,
'media_mimetype': media_mimetype,
'is_voice_note': is_voice_note,
'push_name': push_name,
}