Files
Autoparts-DB/pos/blueprints/warranty_bp.py
Nexus Dev 9ff3dc4c8b FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica
FASE 4:
- Redis cache de stock con fallback graceful
- Multi-moneda (MXN/USD) con contabilidad en MXN
- Proveedores y ordenes de compra completo
- Meilisearch 1.5M+ partes indexadas
- Metabase KPIs con dashboard auto-generado

FASE 5:
- CRM mejorado: activities, tags, loyalty program, analytics
- Imagenes de partes: upload, resize, thumbnails WebP
- Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered
- Garantias/RMA, alertas de reorden, multi-sucursal
- Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi)

FASE 6:
- Notificaciones automaticas: push/WhatsApp/email/in-app
- Reportes de ahorro vs retail_price
- Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber
- API Publica: API keys, rate limiting, catalog search

Migraciones: v1.9-v3.0
Tests: 93/93 pasando
Backup: nexus_backup_20260427_045859.tar.gz
2026-04-27 05:23:30 +00:00

209 lines
7.1 KiB
Python

"""Warranty / RMA blueprint.
Endpoints (all under /pos/api):
GET/POST /warranties
GET /warranties/<id>
GET /customers/<id>/warranties
POST /warranty-claims
GET /warranty-claims
GET /warranty-claims/<id>
PUT /warranty-claims/<id>/resolve
PUT /warranty-claims/<id>/close
"""
from flask import Blueprint, request, jsonify, g
from middleware import require_auth
from tenant_db import get_tenant_conn
from services.warranty_engine import (
register_warranty, create_claim, resolve_claim, close_claim,
get_warranty, list_warranties, get_claim, list_claims, expire_warranties,
)
warranty_bp = Blueprint('warranty', __name__, url_prefix='/pos/api')
# ── WARRANTIES ─────────────────────────────────────────────────────────────
@warranty_bp.route('/warranties', methods=['GET'])
@require_auth('inventory.view')
def get_warranties():
"""List warranties."""
conn = get_tenant_conn(g.tenant_id)
status = request.args.get('status')
customer_id = request.args.get('customer_id', type=int)
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
data = list_warranties(conn, customer_id=customer_id, status=status, limit=limit, offset=offset)
conn.close()
return jsonify({'data': data})
@warranty_bp.route('/warranties', methods=['POST'])
@require_auth('pos.sell')
def post_warranty():
"""Register a warranty (usually called at sale time)."""
data = request.get_json() or {}
required = ['sale_id', 'sale_item_id', 'inventory_id', 'customer_id', 'warranty_months']
missing = [f for f in required if f not in data]
if missing:
return jsonify({'error': f'Missing fields: {missing}'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
w_id = register_warranty(
conn,
sale_id=data['sale_id'],
sale_item_id=data['sale_item_id'],
inventory_id=data['inventory_id'],
customer_id=data['customer_id'],
warranty_months=int(data['warranty_months']),
supplier_id=data.get('supplier_id'),
part_number=data.get('part_number'),
name=data.get('name'),
notes=data.get('notes'),
)
conn.commit()
conn.close()
return jsonify({'id': w_id, 'message': 'Warranty registered'}), 201
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@warranty_bp.route('/warranties/<int:warranty_id>', methods=['GET'])
@require_auth('inventory.view')
def get_warranty_detail(warranty_id):
"""Get warranty by ID."""
conn = get_tenant_conn(g.tenant_id)
w = get_warranty(conn, warranty_id)
conn.close()
if not w:
return jsonify({'error': 'Warranty not found'}), 404
return jsonify(w)
@warranty_bp.route('/customers/<int:customer_id>/warranties', methods=['GET'])
@require_auth('inventory.view')
def get_customer_warranties(customer_id):
"""List warranties for a customer."""
conn = get_tenant_conn(g.tenant_id)
data = list_warranties(conn, customer_id=customer_id)
conn.close()
return jsonify({'data': data})
# ── WARRANTY CLAIMS ────────────────────────────────────────────────────────
@warranty_bp.route('/warranty-claims', methods=['POST'])
@require_auth('inventory.edit')
def post_claim():
"""File a warranty claim."""
data = request.get_json() or {}
if not data.get('warranty_id') or not data.get('reason'):
return jsonify({'error': 'warranty_id and reason are required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
claim_id = create_claim(
conn,
warranty_id=data['warranty_id'],
reason=data['reason'],
employee_id=g.employee_id,
notes=data.get('notes')
)
conn.commit()
conn.close()
return jsonify({'id': claim_id, 'message': 'Claim filed'}), 201
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@warranty_bp.route('/warranty-claims', methods=['GET'])
@require_auth('inventory.view')
def get_claims():
"""List warranty claims."""
conn = get_tenant_conn(g.tenant_id)
status = request.args.get('status')
warranty_id = request.args.get('warranty_id', type=int)
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
data = list_claims(conn, status=status, warranty_id=warranty_id, limit=limit, offset=offset)
conn.close()
return jsonify({'data': data})
@warranty_bp.route('/warranty-claims/<int:claim_id>', methods=['GET'])
@require_auth('inventory.view')
def get_claim_detail(claim_id):
"""Get claim by ID."""
conn = get_tenant_conn(g.tenant_id)
c = get_claim(conn, claim_id)
conn.close()
if not c:
return jsonify({'error': 'Claim not found'}), 404
return jsonify(c)
@warranty_bp.route('/warranty-claims/<int:claim_id>/resolve', methods=['PUT'])
@require_auth('inventory.edit')
def put_resolve_claim(claim_id):
"""Resolve a claim."""
data = request.get_json() or {}
resolution = data.get('resolution')
if not resolution:
return jsonify({'error': 'resolution is required'}), 400
conn = get_tenant_conn(g.tenant_id)
try:
ok = resolve_claim(
conn, claim_id, resolution,
diagnosis=data.get('diagnosis'),
replacement_inventory_id=data.get('replacement_inventory_id'),
refund_amount=data.get('refund_amount'),
labor_cost=data.get('labor_cost'),
supplier_rma_number=data.get('supplier_rma_number'),
notes=data.get('notes')
)
conn.commit()
conn.close()
if not ok:
return jsonify({'error': 'Claim not found or already closed'}), 400
return jsonify({'message': 'Claim resolved'})
except ValueError as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 400
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500
@warranty_bp.route('/warranty-claims/<int:claim_id>/close', methods=['PUT'])
@require_auth('inventory.edit')
def put_close_claim(claim_id):
"""Close a resolved claim."""
conn = get_tenant_conn(g.tenant_id)
try:
ok = close_claim(conn, claim_id)
conn.commit()
conn.close()
if not ok:
return jsonify({'error': 'Claim not found or not resolved'}), 400
return jsonify({'message': 'Claim closed'})
except Exception as e:
conn.rollback()
conn.close()
return jsonify({'error': str(e)}), 500