""" Capa de servicio para lógica de negocio de clientes y proveedores """ from sqlalchemy.orm import Session, joinedload from sqlalchemy.exc import IntegrityError from sqlalchemy import or_, and_ from fastapi import HTTPException from typing import List, Optional import logging from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms from .dto import ( ClientProviderCreateDTO, ClientProviderUpdateDTO, ClientProviderResponseDTO, ClientProviderBasicDTO, ClientProviderListDTO, ClientProviderAddressDTO, ClientProviderProgramsDTO ) logger = logging.getLogger(__name__) class ClientProviderService: """Servicio para gestión de clientes y proveedores""" def __init__(self, db: Session): self.db = db def create_client_provider(self, client_data: ClientProviderCreateDTO) -> ClientProviderResponseDTO: """ Crea un nuevo cliente/proveedor en el sistema Args: client_data: Datos del cliente/proveedor a crear Returns: ClientProviderResponseDTO con información del cliente/proveedor creado Raises: HTTPException: Si el cliente ya existe o error en la creación """ try: # Verificar que no exista el cliente existing = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_data.client_id).first() if existing: raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists") # Crear cliente/proveedor principal db_client = ClientProvider( client_id=client_data.client_id, type_nat_foreign=client_data.type_nat_foreign, name=client_data.name, short_name=client_data.short_name, rfc=client_data.rfc, curp=client_data.curp, client_or_provider=client_data.client_or_provider, linking=client_data.linking, transform_subassembly=client_data.transform_subassembly, extra_information=client_data.extra_information, web_key=client_data.web_key, responsible=client_data.responsible, position=client_data.position, incoterm=client_data.incoterm, is_national_provider=client_data.is_national_provider, enabled_disabled=client_data.enabled_disabled ) self.db.add(db_client) self.db.flush() # Para obtener el ID antes del commit # Crear dirección si se proporciona if client_data.address: db_address = ClientProviderAddress( client_id=client_data.client_id, **client_data.address.model_dump(exclude_unset=True) ) self.db.add(db_address) # Crear programas si se proporciona if client_data.programs: db_programs = ClientProviderPrograms( client_id=client_data.client_id, **client_data.programs.model_dump(exclude_unset=True) ) self.db.add(db_programs) self.db.commit() self.db.refresh(db_client) logger.info(f"Client/Provider created: {db_client.client_id} - {db_client.name}") return self._get_client_with_relations(client_data.client_id) except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating client/provider: {str(e)}") raise HTTPException(status_code=400, detail="Client/Provider with this ID already exists") except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating client/provider: {str(e)}") raise HTTPException(status_code=500, detail="Error creating client/provider") def get_client_provider(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """ Obtiene un cliente/proveedor por ID Args: client_id: ID del cliente/proveedor Returns: ClientProviderResponseDTO o None si no existe """ return self._get_client_with_relations(client_id) def _get_client_with_relations(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """Método privado para obtener cliente con relaciones""" client = self.db.query(ClientProvider).options( joinedload(ClientProvider.address), joinedload(ClientProvider.programs) ).filter(ClientProvider.client_id == client_id).first() if not client: return None return ClientProviderResponseDTO.model_validate(client) def list_clients_providers( self, skip: int = 0, limit: int = 100, search: Optional[str] = None, client_or_provider: Optional[str] = None, enabled_only: bool = False ) -> ClientProviderListDTO: """ Lista clientes/proveedores con filtros Args: skip: Número de registros a omitir limit: Número máximo de registros a retornar search: Texto de búsqueda (nombre, RFC, ID) client_or_provider: Filtrar por tipo (C=Cliente, P=Proveedor) enabled_only: Si True, solo retorna activos Returns: ClientProviderListDTO con la lista paginada """ query = self.db.query(ClientProvider) # Aplicar filtros if search: search_pattern = f"%{search}%" query = query.filter( or_( ClientProvider.name.ilike(search_pattern), ClientProvider.short_name.ilike(search_pattern), ClientProvider.rfc.ilike(search_pattern), ClientProvider.client_id.ilike(search_pattern) ) ) if client_or_provider: query = query.filter(ClientProvider.client_or_provider == client_or_provider) if enabled_only: query = query.filter(ClientProvider.enabled_disabled == 1) # Contar total total = query.count() # Aplicar paginación clients = query.offset(skip).limit(limit).all() # Convertir a DTOs básicos client_dtos = [ClientProviderBasicDTO.model_validate(client) for client in clients] return ClientProviderListDTO( clients=client_dtos, total=total, page=(skip // limit) + 1 if limit > 0 else 1, size=len(client_dtos) ) def update_client_provider(self, client_id: str, client_data: ClientProviderUpdateDTO) -> Optional[ClientProviderResponseDTO]: """ Actualiza un cliente/proveedor Args: client_id: ID del cliente/proveedor a actualizar client_data: Datos a actualizar Returns: ClientProviderResponseDTO actualizado o None si no existe """ client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return None try: # Actualizar campos del cliente principal update_data = client_data.model_dump(exclude_unset=True, exclude={'address', 'programs'}) for field, value in update_data.items(): setattr(client, field, value) # Actualizar dirección if client_data.address: address = self.db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first() if address: # Actualizar dirección existente address_data = client_data.address.model_dump(exclude_unset=True) for field, value in address_data.items(): setattr(address, field, value) else: # Crear nueva dirección address = ClientProviderAddress( client_id=client_id, **client_data.address.model_dump(exclude_unset=True) ) self.db.add(address) # Actualizar programas if client_data.programs: programs = self.db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first() if programs: # Actualizar programas existentes programs_data = client_data.programs.model_dump(exclude_unset=True) for field, value in programs_data.items(): setattr(programs, field, value) else: # Crear nuevos programas programs = ClientProviderPrograms( client_id=client_id, **client_data.programs.model_dump(exclude_unset=True) ) self.db.add(programs) self.db.commit() logger.info(f"Client/Provider updated: {client_id}") return self._get_client_with_relations(client_id) except Exception as e: self.db.rollback() logger.error(f"Error updating client/provider {client_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating client/provider") def delete_client_provider(self, client_id: str) -> bool: """ Elimina un cliente/proveedor Args: client_id: ID del cliente/proveedor a eliminar Returns: True si se eliminó, False si no existe """ client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return False try: self.db.delete(client) # Las relaciones se eliminan en cascada self.db.commit() logger.info(f"Client/Provider deleted: {client_id}") return True except Exception as e: self.db.rollback() logger.error(f"Error deleting client/provider {client_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting client/provider") def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: """Obtiene solo clientes (C)""" query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'C') clients = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: """Obtiene solo proveedores (P)""" query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'P') providers = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(provider) for provider in providers] def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]: """Busca clientes/proveedores por RFC""" clients = self.db.query(ClientProvider).filter(ClientProvider.rfc.ilike(f"%{rfc}%")).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """Cambia el estado habilitado/deshabilitado""" client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() if not client: return None # Toggle status (1 = habilitado, 0 = deshabilitado) client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0 try: self.db.commit() logger.info(f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}") return self._get_client_with_relations(client_id) except Exception as e: self.db.rollback() logger.error(f"Error toggling status for {client_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating status")