- 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
107 lines
4.2 KiB
Python
107 lines
4.2 KiB
Python
"""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.'})
|