# /home/Autopartes/pos/blueprints/marketplace_bp.py """Marketplace B2B: bodegas publish inventory, talleres/refaccionarias browse and order.""" from flask import Blueprint, request, jsonify, g from middleware import require_auth from tenant_db import get_master_conn, get_tenant_conn marketplace_bp = Blueprint('marketplace', __name__, url_prefix='/pos/api/marketplace') @marketplace_bp.route('/sellers', methods=['GET']) @require_auth() def list_sellers(): """List active sellers/bodegas.""" conn = get_master_conn() cur = conn.cursor() cur.execute(""" SELECT id, name, subdomain, rfc FROM tenants WHERE is_active = true AND is_seller = true ORDER BY name """) sellers = [] for r in cur.fetchall(): sellers.append({'id': r[0], 'name': r[1], 'subdomain': r[2], 'rfc': r[3]}) cur.close() conn.close() return jsonify({'data': sellers}) @marketplace_bp.route('/search', methods=['GET']) @require_auth() def search_inventory(): """Search across ALL seller tenant inventories. Query params: q: search term (required, min 2 chars) seller_id: optional filter by specific seller page: page number (default 1) per_page: results per page (default 50, max 200) """ q = request.args.get('q', '').strip() if len(q) < 2: return jsonify({'error': 'Search query must be at least 2 characters'}), 400 seller_id = request.args.get('seller_id') page = int(request.args.get('page', 1)) per_page = min(int(request.args.get('per_page', 50)), 200) offset = (page - 1) * per_page # Get all seller tenants master = get_master_conn() mcur = master.cursor() if seller_id: mcur.execute(""" SELECT id, name, db_name FROM tenants WHERE is_active = true AND is_seller = true AND id = %s """, (seller_id,)) else: mcur.execute(""" SELECT id, name, db_name FROM tenants WHERE is_active = true AND is_seller = true ORDER BY name """) sellers = mcur.fetchall() mcur.close() master.close() results = [] search_pattern = f'%{q}%' for s_id, s_name, db_name in sellers: try: conn = get_tenant_conn(s_id) cur = conn.cursor() cur.execute(""" SELECT i.part_number, i.name, i.brand, i.price_1, i.tax_rate, i.unit, COALESCE(s.stock, 0) AS stock FROM inventory i LEFT JOIN ( SELECT inventory_id, COALESCE(SUM(quantity), 0) AS stock FROM inventory_operations GROUP BY inventory_id ) s ON s.inventory_id = i.id WHERE i.is_active = true AND COALESCE(s.stock, 0) > 0 AND (i.part_number ILIKE %s OR i.name ILIKE %s OR i.brand ILIKE %s) ORDER BY i.name LIMIT %s """, (search_pattern, search_pattern, search_pattern, per_page)) for r in cur.fetchall(): results.append({ 'seller_id': s_id, 'seller_name': s_name, 'part_number': r[0], 'name': r[1], 'brand': r[2], 'price': float(r[3]) if r[3] else 0, 'tax_rate': float(r[4]) if r[4] else 0.16, 'unit': r[5] or 'PZA', 'stock': r[6], }) cur.close() conn.close() except Exception: # Skip tenants with connection issues continue # Sort all results by name, then paginate results.sort(key=lambda x: x['name']) total = len(results) paged = results[offset:offset + per_page] return jsonify({ 'data': paged, 'pagination': { 'page': page, 'per_page': per_page, 'total': total, 'pages': (total + per_page - 1) // per_page if per_page else 1, } }) @marketplace_bp.route('/order', methods=['POST']) @require_auth() def create_order(): """Create a marketplace order from buyer to seller. Body: seller_id: int (required) items: [{ part_number, part_name, quantity, unit_price }] (required) notes: str (optional) """ data = request.get_json() or {} seller_id = data.get('seller_id') items = data.get('items', []) if not seller_id: return jsonify({'error': 'seller_id required'}), 400 if not items: return jsonify({'error': 'items required (non-empty array)'}), 400 buyer_id = g.tenant_id # Get buyer and seller names master = get_master_conn() mcur = master.cursor() mcur.execute("SELECT name FROM tenants WHERE id = %s", (buyer_id,)) buyer_row = mcur.fetchone() mcur.execute("SELECT name FROM tenants WHERE id = %s AND is_seller = true AND is_active = true", (seller_id,)) seller_row = mcur.fetchone() mcur.close() if not buyer_row: master.close() return jsonify({'error': 'Buyer tenant not found'}), 404 if not seller_row: master.close() return jsonify({'error': 'Seller not found or not active'}), 404 buyer_name = buyer_row[0] seller_name = seller_row[0] # Calculate total total = 0 for item in items: qty = item.get('quantity', 0) price = item.get('unit_price', 0) item['subtotal'] = round(qty * price, 2) total += item['subtotal'] mcur2 = master.cursor() mcur2.execute(""" INSERT INTO marketplace_orders (buyer_tenant_id, seller_tenant_id, buyer_name, seller_name, total, notes) VALUES (%s, %s, %s, %s, %s, %s) RETURNING id """, (buyer_id, seller_id, buyer_name, seller_name, round(total, 2), data.get('notes'))) order_id = mcur2.fetchone()[0] for item in items: mcur2.execute(""" INSERT INTO marketplace_order_items (order_id, part_number, part_name, quantity, unit_price, subtotal) VALUES (%s, %s, %s, %s, %s, %s) """, (order_id, item.get('part_number'), item.get('part_name'), item.get('quantity', 0), item.get('unit_price', 0), item.get('subtotal', 0))) master.commit() mcur2.close() master.close() return jsonify({'id': order_id, 'total': round(total, 2), 'message': 'Order created'}), 201 @marketplace_bp.route('/orders', methods=['GET']) @require_auth() def list_orders(): """List marketplace orders (as buyer or seller). Query params: role: 'buyer' or 'seller' (default: both) status: filter by status page: page number per_page: results per page """ tenant_id = g.tenant_id role = request.args.get('role', '') status = request.args.get('status', '') page = int(request.args.get('page', 1)) per_page = min(int(request.args.get('per_page', 50)), 200) offset = (page - 1) * per_page master = get_master_conn() mcur = master.cursor() where_clauses = [] params = [] if role == 'buyer': where_clauses.append("buyer_tenant_id = %s") params.append(tenant_id) elif role == 'seller': where_clauses.append("seller_tenant_id = %s") params.append(tenant_id) else: where_clauses.append("(buyer_tenant_id = %s OR seller_tenant_id = %s)") params.extend([tenant_id, tenant_id]) if status: where_clauses.append("status = %s") params.append(status) where = " AND ".join(where_clauses) mcur.execute(f"SELECT count(*) FROM marketplace_orders WHERE {where}", params) total = mcur.fetchone()[0] mcur.execute(f""" SELECT id, buyer_tenant_id, seller_tenant_id, buyer_name, seller_name, total, status, notes, created_at, updated_at FROM marketplace_orders WHERE {where} ORDER BY created_at DESC LIMIT %s OFFSET %s """, params + [per_page, offset]) orders = [] for r in mcur.fetchall(): orders.append({ 'id': r[0], 'buyer_tenant_id': r[1], 'seller_tenant_id': r[2], 'buyer_name': r[3], 'seller_name': r[4], 'total': float(r[5]) if r[5] else 0, 'status': r[6], 'notes': r[7], 'created_at': str(r[8]), 'updated_at': str(r[9]), }) mcur.close() master.close() return jsonify({ 'data': orders, 'pagination': { 'page': page, 'per_page': per_page, 'total': total, 'pages': (total + per_page - 1) // per_page if per_page else 1, } }) @marketplace_bp.route('/orders//status', methods=['PUT']) @require_auth() def update_order_status(order_id): """Update order status. Seller can confirm/ship/deliver/cancel. Buyer can cancel if pending. Body: status: 'confirmed' | 'shipped' | 'delivered' | 'cancelled' """ data = request.get_json() or {} new_status = data.get('status') valid_statuses = ['confirmed', 'shipped', 'delivered', 'cancelled'] if new_status not in valid_statuses: return jsonify({'error': f'status must be one of: {", ".join(valid_statuses)}'}), 400 tenant_id = g.tenant_id master = get_master_conn() mcur = master.cursor() mcur.execute(""" SELECT buyer_tenant_id, seller_tenant_id, status FROM marketplace_orders WHERE id = %s """, (order_id,)) row = mcur.fetchone() if not row: mcur.close() master.close() return jsonify({'error': 'Order not found'}), 404 buyer_id, seller_id, current_status = row # Permission check if tenant_id == buyer_id: # Buyer can only cancel pending orders if new_status != 'cancelled' or current_status != 'pending': mcur.close() master.close() return jsonify({'error': 'Buyer can only cancel pending orders'}), 403 elif tenant_id == seller_id: # Seller can do any transition pass else: mcur.close() master.close() return jsonify({'error': 'Not authorized for this order'}), 403 mcur.execute(""" UPDATE marketplace_orders SET status = %s, updated_at = NOW() WHERE id = %s """, (new_status, order_id)) master.commit() mcur.close() master.close() return jsonify({'id': order_id, 'status': new_status, 'message': 'Order updated'}) @marketplace_bp.route('/orders//items', methods=['GET']) @require_auth() def get_order_items(order_id): """Get items for a specific order.""" tenant_id = g.tenant_id master = get_master_conn() mcur = master.cursor() # Verify tenant is buyer or seller mcur.execute(""" SELECT buyer_tenant_id, seller_tenant_id FROM marketplace_orders WHERE id = %s """, (order_id,)) row = mcur.fetchone() if not row or (row[0] != tenant_id and row[1] != tenant_id): mcur.close() master.close() return jsonify({'error': 'Not authorized'}), 403 mcur.execute(""" SELECT id, part_number, part_name, quantity, unit_price, subtotal FROM marketplace_order_items WHERE order_id = %s ORDER BY id """, (order_id,)) items = [] for r in mcur.fetchall(): items.append({ 'id': r[0], 'part_number': r[1], 'part_name': r[2], 'quantity': r[3], 'unit_price': float(r[4]) if r[4] else 0, 'subtotal': float(r[5]) if r[5] else 0, }) mcur.close() master.close() return jsonify({'data': items})