Files
plantillas-proyectos/backend/api/v1/modules/a76/client_and_provider/service.py
acazares 52b8fcd434 feat: Implement multi-tenancy support in middleware and security layers
- Enhanced TenantMiddleware to validate tenant information from JWT tokens.
- Added LicenseValidationMiddleware to check tenant licenses before processing requests.
- Updated security utilities to extract tenant information from tokens and validate company access.
- Introduced CompanyStore to manage active company state and handle company switching in the frontend.
- Modified API routes to include company_id in requests for better resource management.
- Improved logging and error handling throughout the middleware and API layers.
- Updated frontend components to reflect changes in company management and selection.
- Added new API route for fetching user's companies with proper authentication handling.
2025-11-11 14:15:31 -06:00

380 lines
13 KiB
Python

"""
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")