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.
This commit is contained in:
2025-11-11 14:00:56 -06:00
parent e1eb6bbd01
commit 52b8fcd434
242 changed files with 7067 additions and 3274 deletions

View File

@@ -1,6 +1,7 @@
"""
Módulo de Company
"""
from .routes import router
__all__ = ["router"]

View File

@@ -2,6 +2,7 @@
DTOs (Data Transfer Objects) para módulo de empresa
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
"""
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
@@ -9,47 +10,80 @@ from datetime import datetime
class CompanyCreateDTO(BaseModel):
"""DTO para crear una empresa"""
id: str = Field(default='EMP', max_length=3, description="Company ID")
id: str = Field(default="EMP", max_length=3, description="Company ID")
consecutive: bool = Field(default=True, description="Unique record control")
name: Optional[str] = Field(None, max_length=255, description="Company name")
rfc: Optional[str] = Field(None, max_length=30, description="Company RFC")
main_activity: Optional[str] = Field(None, max_length=255, description="Main activity")
main_activity: Optional[str] = Field(
None, max_length=255, description="Main activity"
)
# Program information
program: Optional[str] = Field(None, max_length=10, description="Program")
program_number: Optional[str] = Field(None, max_length=40, description="Program number")
program_number: Optional[str] = Field(
None, max_length=40, description="Program number"
)
prosec: Optional[int] = Field(None, description="PROSEC")
prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization")
prosec_authorization: Optional[str] = Field(
None, max_length=20, description="PROSEC authorization"
)
# Identifiers
manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID")
broker_company: Optional[str] = Field(None, max_length=10, description="Broker company")
manufacturer_id: Optional[str] = Field(
None, max_length=25, description="Manufacturer ID"
)
broker_company: Optional[str] = Field(
None, max_length=10, description="Broker company"
)
# Responsible person
responsible: Optional[str] = Field(None, max_length=80, description="Responsible person")
responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name")
responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name")
responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name")
responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC")
position: Optional[str] = Field(None, max_length=30, description="Responsible position")
responsible: Optional[str] = Field(
None, max_length=80, description="Responsible person"
)
responsible_name: Optional[str] = Field(
None, max_length=20, description="Responsible first name"
)
responsible_last_name: Optional[str] = Field(
None, max_length=20, description="Responsible last name"
)
responsible_mother_last_name: Optional[str] = Field(
None, max_length=20, description="Responsible mother's last name"
)
responsible_rfc: Optional[str] = Field(
None, max_length=30, description="Responsible RFC"
)
position: Optional[str] = Field(
None, max_length=30, description="Responsible position"
)
# Configuration
logo: Optional[str] = Field(None, max_length=255, description="Company logo")
has_express_line: Optional[bool] = Field(None, description="Has express line")
order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type")
order_format_type: Optional[str] = Field(
None, max_length=19, description="Order format type"
)
previous_code: Optional[int] = Field(None, description="Previous code")
is_service_company: Optional[bool] = Field(None, description="Is service company")
# Client and subassembly
client_name: Optional[str] = Field(None, max_length=300, description="Client name")
subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode")
subassembly_mode: Optional[str] = Field(
None, max_length=7, description="Subassembly mode"
)
# Additional information
curp: Optional[str] = Field(None, max_length=19, description="CURP")
inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name")
inter_db_name: Optional[str] = Field(
None, max_length=100, description="Inter DB name"
)
ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI")
trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number")
prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key")
trusted_exporter_number: Optional[str] = Field(
None, max_length=50, description="Trusted exporter number"
)
prevalidator_key: Optional[str] = Field(
None, max_length=20, description="Prevalidator key"
)
seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment")
class Config:
@@ -58,45 +92,78 @@ class CompanyCreateDTO(BaseModel):
class CompanyUpdateDTO(BaseModel):
"""DTO para actualizar una empresa"""
name: Optional[str] = Field(None, max_length=255, description="Company name")
rfc: Optional[str] = Field(None, max_length=30, description="Company RFC")
main_activity: Optional[str] = Field(None, max_length=255, description="Main activity")
main_activity: Optional[str] = Field(
None, max_length=255, description="Main activity"
)
# Program information
program: Optional[str] = Field(None, max_length=10, description="Program")
program_number: Optional[str] = Field(None, max_length=40, description="Program number")
program_number: Optional[str] = Field(
None, max_length=40, description="Program number"
)
prosec: Optional[int] = Field(None, description="PROSEC")
prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization")
prosec_authorization: Optional[str] = Field(
None, max_length=20, description="PROSEC authorization"
)
# Identifiers
manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID")
broker_company: Optional[str] = Field(None, max_length=10, description="Broker company")
manufacturer_id: Optional[str] = Field(
None, max_length=25, description="Manufacturer ID"
)
broker_company: Optional[str] = Field(
None, max_length=10, description="Broker company"
)
# Responsible person
responsible: Optional[str] = Field(None, max_length=80, description="Responsible person")
responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name")
responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name")
responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name")
responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC")
position: Optional[str] = Field(None, max_length=30, description="Responsible position")
responsible: Optional[str] = Field(
None, max_length=80, description="Responsible person"
)
responsible_name: Optional[str] = Field(
None, max_length=20, description="Responsible first name"
)
responsible_last_name: Optional[str] = Field(
None, max_length=20, description="Responsible last name"
)
responsible_mother_last_name: Optional[str] = Field(
None, max_length=20, description="Responsible mother's last name"
)
responsible_rfc: Optional[str] = Field(
None, max_length=30, description="Responsible RFC"
)
position: Optional[str] = Field(
None, max_length=30, description="Responsible position"
)
# Configuration
logo: Optional[str] = Field(None, max_length=255, description="Company logo")
has_express_line: Optional[bool] = Field(None, description="Has express line")
order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type")
order_format_type: Optional[str] = Field(
None, max_length=19, description="Order format type"
)
previous_code: Optional[int] = Field(None, description="Previous code")
is_service_company: Optional[bool] = Field(None, description="Is service company")
# Client and subassembly
client_name: Optional[str] = Field(None, max_length=300, description="Client name")
subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode")
subassembly_mode: Optional[str] = Field(
None, max_length=7, description="Subassembly mode"
)
# Additional information
curp: Optional[str] = Field(None, max_length=19, description="CURP")
inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name")
inter_db_name: Optional[str] = Field(
None, max_length=100, description="Inter DB name"
)
ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI")
trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number")
prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key")
trusted_exporter_number: Optional[str] = Field(
None, max_length=50, description="Trusted exporter number"
)
prevalidator_key: Optional[str] = Field(
None, max_length=20, description="Prevalidator key"
)
seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment")
class Config:
@@ -105,22 +172,23 @@ class CompanyUpdateDTO(BaseModel):
class CompanyResponseDTO(BaseModel):
"""DTO para respuesta de empresa"""
id: str
consecutive: bool
id: int
tenant_id: int
name: Optional[str] = None
rfc: Optional[str] = None
main_activity: Optional[str] = None
# Program information
program: Optional[str] = None
program_number: Optional[str] = None
prosec: Optional[int] = None
prosec_authorization: Optional[str] = None
# Identifiers
manufacturer_id: Optional[str] = None
broker_company: Optional[str] = None
# Responsible person
responsible: Optional[str] = None
responsible_name: Optional[str] = None
@@ -128,18 +196,18 @@ class CompanyResponseDTO(BaseModel):
responsible_mother_last_name: Optional[str] = None
responsible_rfc: Optional[str] = None
position: Optional[str] = None
# Configuration
logo: Optional[str] = None
has_express_line: Optional[bool] = None
order_format_type: Optional[str] = None
previous_code: Optional[int] = None
is_service_company: Optional[bool] = None
# Client and subassembly
client_name: Optional[str] = None
subassembly_mode: Optional[str] = None
# Additional information
curp: Optional[str] = None
inter_db_name: Optional[str] = None
@@ -147,11 +215,10 @@ class CompanyResponseDTO(BaseModel):
trusted_exporter_number: Optional[str] = None
prevalidator_key: Optional[str] = None
seventh_amendment: Optional[bool] = None
# Timestamps
created_at: datetime
updated_at: Optional[datetime] = None
class Config:
from_attributes = True

View File

@@ -1,9 +1,20 @@
"""
Modelos ORM para gestión de empresa
"""
from typing import Optional
from datetime import datetime
from sqlalchemy import DateTime, Integer, String, Boolean, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint
from sqlalchemy import (
DateTime,
Integer,
String,
Boolean,
SmallInteger,
ForeignKey,
PrimaryKeyConstraint,
ForeignKeyConstraint,
UniqueConstraint,
)
from sqlalchemy.sql import func
from sqlalchemy.orm import Mapped, mapped_column
from core.database import Base
@@ -13,32 +24,35 @@ class Company(Base):
"""
Modelo para la tabla Company - Información de la empresa
"""
__tablename__ = "company"
__table_args__ = (
PrimaryKeyConstraint('id', name='company_pkey'),
ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_company_tenant'),
{"schema": "a76"}
PrimaryKeyConstraint("id", name="company_pkey"),
ForeignKeyConstraint(
["tenant_id"], ["a76.tenants.id"], name="fk_company_tenant"
),
{"schema": "a76"},
)
# Primary key
id: Mapped[int] = mapped_column(Integer, primary_key=True)
tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
# Información básica de la empresa
name: Mapped[Optional[str]] = mapped_column(String(255))
rfc: Mapped[Optional[str]] = mapped_column(String(30))
main_activity: Mapped[Optional[str]] = mapped_column(String(255))
# Información del programa
program: Mapped[Optional[str]] = mapped_column(String(10))
program_number: Mapped[Optional[str]] = mapped_column(String(40))
prosec: Mapped[Optional[int]] = mapped_column(SmallInteger)
prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20))
# Identificadores
manufacturer_id: Mapped[Optional[str]] = mapped_column(String(25))
broker_company: Mapped[Optional[str]] = mapped_column(String(10))
# Responsable
responsible: Mapped[Optional[str]] = mapped_column(String(80))
responsible_name: Mapped[Optional[str]] = mapped_column(String(20))
@@ -46,29 +60,33 @@ class Company(Base):
responsible_mother_last_name: Mapped[Optional[str]] = mapped_column(String(20))
responsible_rfc: Mapped[Optional[str]] = mapped_column(String(30))
position: Mapped[Optional[str]] = mapped_column(String(30))
# Configuración
logo: Mapped[Optional[str]] = mapped_column(String(255))
has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean)
order_format_type: Mapped[Optional[str]] = mapped_column(String(19))
previous_code: Mapped[Optional[int]] = mapped_column(SmallInteger)
is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean)
# Cliente y submaquila
client_name: Mapped[Optional[str]] = mapped_column(String(300))
subassembly_mode: Mapped[Optional[str]] = mapped_column(String(7))
# Información adicional
curp: Mapped[Optional[str]] = mapped_column(String(19))
inter_db_name: Mapped[Optional[str]] = mapped_column(String(100))
ctpat_svi: Mapped[Optional[str]] = mapped_column(String(100))
trusted_exporter_number: Mapped[Optional[str]] = mapped_column(String(50))
prevalidator_key: Mapped[Optional[str]] = mapped_column(String(20))
seventh_amendment: Mapped[Optional[bool]] = mapped_column(Boolean) # FINALCONTADORAELECTRONICO renombrado
seventh_amendment: Mapped[Optional[bool]] = mapped_column(
Boolean
) # FINALCONTADORAELECTRONICO renombrado
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now())
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=func.now()
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, nullable=False, default=func.now(), onupdate=func.now()
)
deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)

View File

@@ -1,27 +1,30 @@
"""
Endpoints API para gestión de empresa
"""
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import Optional
from core.database import get_core_db
from core.security import get_current_user, has_role
from core.security import get_current_user, has_role, get_tenant_from_token
from .service import CompanyService
from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO
router = APIRouter(prefix="/company")
@router.post("/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED)
@router.post(
"/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED
)
async def create_company(
company_data: CompanyCreateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
current_user: dict = Depends(get_current_user),
):
"""
Create a new company in the system
Only one company can exist per system due to the unique consecutive field.
"""
service = CompanyService(db)
@@ -30,12 +33,11 @@ async def create_company(
@router.get("/", response_model=Optional[CompanyResponseDTO])
async def get_company(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get the registered company information
Returns the unique company in the system or None if it doesn't exist.
"""
service = CompanyService(db)
@@ -45,73 +47,48 @@ async def get_company(
return company
@router.get("/{company_id}", response_model=CompanyResponseDTO)
async def get_company_by_id(
company_id: str,
db: Session = Depends(get_core_db),
@router.get("/my-companies", response_model=list[CompanyResponseDTO])
async def get_my_companies(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
"""
Get company by specific ID
"""
service = CompanyService(db)
company = service.get_company_by_id(company_id)
if not company:
raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found")
return company
@router.put("/{company_id}", response_model=CompanyResponseDTO)
async def update_company(
company_id: str,
company_data: CompanyUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
"""
Update company information
"""
service = CompanyService(db)
company = service.update_company(company_id, company_data)
if not company:
raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found")
return company
@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_company(
company_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
):
"""
Delete company from the system
Get all companies that belong to the user's tenant
Note: This will completely remove the company from the system.
Returns a list of companies associated with the tenant_id from the user's token
"""
tenant_id = get_tenant_from_token(current_user)
if not tenant_id:
raise HTTPException(
status_code=400,
detail="Tenant ID not found in token"
)
service = CompanyService(db)
if not service.delete_company(company_id):
raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found")
companies = service.get_companies_by_tenant(tenant_id)
return companies
@router.get("/status/exists", response_model=dict)
async def check_company_exists(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Check if a company is registered in the system
"""
service = CompanyService(db)
exists = service.exists_company()
return {"exists": exists, "message": "Company found" if exists else "No company registered"}
return {
"exists": exists,
"message": "Company found" if exists else "No company registered",
}
# Specific endpoints for important fields
@router.get("/info/basic", response_model=dict)
async def get_company_basic_info(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get basic company information (name, RFC, main activity)
@@ -120,19 +97,18 @@ async def get_company_basic_info(
company = service.get_company()
if not company:
raise HTTPException(status_code=404, detail="No company found")
return {
"name": company.name,
"rfc": company.rfc,
"main_activity": company.main_activity,
"logo": company.logo
"logo": company.logo,
}
@router.get("/info/responsible", response_model=dict)
async def get_company_responsible_info(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get company responsible person information
@@ -141,21 +117,20 @@ async def get_company_responsible_info(
company = service.get_company()
if not company:
raise HTTPException(status_code=404, detail="No company found")
return {
"responsible": company.responsible,
"responsible_name": company.responsible_name,
"responsible_last_name": company.responsible_last_name,
"responsible_mother_last_name": company.responsible_mother_last_name,
"responsible_rfc": company.responsible_rfc,
"position": company.position
"position": company.position,
}
@router.get("/info/program", response_model=dict)
async def get_company_program_info(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
):
"""
Get company program information
@@ -164,13 +139,67 @@ async def get_company_program_info(
company = service.get_company()
if not company:
raise HTTPException(status_code=404, detail="No company found")
return {
"program": company.program,
"program_number": company.program_number,
"prosec": company.prosec,
"prosec_authorization": company.prosec_authorization,
"manufacturer_id": company.manufacturer_id
"manufacturer_id": company.manufacturer_id,
}
@router.get("/{company_id}", response_model=CompanyResponseDTO)
async def get_company_by_id(
company_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get company by specific ID
"""
service = CompanyService(db)
company = service.get_company_by_id(company_id)
if not company:
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)
return company
@router.put("/{company_id}", response_model=CompanyResponseDTO)
async def update_company(
company_id: str,
company_data: CompanyUpdateDTO,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Update company information
"""
service = CompanyService(db)
company = service.update_company(company_id, company_data)
if not company:
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)
return company
@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_company(
company_id: str,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Delete company from the system
Note: This will completely remove the company from the system.
"""
service = CompanyService(db)
if not service.delete_company(company_id):
raise HTTPException(
status_code=404, detail=f"Company with ID '{company_id}' not found"
)

View File

@@ -1,6 +1,7 @@
"""
Capa de servicio para lógica de negocio de empresa
"""
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from fastapi import HTTPException
@@ -15,29 +16,34 @@ logger = logging.getLogger(__name__)
class CompanyService:
"""Servicio para gestión de empresa"""
def __init__(self, db: Session):
self.db = db
def create_company(self, company_data: CompanyCreateDTO) -> CompanyResponseDTO:
"""
Crea una nueva empresa en el sistema
Args:
company_data: Datos de la empresa a crear
Returns:
CompanyResponseDTO con información de la empresa creada
Raises:
HTTPException: Si ya existe una empresa o error en la creación
"""
try:
# Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único)
existing = self.db.query(Company).filter(Company.consecutive == True).first()
existing = (
self.db.query(Company).filter(Company.consecutive == True).first()
)
if existing:
raise HTTPException(status_code=400, detail="A company is already registered in the system")
raise HTTPException(
status_code=400,
detail="A company is already registered in the system",
)
# Crear empresa
db_company = Company(
id=company_data.id,
@@ -69,32 +75,35 @@ class CompanyService:
ctpat_svi=company_data.ctpat_svi,
trusted_exporter_number=company_data.trusted_exporter_number,
prevalidator_key=company_data.prevalidator_key,
seventh_amendment=company_data.seventh_amendment
seventh_amendment=company_data.seventh_amendment,
)
self.db.add(db_company)
self.db.commit()
self.db.refresh(db_company)
logger.info(f"Company created: {db_company.id} - {db_company.name}")
return CompanyResponseDTO.model_validate(db_company)
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company: {str(e)}")
raise HTTPException(status_code=400, detail="Integrity error: A company already exists in the system")
raise HTTPException(
status_code=400,
detail="Integrity error: A company already exists in the system",
)
except HTTPException:
raise
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating company")
def get_company(self) -> Optional[CompanyResponseDTO]:
"""
Obtiene la empresa (solo puede haber una)
Returns:
CompanyResponseDTO o None si no existe
"""
@@ -102,14 +111,14 @@ class CompanyService:
if not company:
return None
return CompanyResponseDTO.model_validate(company)
def get_company_by_id(self, company_id: str) -> Optional[CompanyResponseDTO]:
"""
Obtiene una empresa por ID
Args:
company_id: ID de la empresa
Returns:
CompanyResponseDTO o None si no existe
"""
@@ -117,27 +126,29 @@ class CompanyService:
if not company:
return None
return CompanyResponseDTO.model_validate(company)
def update_company(self, company_id: str, company_data: CompanyUpdateDTO) -> Optional[CompanyResponseDTO]:
def update_company(
self, company_id: str, company_data: CompanyUpdateDTO
) -> Optional[CompanyResponseDTO]:
"""
Actualiza una empresa
Args:
company_id: ID de la empresa a actualizar
company_data: Datos a actualizar
Returns:
CompanyResponseDTO actualizada o None si no existe
"""
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return None
# Actualizar solo campos proporcionados
update_data = company_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(company, field, value)
try:
self.db.commit()
self.db.refresh(company)
@@ -147,21 +158,21 @@ class CompanyService:
self.db.rollback()
logger.error(f"Error updating company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating company")
def delete_company(self, company_id: str) -> bool:
"""
Elimina una empresa
Args:
company_id: ID de la empresa a eliminar
Returns:
True si se eliminó, False si no existe
"""
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return False
try:
self.db.delete(company)
self.db.commit()
@@ -171,14 +182,34 @@ class CompanyService:
self.db.rollback()
logger.error(f"Error deleting company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error deleting company")
def exists_company(self) -> bool:
"""
Verifica si existe una empresa registrada
Returns:
True si existe una empresa, False en caso contrario
"""
return self.db.query(Company).filter(Company.consecutive == True).first() is not None
return (
self.db.query(Company).filter(Company.consecutive == True).first()
is not None
)
def get_companies_by_tenant(self, tenant_id: int) -> List[CompanyResponseDTO]:
"""
Obtiene todas las compañías que pertenecen a un tenant específico
Args:
tenant_id: ID del tenant
Returns:
Lista de CompanyResponseDTO
"""
companies = (
self.db.query(Company)
.filter(Company.tenant_id == tenant_id)
.order_by(Company.name)
.all()
)
return [CompanyResponseDTO.model_validate(company) for company in companies]