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:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Módulo de Company
|
||||
"""
|
||||
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["router"]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user