# /home/Autopartes/pos/blueprints/b2b_bp.py """Nexus B2B yonke marketplace blueprint. Wholesale trading of used parts between yonkes (deshuesaderos): one tenant publishes its used pieces, another buys them. Listings and orders live in the MASTER DB; the pieces stay in each tenant DB. All endpoints live under /pos/api/b2b. Permissions: yonke.view for reads, yonke.manage for writes. Seller: GET /pos/api/b2b/my-listings my published listings POST /pos/api/b2b/listings publish/refresh a listing PATCH /pos/api/b2b/listings/ edit price/status/notes DELETE /pos/api/b2b/listings/ soft-delete (status='removed') GET /pos/api/b2b/seller/status {is_seller, listings_count, ...} Buyer: GET /pos/api/b2b/catalog available listings from other tenants GET /pos/api/b2b/listings/ listing detail (+ seller_name) Orders (both): POST /pos/api/b2b/orders create an order from listings GET /pos/api/b2b/orders my orders as buyer GET /pos/api/b2b/orders/incoming my orders as seller GET /pos/api/b2b/orders/ order detail (participants only) PUT /pos/api/b2b/orders//status transition the order status """ from flask import Blueprint, g, jsonify, request from middleware import require_auth from services import b2b_service from tenant_db import get_master_conn b2b_bp = Blueprint('b2b', __name__, url_prefix='/pos/api/b2b') def _int_arg(name, default, minimum, maximum): try: val = int(request.args.get(name, default)) except (TypeError, ValueError): return default return min(max(val, minimum), maximum) def _error_response(e): """Map a service exception to (json, status); never leaks a 500.""" if isinstance(e, b2b_service.B2BForbidden): return jsonify({'error': str(e)}), 403 if isinstance(e, b2b_service.B2BNotFound): return jsonify({'error': str(e)}), 404 if isinstance(e, b2b_service.B2BValidationError): return jsonify({'error': str(e)}), 400 return jsonify({'error': str(e)}), 500 # ═══════════════════════════════════════════════════════════════════════════ # SELLER # ═══════════════════════════════════════════════════════════════════════════ @b2b_bp.route('/my-listings', methods=['GET']) @require_auth('yonke.view') def my_listings(): """Listings published by the current tenant.""" master = get_master_conn() try: data = b2b_service.list_my_listings( master, g.tenant_id, status=request.args.get('status')) return jsonify({'data': data}) except Exception as e: return _error_response(e) finally: master.close() @b2b_bp.route('/listings', methods=['POST']) @require_auth('yonke.manage') def publish_listing(): """Publish (or refresh) one of my used/rebuild pieces to the B2B catalog.""" data = request.get_json(silent=True) or {} master = get_master_conn() try: listing = b2b_service.publish_listing(master, g.tenant_id, data) master.commit() return jsonify(listing), 201 except Exception as e: master.rollback() return _error_response(e) finally: master.close() @b2b_bp.route('/listings/', methods=['PATCH']) @require_auth('yonke.manage') def update_listing(listing_id): """Edit price_wholesale / status / notes of one of my listings.""" data = request.get_json(silent=True) or {} master = get_master_conn() try: listing = b2b_service.update_listing(master, g.tenant_id, listing_id, data) master.commit() return jsonify(listing) except Exception as e: master.rollback() return _error_response(e) finally: master.close() @b2b_bp.route('/listings/', methods=['DELETE']) @require_auth('yonke.manage') def remove_listing(listing_id): """Soft-delete a listing (status='removed').""" master = get_master_conn() try: b2b_service.remove_listing(master, g.tenant_id, listing_id) master.commit() return jsonify({'ok': True, 'id': listing_id, 'status': 'removed'}) except Exception as e: master.rollback() return _error_response(e) finally: master.close() @b2b_bp.route('/seller/status', methods=['GET']) @require_auth('yonke.view') def seller_status(): """{is_seller, listings_count, incoming_orders} for the current tenant.""" master = get_master_conn() try: return jsonify(b2b_service.get_seller_status(master, g.tenant_id)) except Exception as e: return _error_response(e) finally: master.close() # ═══════════════════════════════════════════════════════════════════════════ # BUYER — catalog # ═══════════════════════════════════════════════════════════════════════════ @b2b_bp.route('/catalog', methods=['GET']) @require_auth('yonke.view') def catalog(): """Available listings from OTHER tenants, with seller_name and pagination.""" master = get_master_conn() try: items, pagination = b2b_service.list_catalog( master, g.tenant_id, filters={ 'q': request.args.get('q'), 'make': request.args.get('make'), 'model': request.args.get('model'), 'year': request.args.get('year'), 'grade': request.args.get('grade'), }, page=_int_arg('page', 1, 1, 10**9), per_page=_int_arg('per_page', 12, 1, 100), ) return jsonify({'data': items, 'pagination': pagination}) except Exception as e: return _error_response(e) finally: master.close() @b2b_bp.route('/listings/', methods=['GET']) @require_auth('yonke.view') def get_listing(listing_id): """Listing detail with seller_name.""" master = get_master_conn() try: listing = b2b_service.get_listing(master, listing_id) if not listing: return jsonify({'error': 'Listing no encontrado'}), 404 return jsonify(listing) except Exception as e: return _error_response(e) finally: master.close() # ═══════════════════════════════════════════════════════════════════════════ # ORDERS # ═══════════════════════════════════════════════════════════════════════════ @b2b_bp.route('/orders', methods=['POST']) @require_auth('yonke.manage') def create_order(): """Create a pending order from one or more listings (single seller).""" data = request.get_json(silent=True) or {} master = get_master_conn() try: order = b2b_service.create_order( master, g.tenant_id, data.get('items'), notes=data.get('notes')) master.commit() return jsonify(order), 201 except Exception as e: master.rollback() return _error_response(e) finally: master.close() @b2b_bp.route('/orders', methods=['GET']) @require_auth('yonke.view') def list_orders(): """My orders as buyer (role='buyer').""" master = get_master_conn() try: return jsonify({'data': b2b_service.list_orders(master, g.tenant_id, 'buyer')}) except Exception as e: return _error_response(e) finally: master.close() @b2b_bp.route('/orders/incoming', methods=['GET']) @require_auth('yonke.view') def list_incoming_orders(): """Orders I must fulfil as seller (role='seller').""" master = get_master_conn() try: return jsonify({'data': b2b_service.list_orders(master, g.tenant_id, 'seller')}) except Exception as e: return _error_response(e) finally: master.close() @b2b_bp.route('/orders/', methods=['GET']) @require_auth('yonke.view') def get_order(order_id): """Order detail (buyer or seller of that order only).""" master = get_master_conn() try: return jsonify(b2b_service.get_order(master, g.tenant_id, order_id)) except Exception as e: return _error_response(e) finally: master.close() @b2b_bp.route('/orders//status', methods=['PUT']) @require_auth('yonke.manage') def update_order_status(order_id): """Transition an order status (role-checked). 'received' also stocks the buyer.""" data = request.get_json(silent=True) or {} master = get_master_conn() try: result = b2b_service.update_order_status( master, g.tenant_id, order_id, data.get('status'), branch_id=g.branch_id) return jsonify(result) except Exception as e: master.rollback() return _error_response(e) finally: master.close()