"""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/', 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//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.'})