Files
Autoparts-DB/pos/blueprints/b2b_bp.py
consultoria-as a386ca5b4a
Some checks failed
CI / lint-and-test (3.11) (push) Has been cancelled
CI / lint-and-test (3.13) (push) Has been cancelled
Mejoras de sesion: estabilizacion, IA, Nexus Yonke y marketplace B2B
- F0 blindaje: Redis/Celery, pg_hba sin trust LAN, secretos a env 600, backups, webhook auth, jobs muertos
- F1 caja: bugs POS, reembolsos al corte, apartados, CFDI, topes de descuento en cotizacion/apartado
- F2/F7 IA de catalogo: autocategorizacion, dedup de SKUs con embeddings, OCR de facturas, descripciones, claves SAT, limpieza de fitment
- F3 IA en el flujo: vision Yonke, preguntas MeLi, venta cruzada, anomalias, sinonimos, busqueda semantica (rerank/embeddings)
- F4/F8 escala y cartera: /metrics+Prometheus+Grafana, pool, score de credito, churn, metricas multi-tenant
- Nexus Yonke (M1-M6): modelo de datos 1-de-1, alta por donador, vitrina publica, MeLi usado, consignacion, garantia por dias, reportes, etiquetas
- Marketplace B2B entre yonkes (catalogo + pedidos cross-tenant)
- Seguridad: auth en /api/admin/* (35 endpoints), alertas por tenant
- Migraciones v4.27/v4.28; timers expire-demos y rollup-metrics
2026-10-12 00:57:31 +00:00

259 lines
9.4 KiB
Python

# /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/<id> edit price/status/notes
DELETE /pos/api/b2b/listings/<id> 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/<id> 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/<id> order detail (participants only)
PUT /pos/api/b2b/orders/<id>/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/<int:listing_id>', 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/<int:listing_id>', 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/<int:listing_id>', 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/<int:order_id>', 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/<int:order_id>/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()