feat(whatsapp): QWEN primary AI backend, Hermes fallback, conversation history, vehicle persistence, demo prompts
- Add QWEN (qwen3.6) as primary AI backend with short system prompt - Hermes remains as fallback with 45s timeout - Increase QWEN timeout to 35s, max_tokens to 4000 - Add conversation history loading from whatsapp_messages (last 4 msgs) - Persist detected vehicle in whatsapp_sessions table - Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history - Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel - Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.) - Improve no-stock response: conversational with alternatives - Split search_query by | for multi-part lookups - Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
This commit is contained in:
106
pos/blueprints/public_bp.py
Normal file
106
pos/blueprints/public_bp.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""Public blueprint — unauthenticated routes for shared content.
|
||||
|
||||
These routes live outside the /pos/api prefix so they can be accessed
|
||||
by customers without login.
|
||||
"""
|
||||
import jwt
|
||||
from flask import Blueprint, request, jsonify, render_template_string
|
||||
from tenant_db import get_tenant_conn
|
||||
from config import JWT_SECRET
|
||||
from blueprints.pos_bp import PUBLIC_QUOTE_TEMPLATE
|
||||
|
||||
public_bp = Blueprint('public', __name__)
|
||||
|
||||
|
||||
@public_bp.route('/public/quote/<token>', methods=['GET'])
|
||||
def public_quote(token):
|
||||
"""Unauthenticated public view of a quotation."""
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
if payload.get('type') != 'public_quote':
|
||||
return jsonify({'error': 'Invalid token type'}), 400
|
||||
except jwt.ExpiredSignatureError:
|
||||
return jsonify({'error': 'Quote expired'}), 410
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({'error': 'Invalid token'}), 400
|
||||
|
||||
conn = get_tenant_conn(payload['tenant_id'])
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("""
|
||||
SELECT q.id, q.subtotal, q.tax_total, q.total, q.valid_until,
|
||||
q.created_at, q.notes, q.customer_id, q.currency, q.exchange_rate,
|
||||
q.status,
|
||||
c.name as customer_name, c.phone as customer_phone, c.email as customer_email,
|
||||
e.name as employee_name
|
||||
FROM quotations q
|
||||
LEFT JOIN customers c ON q.customer_id = c.id
|
||||
LEFT JOIN employees e ON q.employee_id = e.id
|
||||
WHERE q.id = %s
|
||||
""", (payload['quot_id'],))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Quotation not found'}), 404
|
||||
|
||||
cols = ['id', 'subtotal', 'tax_total', 'total', 'valid_until', 'created_at',
|
||||
'notes', 'customer_id', 'currency', 'exchange_rate', 'status',
|
||||
'customer_name', 'customer_phone', 'customer_email', 'employee_name']
|
||||
quot = dict(zip(cols, row))
|
||||
for k in ('subtotal', 'tax_total', 'total', 'exchange_rate'):
|
||||
if quot.get(k) is not None:
|
||||
quot[k] = float(quot[k])
|
||||
if quot.get('created_at'):
|
||||
quot['created_at'] = str(quot['created_at'])
|
||||
if quot.get('valid_until'):
|
||||
quot['valid_until'] = str(quot['valid_until'])
|
||||
|
||||
cur.execute("""
|
||||
SELECT part_number, name, quantity, unit_price, discount_pct, tax_rate, subtotal
|
||||
FROM quotation_items WHERE quotation_id = %s ORDER BY id
|
||||
""", (payload['quot_id'],))
|
||||
items = []
|
||||
for r in cur.fetchall():
|
||||
items.append({
|
||||
'part_number': r[0], 'name': r[1], 'quantity': r[2],
|
||||
'unit_price': float(r[3]) if r[3] else 0,
|
||||
'discount_pct': float(r[4]) if r[4] else 0,
|
||||
'tax_rate': float(r[5]) if r[5] else 0,
|
||||
'subtotal': float(r[6]) if r[6] else 0,
|
||||
})
|
||||
cur.close(); conn.close()
|
||||
|
||||
html = render_template_string(PUBLIC_QUOTE_TEMPLATE,
|
||||
quot=quot, items=items, host=request.host_url.rstrip('/'),
|
||||
token=token)
|
||||
return html, 200, {'Content-Type': 'text/html; charset=utf-8'}
|
||||
|
||||
|
||||
@public_bp.route('/public/quote/<token>/accept', methods=['POST'])
|
||||
def public_quote_accept(token):
|
||||
"""Customer accepts a public quote."""
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
||||
if payload.get('type') != 'public_quote':
|
||||
return jsonify({'error': 'Invalid token type'}), 400
|
||||
except jwt.ExpiredSignatureError:
|
||||
return jsonify({'error': 'Quote expired'}), 410
|
||||
except jwt.InvalidTokenError:
|
||||
return jsonify({'error': 'Invalid token'}), 400
|
||||
|
||||
conn = get_tenant_conn(payload['tenant_id'])
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT status FROM quotations WHERE id = %s", (payload['quot_id'],))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Quotation not found'}), 404
|
||||
if row[0] != 'active':
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'error': 'Quotation is no longer active'}), 400
|
||||
|
||||
cur.execute("UPDATE quotations SET status = 'converted' WHERE id = %s",
|
||||
(payload['quot_id'],))
|
||||
conn.commit()
|
||||
cur.close(); conn.close()
|
||||
return jsonify({'message': 'Cotizacion aceptada. Un asesor se pondra en contacto contigo.'})
|
||||
Reference in New Issue
Block a user