from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import List from app.database import get_db from app.models import Cliente, SubCliente, MovimientoTimbre, LimiteTimbre, Usuario from app.models.enums import TipoMovimiento, TipoLimite from app.schemas import ( ClienteCreate, ClienteUpdate, ClienteResponse, ClienteWithStats, BalanceResponse, RecargaTimbreCreate, MessageResponse, LimiteHistorialResponse, LimiteResponse ) from app.dependencies import get_current_user router = APIRouter() @router.post("/", response_model=ClienteResponse, status_code=status.HTTP_201_CREATED) def crear_cliente( cliente: ClienteCreate, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Crear un nuevo cliente con su límite de timbres inicial """ # Verificar si el email ya existe if db.query(Cliente).filter(Cliente.email == cliente.email).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="El email ya está registrado" ) # Verificar si el RFC ya existe (si se proporciona) if cliente.rfc and db.query(Cliente).filter(Cliente.rfc == cliente.rfc).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="El RFC ya está registrado" ) db_cliente = Cliente( nombre=cliente.nombre, email=cliente.email, rfc=cliente.rfc, telefono=cliente.telefono, permite_subclientes=cliente.permite_subclientes ) db.add(db_cliente) db.flush() # Para obtener el ID sin hacer commit # Crear el límite inicial si se proporciona if cliente.limite_timbres > 0: limite_inicial = LimiteTimbre( cliente_id=db_cliente.id, tipo=TipoLimite.ASIGNACION_INICIAL, cantidad=cliente.limite_timbres, descripcion="Límite inicial al crear el cliente" ) db.add(limite_inicial) db.commit() db.refresh(db_cliente) # Agregar campos calculados response = ClienteResponse.model_validate(db_cliente) response.limite_timbres = db_cliente.get_limite_actual(db) response.timbres_disponibles = db_cliente.get_timbres_disponibles(db) return response @router.get("/", response_model=List[ClienteResponse]) def listar_clientes( skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Listar todos los clientes """ clientes = db.query(Cliente).offset(skip).limit(limit).all() # Agregar campos calculados a cada cliente response = [] for cliente in clientes: cliente_dict = ClienteResponse.model_validate(cliente) cliente_dict.limite_timbres = cliente.get_limite_actual(db) cliente_dict.timbres_disponibles = cliente.get_timbres_disponibles(db) response.append(cliente_dict) return response @router.get("/{cliente_id}", response_model=ClienteWithStats) def obtener_cliente( cliente_id: int, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Obtener un cliente por ID con estadísticas de subclientes """ cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) # Obtener estadísticas de subclientes total_subclientes = db.query(SubCliente).filter( SubCliente.cliente_id == cliente_id ).count() timbres_consumidos_subclientes = db.query(SubCliente).filter( SubCliente.cliente_id == cliente_id ).with_entities(SubCliente.timbres_consumidos).all() total_consumidos_sub = sum([t[0] for t in timbres_consumidos_subclientes]) # Obtener total de límites asignados from sqlalchemy import func as sql_func total_limites = db.query(sql_func.sum(LimiteTimbre.cantidad)).filter( LimiteTimbre.cliente_id == cliente_id ).scalar() or 0 # Crear respuesta con estadísticas response = ClienteWithStats.model_validate(cliente) response.limite_timbres = cliente.get_limite_actual(db) response.timbres_disponibles = cliente.get_timbres_disponibles(db) response.total_subclientes = total_subclientes response.timbres_consumidos_subclientes = total_consumidos_sub response.total_limites_asignados = total_limites return response @router.put("/{cliente_id}", response_model=ClienteResponse) def actualizar_cliente( cliente_id: int, cliente_update: ClienteUpdate, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Actualizar información de un cliente """ db_cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not db_cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) update_data = cliente_update.model_dump(exclude_unset=True) # Verificar email único si se actualiza if "email" in update_data and update_data["email"] != db_cliente.email: if db.query(Cliente).filter(Cliente.email == update_data["email"]).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="El email ya está registrado" ) # Verificar RFC único si se actualiza if "rfc" in update_data and update_data["rfc"] != db_cliente.rfc: if db.query(Cliente).filter(Cliente.rfc == update_data["rfc"]).first(): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="El RFC ya está registrado" ) # Actualizar campos básicos (excluyendo limite_timbres que ya no existe) update_data_without_limite = {k: v for k, v in update_data.items() if k != 'limite_timbres'} for field, value in update_data_without_limite.items(): setattr(db_cliente, field, value) db.commit() db.refresh(db_cliente) # Preparar respuesta con campos calculados response = ClienteResponse.model_validate(db_cliente) response.limite_timbres = db_cliente.get_limite_actual(db) response.timbres_disponibles = db_cliente.get_timbres_disponibles(db) return response @router.delete("/{cliente_id}", response_model=MessageResponse) def eliminar_cliente( cliente_id: int, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Eliminar un cliente (también elimina sus subclientes y movimientos) """ db_cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not db_cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) db.delete(db_cliente) db.commit() return {"message": f"Cliente {db_cliente.nombre} eliminado exitosamente"} #========================================================================================== @router.get("/{cliente_id}/balance", response_model=BalanceResponse) def obtener_balance( cliente_id: int, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Obtener el balance de timbres de un cliente """ cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) total_subclientes = db.query(SubCliente).filter( SubCliente.cliente_id == cliente_id ).count() timbres_consumidos_subclientes = sum([ s.timbres_consumidos for s in db.query(SubCliente).filter( SubCliente.cliente_id == cliente_id ).all() ]) limite_actual = cliente.get_limite_actual(db) timbres_disponibles = cliente.get_timbres_disponibles(db) porcentaje = (cliente.timbres_consumidos / limite_actual * 100) if limite_actual > 0 else 0 return { "cliente_id": cliente.id, "nombre_cliente": cliente.nombre, "limite_timbres": limite_actual, "timbres_disponibles": timbres_disponibles, "timbres_consumidos": cliente.timbres_consumidos, "porcentaje_uso": round(porcentaje, 2), "total_subclientes": total_subclientes, "timbres_consumidos_subclientes": timbres_consumidos_subclientes } #========================================================================================== @router.post("/{cliente_id}/recargar", response_model=MessageResponse) def recargar_timbres( cliente_id: int, recarga: RecargaTimbreCreate, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Recargar timbres para un cliente (incrementa límite y disponibles) """ cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) # Crear nuevo límite (incremento) nuevo_limite = LimiteTimbre( cliente_id=cliente_id, tipo=TipoLimite.INCREMENTO, cantidad=recarga.cantidad, descripcion=recarga.descripcion or f"Recarga de {recarga.cantidad} timbres" ) db.add(nuevo_limite) db.flush() # Registrar movimiento de recarga limite_actual = cliente.get_limite_actual(db) timbres_disponibles = cliente.get_timbres_disponibles(db) movimiento = MovimientoTimbre( cliente_id=cliente_id, tipo=TipoMovimiento.RECARGA, cantidad=recarga.cantidad, descripcion=recarga.descripcion, balance_cliente=timbres_disponibles ) db.add(movimiento) db.commit() return { "message": f"Se recargaron {recarga.cantidad} timbres exitosamente", "detail": { "nuevo_limite": limite_actual, "timbres_disponibles": timbres_disponibles } } @router.get("/{cliente_id}/limites", response_model=LimiteHistorialResponse) def obtener_historial_limites( cliente_id: int, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Obtener el historial completo de límites de un cliente """ cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) # Obtener todos los límites limites = db.query(LimiteTimbre).filter( LimiteTimbre.cliente_id == cliente_id ).order_by(LimiteTimbre.created_at.desc()).all() # Calcular totales from sqlalchemy import func as sql_func limite_actual = db.query(sql_func.sum(LimiteTimbre.cantidad)).filter( LimiteTimbre.cliente_id == cliente_id, LimiteTimbre.activo == 1 ).scalar() or 0 total_asignado = db.query(sql_func.sum(LimiteTimbre.cantidad)).filter( LimiteTimbre.cliente_id == cliente_id ).scalar() or 0 return { "limite_actual": limite_actual, "total_asignado": total_asignado, "historial": limites } @router.post("/{cliente_id}/ajustar-limite", response_model=MessageResponse) def ajustar_limite( cliente_id: int, cantidad: int, descripcion: str = None, db: Session = Depends(get_db), current_user: Usuario = Depends(get_current_user) ): """ Ajustar el límite de un cliente (puede ser positivo o negativo) """ cliente = db.query(Cliente).filter(Cliente.id == cliente_id).first() if not cliente: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Cliente no encontrado" ) tipo = TipoLimite.INCREMENTO if cantidad > 0 else TipoLimite.DECREMENTO nuevo_limite = LimiteTimbre( cliente_id=cliente_id, tipo=tipo, cantidad=abs(cantidad), descripcion=descripcion or f"Ajuste de límite: {cantidad}" ) db.add(nuevo_limite) db.commit() limite_actual = cliente.get_limite_actual(db) timbres_disponibles = cliente.get_timbres_disponibles(db) return { "message": "Límite ajustado exitosamente", "detail": { "ajuste": cantidad, "nuevo_limite": limite_actual, "timbres_disponibles": timbres_disponibles } }