From b68c4316ffaad67c8026698b634a2ba94adaee1f Mon Sep 17 00:00:00 2001 From: acazares Date: Tue, 11 Nov 2025 17:20:47 -0600 Subject: [PATCH] Refactor backend and frontend code for improved structure and functionality - Rearranged imports in multiple files for consistency and clarity. - Updated logging middleware to exclude specific paths from logging. - Enhanced security module by cleaning up token handling and improving tenant validation. - Added tenant and company scoped mixins for better database model management. - Implemented generic CRUD routes for tenant-scoped resources. - Improved error handling and response management in API routes. - Cleaned up login and logout processes to ensure proper session management. - Introduced mechanisms to clear local storage and cookies on tenant change. - Enhanced company store to detect tenant changes and clear data accordingly. - Added new DTO mixins for currency and value affect flags. --- backend/alembic/env.py | 14 +- ...31bf8cdae06_create_material_types_table.py | 6 +- .../7937209f9718_seed_initial_data.py | 16 +- backend/api/v1/common/base_models.py | 30 ++ backend/api/v1/common/crud_routes.py | 121 ++++++ backend/api/v1/common/dto_mixins.py | 31 ++ backend/api/v1/common/tenant_crud_routes.py | 350 ++++++++++++++++++ .../api/v1/modules/a24/q/q_classes/models.py | 10 +- .../api/v1/modules/a24/s/s_classes/models.py | 16 +- backend/api/v1/modules/a76/aduanal/dto.py | 6 +- backend/api/v1/modules/a76/aduanal/models.py | 47 ++- backend/api/v1/modules/a76/aduanal/routes.py | 46 ++- .../api/v1/modules/a76/aduanal/services.py | 45 ++- backend/api/v1/modules/a76/auth/dto.py | 3 +- backend/api/v1/modules/a76/auth/routes.py | 16 +- backend/api/v1/modules/a76/auth/service.py | 131 +++---- backend/api/v1/modules/a76/classes/dto.py | 4 +- backend/api/v1/modules/a76/classes/models.py | 16 +- backend/api/v1/modules/a76/classes/routes.py | 23 +- backend/api/v1/modules/a76/classes/service.py | 23 +- .../v1/modules/a76/classes/test_classes.py | 11 +- .../v1/modules/a76/client_and_provider/dto.py | 6 +- .../modules/a76/client_and_provider/models.py | 28 +- .../modules/a76/client_and_provider/routes.py | 67 +++- .../a76/client_and_provider/service.py | 27 +- .../test_client_and_provider.py | 19 +- backend/api/v1/modules/a76/company/dto.py | 5 +- backend/api/v1/modules/a76/company/models.py | 36 +- backend/api/v1/modules/a76/company/routes.py | 24 +- backend/api/v1/modules/a76/company/service.py | 28 +- .../v1/modules/a76/company/test_company.py | 11 +- .../v1/modules/a76/country_rule_oct/models.py | 14 +- .../v1/modules/a76/country_rule_oct/routes.py | 17 +- .../modules/a76/country_rule_oct/services.py | 3 +- .../country_rule_oct/test_country_rule_oct.py | 11 +- backend/api/v1/modules/a76/drivers/dto.py | 8 +- backend/api/v1/modules/a76/drivers/models.py | 21 +- backend/api/v1/modules/a76/drivers/routes.py | 20 +- .../api/v1/modules/a76/drivers/services.py | 18 +- .../api/v1/modules/a76/exchange_rate/dto.py | 3 +- .../v1/modules/a76/exchange_rate/models.py | 17 +- .../v1/modules/a76/exchange_rate/routes.py | 21 +- .../v1/modules/a76/exchange_rate/services.py | 3 +- .../a76/exchange_rate/test_exchange_rate.py | 11 +- .../modules/a76/fraction_rule_octave/dto.py | 3 +- .../a76/fraction_rule_octave/models.py | 14 +- .../a76/fraction_rule_octave/routes.py | 25 +- .../a76/fraction_rule_octave/services.py | 3 +- .../test_fraction_rule_octave.py | 11 +- backend/api/v1/modules/a76/licenses/dto.py | 5 +- backend/api/v1/modules/a76/licenses/models.py | 42 +-- backend/api/v1/modules/a76/licenses/routes.py | 8 +- .../api/v1/modules/a76/licenses/service.py | 18 +- backend/api/v1/modules/a76/package/dto.py | 3 +- backend/api/v1/modules/a76/package/models.py | 29 +- backend/api/v1/modules/a76/package/routes.py | 27 +- .../api/v1/modules/a76/package/services.py | 3 +- .../v1/modules/a76/package/test_package.py | 11 +- backend/api/v1/modules/a76/parts/dto.py | 5 +- backend/api/v1/modules/a76/parts/models.py | 24 +- backend/api/v1/modules/a76/parts/routes.py | 67 +++- backend/api/v1/modules/a76/parts/service.py | 14 +- .../api/v1/modules/a76/parts/test_parts.py | 11 +- .../dtos/pedimento_config_additional.py | 5 +- .../dtos/pedimento_config_calculations.py | 5 +- .../dtos/pedimento_config_parameters.py | 7 +- .../dtos/pedimento_config_surcharges.py | 5 +- .../pedimento_config_update_rectification.py | 23 +- .../dtos/pedimento_config_updates.py | 22 +- .../dtos/pedimento_customs_offices.py | 5 +- .../a76/pedmientos/dtos/pedimento_dates.py | 5 +- .../dtos/pedimento_decrementables.py | 7 +- .../dtos/pedimento_incrementables.py | 7 +- .../a76/pedmientos/dtos/pedimento_indexes.py | 7 +- .../a76/pedmientos/dtos/pedimento_payments.py | 7 +- .../pedimento_rectification_destination.py | 3 +- .../dtos/pedimento_rectification_origin.py | 5 +- .../dtos/pedimento_transport_means.py | 5 +- .../pedmientos/dtos/pedimento_validation.py | 5 +- .../modules/a76/pedmientos/dtos/pedimentos.py | 9 +- .../models/pedimento_config_additional.py | 21 +- .../models/pedimento_config_calculations.py | 21 +- .../models/pedimento_config_parameters.py | 22 +- .../models/pedimento_config_surcharges.py | 22 +- .../pedimento_config_update_rectification.py | 22 +- .../models/pedimento_config_updates.py | 22 +- .../models/pedimento_customs_offices.py | 22 +- .../a76/pedmientos/models/pedimento_dates.py | 23 +- .../models/pedimento_decrementables.py | 22 +- .../models/pedimento_incrementables.py | 22 +- .../pedmientos/models/pedimento_indexes.py | 22 +- .../pedmientos/models/pedimento_payments.py | 24 +- .../pedimento_rectification_destination.py | 21 +- .../models/pedimento_rectification_origin.py | 19 +- .../models/pedimento_transport_means.py | 15 +- .../pedmientos/models/pedimento_validation.py | 22 +- .../a76/pedmientos/models/pedimentos.py | 23 +- .../routes/pedimento_config_additional.py | 102 +---- .../routes/pedimento_config_calculations.py | 100 +---- .../routes/pedimento_config_parameters.py | 102 +---- .../routes/pedimento_config_surcharges.py | 102 +---- .../pedimento_config_update_rectification.py | 118 +----- .../routes/pedimento_config_updates.py | 102 +---- .../routes/pedimento_customs_offices.py | 13 +- .../a76/pedmientos/routes/pedimento_dates.py | 95 +---- .../routes/pedimento_decrementables.py | 12 +- .../routes/pedimento_incrementables.py | 12 +- .../pedmientos/routes/pedimento_indexes.py | 100 +---- .../pedmientos/routes/pedimento_payments.py | 12 +- .../pedimento_rectification_destination.py | 118 +----- .../routes/pedimento_rectification_origin.py | 110 +----- .../routes/pedimento_transport_means.py | 12 +- .../pedmientos/routes/pedimento_validation.py | 100 +---- .../a76/pedmientos/routes/pedimentos.py | 126 +------ .../services/pedimento_config_additional.py | 3 +- .../services/pedimento_config_calculations.py | 3 +- .../services/pedimento_config_parameters.py | 3 +- .../services/pedimento_config_surcharges.py | 3 +- .../pedimento_config_update_rectification.py | 7 +- .../services/pedimento_config_updates.py | 3 +- .../services/pedimento_customs_offices.py | 3 +- .../pedmientos/services/pedimento_dates.py | 3 +- .../services/pedimento_decrementables.py | 3 +- .../services/pedimento_incrementables.py | 3 +- .../pedmientos/services/pedimento_indexes.py | 3 +- .../pedmientos/services/pedimento_payments.py | 3 +- .../pedimento_rectification_destination.py | 7 +- .../pedimento_rectification_origin.py | 3 +- .../services/pedimento_transport_means.py | 3 +- .../services/pedimento_validation.py | 3 +- .../a76/pedmientos/services/pedimentos.py | 10 +- .../v1/modules/a76/permission_rule_oct/dto.py | 3 +- .../modules/a76/permission_rule_oct/models.py | 15 +- .../modules/a76/permission_rule_oct/routes.py | 21 +- .../a76/permission_rule_oct/services.py | 3 +- .../test_permission_rule_oct.py | 11 +- backend/api/v1/modules/a76/router.py | 32 +- backend/api/v1/modules/a76/seal/models.py | 14 +- backend/api/v1/modules/a76/seal/routes.py | 21 +- backend/api/v1/modules/a76/seal/services.py | 3 +- backend/api/v1/modules/a76/seal/test_seal.py | 11 +- backend/api/v1/modules/a76/tenants/dto.py | 5 +- backend/api/v1/modules/a76/tenants/models.py | 25 +- backend/api/v1/modules/a76/tenants/routes.py | 13 +- backend/api/v1/modules/a76/tenants/service.py | 13 +- backend/api/v1/modules/a76/trailers/dto.py | 8 +- backend/api/v1/modules/a76/trailers/models.py | 18 +- backend/api/v1/modules/a76/trailers/routes.py | 14 +- .../api/v1/modules/a76/trailers/services.py | 12 +- .../api/v1/modules/a76/transporters/dto.py | 8 +- .../api/v1/modules/a76/transporters/models.py | 18 +- .../api/v1/modules/a76/transporters/routes.py | 24 +- .../v1/modules/a76/transporters/services.py | 12 +- backend/api/v1/modules/a76/user_tenant/dto.py | 5 +- .../api/v1/modules/a76/user_tenant/models.py | 32 +- .../api/v1/modules/a76/user_tenant/routes.py | 9 +- .../api/v1/modules/a76/user_tenant/service.py | 32 +- backend/api/v1/modules/a76/vehicles/dto.py | 8 +- backend/api/v1/modules/a76/vehicles/models.py | 18 +- backend/api/v1/modules/a76/vehicles/routes.py | 24 +- .../api/v1/modules/a76/vehicles/services.py | 12 +- .../code_pedimento_regimens/dto.py | 4 +- .../code_pedimento_regimens/models.py | 11 +- .../code_pedimento_regimens/routes.py | 13 +- .../test_code_pedimento_regimens.py | 2 +- .../modules/public/reference_data/conftest.py | 2 +- .../public/reference_data/containers/dto.py | 3 +- .../reference_data/containers/models.py | 4 +- .../reference_data/containers/routes.py | 13 +- .../containers/test_containers.py | 2 +- .../public/reference_data/countries/dto.py | 3 +- .../public/reference_data/countries/models.py | 4 +- .../public/reference_data/countries/routes.py | 13 +- .../countries/test_countries.py | 2 +- .../reference_data/currency_types/dto.py | 3 +- .../reference_data/currency_types/models.py | 4 +- .../reference_data/currency_types/routes.py | 13 +- .../currency_types/test_currency_types.py | 2 +- .../reference_data/customs_sections/dto.py | 3 +- .../reference_data/customs_sections/models.py | 4 +- .../reference_data/customs_sections/routes.py | 13 +- .../customs_sections/test_customs_sections.py | 2 +- .../reference_data/customs_warehouses/dto.py | 3 +- .../customs_warehouses/models.py | 4 +- .../customs_warehouses/routes.py | 13 +- .../test_customs_warehouses.py | 2 +- .../public/reference_data/incoterms/dto.py | 3 +- .../public/reference_data/incoterms/models.py | 4 +- .../public/reference_data/incoterms/routes.py | 13 +- .../incoterms/test_incoterms.py | 2 +- .../reference_data/invoice_types/dto.py | 4 +- .../reference_data/invoice_types/models.py | 4 +- .../reference_data/invoice_types/routes.py | 13 +- .../invoice_types/test_invoice_types.py | 2 +- .../reference_data/material_types/dto.py | 3 +- .../reference_data/material_types/models.py | 4 +- .../reference_data/material_types/routes.py | 13 +- .../material_types/test_material_types.py | 2 +- .../reference_data/payment_methods/dto.py | 3 +- .../reference_data/payment_methods/models.py | 4 +- .../reference_data/payment_methods/routes.py | 13 +- .../payment_methods/test_payment_methods.py | 2 +- .../reference_data/pedimento_codes/dto.py | 4 +- .../reference_data/pedimento_codes/models.py | 5 +- .../reference_data/pedimento_codes/routes.py | 13 +- .../pedimento_codes/test_pedimento_codes.py | 2 +- .../reference_data/pedimento_regimens/dto.py | 4 +- .../pedimento_regimens/models.py | 5 +- .../pedimento_regimens/routes.py | 13 +- .../test_pedimento_regimens.py | 2 +- .../modules/public/reference_data/router.py | 24 +- .../public/reference_data/sectors/dto.py | 3 +- .../public/reference_data/sectors/models.py | 4 +- .../public/reference_data/sectors/routes.py | 13 +- .../reference_data/sectors/test_sectors.py | 2 +- .../public/reference_data/states/dto.py | 4 +- .../public/reference_data/states/models.py | 5 +- .../public/reference_data/states/routes.py | 13 +- .../reference_data/states/test_states.py | 2 +- .../reference_data/trailer_types/dto.py | 8 +- .../reference_data/trailer_types/models.py | 14 +- .../reference_data/trailer_types/routes.py | 26 +- .../reference_data/trailer_types/services.py | 12 +- .../reference_data/transport_modes/dto.py | 3 +- .../reference_data/transport_modes/models.py | 4 +- .../reference_data/transport_modes/routes.py | 13 +- .../transport_modes/test_transport_modes.py | 2 +- .../reference_data/transport_types/dto.py | 3 +- .../reference_data/transport_types/models.py | 4 +- .../reference_data/transport_types/routes.py | 13 +- .../transport_types/test_transport_types.py | 2 +- .../reference_data/valuation_methods/dto.py | 3 +- .../valuation_methods/models.py | 4 +- .../valuation_methods/routes.py | 13 +- .../test_valuation_methods.py | 2 +- backend/core/__init__.py | 10 +- backend/core/config.py | 3 +- backend/core/database.py | 13 +- backend/core/middleware.py | 27 +- backend/core/security.py | 64 ++-- backend/main.py | 16 +- frontend/src/lib/api.ts | 19 +- frontend/src/lib/components/login-form.svelte | 35 +- frontend/src/lib/stores/company.svelte.ts | 23 +- .../api/company/my-companies/+server.ts | 8 +- frontend/src/routes/login/+page.server.ts | 7 + frontend/src/routes/logout/+server.ts | 3 + 247 files changed, 2248 insertions(+), 2504 deletions(-) create mode 100644 backend/api/v1/common/base_models.py create mode 100644 backend/api/v1/common/crud_routes.py create mode 100644 backend/api/v1/common/dto_mixins.py create mode 100644 backend/api/v1/common/tenant_crud_routes.py diff --git a/backend/alembic/env.py b/backend/alembic/env.py index cf2b4a50..097cb079 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -1,11 +1,14 @@ +import importlib.util +import logging import os +import sys from logging.config import fileConfig -from sqlalchemy import engine_from_config -from sqlalchemy import pool from urllib.parse import quote_plus + from alembic import context from core.config import settings -import logging +from core.database import Base +from sqlalchemy import engine_from_config, pool logger = logging.getLogger(__name__) @@ -65,15 +68,10 @@ config.set_main_option("sqlalchemy.url", database_url) if config.config_file_name is not None: fileConfig(config.config_file_name) -import sys -import importlib.util - # Ajusta la ruta para que puedas importar core y módulos BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) sys.path.insert(0, BASE_DIR) -from core.database import Base - # Configuración de Alembic config = context.config fileConfig(config.config_file_name) diff --git a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py index dbc13ef0..2179bc81 100644 --- a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py +++ b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py @@ -6,11 +6,13 @@ Create Date: 2025-10-19 18:23:39.613953 """ +# pylint: disable=no-member + + from typing import Sequence, Union -from alembic import op import sqlalchemy as sa - +from alembic import op # revision identifiers, used by Alembic. revision: str = "531bf8cdae06" diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py index 7b0644c0..0ce75ac3 100644 --- a/backend/alembic/versions/7937209f9718_seed_initial_data.py +++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py @@ -6,17 +6,11 @@ Create Date: 2025-10-19 18:23:55.258800 """ +# pylint: disable=no-member + from typing import Sequence, Union from alembic import op -import sqlalchemy as sa - -from api.v1.modules.public.reference_data.pedimento_codes.seed import ( - seed as pedimento_codes_seed, -) -from api.v1.modules.public.reference_data.pedimento_regimens.seed import ( - seed as pedimento_regimens_seed, -) from api.v1.modules.public.reference_data.code_pedimento_regimens.seed import ( seed as code_pedimento_regimens_seed, ) @@ -43,6 +37,12 @@ from api.v1.modules.public.reference_data.material_types.seed import ( from api.v1.modules.public.reference_data.payment_methods.seed import ( seed as payment_methods_seed, ) +from api.v1.modules.public.reference_data.pedimento_codes.seed import ( + seed as pedimento_codes_seed, +) +from api.v1.modules.public.reference_data.pedimento_regimens.seed import ( + seed as pedimento_regimens_seed, +) from api.v1.modules.public.reference_data.sectors.seed import seed as sectors_seed from api.v1.modules.public.reference_data.transport_modes.seed import ( seed as transport_modes_seed, diff --git a/backend/api/v1/common/base_models.py b/backend/api/v1/common/base_models.py new file mode 100644 index 00000000..18a3723b --- /dev/null +++ b/backend/api/v1/common/base_models.py @@ -0,0 +1,30 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Integer +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy.sql import func + + +class TimestampMixin: + """Mixin for common timestamp fields""" + + 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) + + +class TenantScopedMixin: + """Mixin for tenant and company scoped entities""" + + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + + +class PedimentoRelatedMixin(TenantScopedMixin): + """Mixin for entities related to pedimentos""" + + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) diff --git a/backend/api/v1/common/crud_routes.py b/backend/api/v1/common/crud_routes.py new file mode 100644 index 00000000..2f208636 --- /dev/null +++ b/backend/api/v1/common/crud_routes.py @@ -0,0 +1,121 @@ +from typing import Any, Callable, Generic, Type, TypeVar + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from sqlalchemy.orm import Session + +ModelType = TypeVar("ModelType") +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) +ResponseSchemaType = TypeVar("ResponseSchemaType", bound=BaseModel) + + +class CRUDRouterFactory( + Generic[ModelType, CreateSchemaType, UpdateSchemaType, ResponseSchemaType] +): + """Factory to create standard CRUD routes""" + + def __init__( + self, + model: Type[ModelType], + create_schema: Type[CreateSchemaType], + update_schema: Type[UpdateSchemaType], + response_schema: Type[ResponseSchemaType], + db_dependency: Callable, + auth_dependency: Callable, + prefix: str, + tags: list[str], + id_field: str = "key", + ): + self.model = model + self.create_schema = create_schema + self.update_schema = update_schema + self.response_schema = response_schema + self.db_dependency = db_dependency + self.auth_dependency = auth_dependency + self.id_field = id_field + self.router = APIRouter(prefix=prefix, tags=tags) + self._register_routes() + + def _register_routes(self): + """Register all CRUD routes""" + + @self.router.get("/", response_model=list[self.response_schema]) + def list_items( + skip: int = 0, + limit: int = 100, + db: Session = Depends(self.db_dependency), + current_user: dict = Depends(self.auth_dependency), + ): + items = db.query(self.model).offset(skip).limit(limit).all() + return items + + @self.router.get(f"/{{{self.id_field}}}", response_model=self.response_schema) + def get_item( + db: Session = Depends(self.db_dependency), + current_user: dict = Depends(self.auth_dependency), + **kwargs, + ): + item_id = kwargs.get(self.id_field) + obj = ( + db.query(self.model) + .filter(getattr(self.model, self.id_field) == item_id) + .first() + ) + if not obj: + raise HTTPException(status_code=404, detail="Not found") + return obj + + @self.router.post("/", response_model=self.response_schema) + def create_item( + data: Any, + db: Session = Depends(self.db_dependency), + current_user: dict = Depends(self.auth_dependency), + ): + obj = self.model(**data.dict()) + db.add(obj) + db.commit() + db.refresh(obj) + return obj + + @self.router.put(f"/{{{self.id_field}}}", response_model=self.response_schema) + def update_item( + data: Any, + db: Session = Depends(self.db_dependency), + current_user: dict = Depends(self.auth_dependency), + **kwargs, + ): + item_id = kwargs.get(self.id_field) + obj = ( + db.query(self.model) + .filter(getattr(self.model, self.id_field) == item_id) + .first() + ) + if not obj: + raise HTTPException(status_code=404, detail="Not found") + + for field, value in data.dict(exclude_unset=True).items(): + setattr(obj, field, value) + + db.commit() + db.refresh(obj) + return obj + + @self.router.delete(f"/{{{self.id_field}}}", status_code=204) + def delete_item( + db: Session = Depends(self.db_dependency), + current_user: dict = Depends(self.auth_dependency), + **kwargs, + ): + item_id = kwargs.get(self.id_field) + obj = ( + db.query(self.model) + .filter(getattr(self.model, self.id_field) == item_id) + .first() + ) + if not obj: + raise HTTPException(status_code=404, detail="Not found") + + db.delete(obj) + db.commit() + return None diff --git a/backend/api/v1/common/dto_mixins.py b/backend/api/v1/common/dto_mixins.py new file mode 100644 index 00000000..ec3f4d71 --- /dev/null +++ b/backend/api/v1/common/dto_mixins.py @@ -0,0 +1,31 @@ +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, Field + + +class CurrencyMixin(BaseModel): + """Mixin for currency-related fields""" + + currency: Optional[str] = Field(None, max_length=3, description="Currency") + currency_factor: Optional[Decimal] = Field(None, description="Currency factor") + + +class AffectValueMixin(BaseModel): + """Mixin for value affect flags""" + + not_affect_usd_value: Optional[int] = Field( + None, description="Not affect USD value" + ) + not_affect_customs_value: Optional[int] = Field( + None, description="Not affect customs value" + ) + + +class UpdateFlagsMixin(BaseModel): + """Mixin for update flags""" + + update_vat: Optional[int] = Field(None, description="Update VAT") + update_advalorem: Optional[int] = Field(None, description="Update advalorem") + update_cc: Optional[int] = Field(None, description="Update CC") + update_ieps: Optional[int] = Field(None, description="Update IEPS") diff --git a/backend/api/v1/common/tenant_crud_routes.py b/backend/api/v1/common/tenant_crud_routes.py new file mode 100644 index 00000000..ec730878 --- /dev/null +++ b/backend/api/v1/common/tenant_crud_routes.py @@ -0,0 +1,350 @@ +from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource +from fastapi import APIRouter, Depends, HTTPException, Path, Query +from pydantic import BaseModel +from sqlalchemy.orm import Session + +# Type variables for generic types +ModelType = TypeVar("ModelType") +CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) +UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel) +ResponseSchemaType = TypeVar("ResponseSchemaType", bound=BaseModel) +ServiceType = TypeVar("ServiceType") + + +class TenantCRUDRoutes( + Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType] +): + """ + Generic CRUD routes factory for tenant-scoped resources + + Supports both parent resources (with list/pagination) and child resources (nested under parent). + + Usage examples: + + 1. Parent resource with list (e.g., /pedimentos): + router = TenantCRUDRoutes( + service=PedimentosService, + create_schema=PedimentosCreate, + update_schema=PedimentosUpdate, + response_schema=PedimentosResponse, + prefix="/pedimentos", + tags=["Pedimentos"], + resource_name="Pedimento", + id_name="pedimento_id", + enable_list=True, + ).router + + 2. Child resource (e.g., /pedimentos/{pedimento_id}/config-additional): + router = TenantCRUDRoutes( + service=PedimentoConfigAdditionalService, + create_schema=PedimentoConfigAdditionalCreate, + update_schema=PedimentoConfigAdditionalUpdate, + response_schema=PedimentoConfigAdditionalResponse, + prefix="/{pedimento_id}/config-additional", + tags=["Pedimento Config Additional"], + resource_name="Config additional", + parent_id_name="pedimento_id", + enable_list=False, + ).router + """ + + def __init__( + self, + service: Type[ServiceType], + create_schema: Type[CreateSchemaType], + update_schema: Type[UpdateSchemaType], + response_schema: Type[ResponseSchemaType], + prefix: str, + tags: list[str], + resource_name: str = "Resource", + id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id") + parent_id_name: Optional[ + str + ] = None, # For child resources (e.g., "pedimento_id") + db_dependency: Callable = get_core_db, + auth_dependency: Callable = get_current_user, + validate_parent_match: bool = True, # Validate parent_id matches in create + enable_list: bool = False, # Enable GET list endpoint with pagination + enable_filters: bool = False, # Enable custom filters in list endpoint + default_page_size: int = 50, + max_page_size: int = 100, + ): + self.service = service + self.create_schema = create_schema + self.update_schema = update_schema + self.response_schema = response_schema + self.resource_name = resource_name + self.id_name = id_name or parent_id_name or "id" + self.parent_id_name = parent_id_name + self.db_dependency = db_dependency + self.auth_dependency = auth_dependency + self.validate_parent_match = validate_parent_match + self.enable_list = enable_list + self.enable_filters = enable_filters + self.default_page_size = default_page_size + self.max_page_size = max_page_size + + self.router = APIRouter(prefix=prefix, tags=tags) + self._register_routes() + + def _register_routes(self): + """Register all CRUD routes""" + + # LIST route (optional, for parent resources) + if self.enable_list: + if self.enable_filters: + + @self.router.get("/", response_model=Dict[str, Any]) + async def list_resources( + company_id: int = Query(..., description="Company ID"), + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query( + self.default_page_size, + ge=1, + le=self.max_page_size, + description="Page size", + ), + status: Optional[str] = Query(None, description="Filter by status"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + ): + f"""List all {self.resource_name}s with pagination""" + tenant_id = validate_access_to_resource( + db, company_id, current_user + ) + + skip = (page - 1) * page_size + filters = {} + if status: + filters["status"] = status + + items, total = self.service.get_all( + db, tenant_id, company_id, skip, page_size, filters + ) + + return { + "items": [ + self.response_schema.model_validate(item) for item in items + ], + "total": total, + "page": page, + "page_size": page_size, + } + + else: + + @self.router.get("/", response_model=Dict[str, Any]) + async def list_resources( + company_id: int = Query(..., description="Company ID"), + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query( + self.default_page_size, + ge=1, + le=self.max_page_size, + description="Page size", + ), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + ): + f"""List all {self.resource_name}s with pagination""" + tenant_id = validate_access_to_resource( + db, company_id, current_user + ) + + skip = (page - 1) * page_size + + items, total = self.service.get_all( + db, tenant_id, company_id, skip, page_size, None + ) + + return { + "items": [ + self.response_schema.model_validate(item) for item in items + ], + "total": total, + "page": page, + "page_size": page_size, + } + + # GET single resource route + # For parent resources: GET /{id} + # For child resources: GET / (parent_id comes from path) + if self.parent_id_name: + # Child resource - single GET without ID in path + @self.router.get("/", response_model=self.response_schema) + async def get_resource( + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + **path_params, + ): + f"""Get {self.resource_name} by {self.parent_id_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + parent_id = path_params.get(self.parent_id_name) + + # Try method with 4 params (pedimento_id, tenant_id, company_id) + if hasattr(self.service, "get_by_pedimento_id"): + resource = self.service.get_by_pedimento_id( + db, parent_id, tenant_id, company_id + ) + # Fallback to method with 3 params + elif hasattr(self.service, "get_by_id"): + resource = self.service.get_by_id( + db, parent_id, tenant_id, company_id + ) + else: + resource = self.service.get(db, parent_id, tenant_id, company_id) + + if not resource: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return resource + + else: + # Parent resource - GET by ID in path + @self.router.get( + f"/{{{self.id_name}}}", response_model=self.response_schema + ) + async def get_resource_by_id( + resource_id: int = Path(..., alias=self.id_name), + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + ): + f"""Get {self.resource_name} by ID""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + + resource = self.service.get_by_id( + db, resource_id, tenant_id, company_id + ) + + if not resource: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return resource + + # POST route + @self.router.post("/", response_model=self.response_schema, status_code=201) + async def create_resource( + data: CreateSchemaType, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + **path_params, + ): + f"""Create {self.resource_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + + # Validate parent ID match if enabled and parent_id_name exists + if self.validate_parent_match and self.parent_id_name: + parent_id = path_params.get(self.parent_id_name) + data_parent_id = getattr(data, self.parent_id_name, None) + if data_parent_id is not None and data_parent_id != parent_id: + raise HTTPException( + status_code=400, + detail=f"{self.parent_id_name.replace('_', ' ').title()} mismatch", + ) + + resource = self.service.create(db, data, tenant_id, company_id) + return resource + + # PUT route + # For parent resources: PUT /{id} + # For child resources: PUT / (parent_id comes from path) + if self.parent_id_name: + # Child resource + @self.router.put("/", response_model=self.response_schema) + async def update_resource( + data: UpdateSchemaType, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + **path_params, + ): + f"""Update {self.resource_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + parent_id = path_params.get(self.parent_id_name) + + resource = self.service.update( + db, parent_id, tenant_id, company_id, data + ) + + if not resource: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return resource + + else: + # Parent resource + @self.router.put( + f"/{{{self.id_name}}}", response_model=self.response_schema + ) + async def update_resource_by_id( + data: UpdateSchemaType, + resource_id: int = Path(..., alias=self.id_name), + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + ): + f"""Update {self.resource_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + + resource = self.service.update( + db, resource_id, tenant_id, company_id, data + ) + + if not resource: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return resource + + # DELETE route + # For parent resources: DELETE /{id} + # For child resources: DELETE / (parent_id comes from path) + if self.parent_id_name: + # Child resource + @self.router.delete("/", status_code=204) + async def delete_resource( + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + **path_params, + ): + f"""Delete {self.resource_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + parent_id = path_params.get(self.parent_id_name) + + success = self.service.delete(db, parent_id, tenant_id, company_id) + + if not success: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return None + + else: + # Parent resource + @self.router.delete(f"/{{{self.id_name}}}", status_code=204) + async def delete_resource_by_id( + resource_id: int = Path(..., alias=self.id_name), + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(self.db_dependency), + current_user: Dict[str, Any] = Depends(self.auth_dependency), + ): + f"""Delete {self.resource_name}""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = self.service.delete(db, resource_id, tenant_id, company_id) + + if not success: + raise HTTPException( + status_code=404, detail=f"{self.resource_name} not found" + ) + return None diff --git a/backend/api/v1/modules/a24/q/q_classes/models.py b/backend/api/v1/modules/a24/q/q_classes/models.py index cf9491e8..98f99c73 100644 --- a/backend/api/v1/modules/a24/q/q_classes/models.py +++ b/backend/api/v1/modules/a24/q/q_classes/models.py @@ -1,4 +1,7 @@ from decimal import Decimal + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( Boolean, ForeignKeyConstraint, @@ -8,12 +11,9 @@ from sqlalchemy import ( String, ) from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.orm.base import Mapped - -from core.database import Base -class QClasses(Base): +class QClasses(Base, TenantScopedMixin): __tablename__ = "q_classes" # QClases __table_args__ = ( PrimaryKeyConstraint("id", name="qclases_pk"), @@ -29,8 +29,6 @@ class QClasses(Base): id: Mapped[int] = mapped_column(Integer, primary_key=True) class_id: Mapped[int] = mapped_column(Integer, nullable=False) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) import_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONIMPO import_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACIMPO diff --git a/backend/api/v1/modules/a24/s/s_classes/models.py b/backend/api/v1/modules/a24/s/s_classes/models.py index 4619887c..f128c2b7 100644 --- a/backend/api/v1/modules/a24/s/s_classes/models.py +++ b/backend/api/v1/modules/a24/s/s_classes/models.py @@ -1,16 +1,10 @@ -from sqlalchemy import ( - ForeignKeyConstraint, - Integer, - PrimaryKeyConstraint, - String, - UniqueConstraint, -) -from sqlalchemy.orm import Mapped, mapped_column -from sqlalchemy.orm.base import Mapped +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column -class SClasses(Base): +class SClasses(Base, TenantScopedMixin): __tablename__ = "s_classes" # SClases __table_args__ = ( ForeignKeyConstraint( @@ -27,8 +21,6 @@ class SClasses(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) class_id: Mapped[int] = mapped_column(Integer, nullable=False) # Id de clase - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) stock_um: Mapped[str] = mapped_column(String(5)) # Unidad de medida para existencia us_tariff_code: Mapped[str] = mapped_column(String(19)) # Fracción americana (USA) diff --git a/backend/api/v1/modules/a76/aduanal/dto.py b/backend/api/v1/modules/a76/aduanal/dto.py index 5bf648c5..0caf53e9 100644 --- a/backend/api/v1/modules/a76/aduanal/dto.py +++ b/backend/api/v1/modules/a76/aduanal/dto.py @@ -1,5 +1,7 @@ +from typing import Optional + from pydantic import BaseModel -from typing import Optional, List + class CustomsBrokerDTO(BaseModel): type: Optional[str] @@ -65,4 +67,4 @@ class CustomsBrokerPersonnelDTO(BaseModel): email: Optional[str] class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/aduanal/models.py b/backend/api/v1/modules/a76/aduanal/models.py index 933c29bb..ee44cd18 100644 --- a/backend/api/v1/modules/a76/aduanal/models.py +++ b/backend/api/v1/modules/a76/aduanal/models.py @@ -1,9 +1,20 @@ -from sqlalchemy import Column, String, Integer, ForeignKey -from sqlalchemy.orm import relationship +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String +from sqlalchemy.orm import relationship -class CustomsBroker(Base): - __tablename__ = "GAAduanal" + +class CustomsBroker(Base, TenantScopedMixin): + __tablename__ = "customs_brokers" + __table_args__ = ( + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_customs_brokers_tenants" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_customs_brokers_company" + ), + {"schema": "a76"}, + ) type = Column(String(9), nullable=True) broker_key = Column(String(5), primary_key=True, nullable=False) @@ -22,17 +33,23 @@ class CustomsBroker(Base): license = Column(String(4), nullable=True) company = Column(String(200), nullable=True) contact = Column(String(80), nullable=True) - tenant_id = Column(String(36), nullable=False) - company_id = Column(String(36), nullable=False) - vu = relationship("CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete") - personnel = relationship("CustomsBrokerPersonnel", back_populates="customs_broker", cascade="all, delete") + vu = relationship( + "CustomsBrokerVU", back_populates="customs_broker", cascade="all, delete" + ) + personnel = relationship( + "CustomsBrokerPersonnel", back_populates="customs_broker", cascade="all, delete" + ) class CustomsBrokerVU(Base): - __tablename__ = "GAAduanal_VU" + __tablename__ = "customs_brokers_vu" - broker_key = Column(String(5), ForeignKey("GAAduanal.broker_key", ondelete="CASCADE"), primary_key=True) + broker_key = Column( + String(5), + ForeignKey("a76.customs_brokers.broker_key", ondelete="CASCADE"), + primary_key=True, + ) certificate_path = Column(String(1499), nullable=True) key_path = Column(String(1499), nullable=True) access_key = Column(String(50), nullable=True) @@ -57,9 +74,13 @@ class CustomsBrokerVU(Base): class CustomsBrokerPersonnel(Base): - __tablename__ = "GAAduanalPersonal" + __tablename__ = "customs_brokers_personnel" - broker_key = Column(String(5), ForeignKey("GAAduanal.broker_key", ondelete="CASCADE"), primary_key=True) + broker_key = Column( + String(5), + ForeignKey("a76.customs_brokers.broker_key", ondelete="CASCADE"), + primary_key=True, + ) line = Column(Integer, primary_key=True, nullable=False) name = Column(String(80), nullable=True) tax_id = Column(String(30), nullable=True) @@ -71,4 +92,4 @@ class CustomsBrokerPersonnel(Base): middle_name = Column(String(80), nullable=True) email = Column(String(100), nullable=True) - customs_broker = relationship("CustomsBroker", back_populates="personnel") \ No newline at end of file + customs_broker = relationship("CustomsBroker", back_populates="personnel") diff --git a/backend/api/v1/modules/a76/aduanal/routes.py b/backend/api/v1/modules/a76/aduanal/routes.py index aa51fa72..794bd197 100644 --- a/backend/api/v1/modules/a76/aduanal/routes.py +++ b/backend/api/v1/modules/a76/aduanal/routes.py @@ -1,10 +1,12 @@ +from core.database import get_core_db from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from . import services, dto -from core.database import get_core_db + +from . import dto, services router = APIRouter() + @router.get("/customs-broker/{broker_key}", response_model=dto.CustomsBrokerDTO) def get_customs_broker(broker_key: str, db: Session = Depends(get_core_db)): broker = services.CustomsBrokerService.get_by_broker_key(db, broker_key) @@ -12,10 +14,14 @@ def get_customs_broker(broker_key: str, db: Session = Depends(get_core_db)): raise HTTPException(status_code=404, detail="Customs Broker not found") return broker + @router.post("/customs-broker", response_model=dto.CustomsBrokerDTO) -def create_customs_broker(broker_data: dto.CustomsBrokerDTO, db: Session = Depends(get_core_db)): +def create_customs_broker( + broker_data: dto.CustomsBrokerDTO, db: Session = Depends(get_core_db) +): return services.CustomsBrokerService.create_customs_broker(db, broker_data) + @router.delete("/customs-broker/{broker_key}", response_model=dto.CustomsBrokerDTO) def delete_customs_broker(broker_key: str, db: Session = Depends(get_core_db)): broker = services.CustomsBrokerService.delete_customs_broker(db, broker_key) @@ -23,16 +29,36 @@ def delete_customs_broker(broker_key: str, db: Session = Depends(get_core_db)): raise HTTPException(status_code=404, detail="Customs Broker not found") return broker -@router.put("/customs-broker-vu/{broker_key}", response_model=dto.CustomsBrokerVUCreateDTO) -def update_customs_broker_vu(broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO, db: Session = Depends(get_core_db)): + +@router.put( + "/customs-broker-vu/{broker_key}", response_model=dto.CustomsBrokerVUCreateDTO +) +def update_customs_broker_vu( + broker_key: str, + vu_data: dto.CustomsBrokerVUCreateDTO, + db: Session = Depends(get_core_db), +): updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data) if not updated_vu: raise HTTPException(status_code=404, detail="Customs Broker VU not found") return updated_vu -@router.put("/customs-broker-personnel/{broker_key}/{line}", response_model=dto.CustomsBrokerPersonnelDTO) -def update_customs_broker_personnel(broker_key: str, line: int, personnel_data: dto.CustomsBrokerPersonnelDTO, db: Session = Depends(get_core_db)): - updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(db, broker_key, line, personnel_data) + +@router.put( + "/customs-broker-personnel/{broker_key}/{line}", + response_model=dto.CustomsBrokerPersonnelDTO, +) +def update_customs_broker_personnel( + broker_key: str, + line: int, + personnel_data: dto.CustomsBrokerPersonnelDTO, + db: Session = Depends(get_core_db), +): + updated_personnel = services.CustomsBrokerPersonnelService.update_personnel( + db, broker_key, line, personnel_data + ) if not updated_personnel: - raise HTTPException(status_code=404, detail="Customs Broker Personnel not found") - return updated_personnel \ No newline at end of file + raise HTTPException( + status_code=404, detail="Customs Broker Personnel not found" + ) + return updated_personnel diff --git a/backend/api/v1/modules/a76/aduanal/services.py b/backend/api/v1/modules/a76/aduanal/services.py index 5d90a0a7..ed5d16ea 100644 --- a/backend/api/v1/modules/a76/aduanal/services.py +++ b/backend/api/v1/modules/a76/aduanal/services.py @@ -1,10 +1,16 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class CustomsBrokerService: @staticmethod def get_by_broker_key(db: Session, broker_key: str): - return db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first() + return ( + db.query(models.CustomsBroker) + .filter(models.CustomsBroker.broker_key == broker_key) + .first() + ) @staticmethod def create_customs_broker(db: Session, broker_data: dto.CustomsBrokerDTO): @@ -26,7 +32,11 @@ class CustomsBrokerService: class CustomsBrokerVUService: @staticmethod def get_by_broker_key(db: Session, broker_key: str): - return db.query(models.CustomsBrokerVU).filter(models.CustomsBrokerVU.broker_key == broker_key).first() + return ( + db.query(models.CustomsBrokerVU) + .filter(models.CustomsBrokerVU.broker_key == broker_key) + .first() + ) @staticmethod def create_vu(db: Session, vu_data: dto.CustomsBrokerVUCreateDTO): @@ -58,10 +68,14 @@ class CustomsBrokerVUService: class CustomsBrokerPersonnelService: @staticmethod def get_by_broker_key_and_line(db: Session, broker_key: str, line: int): - return db.query(models.CustomsBrokerPersonnel).filter( - models.CustomsBrokerPersonnel.broker_key == broker_key, - models.CustomsBrokerPersonnel.line == line - ).first() + return ( + db.query(models.CustomsBrokerPersonnel) + .filter( + models.CustomsBrokerPersonnel.broker_key == broker_key, + models.CustomsBrokerPersonnel.line == line, + ) + .first() + ) @staticmethod def create_personnel(db: Session, personnel_data: dto.CustomsBrokerPersonnelDTO): @@ -72,8 +86,15 @@ class CustomsBrokerPersonnelService: return new_personnel @staticmethod - def update_personnel(db: Session, broker_key: str, line: int, personnel_data: dto.CustomsBrokerPersonnelDTO): - personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(db, broker_key, line) + def update_personnel( + db: Session, + broker_key: str, + line: int, + personnel_data: dto.CustomsBrokerPersonnelDTO, + ): + personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line( + db, broker_key, line + ) if personnel: for key, value in personnel_data.dict(exclude_unset=True).items(): setattr(personnel, key, value) @@ -83,8 +104,10 @@ class CustomsBrokerPersonnelService: @staticmethod def delete_personnel(db: Session, broker_key: str, line: int): - personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(db, broker_key, line) + personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line( + db, broker_key, line + ) if personnel: db.delete(personnel) db.commit() - return personnel \ No newline at end of file + return personnel diff --git a/backend/api/v1/modules/a76/auth/dto.py b/backend/api/v1/modules/a76/auth/dto.py index a9544367..e3ef9969 100644 --- a/backend/api/v1/modules/a76/auth/dto.py +++ b/backend/api/v1/modules/a76/auth/dto.py @@ -2,9 +2,10 @@ DTOs para módulo de autenticación """ -from pydantic import BaseModel, EmailStr, Field from typing import Optional +from pydantic import BaseModel, EmailStr, Field + class LoginRequestDTO(BaseModel): """DTO para solicitud de login""" diff --git a/backend/api/v1/modules/a76/auth/routes.py b/backend/api/v1/modules/a76/auth/routes.py index ea21e101..1d327aee 100644 --- a/backend/api/v1/modules/a76/auth/routes.py +++ b/backend/api/v1/modules/a76/auth/routes.py @@ -2,22 +2,22 @@ Endpoints API para autenticación """ -from fastapi import APIRouter, Depends, HTTPException, Response -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from sqlalchemy.orm import Session - from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Response +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy.orm import Session + from .dto import ( + ExchangeCodeRequestDTO, LoginRequestDTO, - TokenResponseDTO, - RefreshTokenRequestDTO, - UserInfoResponseDTO, LogoutRequestDTO, + RefreshTokenRequestDTO, RegisterRequestDTO, RegisterResponseDTO, - ExchangeCodeRequestDTO, SetCookieRequestDTO, + TokenResponseDTO, + UserInfoResponseDTO, ) from .service import AuthService diff --git a/backend/api/v1/modules/a76/auth/service.py b/backend/api/v1/modules/a76/auth/service.py index 4c19bfc2..c3b230e0 100644 --- a/backend/api/v1/modules/a76/auth/service.py +++ b/backend/api/v1/modules/a76/auth/service.py @@ -2,21 +2,24 @@ Servicio de autenticación con Keycloak """ -from keycloak import KeycloakOpenID, KeycloakAdmin -from keycloak.exceptions import KeycloakError -from fastapi import HTTPException -from sqlalchemy.orm import Session import logging +from api.v1.modules.a76.tenants.service import TenantService +from api.v1.modules.a76.user_tenant.service import UserTenantService from core.config import settings +from fastapi import HTTPException +from keycloak import KeycloakAdmin, KeycloakOpenID +from keycloak.exceptions import KeycloakError +from sqlalchemy.orm import Session + from .dto import ( LoginRequestDTO, - TokenResponseDTO, - RefreshTokenRequestDTO, - UserInfoResponseDTO, LogoutRequestDTO, + RefreshTokenRequestDTO, RegisterRequestDTO, RegisterResponseDTO, + TokenResponseDTO, + UserInfoResponseDTO, ) logger = logging.getLogger(__name__) @@ -49,9 +52,6 @@ class AuthService: """ try: # Verificar que el tenant existe - from api.v1.modules.a76.tenants.service import TenantService - from api.v1.modules.a76.user_tenant.service import UserTenantService - tenant_service = TenantService(self.db) user_tenant_service = UserTenantService(self.db) tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug) @@ -70,54 +70,51 @@ class AuthService: client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, ) - # Obtener token de Keycloak - token_response = keycloak_client.token( - username=login_data.username, - password=login_data.password, - grant_type=["password"], - ) + # PASO 1: Primero actualizamos los atributos del usuario ANTES de autenticar + # Esto es necesario para que los Protocol Mappers incluyan los valores correctos + # en el token que se generará a continuación - # Obtener información del usuario y verificar acceso al tenant - user_info = keycloak_client.userinfo(token_response["access_token"]) - user_id = user_info.get("sub") - - if user_id: - # Verificar si el usuario tiene acceso a este tenant - has_access = user_tenant_service.user_has_access_to_tenant( - user_id, tenant.id + # Para obtener el user_id, necesitamos hacer una autenticación temporal + # o buscar el usuario por username + try: + keycloak_admin = KeycloakAdmin( + server_url=settings.KEYCLOAK_SERVER_URL, + username=settings.KEYCLOAK_ADMIN_USERNAME, + password=settings.KEYCLOAK_ADMIN_PASSWORD, + realm_name=tenant.keycloak_realm, + user_realm_name="master", + verify=True, ) - if not has_access: - logger.warning( - f"User {user_id} tried to access tenant {tenant.id} without permission" - ) - raise HTTPException( - status_code=403, detail="You don't have access to this tenant" + # Buscar usuario por username + users = keycloak_admin.get_users({"username": login_data.username}) + + if users and len(users) > 0: + user_id = users[0]["id"] + + # Verificar si el usuario tiene acceso a este tenant + has_access = user_tenant_service.user_has_access_to_tenant( + user_id, tenant.id ) - # Actualizar el tenant_id del usuario en Keycloak basado en el slug usado - try: - # Crear instancia de KeycloakAdmin para actualizar atributos - keycloak_admin = KeycloakAdmin( - server_url=settings.KEYCLOAK_SERVER_URL, - username=settings.KEYCLOAK_ADMIN_USERNAME, - password=settings.KEYCLOAK_ADMIN_PASSWORD, - realm_name=tenant.keycloak_realm, - user_realm_name="master", - verify=True, - ) + if not has_access: + logger.warning( + f"User {user_id} tried to access tenant {tenant.id} without permission" + ) + raise HTTPException( + status_code=403, + detail="You don't have access to this tenant", + ) - # Obtener los datos actuales del usuario para no sobrescribirlos + # Obtener los datos actuales del usuario current_user = keycloak_admin.get_user(user_id) - - # Obtener los atributos actuales o crear un dict vacío current_attributes = current_user.get("attributes", {}) - # Actualizar solo los atributos de tenant + # Actualizar los atributos de tenant current_attributes["tenant_id"] = [str(tenant.id)] current_attributes["tenant_slug"] = [tenant.slug] - # Actualizar el usuario enviando TODOS los campos para evitar que se borren + # Actualizar el usuario con los nuevos atributos update_payload = { "email": current_user.get("email"), "firstName": current_user.get("firstName"), @@ -128,13 +125,23 @@ class AuthService: } keycloak_admin.update_user(user_id=user_id, payload=update_payload) - logger.info( - f"Updated tenant_id={tenant.id} for user {login_data.username}" - ) - except Exception as e: - # No queremos que falle el login si no se puede actualizar el atributo - logger.warning(f"Could not update tenant_id attribute: {str(e)}") + except KeycloakError as e: + logger.warning(f"Could not pre-update user attributes: {str(e)}") + # Continuamos con el login aunque falle la actualización + except HTTPException: + raise # Re-lanzamos las excepciones HTTP (como acceso denegado) + except Exception as e: + logger.warning(f"Error pre-updating user attributes: {str(e)}") + + # PASO 2: Ahora autenticamos al usuario + # Si los Protocol Mappers están configurados, el token incluirá + # automáticamente los atributos tenant_id y tenant_slug actualizados + token_response = keycloak_client.token( + username=login_data.username, + password=login_data.password, + grant_type=["password"], + ) return TokenResponseDTO( access_token=token_response["access_token"], @@ -234,7 +241,6 @@ class AuthService: """ try: self.keycloak_openid.logout(logout_data.refresh_token) - logger.info("User logged out successfully") return {"message": "Logged out successfully"} except KeycloakError as e: @@ -307,7 +313,6 @@ class AuthService: user_role = keycloak_admin.get_realm_role("user") if user_role: keycloak_admin.assign_realm_roles(user_id, [user_role]) - logger.info(f"Assigned 'user' role to {register_data.username}") except KeycloakError as e: # El rol 'user' no existe, no es un error crítico logger.warning(f"Could not assign 'user' role: {str(e)}") @@ -322,23 +327,17 @@ class AuthService: tenant_id=tenant.id, role="user", # Rol por defecto ) - logger.info(f"Added user {user_id} to tenant {tenant.id} in database") except Exception as e: # Si falla, hacer rollback del usuario en Keycloak logger.error(f"Failed to add user to tenant in database: {str(e)}") try: keycloak_admin.delete_user(user_id) - logger.info(f"Rolled back user creation in Keycloak") - except: + except Exception as e: pass raise HTTPException( status_code=500, detail="Failed to register user in database" ) - logger.info( - f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})" - ) - return RegisterResponseDTO( user_id=user_id, username=register_data.username, @@ -385,7 +384,6 @@ class AuthService: """ try: # Importar el DTO aquí para evitar referencias circulares - from .dto import ExchangeCodeRequestDTO # Intercambiar código por tokens usando Keycloak token_response = self.keycloak_openid.token( @@ -394,20 +392,11 @@ class AuthService: redirect_uri=exchange_data.redirect_uri, ) - logger.info(f"Code exchanged successfully") - # Si se proporciona tenant_slug, podríamos validar que el usuario pertenece a ese tenant # Por ahora simplemente retornamos los tokens if exchange_data.tenant_slug: - # Decodificar token para obtener tenant_id del usuario - user_info = self.keycloak_openid.introspect( - token_response["access_token"] - ) - user_tenant_id = user_info.get("tenant_id") # Validar que el tenant existe y está activo - from api.v1.modules.a76.tenants.service import TenantService - tenant_service = TenantService(self.db) tenant = tenant_service.get_tenant_by_slug(exchange_data.tenant_slug) diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index b67cc6ef..4d3cc9fa 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -3,9 +3,9 @@ DTOs (Data Transfer Objects) para módulo de clases SCAII y SCAF Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ -from pydantic import BaseModel, Field from typing import Optional -from datetime import datetime + +from pydantic import BaseModel, Field class ClassCreateDTO(BaseModel): diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index 65980816..4ae1d80b 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -3,24 +3,26 @@ Modelos ORM para gestión de clases SCAII y SCAF """ from typing import TYPE_CHECKING, Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - SmallInteger, ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.parts.models import Part from api.v1.modules.public.reference_data.material_types.models import MaterialType -class Class(Base): +class Class(Base, TenantScopedMixin): """ Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF """ @@ -52,8 +54,6 @@ class Class(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) client_id: Mapped[int] = mapped_column(Integer) # Unique constraint compuesta diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index ef4c85fd..6293fb49 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -2,21 +2,22 @@ Endpoints API para gestión de clases SCAII y SCAF """ -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session from typing import List, Optional from core.database import get_core_db -from core.security import get_current_user, has_role -from .service import ClassService +from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + from .dto import ( - ClassCreateDTO, - ClassUpdateDTO, - ClassResponseDTO, ClassBasicDTO, + ClassCreateDTO, ClassListDTO, + ClassResponseDTO, ClassSearchDTO, + ClassUpdateDTO, ) +from .service import ClassService router = APIRouter(prefix="/classes", tags=["Classes"]) @@ -46,7 +47,9 @@ async def list_classes( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClassService(db) search_params = ClassSearchDTO( @@ -76,7 +79,9 @@ async def get_classes_by_client( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClassService(db) return service.search_by_client(client_id, skip, limit) diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index d515cce7..af5e18a4 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -2,22 +2,23 @@ Capa de servicio para lógica de negocio de clases SCAII y SCAF """ -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from sqlalchemy import or_, and_, func -from fastapi import HTTPException -from typing import List, Optional import logging +from typing import List, Optional + +from fastapi import HTTPException +from sqlalchemy import and_, or_ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session -from .models import Class from .dto import ( - ClassCreateDTO, - ClassUpdateDTO, - ClassResponseDTO, ClassBasicDTO, + ClassCreateDTO, ClassListDTO, + ClassResponseDTO, ClassSearchDTO, + ClassUpdateDTO, ) +from .models import Class logger = logging.getLogger(__name__) @@ -79,8 +80,6 @@ class ClassService: self.db.commit() self.db.refresh(db_class) - logger.info(f"Class created: {db_class.client_id}-{db_class.class_code}") - return ClassResponseDTO.model_validate(db_class) except IntegrityError as e: @@ -218,7 +217,6 @@ class ClassService: self.db.commit() self.db.refresh(class_obj) - logger.info(f"Class updated: {client_id}-{class_code}") return ClassResponseDTO.model_validate(class_obj) @@ -250,7 +248,6 @@ class ClassService: try: self.db.delete(class_obj) self.db.commit() - logger.info(f"Class deleted: {client_id}-{class_code}") return True except Exception as e: self.db.rollback() diff --git a/backend/api/v1/modules/a76/classes/test_classes.py b/backend/api/v1/modules/a76/classes/test_classes.py index 0b907703..f899748c 100644 --- a/backend/api/v1/modules/a76/classes/test_classes.py +++ b/backend/api/v1/modules/a76/classes/test_classes.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_classes(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_classes(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_class_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/classes/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_class_forbidden(): response = client.post("/classes/", json={"name": "Test Class"}) assert response.status_code in (403, 405, 404) + def test_update_class_forbidden(): response = client.put("/classes/1", json={"name": "Updated Class"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/client_and_provider/dto.py b/backend/api/v1/modules/a76/client_and_provider/dto.py index 25bc4d59..a69c82b3 100644 --- a/backend/api/v1/modules/a76/client_and_provider/dto.py +++ b/backend/api/v1/modules/a76/client_and_provider/dto.py @@ -3,10 +3,10 @@ DTOs (Data Transfer Objects) para módulo de clientes y proveedores Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ -from pydantic import BaseModel, Field, EmailStr -from typing import Optional -from datetime import datetime from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, Field # DTOs para dirección diff --git a/backend/api/v1/modules/a76/client_and_provider/models.py b/backend/api/v1/modules/a76/client_and_provider/models.py index 2730e15e..ed97f149 100644 --- a/backend/api/v1/modules/a76/client_and_provider/models.py +++ b/backend/api/v1/modules/a76/client_and_provider/models.py @@ -2,22 +2,24 @@ Modelos ORM para gestión de clientes y proveedores """ -from typing import Optional from decimal import Decimal +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - SmallInteger, - Numeric, ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + String, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from core.database import Base -class ClientProvider(Base): +class ClientProvider(Base, TenantScopedMixin): """ Modelo para la tabla GClientesPro - Información de clientes y proveedores """ @@ -36,8 +38,6 @@ class ClientProvider(Base): # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # Basic information type_nat_foreign: Mapped[Optional[str]] = mapped_column( @@ -67,7 +67,7 @@ class ClientProvider(Base): ) -class ClientProviderAddress(Base): +class ClientProviderAddress(Base, TenantScopedMixin): """ Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores """ @@ -93,8 +93,6 @@ class ClientProviderAddress(Base): Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") ) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - # Address information municipality: Mapped[Optional[str]] = mapped_column(String(150)) streets: Mapped[Optional[str]] = mapped_column(String(100)) @@ -115,7 +113,7 @@ class ClientProviderAddress(Base): client_provider: Mapped["ClientProvider"] = relationship(back_populates="address") -class ClientProviderPrograms(Base): +class ClientProviderPrograms(Base, TenantScopedMixin): """ Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores """ @@ -141,8 +139,6 @@ class ClientProviderPrograms(Base): Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") ) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - # Program information program: Mapped[Optional[str]] = mapped_column(String(7)) program_number: Mapped[Optional[str]] = mapped_column(String(40)) diff --git a/backend/api/v1/modules/a76/client_and_provider/routes.py b/backend/api/v1/modules/a76/client_and_provider/routes.py index 02e7183b..94f102d9 100644 --- a/backend/api/v1/modules/a76/client_and_provider/routes.py +++ b/backend/api/v1/modules/a76/client_and_provider/routes.py @@ -2,20 +2,21 @@ Endpoints API para gestión de clientes y proveedores """ -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session from typing import List, Optional from core.database import get_core_db -from core.security import get_current_user, has_role -from .service import ClientProviderService +from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + from .dto import ( - ClientProviderCreateDTO, - ClientProviderUpdateDTO, - ClientProviderResponseDTO, ClientProviderBasicDTO, + ClientProviderCreateDTO, ClientProviderListDTO, + ClientProviderResponseDTO, + ClientProviderUpdateDTO, ) +from .service import ClientProviderService router = APIRouter(prefix="/clients-providers") @@ -36,11 +37,15 @@ async def create_client_provider( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) # Ensure the client_data is associated with the correct tenant and company if client_data.tenant_id != tenant_id or client_data.company_id != company_id: - raise HTTPException(status_code=400, detail="Mismatch in tenant or company association") + raise HTTPException( + status_code=400, detail="Mismatch in tenant or company association" + ) service = ClientProviderService(db) return service.create_client_provider(client_data) @@ -68,7 +73,9 @@ async def list_clients_providers( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) return service.list_clients_providers( @@ -91,7 +98,9 @@ async def get_clients_only( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) return service.get_clients_only(skip, limit) @@ -112,7 +121,9 @@ async def get_providers_only( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) return service.get_providers_only(skip, limit) @@ -132,7 +143,9 @@ async def search_by_rfc( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) return service.search_by_rfc(rfc) @@ -152,7 +165,9 @@ async def get_client_provider( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.get_client_provider(client_id) @@ -178,7 +193,9 @@ async def update_client_provider( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.update_client_provider(client_id, client_data) @@ -205,7 +222,9 @@ async def delete_client_provider( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) if not service.delete_client_provider(client_id): @@ -228,7 +247,9 @@ async def toggle_client_provider_status( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.toggle_status(client_id) @@ -254,7 +275,9 @@ async def get_client_provider_address( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.get_client_provider(client_id) @@ -280,7 +303,9 @@ async def get_client_provider_programs( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.get_client_provider(client_id) @@ -306,7 +331,9 @@ async def get_client_provider_basic_info( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = ClientProviderService(db) client = service.get_client_provider(client_id) diff --git a/backend/api/v1/modules/a76/client_and_provider/service.py b/backend/api/v1/modules/a76/client_and_provider/service.py index 41b6d479..613bfbac 100644 --- a/backend/api/v1/modules/a76/client_and_provider/service.py +++ b/backend/api/v1/modules/a76/client_and_provider/service.py @@ -2,23 +2,22 @@ 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 typing import List, Optional + +from fastapi import HTTPException +from sqlalchemy import or_ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session, joinedload -from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms from .dto import ( - ClientProviderCreateDTO, - ClientProviderUpdateDTO, - ClientProviderResponseDTO, ClientProviderBasicDTO, + ClientProviderCreateDTO, ClientProviderListDTO, - ClientProviderAddressDTO, - ClientProviderProgramsDTO, + ClientProviderResponseDTO, + ClientProviderUpdateDTO, ) +from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms logger = logging.getLogger(__name__) @@ -99,10 +98,6 @@ class ClientProviderService: 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: @@ -282,7 +277,6 @@ class ClientProviderService: self.db.add(programs) self.db.commit() - logger.info(f"Client/Provider updated: {client_id}") return self._get_client_with_relations(client_id) @@ -314,7 +308,6 @@ class ClientProviderService: 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() diff --git a/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py b/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py index 55f319b6..ba8afa4a 100644 --- a/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py +++ b/backend/api/v1/modules/a76/client_and_provider/test_client_and_provider.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_clients_and_providers(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,23 @@ def test_list_clients_and_providers(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_client_or_provider_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/client_and_provider/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_client_or_provider_forbidden(): - response = client.post("/client_and_provider/", json={"name": "Test Client/Provider"}) + response = client.post( + "/client_and_provider/", json={"name": "Test Client/Provider"} + ) assert response.status_code in (403, 405, 404) + def test_update_client_or_provider_forbidden(): - response = client.put("/client_and_provider/1", json={"name": "Updated Client/Provider"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + response = client.put( + "/client_and_provider/1", json={"name": "Updated Client/Provider"} + ) + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/company/dto.py b/backend/api/v1/modules/a76/company/dto.py index 104a014f..d42749db 100644 --- a/backend/api/v1/modules/a76/company/dto.py +++ b/backend/api/v1/modules/a76/company/dto.py @@ -3,9 +3,10 @@ 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 +from typing import Optional + +from pydantic import BaseModel, Field class CompanyCreateDTO(BaseModel): diff --git a/backend/api/v1/modules/a76/company/models.py b/backend/api/v1/modules/a76/company/models.py index 6809b658..aee82269 100644 --- a/backend/api/v1/modules/a76/company/models.py +++ b/backend/api/v1/modules/a76/company/models.py @@ -3,24 +3,21 @@ 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.sql import func -from sqlalchemy.orm import Mapped, mapped_column + +from api.v1.common.base_models import TimestampMixin from core.database import Base +from sqlalchemy import ( + Boolean, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, +) +from sqlalchemy.orm import Mapped, mapped_column -class Company(Base): +class Company(Base, TimestampMixin): """ Modelo para la tabla Company - Información de la empresa """ @@ -81,12 +78,3 @@ class Company(Base): 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) diff --git a/backend/api/v1/modules/a76/company/routes.py b/backend/api/v1/modules/a76/company/routes.py index a948dfe0..119c8e82 100644 --- a/backend/api/v1/modules/a76/company/routes.py +++ b/backend/api/v1/modules/a76/company/routes.py @@ -2,14 +2,15 @@ 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, get_tenant_from_token +from core.security import get_current_user, get_tenant_from_token +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO from .service import CompanyService -from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO router = APIRouter(prefix="/company") @@ -49,24 +50,20 @@ async def get_company( @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) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get all companies that belong to the user's tenant - + 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" - ) - + raise HTTPException(status_code=400, detail="Tenant ID not found in token") + service = CompanyService(db) companies = service.get_companies_by_tenant(tenant_id) - + return companies @@ -202,4 +199,3 @@ async def delete_company( raise HTTPException( status_code=404, detail=f"Company with ID '{company_id}' not found" ) - diff --git a/backend/api/v1/modules/a76/company/service.py b/backend/api/v1/modules/a76/company/service.py index 67ccdfbf..341e24b5 100644 --- a/backend/api/v1/modules/a76/company/service.py +++ b/backend/api/v1/modules/a76/company/service.py @@ -2,14 +2,15 @@ Capa de servicio para lógica de negocio de empresa """ -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from fastapi import HTTPException -from typing import List, Optional import logging +from typing import List, Optional +from fastapi import HTTPException +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from .dto import CompanyCreateDTO, CompanyResponseDTO, CompanyUpdateDTO from .models import Company -from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO logger = logging.getLogger(__name__) @@ -35,9 +36,7 @@ class CompanyService: """ 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.id).first() if existing: raise HTTPException( status_code=400, @@ -82,8 +81,6 @@ class CompanyService: 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: @@ -107,7 +104,7 @@ class CompanyService: Returns: CompanyResponseDTO o None si no existe """ - company = self.db.query(Company).filter(Company.consecutive == True).first() + company = self.db.query(Company).filter(Company.id).first() if not company: return None return CompanyResponseDTO.model_validate(company) @@ -152,7 +149,6 @@ class CompanyService: try: self.db.commit() self.db.refresh(company) - logger.info(f"Company updated: {company_id}") return CompanyResponseDTO.model_validate(company) except Exception as e: self.db.rollback() @@ -176,7 +172,6 @@ class CompanyService: try: self.db.delete(company) self.db.commit() - logger.info(f"Company deleted: {company_id}") return True except Exception as e: self.db.rollback() @@ -190,10 +185,7 @@ class CompanyService: 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.id).first() is not None def get_companies_by_tenant(self, tenant_id: int) -> List[CompanyResponseDTO]: """ @@ -211,5 +203,5 @@ class CompanyService: .order_by(Company.name) .all() ) - + return [CompanyResponseDTO.model_validate(company) for company in companies] diff --git a/backend/api/v1/modules/a76/company/test_company.py b/backend/api/v1/modules/a76/company/test_company.py index 206c8c38..0b718bbd 100644 --- a/backend/api/v1/modules/a76/company/test_company.py +++ b/backend/api/v1/modules/a76/company/test_company.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_companies(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_companies(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_company_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/company/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_company_forbidden(): response = client.post("/company/", json={"name": "Test Company"}) assert response.status_code in (403, 405, 404) + def test_update_company_forbidden(): response = client.put("/company/1", json={"name": "Updated Company"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/country_rule_oct/models.py b/backend/api/v1/modules/a76/country_rule_oct/models.py index 2e02b91d..cb305a6e 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/models.py +++ b/backend/api/v1/modules/a76/country_rule_oct/models.py @@ -1,16 +1,16 @@ +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class CountryRuleOct(Base): +class CountryRuleOct(Base, TenantScopedMixin): __tablename__ = "country_rule_oct" __table_args__ = ( PrimaryKeyConstraint("id", name="country_rule_oct_pkey"), @@ -45,8 +45,6 @@ class CountryRuleOct(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column() diff --git a/backend/api/v1/modules/a76/country_rule_oct/routes.py b/backend/api/v1/modules/a76/country_rule_oct/routes.py index 85a75ec0..cd7deec3 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/country_rule_oct/routes.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import CountryRuleOctCreateDTO, CountryRuleOctResponseDTO from .services import CountryRuleOctService @@ -22,7 +23,9 @@ async def list_countries( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(CountryRuleOctService).all() @@ -47,9 +50,13 @@ async def read_country_rule( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) - country = CountryRuleOctService.get_country_by_keys(db, permission, line, fraction, country_code) + country = CountryRuleOctService.get_country_by_keys( + db, permission, line, fraction, country_code + ) if not country: raise HTTPException(status_code=404, detail="CountryRuleOct not found") return country diff --git a/backend/api/v1/modules/a76/country_rule_oct/services.py b/backend/api/v1/modules/a76/country_rule_oct/services.py index 1ccb6887..6a355b7e 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/services.py +++ b/backend/api/v1/modules/a76/country_rule_oct/services.py @@ -3,7 +3,8 @@ Service layer for CountryRuleOct. """ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models class CountryRuleOctService: diff --git a/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py b/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py index b4354146..93ffbd21 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py +++ b/backend/api/v1/modules/a76/country_rule_oct/test_country_rule_oct.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_country_rules(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_country_rules(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_country_rule_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/country-rule-oct/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_country_rule_forbidden(): response = client.post("/country-rule-oct/", json={"rule": "Test Rule"}) assert response.status_code in (403, 405, 404) + def test_update_country_rule_forbidden(): response = client.put("/country-rule-oct/1", json={"rule": "Updated Rule"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/drivers/dto.py b/backend/api/v1/modules/a76/drivers/dto.py index 741398b5..014459a6 100644 --- a/backend/api/v1/modules/a76/drivers/dto.py +++ b/backend/api/v1/modules/a76/drivers/dto.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + + class DriverBaseDTO(BaseModel): transporter_key: str line: int @@ -29,9 +31,11 @@ class DriverBaseDTO(BaseModel): company_id: str tenant_id: str + class DriverCreateDTO(DriverBaseDTO): pass + class DriverResponseDTO(DriverBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/drivers/models.py b/backend/api/v1/modules/a76/drivers/models.py index 82d65029..c52f28e5 100644 --- a/backend/api/v1/modules/a76/drivers/models.py +++ b/backend/api/v1/modules/a76/drivers/models.py @@ -1,11 +1,22 @@ -from sqlalchemy import Column, String, Integer, ForeignKey +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, Integer, String -class Driver(Base): + +class Driver(Base, TenantScopedMixin): __tablename__ = "driver" - __table_args__ = {"schema": "a76"} + __table_args__ = ( + ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + {"schema": "a76"}, + ) - transporter_key = Column(String(5), ForeignKey("a76.transporter.transporter_key", ondelete="CASCADE"), primary_key=True, nullable=False) + transporter_key = Column( + String(5), + ForeignKey("a76.transporter.transporter_key", ondelete="CASCADE"), + primary_key=True, + nullable=False, + ) line = Column(Integer, primary_key=True, nullable=False) driver_name = Column(String(80), nullable=True) license_number = Column(String(29), nullable=True) @@ -29,5 +40,3 @@ class Driver(Base): badge_number = Column(String(20), nullable=True) class_type = Column(String(1), nullable=True) unique_badge_number = Column(String(100), nullable=True) - company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/drivers/routes.py b/backend/api/v1/modules/a76/drivers/routes.py index 5a2a661e..a385b0d8 100644 --- a/backend/api/v1/modules/a76/drivers/routes.py +++ b/backend/api/v1/modules/a76/drivers/routes.py @@ -1,48 +1,52 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import DriverCreateDTO, DriverResponseDTO from .services import DriverService router = APIRouter(prefix="/drivers", tags=["Drivers"]) + @router.get("/", response_model=List[DriverResponseDTO]) async def list_drivers( - 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) ): return db.query(DriverService).all() + @router.get("/{transporter_key}/{line}", response_model=DriverResponseDTO) async def read_driver( transporter_key: str, line: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): driver = DriverService.get_driver_by_key_and_line(db, transporter_key, line) if not driver: raise HTTPException(status_code=404, detail="Driver not found") return driver + @router.post("/", response_model=DriverResponseDTO, status_code=status.HTTP_201_CREATED) async def create_driver( driver_data: DriverCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): return DriverService.create_driver(db, driver_data) + @router.delete("/{transporter_key}/{line}", status_code=status.HTTP_204_NO_CONTENT) async def delete_driver( transporter_key: str, line: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): driver = DriverService.delete_driver(db, transporter_key, line) if not driver: - raise HTTPException(status_code=404, detail="Driver not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="Driver not found") diff --git a/backend/api/v1/modules/a76/drivers/services.py b/backend/api/v1/modules/a76/drivers/services.py index 6f1f1099..9800b62f 100644 --- a/backend/api/v1/modules/a76/drivers/services.py +++ b/backend/api/v1/modules/a76/drivers/services.py @@ -1,13 +1,19 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class DriverService: @staticmethod def get_driver_by_key_and_line(db: Session, transporter_key: str, line: int): - return db.query(models.Driver).filter( - models.Driver.transporter_key == transporter_key, - models.Driver.line == line - ).first() + return ( + db.query(models.Driver) + .filter( + models.Driver.transporter_key == transporter_key, + models.Driver.line == line, + ) + .first() + ) @staticmethod def create_driver(db: Session, driver_data: dto.DriverCreateDTO): @@ -23,4 +29,4 @@ class DriverService: if driver: db.delete(driver) db.commit() - return driver \ No newline at end of file + return driver diff --git a/backend/api/v1/modules/a76/exchange_rate/dto.py b/backend/api/v1/modules/a76/exchange_rate/dto.py index d5ca838d..7bfedf19 100644 --- a/backend/api/v1/modules/a76/exchange_rate/dto.py +++ b/backend/api/v1/modules/a76/exchange_rate/dto.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class ExchangeRateBaseDTO(BaseModel): date: int diff --git a/backend/api/v1/modules/a76/exchange_rate/models.py b/backend/api/v1/modules/a76/exchange_rate/models.py index 33beb269..9832e756 100644 --- a/backend/api/v1/modules/a76/exchange_rate/models.py +++ b/backend/api/v1/modules/a76/exchange_rate/models.py @@ -1,20 +1,21 @@ -from typing import Optional from decimal import Decimal +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, DECIMAL, - PrimaryKeyConstraint, DateTime, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, - ForeignKey, ) from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class ExchangeRate(Base): +class ExchangeRate(Base, TenantScopedMixin): __tablename__ = "exchange_rate" __table_args__ = ( PrimaryKeyConstraint("id", name="exchange_rate_pkey"), @@ -31,8 +32,6 @@ class ExchangeRate(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) date: Mapped[int] = mapped_column(DateTime) value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6)) diff --git a/backend/api/v1/modules/a76/exchange_rate/routes.py b/backend/api/v1/modules/a76/exchange_rate/routes.py index 51ccaea9..a3bba942 100644 --- a/backend/api/v1/modules/a76/exchange_rate/routes.py +++ b/backend/api/v1/modules/a76/exchange_rate/routes.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO from .services import ExchangeRateService @@ -22,7 +23,9 @@ async def list_exchange_rates( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(ExchangeRateService).all() @@ -41,7 +44,9 @@ async def read_exchange_rate( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) exchange_rate = ExchangeRateService.get_exchange_rate_by_date(db, date) if not exchange_rate: @@ -65,7 +70,9 @@ async def create_exchange_rate( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return ExchangeRateService.create_exchange_rate(db, exchange_rate_data) @@ -84,7 +91,9 @@ async def delete_exchange_rate( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) exchange_rate = ExchangeRateService.delete_exchange_rate(db, date) if not exchange_rate: diff --git a/backend/api/v1/modules/a76/exchange_rate/services.py b/backend/api/v1/modules/a76/exchange_rate/services.py index 3ab65bb4..72d16a27 100644 --- a/backend/api/v1/modules/a76/exchange_rate/services.py +++ b/backend/api/v1/modules/a76/exchange_rate/services.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models class ExchangeRateService: diff --git a/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py b/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py index caf3d0b9..3bf1cb12 100644 --- a/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py +++ b/backend/api/v1/modules/a76/exchange_rate/test_exchange_rate.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_exchange_rates(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_exchange_rates(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_exchange_rate_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/exchange-rate/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_exchange_rate_forbidden(): response = client.post("/exchange-rate/", json={"rate": 1.23}) assert response.status_code in (403, 405, 404) + def test_update_exchange_rate_forbidden(): response = client.put("/exchange-rate/1", json={"rate": 1.45}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/dto.py b/backend/api/v1/modules/a76/fraction_rule_octave/dto.py index 7c312028..562ef34d 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/dto.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/dto.py @@ -2,9 +2,10 @@ DTOs for FractionRuleOctave. """ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class FractionRuleOctaveBaseDTO(BaseModel): PERMISSION: str diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/models.py b/backend/api/v1/modules/a76/fraction_rule_octave/models.py index dba563cf..9e60640e 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/models.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/models.py @@ -1,16 +1,16 @@ +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class FractionRuleOctave(Base): +class FractionRuleOctave(Base, TenantScopedMixin): __tablename__ = "fraction_rule_octave" __table_args__ = ( PrimaryKeyConstraint("id", name="fraction_rule_octave_pkey"), @@ -32,8 +32,6 @@ class FractionRuleOctave(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column(Integer) diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py index 3a6fc8cc..237cdb51 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import FractionRuleOctaveCreateDTO, FractionRuleOctaveResponseDTO from .services import FractionRuleOctaveService @@ -22,7 +23,9 @@ async def list_fractions( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(FractionRuleOctaveService).all() @@ -45,9 +48,13 @@ async def read_fraction( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) - frac = FractionRuleOctaveService.get_fraction_by_permission_line(db, permission, line, fraction) + frac = FractionRuleOctaveService.get_fraction_by_permission_line( + db, permission, line, fraction + ) if not frac: raise HTTPException(status_code=404, detail="FractionRuleOctave not found") return frac @@ -71,7 +78,9 @@ async def create_frac( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return FractionRuleOctaveService.create_frac(db, frac_data) @@ -94,7 +103,9 @@ async def delete_fraction( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) frac = FractionRuleOctaveService.delete_fraction(db, permission, line, fraction) if not frac: diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/services.py b/backend/api/v1/modules/a76/fraction_rule_octave/services.py index ed910327..bd8d1dc9 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/services.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/services.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models """ Service layer for FractionRuleOctave. diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py b/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py index c4d67c5b..5ec8416e 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/test_fraction_rule_octave.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_fraction_rules(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_fraction_rules(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_fraction_rule_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/fraction_rule_octave/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_fraction_rule_forbidden(): response = client.post("/fraction_rule_octave/", json={"rule": "Test Rule"}) assert response.status_code in (403, 405, 404) + def test_update_fraction_rule_forbidden(): response = client.put("/fraction_rule_octave/1", json={"rule": "Updated Rule"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/licenses/dto.py b/backend/api/v1/modules/a76/licenses/dto.py index 0e8937f8..4636d70d 100644 --- a/backend/api/v1/modules/a76/licenses/dto.py +++ b/backend/api/v1/modules/a76/licenses/dto.py @@ -2,10 +2,11 @@ DTOs para módulo de licencias """ -from pydantic import BaseModel, Field -from typing import Optional from datetime import datetime from enum import Enum +from typing import Optional + +from pydantic import BaseModel, Field class LicensePlanDTO(str, Enum): diff --git a/backend/api/v1/modules/a76/licenses/models.py b/backend/api/v1/modules/a76/licenses/models.py index 39c8f189..e2e0590f 100644 --- a/backend/api/v1/modules/a76/licenses/models.py +++ b/backend/api/v1/modules/a76/licenses/models.py @@ -2,22 +2,14 @@ Modelos ORM para gestión de licencias """ -from datetime import datetime -from sqlalchemy import ( - Column, - Integer, - String, - DateTime, - Boolean, - ForeignKey, - Enum as SQLEnum, -) -from sqlalchemy.sql import func -from sqlalchemy.orm import relationship -from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base import enum +from api.v1.common.base_models import TimestampMixin +from core.database import Base +from sqlalchemy import Boolean, Column, DateTime +from sqlalchemy import Enum as SQLEnum +from sqlalchemy import ForeignKey, Integer + class LicensePlan(enum.Enum): """Planes de licencia disponibles""" @@ -38,7 +30,7 @@ class LicenseStatus(enum.Enum): CANCELLED = "cancelled" -class License(Base): +class License(Base, TimestampMixin): """ Modelo de Licencia - Control de planes y límites por tenant """ @@ -72,20 +64,11 @@ class License(Base): starts_at = Column(DateTime(timezone=True), nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False) - # 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) - def __repr__(self): return f"" -class LicenseUsage(Base): +class LicenseUsage(Base, TimestampMixin): """ Modelo para tracking de uso de licencia """ @@ -107,14 +90,5 @@ class LicenseUsage(Base): operations_count = Column(Integer, default=0) api_calls_count = Column(Integer, default=0) - # 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) - def __repr__(self): return f"" diff --git a/backend/api/v1/modules/a76/licenses/routes.py b/backend/api/v1/modules/a76/licenses/routes.py index b66173cc..d88b4ae6 100644 --- a/backend/api/v1/modules/a76/licenses/routes.py +++ b/backend/api/v1/modules/a76/licenses/routes.py @@ -2,17 +2,17 @@ Endpoints API para gestión de licencias """ +from core.database import get_core_db +from core.security import get_current_user, has_role from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role from .dto import ( LicenseCreateDTO, - LicenseUpdateDTO, LicenseResponseDTO, - LicenseValidationResponseDTO, + LicenseUpdateDTO, LicenseUsageResponseDTO, + LicenseValidationResponseDTO, ) from .service import LicenseService diff --git a/backend/api/v1/modules/a76/licenses/service.py b/backend/api/v1/modules/a76/licenses/service.py index 8e0a98c9..8c379e19 100644 --- a/backend/api/v1/modules/a76/licenses/service.py +++ b/backend/api/v1/modules/a76/licenses/service.py @@ -2,21 +2,21 @@ Servicio de lógica de negocio para licencias """ -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from fastapi import HTTPException -from typing import Optional -from datetime import datetime, timezone import logging +from datetime import datetime, timezone +from typing import Optional + +from fastapi import HTTPException +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session -from .models import License, LicenseUsage, LicensePlan, LicenseStatus from .dto import ( LicenseCreateDTO, - LicenseUpdateDTO, LicenseResponseDTO, - LicenseValidationResponseDTO, + LicenseUpdateDTO, LicenseUsageResponseDTO, ) +from .models import License, LicensePlan, LicenseStatus, LicenseUsage logger = logging.getLogger(__name__) @@ -74,8 +74,6 @@ class LicenseService: self.db.commit() self.db.refresh(db_license) - logger.info(f"License created for tenant {license_data.tenant_id}") - return LicenseResponseDTO.model_validate(db_license) except IntegrityError as e: diff --git a/backend/api/v1/modules/a76/package/dto.py b/backend/api/v1/modules/a76/package/dto.py index e05d91c3..d9449f7e 100644 --- a/backend/api/v1/modules/a76/package/dto.py +++ b/backend/api/v1/modules/a76/package/dto.py @@ -2,9 +2,10 @@ DTOs for GBultos. """ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class GBultoBaseDTO(BaseModel): CODE: str diff --git a/backend/api/v1/modules/a76/package/models.py b/backend/api/v1/modules/a76/package/models.py index f24cf07d..4843482f 100644 --- a/backend/api/v1/modules/a76/package/models.py +++ b/backend/api/v1/modules/a76/package/models.py @@ -1,22 +1,20 @@ -from typing import Optional -from datetime import datetime from decimal import Decimal +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, - Integer, - String, DECIMAL, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, ) -from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class Package(Base): +class Package(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "packages" # GBultos __table_args__ = ( PrimaryKeyConstraint("id", name="packages_pkey"), @@ -31,8 +29,6 @@ class Package(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) key: Mapped[str] = mapped_column(String(5)) description_es: Mapped[Optional[str]] = mapped_column(String(40)) @@ -42,12 +38,3 @@ class Package(Base): plural_in: Mapped[Optional[str]] = mapped_column(String(4)) code_ace: Mapped[Optional[str]] = mapped_column(String(4)) code_aamex: Mapped[Optional[str]] = mapped_column(String(9)) - - # 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) diff --git a/backend/api/v1/modules/a76/package/routes.py b/backend/api/v1/modules/a76/package/routes.py index 24a9c850..ff87bde2 100644 --- a/backend/api/v1/modules/a76/package/routes.py +++ b/backend/api/v1/modules/a76/package/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from .dto import GBultoCreateDTO, GBultoResponseDTO, GBultoUpdateDTO from .models import Package -from .dto import GBultoCreateDTO, GBultoUpdateDTO, GBultoResponseDTO from .services import GBultoService router = APIRouter(prefix="/bultos", tags=["GBultos"]) @@ -26,7 +27,9 @@ async def list_bultos( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(Package).offset(skip).limit(limit).all() @@ -45,7 +48,9 @@ async def read_bulto( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) bulto = GBultoService.get_bulto_by_code(db, code) if not bulto: @@ -67,7 +72,9 @@ async def create_gbulto( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return GBultoService.create_gbulto(db, bulto_data) @@ -87,7 +94,9 @@ async def update_bulto( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) bulto = GBultoService.update_bulto(db, code, bulto_data) if not bulto: @@ -109,7 +118,9 @@ async def delete_bulto( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) bulto = GBultoService.delete_bulto(db, code) if not bulto: diff --git a/backend/api/v1/modules/a76/package/services.py b/backend/api/v1/modules/a76/package/services.py index 42559633..0490a28a 100644 --- a/backend/api/v1/modules/a76/package/services.py +++ b/backend/api/v1/modules/a76/package/services.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models class GBultoService: diff --git a/backend/api/v1/modules/a76/package/test_package.py b/backend/api/v1/modules/a76/package/test_package.py index 4d0d5bd7..4bb4c007 100644 --- a/backend/api/v1/modules/a76/package/test_package.py +++ b/backend/api/v1/modules/a76/package/test_package.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_packages(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_packages(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_package_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/bultos/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_package_forbidden(): response = client.post("/bultos/", json={"name": "Test Package"}) assert response.status_code in (403, 405, 404) + def test_update_package_forbidden(): response = client.put("/bultos/1", json={"name": "Updated Package"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index 79156199..1414fd82 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -3,10 +3,11 @@ DTOs (Data Transfer Objects) para módulo de partes/componentes Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ -from pydantic import BaseModel, Field -from typing import Optional from datetime import datetime from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, Field class PartCreateDTO(BaseModel): diff --git a/backend/api/v1/modules/a76/parts/models.py b/backend/api/v1/modules/a76/parts/models.py index ea824e61..ef8b325b 100644 --- a/backend/api/v1/modules/a76/parts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -2,30 +2,30 @@ Modelos ORM para gestión de partes/componentes """ -from typing import TYPE_CHECKING, Optional from datetime import datetime from decimal import Decimal +from typing import TYPE_CHECKING, Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - Numeric, - SmallInteger, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + String, UniqueConstraint, ) -from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column, relationship -from core.database import Base if TYPE_CHECKING: + from api.v1.modules.a76.classes.models import Class from api.v1.modules.public.reference_data.countries.models import Country from api.v1.modules.public.reference_data.currency_types.models import CurrencyType - from api.v1.modules.a76.classes.models import Class -class Part(Base): +class Part(Base, TenantScopedMixin): """ Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) """ @@ -50,8 +50,6 @@ class Part(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) # Unique constraint compuesta client_id: Mapped[int] = mapped_column(Integer) diff --git a/backend/api/v1/modules/a76/parts/routes.py b/backend/api/v1/modules/a76/parts/routes.py index e7ab6fe8..e271cb12 100644 --- a/backend/api/v1/modules/a76/parts/routes.py +++ b/backend/api/v1/modules/a76/parts/routes.py @@ -2,21 +2,22 @@ Endpoints API para gestión de partes/componentes """ -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session from typing import List, Optional from core.database import get_core_db -from core.security import get_current_user, has_role -from .service import PartService +from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.orm import Session + from .dto import ( - PartCreateDTO, - PartUpdateDTO, - PartResponseDTO, PartBasicDTO, + PartCreateDTO, PartListDTO, + PartResponseDTO, PartSearchDTO, + PartUpdateDTO, ) +from .service import PartService router = APIRouter(prefix="/parts") @@ -35,7 +36,9 @@ async def create_part( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.create_part(part_data) @@ -64,7 +67,9 @@ async def list_parts( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) search_params = PartSearchDTO( @@ -94,7 +99,9 @@ async def get_parts_by_client( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.search_by_client(client_id, skip, limit) @@ -114,7 +121,9 @@ async def search_by_fraction( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.search_by_fraction(fraction) @@ -134,7 +143,9 @@ async def search_by_supplier( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.search_by_supplier(supplier) @@ -154,7 +165,9 @@ async def get_parts_by_country( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.get_parts_by_country(country_code) @@ -172,7 +185,9 @@ async def get_parts_statistics( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) return service.get_parts_statistics() @@ -193,7 +208,9 @@ async def get_part( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) part = service.get_part(client_id, part_number) @@ -221,7 +238,9 @@ async def update_part( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) part = service.update_part(client_id, part_number, part_data) @@ -250,7 +269,9 @@ async def delete_part( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) if not service.delete_part(client_id, part_number): @@ -277,7 +298,9 @@ async def toggle_part_status( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) part = service.toggle_status(client_id, part_number) @@ -305,7 +328,9 @@ async def get_part_basic_info( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) part = service.get_part(client_id, part_number) @@ -342,7 +367,9 @@ async def get_part_regulatory_info( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) service = PartService(db) part = service.get_part(client_id, part_number) diff --git a/backend/api/v1/modules/a76/parts/service.py b/backend/api/v1/modules/a76/parts/service.py index e80c15e5..d8ba3d06 100644 --- a/backend/api/v1/modules/a76/parts/service.py +++ b/backend/api/v1/modules/a76/parts/service.py @@ -2,16 +2,16 @@ Capa de servicio para lógica de negocio de partes/componentes """ -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from sqlalchemy import or_, and_, func -from fastapi import HTTPException -from typing import List, Optional import logging -from datetime import datetime +from typing import List, Optional + +from fastapi import HTTPException +from sqlalchemy import and_, func, or_ +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session -from .models import Part from .dto import PartCreateDTO, PartUpdateDTO +from .models import Part logger = logging.getLogger(__name__) diff --git a/backend/api/v1/modules/a76/parts/test_parts.py b/backend/api/v1/modules/a76/parts/test_parts.py index 1596c8b0..bb54e2b0 100644 --- a/backend/api/v1/modules/a76/parts/test_parts.py +++ b/backend/api/v1/modules/a76/parts/test_parts.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_parts(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_parts(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_part_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/parts/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_part_forbidden(): response = client.post("/parts/", json={"name": "Test Part"}) assert response.status_code in (403, 405, 404) + def test_update_part_forbidden(): response = client.put("/parts/1", json={"name": "Updated Part"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py index d2295e0b..028a1dd4 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoConfigAdditionalBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py index 9c3a1813..692b9e9c 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoConfigCalculationsBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py index e48affa1..3f5f04ed 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from decimal import Decimal from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoConfigParametersBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py index fa19fe27..385ebcf9 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoConfigSurchargesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py index bfea2b18..62bbf8f7 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py @@ -1,17 +1,16 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + +from api.v1.common.dto_mixins import UpdateFlagsMixin -class PedimentoConfigUpdateRectificationBase(BaseModel): +class PedimentoConfigUpdateRectificationBase(BaseModel, UpdateFlagsMixin): """Base schema for Pedimento Config Update Rectification""" pedimento_id: int = Field(..., description="Pedimento ID") - tenant_id: int = Field(..., description="Tenant ID") - update_vat: Optional[int] = Field(None, description="Update VAT") - update_advalorem: Optional[int] = Field(None, description="Update advalorem") - update_cc: Optional[int] = Field(None, description="Update CC") - update_ieps: Optional[int] = Field(None, description="Update IEPS") + tenant_id: int = Field(..., description="Tenant ID") calculate_surcharge: Optional[int] = Field(None, description="Calculate surcharge") @@ -21,13 +20,9 @@ class PedimentoConfigUpdateRectificationCreate(PedimentoConfigUpdateRectificatio pass -class PedimentoConfigUpdateRectificationUpdate(BaseModel): +class PedimentoConfigUpdateRectificationUpdate(BaseModel, UpdateFlagsMixin): """Schema for updating a Pedimento Config Update Rectification""" - - update_vat: Optional[int] = None - update_advalorem: Optional[int] = None - update_cc: Optional[int] = None - update_ieps: Optional[int] = None + calculate_surcharge: Optional[int] = None diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py index f76aeacb..da2700f1 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py @@ -1,18 +1,16 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + +from backend.api.v1.common.dto_mixins import UpdateFlagsMixin -class PedimentoConfigUpdatesBase(BaseModel): +class PedimentoConfigUpdatesBase(BaseModel, UpdateFlagsMixin): """Base schema for Pedimento Config Updates""" pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") - update_vat: Optional[int] = Field(None, description="Update VAT") - update_advalorem: Optional[int] = Field(None, description="Update advalorem") - update_cc: Optional[int] = Field(None, description="Update CC") - update_ieps: Optional[int] = Field(None, description="Update IEPS") - class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase): """Schema for creating a new Pedimento Config Updates""" @@ -20,13 +18,9 @@ class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase): pass -class PedimentoConfigUpdatesUpdate(BaseModel): +class PedimentoConfigUpdatesUpdate(BaseModel, UpdateFlagsMixin): """Schema for updating a Pedimento Config Updates""" - - update_vat: Optional[int] = None - update_advalorem: Optional[int] = None - update_cc: Optional[int] = None - update_ieps: Optional[int] = None + pass class PedimentoConfigUpdatesResponse(PedimentoConfigUpdatesBase): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py index 532541ce..31444672 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoCustomsOfficesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py index db01215e..bd69786e 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime, time +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoDatesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py index 199d1983..aabce8cf 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from decimal import Decimal from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoDecrementablesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py index e7c347dd..59beceb2 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from decimal import Decimal from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoIncrementablesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py index 4e228798..b304abee 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py @@ -1,7 +1,8 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from decimal import Decimal from datetime import datetime +from decimal import Decimal +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoIndexesBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py index c2897b88..eeaf384c 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py @@ -1,6 +1,9 @@ -from pydantic import BaseModel, Field, ConfigDict +from datetime import date as Date +from datetime import datetime +from datetime import time as Time from typing import Optional -from datetime import datetime, date as Date, time as Time + +from pydantic import BaseModel, ConfigDict, Field class PedimentoPaymentsBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py index 6462e0ed..b021cc11 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + class PedimentoRectificationDestinationBase(BaseModel): """Base schema for Pedimento Rectification Destination""" diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py index 44e48461..93caf94f 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoRectificationOriginBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py index f30b4975..560446f2 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoTransportMeansBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py index e084a61b..568f784d 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class PedimentoValidationBase(BaseModel): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py index 31f92fc8..693984b0 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py @@ -1,8 +1,9 @@ -from enum import IntEnum -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional -from decimal import Decimal from datetime import datetime +from decimal import Decimal +from enum import IntEnum +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field class OperationType(IntEnum): diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py index 6949395d..b3762299 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py @@ -1,24 +1,22 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigAdditional(Base): +class PedimentoConfigAdditional(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_additional" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_additional_pkey"), @@ -48,8 +46,6 @@ class PedimentoConfigAdditional(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) add_po_identifier: Mapped[int] = mapped_column(SmallInteger) @@ -59,15 +55,6 @@ class PedimentoConfigAdditional(Base): send_502_validation_file_for_consolidated: Mapped[int] = mapped_column(SmallInteger) add_remove_norms: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_additional" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py index 47a309d3..f06e893e 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py @@ -1,24 +1,22 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigCalculations(Base): +class PedimentoConfigCalculations(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_calculations" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_calculations_pkey"), @@ -40,8 +38,6 @@ class PedimentoConfigCalculations(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) dta_type: Mapped[str] = mapped_column(String(1)) @@ -55,15 +51,6 @@ class PedimentoConfigCalculations(Base): additional_fixed_fee: Mapped[int] = mapped_column(SmallInteger) additional_fixed_fee_payment_method: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_calculations" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py index 31b85708..2bac4c75 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py @@ -1,26 +1,23 @@ from decimal import Decimal from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigParameters(Base): +class PedimentoConfigParameters(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_parameters" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_parameters_pkey"), @@ -50,8 +47,6 @@ class PedimentoConfigParameters(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) is_embassy: Mapped[int] = mapped_column(SmallInteger) @@ -66,15 +61,6 @@ class PedimentoConfigParameters(Base): is_national_supplier: Mapped[int] = mapped_column(SmallInteger) is_consolidated: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_parameters" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py index dd622e79..aaa30ed2 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py @@ -1,24 +1,21 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigSurcharges(Base): +class PedimentoConfigSurcharges(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_surcharges" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_surcharges_pkey"), @@ -48,8 +45,6 @@ class PedimentoConfigSurcharges(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) surcharge_igi: Mapped[int] = mapped_column(SmallInteger) @@ -59,15 +54,6 @@ class PedimentoConfigSurcharges(Base): surcharge_ieps: Mapped[int] = mapped_column(SmallInteger) surcharge_cc: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_surcharges" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py index e5535aa2..228e92cd 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py @@ -1,24 +1,21 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigUpdateRectification(Base): +class PedimentoConfigUpdateRectification(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_update_rectification" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_update_rectification_pkey"), @@ -48,8 +45,6 @@ class PedimentoConfigUpdateRectification(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) update_vat: Mapped[int] = mapped_column(SmallInteger) @@ -58,15 +53,6 @@ class PedimentoConfigUpdateRectification(Base): update_ieps: Mapped[int] = mapped_column(SmallInteger) calculate_surcharge: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_update_rectification" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py index da9aba32..495d2ea4 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py @@ -1,24 +1,21 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoConfigUpdates(Base): +class PedimentoConfigUpdates(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_config_updates" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_config_updates_pkey"), @@ -46,8 +43,6 @@ class PedimentoConfigUpdates(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) update_vat: Mapped[int] = mapped_column(SmallInteger) @@ -55,15 +50,6 @@ class PedimentoConfigUpdates(Base): update_cc: Mapped[int] = mapped_column(SmallInteger) update_ieps: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_updates" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py index c41fdb4d..56d406dc 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py @@ -1,24 +1,21 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoCustomsOffices(Base): +class PedimentoCustomsOffices(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_customs_offices" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_customs_offices_pkey"), @@ -48,22 +45,11 @@ class PedimentoCustomsOffices(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) dispatch_customs: Mapped[str] = mapped_column(String(3)) entry_exit_customs: Mapped[str] = mapped_column(String(3)) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_customs_offices" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py index 0ea9147a..a6f1958b 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py @@ -1,4 +1,9 @@ +from datetime import datetime +from datetime import time as datetime_time from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( DateTime, ForeignKeyConstraint, @@ -7,19 +12,14 @@ from sqlalchemy import ( PrimaryKeyConstraint, Time, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime, time as datetime_time -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoDates(Base): +class PedimentoDates(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_dates" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_dates_pkey"), @@ -46,8 +46,6 @@ class PedimentoDates(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) entry_date: Mapped[datetime] = mapped_column(DateTime) @@ -63,15 +61,6 @@ class PedimentoDates(Base): capture_date: Mapped[datetime] = mapped_column(DateTime) capture_time: Mapped[datetime_time] = mapped_column(Time) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_dates" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py index d806a0fc..7c585cb4 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, Numeric, @@ -9,19 +11,14 @@ from sqlalchemy import ( SmallInteger, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoDecrementables(Base): +class PedimentoDecrementables(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_decrementables" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_decrementables_pkey"), @@ -49,8 +46,6 @@ class PedimentoDecrementables(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) freight: Mapped[Decimal] = mapped_column(Numeric(13, 2)) @@ -63,15 +58,6 @@ class PedimentoDecrementables(Base): not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_decrementables" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py index 3bb0112b..dbab7306 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, Numeric, @@ -9,19 +11,14 @@ from sqlalchemy import ( SmallInteger, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoIncrementables(Base): +class PedimentoIncrementables(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_incrementables" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_incrementables_pkey"), @@ -49,8 +46,6 @@ class PedimentoIncrementables(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) insured_value: Mapped[Decimal] = mapped_column(Numeric(13, 2)) @@ -64,15 +59,6 @@ class PedimentoIncrementables(Base): not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_incrementables" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py index 73133176..3720776f 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py @@ -1,26 +1,23 @@ from decimal import Decimal from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoIndexes(Base): +class PedimentoIndexes(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_indexes" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_indexes_pkey"), @@ -46,23 +43,12 @@ class PedimentoIndexes(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) update_factor_type: Mapped[int] = mapped_column(SmallInteger) update_factor: Mapped[Decimal] = mapped_column(Numeric(7, 4)) manual_update_factor: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_indexes" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py index 8f1c3780..f67f06c5 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py @@ -1,7 +1,11 @@ +from datetime import date as Date2 +from datetime import time as Time2 from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( Date, - DateTime, ForeignKeyConstraint, Index, Integer, @@ -10,19 +14,14 @@ from sqlalchemy import ( String, Time, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime, time as Time2, date as Date2 -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoPayments(Base): +class PedimentoPayments(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_payments" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_payments_pkey"), @@ -49,8 +48,6 @@ class PedimentoPayments(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) payment_id: Mapped[int] = mapped_column(Integer) @@ -66,15 +63,6 @@ class PedimentoPayments(Base): counter_payment: Mapped[int] = mapped_column(SmallInteger) pece_code: Mapped[str] = mapped_column(String(5)) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_payments" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py index 8e68a7c1..7fab6913 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py @@ -1,23 +1,21 @@ -from datetime import datetime from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, - func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoRectificationDestination(Base): +class PedimentoRectificationDestination(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_rectification_destination" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_rectification_destination_pkey"), @@ -47,8 +45,6 @@ class PedimentoRectificationDestination(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) destination_pedimento_year: Mapped[str] = mapped_column(String(2)) @@ -56,15 +52,6 @@ class PedimentoRectificationDestination(Base): destination_license: Mapped[str] = mapped_column(String(4)) destination_pedimento_number: Mapped[str] = mapped_column(String(7)) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_rectification_destination" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py index be6ab24d..c73f8450 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py @@ -1,5 +1,8 @@ from datetime import datetime from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( DateTime, ForeignKeyConstraint, @@ -8,17 +11,14 @@ from sqlalchemy import ( SmallInteger, String, UniqueConstraint, - func, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoRectificationOrigin(Base): +class PedimentoRectificationOrigin(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_rectification_origin" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_rectification_origin_pkey"), @@ -48,8 +48,6 @@ class PedimentoRectificationOrigin(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) original_pedimento_year: Mapped[str] = mapped_column(String(2)) @@ -68,15 +66,6 @@ class PedimentoRectificationOrigin(Base): manual_calculation: Mapped[int] = mapped_column(SmallInteger) original_pedimento_norms: Mapped[int] = mapped_column(SmallInteger) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_rectification_origin" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py index 6fad41e5..edbfb306 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py @@ -1,24 +1,22 @@ -from datetime import datetime from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoTransportMeans(Base): +class PedimentoTransportMeans(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_transport_means" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_transport_means_pkey"), @@ -48,17 +46,12 @@ class PedimentoTransportMeans(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) destination: Mapped[int] = mapped_column(SmallInteger) entry_exit: Mapped[str] = mapped_column(String(2)) arrival: Mapped[str] = mapped_column(String(2)) departure: Mapped[str] = mapped_column(String(2)) - created_at: Mapped[datetime] = mapped_column( - DateTime, server_default=text("CURRENT_TIMESTAMP") - ) pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_transport_means" diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py index bd7239a4..16730e21 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py @@ -1,24 +1,21 @@ from typing import TYPE_CHECKING + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos -class PedimentoValidation(Base): +class PedimentoValidation(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimento_validation" # PedimentoValidacion __table_args__ = ( PrimaryKeyConstraint("id", name="pedimento_validation_pkey"), @@ -39,8 +36,6 @@ class PedimentoValidation(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) validator: Mapped[str] = mapped_column(String(3)) # validador @@ -52,15 +47,6 @@ class PedimentoValidation(Base): validator_id: Mapped[int] = mapped_column(Integer) responsible_id: Mapped[int] = mapped_column(Integer) - # 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) - pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_validation" ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index 95c6b00c..0805d5f4 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -1,7 +1,9 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base from sqlalchemy import ( - DateTime, ForeignKeyConstraint, Index, Integer, @@ -9,14 +11,8 @@ from sqlalchemy import ( PrimaryKeyConstraint, String, UniqueConstraint, - func, - text, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from sqlalchemy.orm.base import Mapped -from datetime import datetime -from enum import IntEnum -from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimento_config_additional import ( @@ -65,7 +61,7 @@ if TYPE_CHECKING: ) -class Pedimentos(Base): +class Pedimentos(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "pedimentos" __table_args__ = ( PrimaryKeyConstraint("id", name="pedimentos_pkey"), @@ -102,8 +98,6 @@ class Pedimentos(Base): ) id: Mapped[int] = mapped_column(Integer) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) year: Mapped[str] = mapped_column(String(2)) customs_office: Mapped[str] = mapped_column(String(2)) @@ -120,15 +114,6 @@ class Pedimentos(Base): gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 3)) exchange_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(9, 5)) - # 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) - pedimento_config_additional: Mapped["PedimentoConfigAdditional"] = relationship( "PedimentoConfigAdditional", uselist=False, back_populates="pedimento" ) diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py index d882663d..650cb92d 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py @@ -2,95 +2,25 @@ Routes for PedimentoConfigAdditional CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_config_additional import PedimentoConfigAdditionalService from ..dtos.pedimento_config_additional import ( PedimentoConfigAdditionalCreate, - PedimentoConfigAdditionalUpdate, PedimentoConfigAdditionalResponse, + PedimentoConfigAdditionalUpdate, ) +from ..services.pedimento_config_additional import PedimentoConfigAdditionalService - -router = APIRouter(prefix="/{pedimento_id}/config-additional") - - -@router.get("/", response_model=PedimentoConfigAdditionalResponse) -async def get_config_additional( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config additional by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigAdditionalService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException(status_code=404, detail="Config additional not found") - - return config - - -@router.post("/", response_model=PedimentoConfigAdditionalResponse, status_code=201) -async def create_config_additional( - pedimento_id: int, - data: PedimentoConfigAdditionalCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config additional""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id and company_id match - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigAdditionalService.create(db, data, tenant_id, company_id) - return config - - -@router.put("/", response_model=PedimentoConfigAdditionalResponse) -async def update_config_additional( - pedimento_id: int, - data: PedimentoConfigAdditionalUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config additional""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigAdditionalService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException(status_code=404, detail="Config additional not found") - - return config - - -@router.delete("/", status_code=204) -async def delete_config_additional( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config additional""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigAdditionalService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException(status_code=404, detail="Config additional not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigAdditionalService, + create_schema=PedimentoConfigAdditionalCreate, + update_schema=PedimentoConfigAdditionalUpdate, + response_schema=PedimentoConfigAdditionalResponse, + prefix="/{pedimento_id}/config-additional", + tags=[], + resource_name="Config additional", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py index 33af3f06..ceb572c4 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py @@ -2,93 +2,25 @@ Routes for PedimentoConfigCalculations CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService from ..dtos.pedimento_config_calculations import ( PedimentoConfigCalculationsCreate, - PedimentoConfigCalculationsUpdate, PedimentoConfigCalculationsResponse, + PedimentoConfigCalculationsUpdate, ) +from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService - -router = APIRouter(prefix="/{pedimento_id}/config-calculations") - - -@router.get("/", response_model=PedimentoConfigCalculationsResponse) -async def get_config_calculations( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config calculations by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigCalculationsService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException(status_code=404, detail="Config calculations not found") - - return config - - -@router.post("/", response_model=PedimentoConfigCalculationsResponse, status_code=201) -async def create_config_calculations( - pedimento_id: int, - data: PedimentoConfigCalculationsCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config calculations""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigCalculationsService.create(db, data, tenant_id, company_id) - return config - - -@router.put("/", response_model=PedimentoConfigCalculationsResponse) -async def update_config_calculations( - pedimento_id: int, - data: PedimentoConfigCalculationsUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config calculations""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigCalculationsService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException(status_code=404, detail="Config calculations not found") - - return config - - -@router.delete("/", status_code=204) -async def delete_config_calculations( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config calculations""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigCalculationsService.delete(db, pedimento_id, company_id) - if not success: - raise HTTPException(status_code=404, detail="Config calculations not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigCalculationsService, + create_schema=PedimentoConfigCalculationsCreate, + update_schema=PedimentoConfigCalculationsUpdate, + response_schema=PedimentoConfigCalculationsResponse, + prefix="/{pedimento_id}/config-calculations", + tags=[], + resource_name="Config calculations", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py index 9c3e5743..c182a1fc 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py @@ -2,95 +2,25 @@ Routes for PedimentoConfigParameters CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_config_parameters import PedimentoConfigParametersService from ..dtos.pedimento_config_parameters import ( PedimentoConfigParametersCreate, - PedimentoConfigParametersUpdate, PedimentoConfigParametersResponse, + PedimentoConfigParametersUpdate, ) +from ..services.pedimento_config_parameters import PedimentoConfigParametersService - -router = APIRouter(prefix="/{pedimento_id}/config-parameters") - - -@router.get("/", response_model=PedimentoConfigParametersResponse) -async def get_config_parameters( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config parameters by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigParametersService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException(status_code=404, detail="Config parameters not found") - - return config - - -@router.post("/", response_model=PedimentoConfigParametersResponse, status_code=201) -async def create_config_parameters( - pedimento_id: int, - data: PedimentoConfigParametersCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config parameters""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigParametersService.create(db, data, tenant_id, company_id) - return config - - -@router.put("/", response_model=PedimentoConfigParametersResponse) -async def update_config_parameters( - pedimento_id: int, - data: PedimentoConfigParametersUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config parameters""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigParametersService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException(status_code=404, detail="Config parameters not found") - - return config - - -@router.delete("/", status_code=204) -async def delete_config_parameters( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config parameters""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigParametersService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException(status_code=404, detail="Config parameters not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigParametersService, + create_schema=PedimentoConfigParametersCreate, + update_schema=PedimentoConfigParametersUpdate, + response_schema=PedimentoConfigParametersResponse, + prefix="/{pedimento_id}/config-parameters", + tags=[], + resource_name="Config parameters", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py index 0fd6bea6..d2575d18 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py @@ -2,95 +2,25 @@ Routes for PedimentoConfigSurcharges CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_config_surcharges import PedimentoConfigSurchargesService from ..dtos.pedimento_config_surcharges import ( PedimentoConfigSurchargesCreate, - PedimentoConfigSurchargesUpdate, PedimentoConfigSurchargesResponse, + PedimentoConfigSurchargesUpdate, ) +from ..services.pedimento_config_surcharges import PedimentoConfigSurchargesService - -router = APIRouter(prefix="/{pedimento_id}/config-surcharges") - - -@router.get("/", response_model=PedimentoConfigSurchargesResponse) -async def get_config_surcharges( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config surcharges by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigSurchargesService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException(status_code=404, detail="Config surcharges not found") - - return config - - -@router.post("/", response_model=PedimentoConfigSurchargesResponse, status_code=201) -async def create_config_surcharges( - pedimento_id: int, - data: PedimentoConfigSurchargesCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config surcharges""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigSurchargesService.create(db, data, tenant_id, company_id) - return config - - -@router.put("/", response_model=PedimentoConfigSurchargesResponse) -async def update_config_surcharges( - pedimento_id: int, - data: PedimentoConfigSurchargesUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config surcharges""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigSurchargesService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException(status_code=404, detail="Config surcharges not found") - - return config - - -@router.delete("/", status_code=204) -async def delete_config_surcharges( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config surcharges""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigSurchargesService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException(status_code=404, detail="Config surcharges not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigSurchargesService, + create_schema=PedimentoConfigSurchargesCreate, + update_schema=PedimentoConfigSurchargesUpdate, + response_schema=PedimentoConfigSurchargesResponse, + prefix="/{pedimento_id}/config-surcharges", + tags=[], + resource_name="Config surcharges", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py index 8343ce16..c16ce5a8 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py @@ -2,107 +2,27 @@ Routes for PedimentoConfigUpdateRectification CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from ..dtos.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectificationCreate, + PedimentoConfigUpdateRectificationResponse, + PedimentoConfigUpdateRectificationUpdate, +) from ..services.pedimento_config_update_rectification import ( PedimentoConfigUpdateRectificationService, ) -from ..dtos.pedimento_config_update_rectification import ( - PedimentoConfigUpdateRectificationCreate, - PedimentoConfigUpdateRectificationUpdate, - PedimentoConfigUpdateRectificationResponse, -) - -router = APIRouter(prefix="/{pedimento_id}/config-update-rectification") - - -@router.get("/", response_model=PedimentoConfigUpdateRectificationResponse) -async def get_config_update_rectification( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config update rectification by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException( - status_code=404, detail="Config update rectification not found" - ) - - return config - - -@router.post( - "/", response_model=PedimentoConfigUpdateRectificationResponse, status_code=201 -) -async def create_config_update_rectification( - pedimento_id: int, - data: PedimentoConfigUpdateRectificationCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config update rectification""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigUpdateRectificationService.create( - db, data, tenant_id, company_id - ) - return config - - -@router.put("/", response_model=PedimentoConfigUpdateRectificationResponse) -async def update_config_update_rectification( - pedimento_id: int, - data: PedimentoConfigUpdateRectificationUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config update rectification""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigUpdateRectificationService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException( - status_code=404, detail="Config update rectification not found" - ) - - return config - - -@router.delete("/", status_code=204) -async def delete_config_update_rectification( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config update rectification""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigUpdateRectificationService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException( - status_code=404, detail="Config update rectification not found" - ) - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigUpdateRectificationService, + create_schema=PedimentoConfigUpdateRectificationCreate, + update_schema=PedimentoConfigUpdateRectificationUpdate, + response_schema=PedimentoConfigUpdateRectificationResponse, + prefix="/{pedimento_id}/config-update-rectification", + tags=[], + resource_name="Config update rectification", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py index e46c25b8..d026b2bf 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py @@ -2,95 +2,25 @@ Routes for PedimentoConfigUpdates CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_config_updates import PedimentoConfigUpdatesService from ..dtos.pedimento_config_updates import ( PedimentoConfigUpdatesCreate, - PedimentoConfigUpdatesUpdate, PedimentoConfigUpdatesResponse, + PedimentoConfigUpdatesUpdate, ) +from ..services.pedimento_config_updates import PedimentoConfigUpdatesService - -router = APIRouter(prefix="/{pedimento_id}/config-updates") - - -@router.get("/", response_model=PedimentoConfigUpdatesResponse) -async def get_config_updates( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get config updates by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigUpdatesService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not config: - raise HTTPException(status_code=404, detail="Config updates not found") - - return config - - -@router.post("/", response_model=PedimentoConfigUpdatesResponse, status_code=201) -async def create_config_updates( - pedimento_id: int, - data: PedimentoConfigUpdatesCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create config updates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigUpdatesService.create(db, data, tenant_id, company_id) - return config - - -@router.put("/", response_model=PedimentoConfigUpdatesResponse) -async def update_config_updates( - pedimento_id: int, - data: PedimentoConfigUpdatesUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update config updates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - config = PedimentoConfigUpdatesService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not config: - raise HTTPException(status_code=404, detail="Config updates not found") - - return config - - -@router.delete("/", status_code=204) -async def delete_config_updates( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete config updates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoConfigUpdatesService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException(status_code=404, detail="Config updates not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoConfigUpdatesService, + create_schema=PedimentoConfigUpdatesCreate, + update_schema=PedimentoConfigUpdatesUpdate, + response_schema=PedimentoConfigUpdatesResponse, + prefix="/{pedimento_id}/config-updates", + tags=[], + resource_name="Config updates", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py index 92bb05a4..255ffef1 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py @@ -2,18 +2,19 @@ Routes for PedimentoCustomsOffices CRUD operations """ +from typing import Any, Dict, List + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import Dict, Any, List -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_customs_offices import PedimentoCustomsOfficesService + from ..dtos.pedimento_customs_offices import ( PedimentoCustomsOfficesCreate, - PedimentoCustomsOfficesUpdate, PedimentoCustomsOfficesResponse, + PedimentoCustomsOfficesUpdate, ) - +from ..services.pedimento_customs_offices import PedimentoCustomsOfficesService router = APIRouter(prefix="/{pedimento_id}/customs-offices") diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py index 9e641747..4b3788cd 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py @@ -2,88 +2,25 @@ Routes for PedimentoDates CRUD operations """ -import logging -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_dates import PedimentoDatesService from ..dtos.pedimento_dates import ( PedimentoDatesCreate, - PedimentoDatesUpdate, PedimentoDatesResponse, + PedimentoDatesUpdate, ) +from ..services.pedimento_dates import PedimentoDatesService - -router = APIRouter(prefix="/{pedimento_id}/dates") -logger = logging.getLogger(__name__) - - -@router.get("/", response_model=PedimentoDatesResponse) -async def get_dates( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get dates by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - dates = PedimentoDatesService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not dates: - raise HTTPException(status_code=404, detail="Pedimento dates not found") - - return dates - - -@router.post("/", response_model=PedimentoDatesResponse, status_code=201) -async def create_dates( - data: PedimentoDatesCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create pedimento dates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - dates = PedimentoDatesService.create(db, data, tenant_id, company_id) - return dates - - -@router.put("/", response_model=PedimentoDatesResponse) -async def update_dates( - pedimento_id: int, - data: PedimentoDatesUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update pedimento dates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - dates = PedimentoDatesService.update(db, pedimento_id, tenant_id, company_id, data) - if not dates: - raise HTTPException(status_code=404, detail="Pedimento dates not found") - - return dates - - -@router.delete("/", status_code=204) -async def delete_dates( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete pedimento dates""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoDatesService.delete(db, pedimento_id, tenant_id, company_id) - if not success: - raise HTTPException(status_code=404, detail="Pedimento dates not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoDatesService, + create_schema=PedimentoDatesCreate, + update_schema=PedimentoDatesUpdate, + response_schema=PedimentoDatesResponse, + prefix="/{pedimento_id}/dates", + tags=[], + resource_name="Pedimento dates", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py index d658c479..3dcb2c4a 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py @@ -2,19 +2,19 @@ Routes for PedimentoDecrementables CRUD operations """ +from typing import Any, Dict, List + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import Dict, Any, List -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_decrementables import PedimentoDecrementablesService from ..dtos.pedimento_decrementables import ( PedimentoDecrementablesCreate, - PedimentoDecrementablesUpdate, PedimentoDecrementablesResponse, + PedimentoDecrementablesUpdate, ) - +from ..services.pedimento_decrementables import PedimentoDecrementablesService router = APIRouter(prefix="/{pedimento_id}/decrementables") diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py index 6fa3ecbb..fcfc2778 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py @@ -2,19 +2,19 @@ Routes for PedimentoIncrementables CRUD operations """ +from typing import Any, Dict, List + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import Dict, Any, List -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_incrementables import PedimentoIncrementablesService from ..dtos.pedimento_incrementables import ( PedimentoIncrementablesCreate, - PedimentoIncrementablesUpdate, PedimentoIncrementablesResponse, + PedimentoIncrementablesUpdate, ) - +from ..services.pedimento_incrementables import PedimentoIncrementablesService router = APIRouter(prefix="/{pedimento_id}/incrementables") diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py index 8e1a3030..c808b559 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py @@ -2,93 +2,25 @@ Routes for PedimentoIndexes CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_indexes import PedimentoIndexesService from ..dtos.pedimento_indexes import ( PedimentoIndexesCreate, - PedimentoIndexesUpdate, PedimentoIndexesResponse, + PedimentoIndexesUpdate, ) +from ..services.pedimento_indexes import PedimentoIndexesService - -router = APIRouter(prefix="/{pedimento_id}/indexes") - - -@router.get("/", response_model=PedimentoIndexesResponse) -async def get_indexes( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get indexes by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - indexes = PedimentoIndexesService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not indexes: - raise HTTPException(status_code=404, detail="Pedimento indexes not found") - - return indexes - - -@router.post("/", response_model=PedimentoIndexesResponse, status_code=201) -async def create_indexes( - pedimento_id: int, - data: PedimentoIndexesCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create pedimento indexes""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - indexes = PedimentoIndexesService.create(db, data, tenant_id, company_id) - return indexes - - -@router.put("/", response_model=PedimentoIndexesResponse) -async def update_indexes( - pedimento_id: int, - data: PedimentoIndexesUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update pedimento indexes""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - indexes = PedimentoIndexesService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not indexes: - raise HTTPException(status_code=404, detail="Pedimento indexes not found") - - return indexes - - -@router.delete("/", status_code=204) -async def delete_indexes( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete pedimento indexes""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoIndexesService.delete(db, pedimento_id, tenant_id, company_id) - if not success: - raise HTTPException(status_code=404, detail="Pedimento indexes not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoIndexesService, + create_schema=PedimentoIndexesCreate, + update_schema=PedimentoIndexesUpdate, + response_schema=PedimentoIndexesResponse, + prefix="/{pedimento_id}/indexes", + tags=[], + resource_name="Pedimento indexes", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py index fe863d7f..02dbfd0d 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py @@ -2,19 +2,19 @@ Routes for PedimentoPayments CRUD operations """ +from typing import Any, Dict, List + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import Dict, Any, List -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_payments import PedimentoPaymentsService from ..dtos.pedimento_payments import ( PedimentoPaymentsCreate, - PedimentoPaymentsUpdate, PedimentoPaymentsResponse, + PedimentoPaymentsUpdate, ) - +from ..services.pedimento_payments import PedimentoPaymentsService router = APIRouter(prefix="/{pedimento_id}/payments") diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py index 4a54af28..59dec341 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py @@ -2,107 +2,27 @@ Routes for PedimentoRectificationDestination CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from ..dtos.pedimento_rectification_destination import ( + PedimentoRectificationDestinationCreate, + PedimentoRectificationDestinationResponse, + PedimentoRectificationDestinationUpdate, +) from ..services.pedimento_rectification_destination import ( PedimentoRectificationDestinationService, ) -from ..dtos.pedimento_rectification_destination import ( - PedimentoRectificationDestinationCreate, - PedimentoRectificationDestinationUpdate, - PedimentoRectificationDestinationResponse, -) - -router = APIRouter(prefix="/{pedimento_id}/rectification-destination") - - -@router.get("/", response_model=PedimentoRectificationDestinationResponse) -async def get_rectification_destination( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get rectification destination by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - rectification = PedimentoRectificationDestinationService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not rectification: - raise HTTPException( - status_code=404, detail="Rectification destination not found" - ) - - return rectification - - -@router.post( - "/", response_model=PedimentoRectificationDestinationResponse, status_code=201 -) -async def create_rectification_destination( - pedimento_id: int, - data: PedimentoRectificationDestinationCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create rectification destination""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - rectification = PedimentoRectificationDestinationService.create( - db, data, tenant_id, company_id - ) - return rectification - - -@router.put("/", response_model=PedimentoRectificationDestinationResponse) -async def update_rectification_destination( - pedimento_id: int, - data: PedimentoRectificationDestinationUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update rectification destination""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - rectification = PedimentoRectificationDestinationService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not rectification: - raise HTTPException( - status_code=404, detail="Rectification destination not found" - ) - - return rectification - - -@router.delete("/", status_code=204) -async def delete_rectification_destination( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete rectification destination""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoRectificationDestinationService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException( - status_code=404, detail="Rectification destination not found" - ) - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoRectificationDestinationService, + create_schema=PedimentoRectificationDestinationCreate, + update_schema=PedimentoRectificationDestinationUpdate, + response_schema=PedimentoRectificationDestinationResponse, + prefix="/{pedimento_id}/rectification-destination", + tags=[], + resource_name="Rectification destination", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py index 0539c945..0b20fce4 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py @@ -2,99 +2,27 @@ Routes for PedimentoRectificationOrigin CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from ..dtos.pedimento_rectification_origin import ( + PedimentoRectificationOriginCreate, + PedimentoRectificationOriginResponse, + PedimentoRectificationOriginUpdate, +) from ..services.pedimento_rectification_origin import ( PedimentoRectificationOriginService, ) -from ..dtos.pedimento_rectification_origin import ( - PedimentoRectificationOriginCreate, - PedimentoRectificationOriginUpdate, - PedimentoRectificationOriginResponse, -) - -router = APIRouter(prefix="/{pedimento_id}/rectification-origin") - - -@router.get("/", response_model=PedimentoRectificationOriginResponse) -async def get_rectification_origin( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get rectification origin by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - rectification = PedimentoRectificationOriginService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not rectification: - raise HTTPException(status_code=404, detail="Rectification origin not found") - - return rectification - - -@router.post("/", response_model=PedimentoRectificationOriginResponse, status_code=201) -async def create_rectification_origin( - pedimento_id: int, - data: PedimentoRectificationOriginCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create rectification origin""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - rectification = PedimentoRectificationOriginService.create( - db, data, tenant_id, company_id - ) - return rectification - - -@router.put("/", response_model=PedimentoRectificationOriginResponse) -async def update_rectification_origin( - pedimento_id: int, - data: PedimentoRectificationOriginUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update rectification origin""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - rectification = PedimentoRectificationOriginService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not rectification: - raise HTTPException(status_code=404, detail="Rectification origin not found") - - return rectification - - -@router.delete("/", status_code=204) -async def delete_rectification_origin( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete rectification origin""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoRectificationOriginService.delete( - db, pedimento_id, tenant_id, company_id - ) - if not success: - raise HTTPException(status_code=404, detail="Rectification origin not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoRectificationOriginService, + create_schema=PedimentoRectificationOriginCreate, + update_schema=PedimentoRectificationOriginUpdate, + response_schema=PedimentoRectificationOriginResponse, + prefix="/{pedimento_id}/rectification-origin", + tags=[], + resource_name="Rectification origin", + parent_id_name="pedimento_id", + enable_list=False, + validate_parent_match=True, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py index bbf27928..f3015a49 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py @@ -2,19 +2,19 @@ Routes for PedimentoTransportMeans CRUD operations """ +from typing import Any, Dict, List + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import Dict, Any, List -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_transport_means import PedimentoTransportMeansService from ..dtos.pedimento_transport_means import ( PedimentoTransportMeansCreate, - PedimentoTransportMeansUpdate, PedimentoTransportMeansResponse, + PedimentoTransportMeansUpdate, ) - +from ..services.pedimento_transport_means import PedimentoTransportMeansService router = APIRouter(prefix="/{pedimento_id}/transport-means") diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py index 5f78c617..47af93a5 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py @@ -2,93 +2,25 @@ Routes for PedimentoValidation CRUD operations """ -from typing import Dict, Any -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from ..services.pedimento_validation import PedimentoValidationService from ..dtos.pedimento_validation import ( PedimentoValidationCreate, - PedimentoValidationUpdate, PedimentoValidationResponse, + PedimentoValidationUpdate, ) +from ..services.pedimento_validation import PedimentoValidationService - -router = APIRouter(prefix="/{pedimento_id}/validation") - - -@router.get("/", response_model=PedimentoValidationResponse) -async def get_validation( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get validation by pedimento ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - validation = PedimentoValidationService.get_by_pedimento_id( - db, pedimento_id, tenant_id, company_id - ) - if not validation: - raise HTTPException(status_code=404, detail="Pedimento validation not found") - - return validation - - -@router.post("/", response_model=PedimentoValidationResponse, status_code=201) -async def create_validation( - pedimento_id: int, - data: PedimentoValidationCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create pedimento validation""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - # Ensure pedimento_id matches - if data.pedimento_id != pedimento_id: - raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - validation = PedimentoValidationService.create(db, data, tenant_id, company_id) - return validation - - -@router.put("/", response_model=PedimentoValidationResponse) -async def update_validation( - pedimento_id: int, - data: PedimentoValidationUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update pedimento validation""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - validation = PedimentoValidationService.update( - db, pedimento_id, tenant_id, company_id, data - ) - if not validation: - raise HTTPException(status_code=404, detail="Pedimento validation not found") - - return validation - - -@router.delete("/", status_code=204) -async def delete_validation( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete pedimento validation""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentoValidationService.delete(db, pedimento_id, tenant_id, company_id) - if not success: - raise HTTPException(status_code=404, detail="Pedimento validation not found") - - return None +# Create router with generic CRUD routes for child resource +router = TenantCRUDRoutes( + service=PedimentoValidationService, + create_schema=PedimentoValidationCreate, + update_schema=PedimentoValidationUpdate, + response_schema=PedimentoValidationResponse, + prefix="/{pedimento_id}/validation", + tags=[], + resource_name="Pedimento validation", + parent_id_name="pedimento_id", + enable_list=False, # Child resource - no list endpoint + validate_parent_match=True, # Validate pedimento_id matches in create +).router diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py index 005d219a..97f6ffb5 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py @@ -2,115 +2,23 @@ Routes for Pedimentos CRUD operations """ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from typing import Dict, Any, Optional -from core.database import get_core_db -from core.security import validate_access_to_resource, get_current_user +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes +from ..dtos.pedimentos import PedimentosCreate, PedimentosResponse, PedimentosUpdate from ..services.pedimentos import PedimentosService -from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate, PedimentosResponse - -router = APIRouter() - - -@router.get("/", response_model=Dict[str, Any]) -async def list_pedimentos( - company_id: int = Query(..., description="Company ID"), - page: int = Query(1, ge=1, description="Page number"), - page_size: int = Query(50, ge=1, le=100, description="Page size"), - status: Optional[str] = Query(None, description="Filter by status"), - client_id: Optional[int] = Query(None, description="Filter by client ID"), - year: Optional[str] = Query(None, description="Filter by year"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get all pedimentos with pagination and filters""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - filters = {} - if status: - filters["status"] = status - if client_id: - filters["client_id"] = client_id - if year: - filters["year"] = year - - skip = (page - 1) * page_size - items, total = PedimentosService.get_all( - db, tenant_id, company_id, skip, page_size, filters - ) - - return { - "items": [PedimentosResponse.model_validate(item) for item in items], - "total": total, - "page": page, - "page_size": page_size, - } - - -@router.get("/{pedimento_id}", response_model=PedimentosResponse) -async def get_pedimento( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Get a pedimento by ID""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id, company_id) - if not pedimento: - raise HTTPException(status_code=404, detail="Pedimento not found") - - return pedimento - - -@router.post("/", response_model=PedimentosResponse, status_code=201) -async def create_pedimento( - data: PedimentosCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Create a new pedimento""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - pedimento = PedimentosService.create(db, data, tenant_id, company_id) - return pedimento - - -@router.put("/{pedimento_id}", response_model=PedimentosResponse) -async def update_pedimento( - pedimento_id: int, - data: PedimentosUpdate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Update a pedimento""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - pedimento = PedimentosService.update(db, pedimento_id, tenant_id, company_id, data) - if not pedimento: - raise HTTPException(status_code=404, detail="Pedimento not found") - - return pedimento - - -@router.delete("/{pedimento_id}", status_code=204) -async def delete_pedimento( - pedimento_id: int, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Delete a pedimento""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - success = PedimentosService.delete(db, pedimento_id, tenant_id, company_id) - if not success: - raise HTTPException(status_code=404, detail="Pedimento not found") - - return None +# Create router with generic CRUD routes +router = TenantCRUDRoutes( + service=PedimentosService, + create_schema=PedimentosCreate, + update_schema=PedimentosUpdate, + response_schema=PedimentosResponse, + prefix="", # No prefix here, will be added in main router + tags=[], + resource_name="Pedimento", + id_name="pedimento_id", + enable_list=True, # Enable GET / with pagination + enable_filters=True, # Enable status, client_id, year filters + default_page_size=50, + max_page_size=100, +).router diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py index fbb2c1f5..5f3a8141 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py @@ -3,13 +3,14 @@ Service layer for PedimentoConfigAdditional CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_additional import PedimentoConfigAdditional from ..dtos.pedimento_config_additional import ( PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalUpdate, ) +from ..models.pedimento_config_additional import PedimentoConfigAdditional class PedimentoConfigAdditionalService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py index 9b9ce5a5..50dbaeb0 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py @@ -3,13 +3,14 @@ Service layer for PedimentoConfigCalculations CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_calculations import PedimentoConfigCalculations from ..dtos.pedimento_config_calculations import ( PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsUpdate, ) +from ..models.pedimento_config_calculations import PedimentoConfigCalculations class PedimentoConfigCalculationsService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py index 6cb35de3..b3a7fab5 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py @@ -3,13 +3,14 @@ Service layer for PedimentoConfigParameters CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_parameters import PedimentoConfigParameters from ..dtos.pedimento_config_parameters import ( PedimentoConfigParametersCreate, PedimentoConfigParametersUpdate, ) +from ..models.pedimento_config_parameters import PedimentoConfigParameters class PedimentoConfigParametersService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py index 5e837314..95f15752 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py @@ -3,13 +3,14 @@ Service layer for PedimentoConfigSurcharges CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges from ..dtos.pedimento_config_surcharges import ( PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesUpdate, ) +from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges class PedimentoConfigSurchargesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py index 4d31443d..27fb724a 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py @@ -3,15 +3,16 @@ Service layer for PedimentoConfigUpdateRectification CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_update_rectification import ( - PedimentoConfigUpdateRectification, -) from ..dtos.pedimento_config_update_rectification import ( PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationUpdate, ) +from ..models.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectification, +) class PedimentoConfigUpdateRectificationService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py index 3e0c4364..b602f8f6 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py @@ -3,13 +3,14 @@ Service layer for PedimentoConfigUpdates CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_config_updates import PedimentoConfigUpdates from ..dtos.pedimento_config_updates import ( PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesUpdate, ) +from ..models.pedimento_config_updates import PedimentoConfigUpdates class PedimentoConfigUpdatesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py index ac8d865d..8f8db39e 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py @@ -3,13 +3,14 @@ Service layer for PedimentoCustomsOffices CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_customs_offices import PedimentoCustomsOffices from ..dtos.pedimento_customs_offices import ( PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesUpdate, ) +from ..models.pedimento_customs_offices import PedimentoCustomsOffices class PedimentoCustomsOfficesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py index 06c31e87..59e6c75a 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py @@ -4,10 +4,11 @@ Service layer for PedimentoDates CRUD operations import logging from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_dates import PedimentoDates from ..dtos.pedimento_dates import PedimentoDatesCreate, PedimentoDatesUpdate +from ..models.pedimento_dates import PedimentoDates logger = logging.getLogger(__name__) diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py index 39f8754a..73b7d690 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py @@ -3,13 +3,14 @@ Service layer for PedimentoDecrementables CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_decrementables import PedimentoDecrementables from ..dtos.pedimento_decrementables import ( PedimentoDecrementablesCreate, PedimentoDecrementablesUpdate, ) +from ..models.pedimento_decrementables import PedimentoDecrementables class PedimentoDecrementablesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py index 9d99b3a1..2cdffadc 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py @@ -3,13 +3,14 @@ Service layer for PedimentoIncrementables CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_incrementables import PedimentoIncrementables from ..dtos.pedimento_incrementables import ( PedimentoIncrementablesCreate, PedimentoIncrementablesUpdate, ) +from ..models.pedimento_incrementables import PedimentoIncrementables class PedimentoIncrementablesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py index c702dff3..f671a27c 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py @@ -3,10 +3,11 @@ Service layer for PedimentoIndexes CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_indexes import PedimentoIndexes from ..dtos.pedimento_indexes import PedimentoIndexesCreate, PedimentoIndexesUpdate +from ..models.pedimento_indexes import PedimentoIndexes class PedimentoIndexesService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py index 95471291..1b56e2b9 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py @@ -3,10 +3,11 @@ Service layer for PedimentoPayments CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_payments import PedimentoPayments from ..dtos.pedimento_payments import PedimentoPaymentsCreate, PedimentoPaymentsUpdate +from ..models.pedimento_payments import PedimentoPayments class PedimentoPaymentsService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py index 1790d1e1..de32adfc 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py @@ -3,15 +3,16 @@ Service layer for PedimentoRectificationDestination CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_rectification_destination import ( - PedimentoRectificationDestination, -) from ..dtos.pedimento_rectification_destination import ( PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationUpdate, ) +from ..models.pedimento_rectification_destination import ( + PedimentoRectificationDestination, +) class PedimentoRectificationDestinationService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py index 74ca4be4..080bec9f 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py @@ -3,13 +3,14 @@ Service layer for PedimentoRectificationOrigin CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_rectification_origin import PedimentoRectificationOrigin from ..dtos.pedimento_rectification_origin import ( PedimentoRectificationOriginCreate, PedimentoRectificationOriginUpdate, ) +from ..models.pedimento_rectification_origin import PedimentoRectificationOrigin class PedimentoRectificationOriginService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py index d5d395c5..ae88a3d1 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py @@ -3,13 +3,14 @@ Service layer for PedimentoTransportMeans CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_transport_means import PedimentoTransportMeans from ..dtos.pedimento_transport_means import ( PedimentoTransportMeansCreate, PedimentoTransportMeansUpdate, ) +from ..models.pedimento_transport_means import PedimentoTransportMeans class PedimentoTransportMeansService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py index 4ebae004..ecb17148 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py @@ -3,13 +3,14 @@ Service layer for PedimentoValidation CRUD operations """ from typing import Optional + from sqlalchemy.orm import Session -from ..models.pedimento_validation import PedimentoValidation from ..dtos.pedimento_validation import ( PedimentoValidationCreate, PedimentoValidationUpdate, ) +from ..models.pedimento_validation import PedimentoValidation class PedimentoValidationService: diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py index 5dcc1652..a669a591 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py @@ -2,13 +2,13 @@ Service layer for Pedimentos CRUD operations """ -from typing import List, Optional, Dict, Any -from sqlalchemy.orm import Session -from sqlalchemy import desc -from fastapi import HTTPException +from typing import Any, Dict, List, Optional + +from sqlalchemy import desc +from sqlalchemy.orm import Session -from ..models.pedimentos import Pedimentos from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate +from ..models.pedimentos import Pedimentos class PedimentosService: diff --git a/backend/api/v1/modules/a76/permission_rule_oct/dto.py b/backend/api/v1/modules/a76/permission_rule_oct/dto.py index 9b79ca74..7f86e5a4 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/dto.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/dto.py @@ -1,6 +1,7 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + class PermissionRuleOctBaseDTO(BaseModel): permission: str diff --git a/backend/api/v1/modules/a76/permission_rule_oct/models.py b/backend/api/v1/modules/a76/permission_rule_oct/models.py index 894276df..6cd92156 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/models.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/models.py @@ -1,17 +1,18 @@ from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class PermissionRuleOct(Base): +class PermissionRuleOct(Base, TenantScopedMixin): __tablename__ = "permission_rule_oct" __table_args__ = ( PrimaryKeyConstraint("id", name="permission_rule_oct_pkey"), @@ -30,8 +31,6 @@ class PermissionRuleOct(Base): {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) permission: Mapped[str] = mapped_column(String(20)) start_date: Mapped[Optional[int]] = mapped_column() diff --git a/backend/api/v1/modules/a76/permission_rule_oct/routes.py b/backend/api/v1/modules/a76/permission_rule_oct/routes.py index 4142a2e2..4bfa5d5a 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/routes.py @@ -1,9 +1,10 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import PermissionRuleOctCreateDTO, PermissionRuleOctResponseDTO from .services import PermissionRuleOctService @@ -22,7 +23,9 @@ async def list_permissions( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(PermissionRuleOctService).all() @@ -41,7 +44,9 @@ async def read_permission( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) permission = PermissionRuleOctService.get_permission_by_id(db, permission) if not permission: @@ -67,7 +72,9 @@ async def create_permission( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return PermissionRuleOctService.create_permission(db, permission_data) @@ -86,7 +93,9 @@ async def delete_permission( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) permission = PermissionRuleOctService.delete_permission(db, permission) if not permission: diff --git a/backend/api/v1/modules/a76/permission_rule_oct/services.py b/backend/api/v1/modules/a76/permission_rule_oct/services.py index 1fe0d0e9..ca9b87ac 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/services.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/services.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models class PermissionRuleOctService: diff --git a/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py b/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py index f4ee706b..b7dbdc04 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/test_permission_rule_oct.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_permission_rules(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_permission_rules(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_permission_rule_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/permission_rule_oct/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_permission_rule_forbidden(): response = client.post("/permission_rule_oct/", json={"rule": "Test Rule"}) assert response.status_code in (403, 405, 404) + def test_update_permission_rule_forbidden(): response = client.put("/permission_rule_oct/1", json={"rule": "Updated Rule"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 1d17e615..fb309176 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -5,26 +5,27 @@ Agrega todos los módulos de la aplicación from fastapi import APIRouter +from .aduanal.routes import router as customs_broker_router + # Importar routers de módulos from .auth import router as auth_router -from .tenants import router as tenants_router -from .user_tenant.routes import router as user_tenant_router -from .licenses import router as licenses_router -from .pedmientos.router import router as pedimentos_router +from .classes import router as classes_router from .client_and_provider import router as client_and_provider_router from .company import router as company_router -from .classes import router as classes_router -from .parts import router as parts_router -from .permission_rule_oct.routes import router as permission_rule_oct_router -from .package.routes import router as package_router -from .seal.routes import router as seal_router -from .fraction_rule_octave.routes import router as fraction_rule_octave_router from .country_rule_oct.routes import router as country_rule_oct_router -from .exchange_rate.routes import router as exchange_rate_router -from .trailers.routes import router as trailers_router -from .aduanal.routes import router as customs_broker_router from .drivers.routes import router as drivers_router +from .exchange_rate.routes import router as exchange_rate_router +from .fraction_rule_octave.routes import router as fraction_rule_octave_router +from .licenses import router as licenses_router +from .package.routes import router as package_router +from .parts import router as parts_router +from .pedmientos.router import router as pedimentos_router +from .permission_rule_oct.routes import router as permission_rule_oct_router +from .seal.routes import router as seal_router +from .tenants import router as tenants_router +from .trailers.routes import router as trailers_router from .transporters.routes import router as transporters_router +from .user_tenant.routes import router as user_tenant_router from .vehicles.routes import router as vehicles_router # Router principal @@ -55,8 +56,9 @@ router.include_router( ) router.include_router(exchange_rate_router, prefix="/a76", tags=["a76 / ExchangeRate"]) router.include_router(trailers_router, prefix="/a76", tags=["a76 / Trailers"]) -router.include_router(customs_broker_router, prefix="/a76", tags=["a76 / CustomsBroker"]) +router.include_router( + customs_broker_router, prefix="/a76", tags=["a76 / CustomsBroker"] +) router.include_router(drivers_router, prefix="/a76", tags=["a76 / Drivers"]) router.include_router(transporters_router, prefix="/a76", tags=["a76 / Transporters"]) router.include_router(vehicles_router, prefix="/a76", tags=["a76 / Vehicles"]) - diff --git a/backend/api/v1/modules/a76/seal/models.py b/backend/api/v1/modules/a76/seal/models.py index fa9ae245..4bea75a7 100644 --- a/backend/api/v1/modules/a76/seal/models.py +++ b/backend/api/v1/modules/a76/seal/models.py @@ -1,16 +1,16 @@ +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base from sqlalchemy import ( - Integer, - String, - ForeignKey, - PrimaryKeyConstraint, ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column -from core.database import Base -class Seal(Base): +class Seal(Base, TenantScopedMixin): __tablename__ = "seal" __table_args__ = ( PrimaryKeyConstraint("id", name="seal_pkey"), @@ -23,7 +23,5 @@ class Seal(Base): ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) seal: Mapped[str] = mapped_column(String(15)) diff --git a/backend/api/v1/modules/a76/seal/routes.py b/backend/api/v1/modules/a76/seal/routes.py index 2d7aab19..2d24132e 100644 --- a/backend/api/v1/modules/a76/seal/routes.py +++ b/backend/api/v1/modules/a76/seal/routes.py @@ -2,12 +2,13 @@ Routes for managing Seal entries. """ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import SealCreateDTO, SealResponseDTO from .services import SealService @@ -26,7 +27,9 @@ async def list_seals( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return db.query(SealService).all() @@ -45,7 +48,9 @@ async def read_seal( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) seal = SealService.get_seal_by_id(db, seal) if not seal: @@ -67,7 +72,9 @@ async def create_seal( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) return SealService.create_seal(db, seal_data) @@ -86,7 +93,9 @@ async def delete_seal( company_id = current_user.get("company_id") if not tenant_id or not company_id: - raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") + raise HTTPException( + status_code=403, detail="Access denied: Tenant or Company not found" + ) seal = SealService.delete_seal(db, seal) if not seal: diff --git a/backend/api/v1/modules/a76/seal/services.py b/backend/api/v1/modules/a76/seal/services.py index d62ad6fc..74751ac8 100644 --- a/backend/api/v1/modules/a76/seal/services.py +++ b/backend/api/v1/modules/a76/seal/services.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models """ Service layer for Seal. diff --git a/backend/api/v1/modules/a76/seal/test_seal.py b/backend/api/v1/modules/a76/seal/test_seal.py index 80e35b91..22ac8ece 100644 --- a/backend/api/v1/modules/a76/seal/test_seal.py +++ b/backend/api/v1/modules/a76/seal/test_seal.py @@ -1,12 +1,14 @@ import pytest -from fastapi.testclient import TestClient -from .routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient + +from .routes import router app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_seals(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,16 +18,19 @@ def test_list_seals(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_seal_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/seal/invalid_id", headers=headers) assert response.status_code == 404 + def test_create_seal_forbidden(): response = client.post("/seal/", json={"name": "Test Seal"}) assert response.status_code in (403, 405, 404) + def test_update_seal_forbidden(): response = client.put("/seal/1", json={"name": "Updated Seal"}) - assert response.status_code in (403, 405, 404) \ No newline at end of file + assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/a76/tenants/dto.py b/backend/api/v1/modules/a76/tenants/dto.py index c19dbfe9..46094676 100644 --- a/backend/api/v1/modules/a76/tenants/dto.py +++ b/backend/api/v1/modules/a76/tenants/dto.py @@ -3,10 +3,11 @@ DTOs (Data Transfer Objects) para módulo de tenants Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ -from pydantic import BaseModel, Field, EmailStr -from typing import Optional from datetime import datetime from enum import Enum +from typing import Optional + +from pydantic import BaseModel, EmailStr, Field class TenantTypeDTO(str, Enum): diff --git a/backend/api/v1/modules/a76/tenants/models.py b/backend/api/v1/modules/a76/tenants/models.py index adf88dff..29453c61 100644 --- a/backend/api/v1/modules/a76/tenants/models.py +++ b/backend/api/v1/modules/a76/tenants/models.py @@ -2,13 +2,15 @@ Modelos ORM para gestión de tenants """ -from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum as SQLEnum -from sqlalchemy.sql import func -from sqlalchemy.orm import Mapped, mapped_column, relationship -from datetime import datetime -from typing import List, TYPE_CHECKING -from core.database import Base import enum +from typing import TYPE_CHECKING, List + +from api.v1.common.base_models import TimestampMixin +from core.database import Base +from sqlalchemy import Boolean, Column +from sqlalchemy import Enum as SQLEnum +from sqlalchemy import Integer, String, Text +from sqlalchemy.orm import Mapped, relationship if TYPE_CHECKING: from api.v1.modules.a76.user_tenant.models import UserTenant @@ -21,7 +23,7 @@ class TenantType(enum.Enum): DEDICATED = "dedicated" # BD dedicada -class Tenant(Base): +class Tenant(Base, TimestampMixin): """ Modelo de Tenant - Cliente/Organización en el sistema Cada tenant puede tener BD compartida o dedicada @@ -51,15 +53,6 @@ class Tenant(Base): # Estado is_active = Column(Boolean, default=True, nullable=False) - # 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) - # Relación con UserTenant user_relations: Mapped[List["UserTenant"]] = relationship( "UserTenant", back_populates="tenant" diff --git a/backend/api/v1/modules/a76/tenants/routes.py b/backend/api/v1/modules/a76/tenants/routes.py index c6679dec..ef3c5a5f 100644 --- a/backend/api/v1/modules/a76/tenants/routes.py +++ b/backend/api/v1/modules/a76/tenants/routes.py @@ -2,17 +2,16 @@ Endpoints API para gestión de tenants """ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from typing import List - from core.database import get_core_db from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + from .dto import ( TenantCreateDTO, - TenantUpdateDTO, - TenantResponseDTO, TenantListResponseDTO, + TenantResponseDTO, + TenantUpdateDTO, ) from .service import TenantService @@ -56,7 +55,7 @@ async def list_tenants( query = db.query(Tenant) if active_only: - query = query.filter(Tenant.is_active == True) + query = query.filter(Tenant.is_active) total = query.count() return TenantListResponseDTO( diff --git a/backend/api/v1/modules/a76/tenants/service.py b/backend/api/v1/modules/a76/tenants/service.py index 779502be..c3169e75 100644 --- a/backend/api/v1/modules/a76/tenants/service.py +++ b/backend/api/v1/modules/a76/tenants/service.py @@ -2,15 +2,16 @@ Capa de servicio para lógica de negocio de tenants """ -from sqlalchemy.orm import Session -from sqlalchemy.exc import IntegrityError -from fastapi import HTTPException -from typing import List, Optional import json import logging +from typing import List, Optional +from fastapi import HTTPException +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from .dto import TenantCreateDTO, TenantResponseDTO, TenantUpdateDTO from .models import Tenant, TenantType -from .dto import TenantCreateDTO, TenantUpdateDTO, TenantResponseDTO logger = logging.getLogger(__name__) @@ -117,7 +118,7 @@ class TenantService: query = self.db.query(Tenant) if active_only: - query = query.filter(Tenant.is_active == True) + query = query.filter(Tenant.is_active) tenants = query.offset(skip).limit(limit).all() return [TenantResponseDTO.model_validate(t) for t in tenants] diff --git a/backend/api/v1/modules/a76/trailers/dto.py b/backend/api/v1/modules/a76/trailers/dto.py index b35316a3..f8fcd81e 100644 --- a/backend/api/v1/modules/a76/trailers/dto.py +++ b/backend/api/v1/modules/a76/trailers/dto.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + + class TrailerBaseDTO(BaseModel): trailer_number: str ace_trailer_number: Optional[str] @@ -14,9 +16,11 @@ class TrailerBaseDTO(BaseModel): company_id: str tenant_id: str + class TrailerCreateDTO(TrailerBaseDTO): pass + class TrailerResponseDTO(TrailerBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/trailers/models.py b/backend/api/v1/modules/a76/trailers/models.py index e052d0ad..a76777d1 100644 --- a/backend/api/v1/modules/a76/trailers/models.py +++ b/backend/api/v1/modules/a76/trailers/models.py @@ -1,18 +1,24 @@ -from sqlalchemy import Column, Integer, String, ForeignKey +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import Column, ForeignKey, ForeignKeyConstraint, String -class Trailer(Base): + +class Trailer(Base, TenantScopedMixin): __tablename__ = "trailer" - __table_args__ = {"schema": "a76"} + __table_args__ = ( + ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + {"schema": "a76"}, + ) trailer_number = Column(String(20), primary_key=True, nullable=False) ace_trailer_number = Column(String(10), nullable=True) - trailer_type_key = Column(String(2), ForeignKey("a76.trailer_type.trailer_type_key"), nullable=True) + trailer_type_key = Column( + String(2), ForeignKey("a76.trailer_type.trailer_type_key"), nullable=True + ) seal = Column(String(15), nullable=True) entity_code = Column(String(1), nullable=True) plate_number = Column(String(17), nullable=True) state = Column(String(30), nullable=True) country = Column(String(3), nullable=True) container_key = Column(String(3), nullable=True) - company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/trailers/routes.py b/backend/api/v1/modules/a76/trailers/routes.py index e1da08ff..e353a5cc 100644 --- a/backend/api/v1/modules/a76/trailers/routes.py +++ b/backend/api/v1/modules/a76/trailers/routes.py @@ -1,10 +1,12 @@ +from core.database import get_core_db from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from . import services, dto -from core.database import get_core_db + +from . import dto, services router = APIRouter() + @router.get("/trailers/{trailer_number}", response_model=dto.TrailerResponseDTO) def get_trailer(trailer_number: str, db: Session = Depends(get_core_db)): trailer = services.TrailerService.get_trailer_by_number(db, trailer_number) @@ -12,13 +14,17 @@ def get_trailer(trailer_number: str, db: Session = Depends(get_core_db)): raise HTTPException(status_code=404, detail="Trailer not found") return trailer + @router.post("/trailers", response_model=dto.TrailerResponseDTO) -def create_trailer(trailer_data: dto.TrailerCreateDTO, db: Session = Depends(get_core_db)): +def create_trailer( + trailer_data: dto.TrailerCreateDTO, db: Session = Depends(get_core_db) +): return services.TrailerService.create_trailer(db, trailer_data) + @router.delete("/trailers/{trailer_number}", response_model=dto.TrailerResponseDTO) def delete_trailer(trailer_number: str, db: Session = Depends(get_core_db)): trailer = services.TrailerService.delete_trailer(db, trailer_number) if not trailer: raise HTTPException(status_code=404, detail="Trailer not found") - return trailer \ No newline at end of file + return trailer diff --git a/backend/api/v1/modules/a76/trailers/services.py b/backend/api/v1/modules/a76/trailers/services.py index 2629fd4a..fc9669d5 100644 --- a/backend/api/v1/modules/a76/trailers/services.py +++ b/backend/api/v1/modules/a76/trailers/services.py @@ -1,10 +1,16 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class TrailerService: @staticmethod def get_trailer_by_number(db: Session, trailer_number: str): - return db.query(models.Trailer).filter(models.Trailer.trailer_number == trailer_number).first() + return ( + db.query(models.Trailer) + .filter(models.Trailer.trailer_number == trailer_number) + .first() + ) @staticmethod def create_trailer(db: Session, trailer_data: dto.TrailerCreateDTO): @@ -20,4 +26,4 @@ class TrailerService: if trailer: db.delete(trailer) db.commit() - return trailer \ No newline at end of file + return trailer diff --git a/backend/api/v1/modules/a76/transporters/dto.py b/backend/api/v1/modules/a76/transporters/dto.py index c62596c5..df548455 100644 --- a/backend/api/v1/modules/a76/transporters/dto.py +++ b/backend/api/v1/modules/a76/transporters/dto.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + + class TransporterBaseDTO(BaseModel): transporter_key: str name: Optional[str] @@ -24,9 +26,11 @@ class TransporterBaseDTO(BaseModel): company_id: str tenant_id: str + class TransporterCreateDTO(TransporterBaseDTO): pass + class TransporterResponseDTO(TransporterBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/transporters/models.py b/backend/api/v1/modules/a76/transporters/models.py index 2ad4aff0..d0b13700 100644 --- a/backend/api/v1/modules/a76/transporters/models.py +++ b/backend/api/v1/modules/a76/transporters/models.py @@ -1,9 +1,19 @@ -from sqlalchemy import Column, Integer, String, ForeignKey +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import Column, ForeignKeyConstraint, String -class Transporter(Base): + +class Transporter(Base, TenantScopedMixin): __tablename__ = "transporter" - __table_args__ = {"schema": "a76"} + __table_args__ = ( + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_transporter_tenants" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_transporter_company" + ), + {"schema": "a76"}, + ) transporter_key = Column(String(5), primary_key=True, nullable=False) name = Column(String(256), nullable=True) @@ -24,5 +34,3 @@ class Transporter(Base): ftp_password = Column(String(100), nullable=True) ftp_directory = Column(String(1000), nullable=True) filler_code = Column(String(4), nullable=True) - company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/transporters/routes.py b/backend/api/v1/modules/a76/transporters/routes.py index 9d1bd1f4..bae1833b 100644 --- a/backend/api/v1/modules/a76/transporters/routes.py +++ b/backend/api/v1/modules/a76/transporters/routes.py @@ -1,46 +1,52 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import TransporterCreateDTO, TransporterResponseDTO from .services import TransporterService router = APIRouter(prefix="/transporters", tags=["Transporters"]) + @router.get("/", response_model=List[TransporterResponseDTO]) async def list_transporters( - 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) ): return db.query(TransporterService).all() + @router.get("/{transporter_key}", response_model=TransporterResponseDTO) async def read_transporter( transporter_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): transporter = TransporterService.get_transporter_by_key(db, transporter_key) if not transporter: raise HTTPException(status_code=404, detail="Transporter not found") return transporter -@router.post("/", response_model=TransporterResponseDTO, status_code=status.HTTP_201_CREATED) + +@router.post( + "/", response_model=TransporterResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_transporter( transporter_data: TransporterCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): return TransporterService.create_transporter(db, transporter_data) + @router.delete("/{transporter_key}", status_code=status.HTTP_204_NO_CONTENT) async def delete_transporter( transporter_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): transporter = TransporterService.delete_transporter(db, transporter_key) if not transporter: - raise HTTPException(status_code=404, detail="Transporter not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="Transporter not found") diff --git a/backend/api/v1/modules/a76/transporters/services.py b/backend/api/v1/modules/a76/transporters/services.py index ce1cb8c5..ed1f0569 100644 --- a/backend/api/v1/modules/a76/transporters/services.py +++ b/backend/api/v1/modules/a76/transporters/services.py @@ -1,10 +1,16 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class TransporterService: @staticmethod def get_transporter_by_key(db: Session, transporter_key: str): - return db.query(models.Transporter).filter(models.Transporter.transporter_key == transporter_key).first() + return ( + db.query(models.Transporter) + .filter(models.Transporter.transporter_key == transporter_key) + .first() + ) @staticmethod def create_transporter(db: Session, transporter_data: dto.TransporterCreateDTO): @@ -20,4 +26,4 @@ class TransporterService: if transporter: db.delete(transporter) db.commit() - return transporter \ No newline at end of file + return transporter diff --git a/backend/api/v1/modules/a76/user_tenant/dto.py b/backend/api/v1/modules/a76/user_tenant/dto.py index 0575d23a..8c648aad 100644 --- a/backend/api/v1/modules/a76/user_tenant/dto.py +++ b/backend/api/v1/modules/a76/user_tenant/dto.py @@ -2,9 +2,10 @@ DTOs para gestión de relaciones usuario-tenant """ -from pydantic import BaseModel, Field -from typing import Optional from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field class AddUserToTenantRequestDTO(BaseModel): diff --git a/backend/api/v1/modules/a76/user_tenant/models.py b/backend/api/v1/modules/a76/user_tenant/models.py index feb5ab68..bc4bf6df 100644 --- a/backend/api/v1/modules/a76/user_tenant/models.py +++ b/backend/api/v1/modules/a76/user_tenant/models.py @@ -2,25 +2,18 @@ Modelo de relación entre usuarios (Keycloak) y tenants """ -from sqlalchemy import ( - Integer, - String, - DateTime, - Boolean, - UniqueConstraint, - ForeignKeyConstraint, -) -from sqlalchemy.sql import func -from sqlalchemy.orm import Mapped, mapped_column, relationship -from datetime import datetime -from typing import Optional, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base +from sqlalchemy import Boolean, ForeignKeyConstraint, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: from api.v1.modules.a76.tenants.models import Tenant -class UserTenant(Base): +class UserTenant(Base, TenantScopedMixin, TimestampMixin): """ Relación muchos-a-muchos entre usuarios de Keycloak y tenants @@ -46,24 +39,11 @@ class UserTenant(Base): String(255), nullable=False, index=True ) - # ID del tenant - tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - # Estado de la relación is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) # Información adicional - Rol del usuario en este tenant (opcional) role: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) - # Timestamps - created_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, server_default=func.now() - ) - updated_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, server_default=func.now(), onupdate=func.now() - ) - deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) - # Relación con Tenant tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="user_relations") diff --git a/backend/api/v1/modules/a76/user_tenant/routes.py b/backend/api/v1/modules/a76/user_tenant/routes.py index 13bc168d..67f7603d 100644 --- a/backend/api/v1/modules/a76/user_tenant/routes.py +++ b/backend/api/v1/modules/a76/user_tenant/routes.py @@ -2,21 +2,22 @@ Rutas para gestión de relaciones usuario-tenant """ -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user -from .service import UserTenantService +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + from .dto import ( AddUserToTenantRequestDTO, RemoveUserFromTenantRequestDTO, + TenantBasicInfoDTO, UpdateUserRoleRequestDTO, UserTenantResponseDTO, UserTenantsResponseDTO, - TenantBasicInfoDTO, ) +from .service import UserTenantService router = APIRouter(prefix="/user-tenants", tags=["User-Tenant Relations"]) diff --git a/backend/api/v1/modules/a76/user_tenant/service.py b/backend/api/v1/modules/a76/user_tenant/service.py index e168a418..d474a672 100644 --- a/backend/api/v1/modules/a76/user_tenant/service.py +++ b/backend/api/v1/modules/a76/user_tenant/service.py @@ -2,14 +2,15 @@ Servicio para gestionar relaciones entre usuarios y tenants """ -from sqlalchemy.orm import Session -from sqlalchemy import and_ -from typing import List, Optional -from fastapi import HTTPException import logging +from typing import List, Optional + +from fastapi import HTTPException +from sqlalchemy import and_ +from sqlalchemy.orm import Session -from .models import UserTenant from ..tenants.models import Tenant +from .models import UserTenant logger = logging.getLogger(__name__) @@ -58,9 +59,6 @@ class UserTenantService: existing.role = role self.db.commit() self.db.refresh(existing) - logger.info( - f"Reactivated user {keycloak_user_id} in tenant {tenant_id}" - ) return existing else: raise HTTPException( @@ -78,8 +76,6 @@ class UserTenantService: self.db.add(user_tenant) self.db.commit() self.db.refresh(user_tenant) - - logger.info(f"Added user {keycloak_user_id} to tenant {tenant_id}") return user_tenant def remove_user_from_tenant( @@ -115,11 +111,9 @@ class UserTenantService: if soft_delete: user_tenant.is_active = False self.db.commit() - logger.info(f"Deactivated user {keycloak_user_id} from tenant {tenant_id}") else: self.db.delete(user_tenant) self.db.commit() - logger.info(f"Deleted user {keycloak_user_id} from tenant {tenant_id}") return True @@ -138,7 +132,7 @@ class UserTenantService: .filter( and_( UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.is_active == True, + UserTenant.is_active, ) ) .all() @@ -148,7 +142,7 @@ class UserTenantService: tenants = ( self.db.query(Tenant) - .filter(and_(Tenant.id.in_(tenant_ids), Tenant.is_active == True)) + .filter(and_(Tenant.id.in_(tenant_ids), Tenant.is_active)) .all() ) @@ -166,9 +160,7 @@ class UserTenantService: """ return ( self.db.query(UserTenant) - .filter( - and_(UserTenant.tenant_id == tenant_id, UserTenant.is_active == True) - ) + .filter(and_(UserTenant.tenant_id == tenant_id, UserTenant.is_active)) .all() ) @@ -189,7 +181,7 @@ class UserTenantService: and_( UserTenant.keycloak_user_id == keycloak_user_id, UserTenant.tenant_id == tenant_id, - UserTenant.is_active == True, + UserTenant.is_active, ) ) .first() @@ -230,8 +222,4 @@ class UserTenantService: user_tenant.role = role self.db.commit() self.db.refresh(user_tenant) - - logger.info( - f"Updated role for user {keycloak_user_id} in tenant {tenant_id} to {role}" - ) return user_tenant diff --git a/backend/api/v1/modules/a76/vehicles/dto.py b/backend/api/v1/modules/a76/vehicles/dto.py index 2ab09888..52e45241 100644 --- a/backend/api/v1/modules/a76/vehicles/dto.py +++ b/backend/api/v1/modules/a76/vehicles/dto.py @@ -1,6 +1,8 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + + class VehicleBaseDTO(BaseModel): vehicle_key: str ace_vehicle_key: Optional[str] @@ -31,9 +33,11 @@ class VehicleBaseDTO(BaseModel): company_id: str tenant_id: str + class VehicleCreateDTO(VehicleBaseDTO): pass + class VehicleResponseDTO(VehicleBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/vehicles/models.py b/backend/api/v1/modules/a76/vehicles/models.py index 6b50391e..c60f0383 100644 --- a/backend/api/v1/modules/a76/vehicles/models.py +++ b/backend/api/v1/modules/a76/vehicles/models.py @@ -1,9 +1,19 @@ -from sqlalchemy import Column, String, DECIMAL, Integer, ForeignKey +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import DECIMAL, Column, ForeignKeyConstraint, Integer, String -class Vehicle(Base): + +class Vehicle(Base, TenantScopedMixin): __tablename__ = "vehicle" - __table_args__ = {"schema": "a76"} + __table_args__ = ( + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_vehicle_tenants" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_vehicle_company" + ), + {"schema": "a76"}, + ) vehicle_key = Column(String(14), primary_key=True, nullable=False) ace_vehicle_key = Column(String(10), nullable=True) @@ -31,5 +41,3 @@ class Vehicle(Base): sct_permission = Column(String(40), nullable=True) color = Column(String(20), nullable=True) container_key = Column(String(3), nullable=True) - company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/vehicles/routes.py b/backend/api/v1/modules/a76/vehicles/routes.py index 13dbc83f..eb699241 100644 --- a/backend/api/v1/modules/a76/vehicles/routes.py +++ b/backend/api/v1/modules/a76/vehicles/routes.py @@ -1,46 +1,52 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + from .dto import VehicleCreateDTO, VehicleResponseDTO from .services import VehicleService router = APIRouter(prefix="/vehicles", tags=["Vehicles"]) + @router.get("/", response_model=List[VehicleResponseDTO]) async def list_vehicles( - 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) ): return db.query(VehicleService).all() + @router.get("/{vehicle_key}", response_model=VehicleResponseDTO) async def read_vehicle( vehicle_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): vehicle = VehicleService.get_vehicle_by_key(db, vehicle_key) if not vehicle: raise HTTPException(status_code=404, detail="Vehicle not found") return vehicle -@router.post("/", response_model=VehicleResponseDTO, status_code=status.HTTP_201_CREATED) + +@router.post( + "/", response_model=VehicleResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_vehicle( vehicle_data: VehicleCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): return VehicleService.create_vehicle(db, vehicle_data) + @router.delete("/{vehicle_key}", status_code=status.HTTP_204_NO_CONTENT) async def delete_vehicle( vehicle_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): vehicle = VehicleService.delete_vehicle(db, vehicle_key) if not vehicle: - raise HTTPException(status_code=404, detail="Vehicle not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="Vehicle not found") diff --git a/backend/api/v1/modules/a76/vehicles/services.py b/backend/api/v1/modules/a76/vehicles/services.py index 5aae3a0f..53854b8f 100644 --- a/backend/api/v1/modules/a76/vehicles/services.py +++ b/backend/api/v1/modules/a76/vehicles/services.py @@ -1,10 +1,16 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class VehicleService: @staticmethod def get_vehicle_by_key(db: Session, vehicle_key: str): - return db.query(models.Vehicle).filter(models.Vehicle.vehicle_key == vehicle_key).first() + return ( + db.query(models.Vehicle) + .filter(models.Vehicle.vehicle_key == vehicle_key) + .first() + ) @staticmethod def create_vehicle(db: Session, vehicle_data: dto.VehicleCreateDTO): @@ -20,4 +26,4 @@ class VehicleService: if vehicle: db.delete(vehicle) db.commit() - return vehicle \ No newline at end of file + return vehicle diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py index b41c472d..b36e3d0e 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + class CodePedimentoRegimenDTO(BaseModel): id: Optional[int] = None diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py index 5f39b04c..0593cafe 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py @@ -1,13 +1,8 @@ from typing import TYPE_CHECKING, Optional -from sqlalchemy import ( - String, - Integer, - ForeignKey, - ForeignKeyConstraint, - PrimaryKeyConstraint, -) -from sqlalchemy.orm import mapped_column, Mapped, relationship + from core.database import Base +from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: from ..pedimento_codes.models import PedimentoCode diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py index d946e919..de7fc176 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import CodePedimentoRegimen -from .dto import CodePedimentoRegimenDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import CodePedimentoRegimenDTO +from .models import CodePedimentoRegimen router = APIRouter(prefix="/code-pedimento-regimens") diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py index c0eb35f7..4f9f6041 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.code_pedimento_regimens.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/conftest.py b/backend/api/v1/modules/public/reference_data/conftest.py index a31d4689..2b5b3377 100644 --- a/backend/api/v1/modules/public/reference_data/conftest.py +++ b/backend/api/v1/modules/public/reference_data/conftest.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.transport_types.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient @pytest.fixture(scope="session") diff --git a/backend/api/v1/modules/public/reference_data/containers/dto.py b/backend/api/v1/modules/public/reference_data/containers/dto.py index 872729ce..e16fea52 100644 --- a/backend/api/v1/modules/public/reference_data/containers/dto.py +++ b/backend/api/v1/modules/public/reference_data/containers/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class ContainerDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/containers/models.py b/backend/api/v1/modules/public/reference_data/containers/models.py index 94ad14b6..2db64a49 100644 --- a/backend/api/v1/modules/public/reference_data/containers/models.py +++ b/backend/api/v1/modules/public/reference_data/containers/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class Container(Base): diff --git a/backend/api/v1/modules/public/reference_data/containers/routes.py b/backend/api/v1/modules/public/reference_data/containers/routes.py index 944098b4..ea0f18e1 100644 --- a/backend/api/v1/modules/public/reference_data/containers/routes.py +++ b/backend/api/v1/modules/public/reference_data/containers/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import Container -from .dto import ContainerDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import ContainerDTO +from .models import Container router = APIRouter(prefix="/containers") diff --git a/backend/api/v1/modules/public/reference_data/containers/test_containers.py b/backend/api/v1/modules/public/reference_data/containers/test_containers.py index c6381063..3527be0c 100644 --- a/backend/api/v1/modules/public/reference_data/containers/test_containers.py +++ b/backend/api/v1/modules/public/reference_data/containers/test_containers.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.containers.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/countries/dto.py b/backend/api/v1/modules/public/reference_data/countries/dto.py index 8814972f..3b197100 100644 --- a/backend/api/v1/modules/public/reference_data/countries/dto.py +++ b/backend/api/v1/modules/public/reference_data/countries/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class CountryDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/countries/models.py b/backend/api/v1/modules/public/reference_data/countries/models.py index 315d2cf1..483ac234 100644 --- a/backend/api/v1/modules/public/reference_data/countries/models.py +++ b/backend/api/v1/modules/public/reference_data/countries/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint, Index -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import Index, PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class Country(Base): diff --git a/backend/api/v1/modules/public/reference_data/countries/routes.py b/backend/api/v1/modules/public/reference_data/countries/routes.py index 0fdb0ee1..626028b9 100644 --- a/backend/api/v1/modules/public/reference_data/countries/routes.py +++ b/backend/api/v1/modules/public/reference_data/countries/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import Country -from .dto import CountryDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import CountryDTO +from .models import Country router = APIRouter(prefix="/countries") diff --git a/backend/api/v1/modules/public/reference_data/countries/test_countries.py b/backend/api/v1/modules/public/reference_data/countries/test_countries.py index c3b5f73a..ab4da9f8 100644 --- a/backend/api/v1/modules/public/reference_data/countries/test_countries.py +++ b/backend/api/v1/modules/public/reference_data/countries/test_countries.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.countries.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/currency_types/dto.py b/backend/api/v1/modules/public/reference_data/currency_types/dto.py index cf2b34ce..b78be839 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class CurrencyTypeDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/currency_types/models.py b/backend/api/v1/modules/public/reference_data/currency_types/models.py index 176babe9..53f24b15 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/models.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class CurrencyType(Base): diff --git a/backend/api/v1/modules/public/reference_data/currency_types/routes.py b/backend/api/v1/modules/public/reference_data/currency_types/routes.py index d8fd6bec..988bf086 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import CurrencyType -from .dto import CurrencyTypeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import CurrencyTypeDTO +from .models import CurrencyType router = APIRouter(prefix="/currency-types") diff --git a/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py index 123705b9..6e98d40a 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.currency_types.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py index 3db9ccdc..410d5028 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class CustomsSectionDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/models.py b/backend/api/v1/modules/public/reference_data/customs_sections/models.py index 22e9e3bc..90d3e18d 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import mapped_column class CustomsSection(Base): diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py index 8718ec1e..62ceac7c 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import CustomsSection -from .dto import CustomsSectionDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import CustomsSectionDTO +from .models import CustomsSection router = APIRouter(prefix="/customs-sections") diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py index 1fd3924d..c32571af 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.customs_sections.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py index e2087b9f..57bb73db 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class CustomsWarehouseDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py index 362b534f..80dd65d9 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class CustomsWarehouse(Base): diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py index 928e5d03..802e1c9c 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import CustomsWarehouse -from .dto import CustomsWarehouseDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import CustomsWarehouseDTO +from .models import CustomsWarehouse router = APIRouter(prefix="/customs-warehouses") diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py index 5cbb106f..9d23e837 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.customs_warehouses.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/dto.py b/backend/api/v1/modules/public/reference_data/incoterms/dto.py index b95e485d..03c40872 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/dto.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class IncotermDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/incoterms/models.py b/backend/api/v1/modules/public/reference_data/incoterms/models.py index 3e68e48e..7954fa9d 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/models.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class Incoterm(Base): diff --git a/backend/api/v1/modules/public/reference_data/incoterms/routes.py b/backend/api/v1/modules/public/reference_data/incoterms/routes.py index 5ffec8f6..20a7e315 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/routes.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import Incoterm -from .dto import IncotermDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import IncotermDTO +from .models import Incoterm router = APIRouter(prefix="/incoterms") diff --git a/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py index 890951f2..a8394a68 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.incoterms.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py index c6245814..7e84a068 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + class InvoiceTypeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=5) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/models.py b/backend/api/v1/modules/public/reference_data/invoice_types/models.py index 6e0a6785..f8b04633 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/models.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class InvoiceType(Base): diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py index 9078ee13..936d7fb3 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import InvoiceType -from .dto import InvoiceTypeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import InvoiceTypeDTO +from .models import InvoiceType router = APIRouter(prefix="/invoice-types") diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py index ce80a933..5a314b93 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.invoice_types.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/material_types/dto.py b/backend/api/v1/modules/public/reference_data/material_types/dto.py index 6619710b..1bcdc3ee 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/material_types/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class MaterialTypeDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/material_types/models.py b/backend/api/v1/modules/public/reference_data/material_types/models.py index a537f721..25b27f3f 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/models.py +++ b/backend/api/v1/modules/public/reference_data/material_types/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class MaterialType(Base): diff --git a/backend/api/v1/modules/public/reference_data/material_types/routes.py b/backend/api/v1/modules/public/reference_data/material_types/routes.py index 2648b2da..a25d3fb9 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/material_types/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import MaterialType -from .dto import MaterialTypeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import MaterialTypeDTO +from .models import MaterialType router = APIRouter(prefix="/material-types") diff --git a/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py index c9bf825f..1c5d950c 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py +++ b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.material_types.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py index 8ce56d6a..b65ec3b2 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class PaymentMethodDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/models.py b/backend/api/v1/modules/public/reference_data/payment_methods/models.py index 3162c7e1..174019db 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class PaymentMethod(Base): diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py index 3624878d..465630fc 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import PaymentMethod -from .dto import PaymentMethodDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import PaymentMethodDTO +from .models import PaymentMethod router = APIRouter(prefix="/payment-methods") diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py index c0ca8296..4d80a60e 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.payment_methods.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py index d9e918dd..ac7dc60c 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py @@ -1,6 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict -from typing import Optional +from pydantic import BaseModel, ConfigDict, Field class PedimentoCodeDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py index 8c951c07..ee5d74f2 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING, List -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped, relationship + from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: from ..code_pedimento_regimens.models import CodePedimentoRegimen diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py index e27d3a66..d7d69e4e 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import PedimentoCode -from .dto import PedimentoCodeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import PedimentoCodeDTO +from .models import PedimentoCode router = APIRouter(prefix="/pedimento-codes") diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py index 28914df1..4c337331 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.pedimento_codes.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py index d5ae9e01..7e116920 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py @@ -1,6 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict -from typing import List +from pydantic import BaseModel, ConfigDict, Field class RegimenPedimentoDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py index 4c9e7f95..e24bcc65 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py @@ -1,7 +1,8 @@ from typing import TYPE_CHECKING, List -from sqlalchemy import String, PrimaryKeyConstraint, ForeignKey -from sqlalchemy.orm import mapped_column, Mapped, relationship + from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column, relationship if TYPE_CHECKING: from ..code_pedimento_regimens.models import CodePedimentoRegimen diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py index 1a3b1bcc..d788510d 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import RegimenPedimento -from .dto import RegimenPedimentoDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import RegimenPedimentoDTO +from .models import RegimenPedimento router = APIRouter(prefix="/pedimento-regimens") diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py index ac9dfb9a..0120ddd3 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.pedimento_regimens.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/router.py b/backend/api/v1/modules/public/reference_data/router.py index 1f182b5b..6d5756f1 100644 --- a/backend/api/v1/modules/public/reference_data/router.py +++ b/backend/api/v1/modules/public/reference_data/router.py @@ -5,24 +5,24 @@ Agrega todos los módulos de la aplicación from fastapi import APIRouter -from .pedimento_codes.routes import router as pedimento_codes_router -from .payment_methods.routes import router as payment_methods_router +from .code_pedimento_regimens.routes import router as code_pedimento_regimens_router from .containers.routes import router as containers_router from .countries.routes import router as countries_router -from .material_types.routes import router as material_types_router from .currency_types.routes import router as currency_types_router -from .states.routes import router as states_router -from .transport_types.routes import router as transport_types_router -from .customs_warehouses.routes import router as customs_warehouses_router -from .valuation_methods.routes import router as valuation_methods_router -from .sectors.routes import router as sectors_router -from .transport_modes.routes import router as transport_modes_router from .customs_sections.routes import router as customs_sections_router -from .invoice_types.routes import router as invoice_types_router -from .code_pedimento_regimens.routes import router as code_pedimento_regimens_router -from .pedimento_regimens.routes import router as pedimento_regimens_router +from .customs_warehouses.routes import router as customs_warehouses_router from .incoterms.routes import router as incoterms_router +from .invoice_types.routes import router as invoice_types_router +from .material_types.routes import router as material_types_router +from .payment_methods.routes import router as payment_methods_router +from .pedimento_codes.routes import router as pedimento_codes_router +from .pedimento_regimens.routes import router as pedimento_regimens_router +from .sectors.routes import router as sectors_router +from .states.routes import router as states_router from .trailer_types.routes import router as trailer_types_router +from .transport_modes.routes import router as transport_modes_router +from .transport_types.routes import router as transport_types_router +from .valuation_methods.routes import router as valuation_methods_router # Router principal router = APIRouter() diff --git a/backend/api/v1/modules/public/reference_data/sectors/dto.py b/backend/api/v1/modules/public/reference_data/sectors/dto.py index 770ed965..f25ca5dd 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/dto.py +++ b/backend/api/v1/modules/public/reference_data/sectors/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class SectorDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/sectors/models.py b/backend/api/v1/modules/public/reference_data/sectors/models.py index 9e292ee3..426c5357 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/models.py +++ b/backend/api/v1/modules/public/reference_data/sectors/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, SmallInteger, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, SmallInteger, String +from sqlalchemy.orm import Mapped, mapped_column class Sector(Base): diff --git a/backend/api/v1/modules/public/reference_data/sectors/routes.py b/backend/api/v1/modules/public/reference_data/sectors/routes.py index 3c9839f6..05aa0f72 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/routes.py +++ b/backend/api/v1/modules/public/reference_data/sectors/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import Sector -from .dto import SectorDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import SectorDTO +from .models import Sector router = APIRouter(prefix="/sectors") diff --git a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py index c81e205c..872ea295 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py +++ b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.sectors.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/states/dto.py b/backend/api/v1/modules/public/reference_data/states/dto.py index b54bd4b5..8262e875 100644 --- a/backend/api/v1/modules/public/reference_data/states/dto.py +++ b/backend/api/v1/modules/public/reference_data/states/dto.py @@ -1,7 +1,7 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict from typing import Optional +from pydantic import BaseModel, ConfigDict, Field + class StateDTO(BaseModel): m3_key: str = Field(..., min_length=1, max_length=3) diff --git a/backend/api/v1/modules/public/reference_data/states/models.py b/backend/api/v1/modules/public/reference_data/states/models.py index c1800828..7a883a31 100644 --- a/backend/api/v1/modules/public/reference_data/states/models.py +++ b/backend/api/v1/modules/public/reference_data/states/models.py @@ -1,7 +1,8 @@ from typing import Optional -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped + from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class State(Base): diff --git a/backend/api/v1/modules/public/reference_data/states/routes.py b/backend/api/v1/modules/public/reference_data/states/routes.py index f5859ea4..151c1172 100644 --- a/backend/api/v1/modules/public/reference_data/states/routes.py +++ b/backend/api/v1/modules/public/reference_data/states/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import State -from .dto import StateDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import StateDTO +from .models import State router = APIRouter(prefix="/states") diff --git a/backend/api/v1/modules/public/reference_data/states/test_states.py b/backend/api/v1/modules/public/reference_data/states/test_states.py index 3719c88b..b38b5202 100644 --- a/backend/api/v1/modules/public/reference_data/states/test_states.py +++ b/backend/api/v1/modules/public/reference_data/states/test_states.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.states.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/trailer_types/dto.py b/backend/api/v1/modules/public/reference_data/trailer_types/dto.py index 322365db..13a3f3fe 100644 --- a/backend/api/v1/modules/public/reference_data/trailer_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/trailer_types/dto.py @@ -1,15 +1,19 @@ -from pydantic import BaseModel from typing import Optional +from pydantic import BaseModel + + class TrailerTypeBaseDTO(BaseModel): trailer_type_key: str description: Optional[str] company_id: str tenant_id: str + class TrailerTypeCreateDTO(TrailerTypeBaseDTO): pass + class TrailerTypeResponseDTO(TrailerTypeBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/public/reference_data/trailer_types/models.py b/backend/api/v1/modules/public/reference_data/trailer_types/models.py index 5db5078a..abdbb22f 100644 --- a/backend/api/v1/modules/public/reference_data/trailer_types/models.py +++ b/backend/api/v1/modules/public/reference_data/trailer_types/models.py @@ -1,11 +1,15 @@ -from sqlalchemy import Column, Integer, String, ForeignKey +from api.v1.common.base_models import TenantScopedMixin from core.database import Base +from sqlalchemy import Column, ForeignKeyConstraint, String -class TrailerType(Base): + +class TrailerType(Base, TenantScopedMixin): __tablename__ = "trailer_type" - __table_args__ = {"schema": "a76"} + __table_args__ = ( + ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + {"schema": "a76"}, + ) trailer_type_key = Column(String(2), primary_key=True, nullable=False) description = Column(String(100), nullable=True) - company_id = Column(Integer, ForeignKey("a76.company.id"), nullable=False) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False) \ No newline at end of file diff --git a/backend/api/v1/modules/public/reference_data/trailer_types/routes.py b/backend/api/v1/modules/public/reference_data/trailer_types/routes.py index 32ffdc7d..079585fc 100644 --- a/backend/api/v1/modules/public/reference_data/trailer_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/trailer_types/routes.py @@ -1,24 +1,36 @@ +from core.database import get_core_db from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session -from . import services, dto -from core.database import get_core_db + +from . import dto, services router = APIRouter() -@router.get("/trailer-types/{trailer_type_key}", response_model=dto.TrailerTypeResponseDTO) + +@router.get( + "/trailer-types/{trailer_type_key}", response_model=dto.TrailerTypeResponseDTO +) def get_trailer_type(trailer_type_key: str, db: Session = Depends(get_core_db)): - trailer_type = services.TrailerTypeService.get_trailer_type_by_key(db, trailer_type_key) + trailer_type = services.TrailerTypeService.get_trailer_type_by_key( + db, trailer_type_key + ) if not trailer_type: raise HTTPException(status_code=404, detail="Trailer type not found") return trailer_type + @router.post("/trailer-types", response_model=dto.TrailerTypeResponseDTO) -def create_trailer_type(trailer_type_data: dto.TrailerTypeCreateDTO, db: Session = Depends(get_core_db)): +def create_trailer_type( + trailer_type_data: dto.TrailerTypeCreateDTO, db: Session = Depends(get_core_db) +): return services.TrailerTypeService.create_trailer_type(db, trailer_type_data) -@router.delete("/trailer-types/{trailer_type_key}", response_model=dto.TrailerTypeResponseDTO) + +@router.delete( + "/trailer-types/{trailer_type_key}", response_model=dto.TrailerTypeResponseDTO +) def delete_trailer_type(trailer_type_key: str, db: Session = Depends(get_core_db)): trailer_type = services.TrailerTypeService.delete_trailer_type(db, trailer_type_key) if not trailer_type: raise HTTPException(status_code=404, detail="Trailer type not found") - return trailer_type \ No newline at end of file + return trailer_type diff --git a/backend/api/v1/modules/public/reference_data/trailer_types/services.py b/backend/api/v1/modules/public/reference_data/trailer_types/services.py index 55ea05a2..7e671c52 100644 --- a/backend/api/v1/modules/public/reference_data/trailer_types/services.py +++ b/backend/api/v1/modules/public/reference_data/trailer_types/services.py @@ -1,10 +1,16 @@ from sqlalchemy.orm import Session -from . import models, dto + +from . import dto, models + class TrailerTypeService: @staticmethod def get_trailer_type_by_key(db: Session, trailer_type_key: str): - return db.query(models.TrailerType).filter(models.TrailerType.trailer_type_key == trailer_type_key).first() + return ( + db.query(models.TrailerType) + .filter(models.TrailerType.trailer_type_key == trailer_type_key) + .first() + ) @staticmethod def create_trailer_type(db: Session, trailer_type_data: dto.TrailerTypeCreateDTO): @@ -20,4 +26,4 @@ class TrailerTypeService: if trailer_type: db.delete(trailer_type) db.commit() - return trailer_type \ No newline at end of file + return trailer_type diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py index 71f8b688..0923d4fb 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class TransportModeDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/models.py b/backend/api/v1/modules/public/reference_data/transport_modes/models.py index daccbf2b..19ae71c5 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class TransportMode(Base): diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py index efba117e..25a22036 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import has_role, get_current_user -from .models import TransportMode -from .dto import TransportModeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import TransportModeDTO +from .models import TransportMode router = APIRouter(prefix="/transport-modes") diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py index 4ef760dc..f88d414c 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.transport_modes.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/dto.py b/backend/api/v1/modules/public/reference_data/transport_types/dto.py index f9367e0c..a026e496 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class TransportTypeDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/transport_types/models.py b/backend/api/v1/modules/public/reference_data/transport_types/models.py index fff49b2d..de4bbb3e 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class TransportType(Base): diff --git a/backend/api/v1/modules/public/reference_data/transport_types/routes.py b/backend/api/v1/modules/public/reference_data/transport_types/routes.py index a9a5da23..69bf624e 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import has_role, get_current_user -from .models import TransportType -from .dto import TransportTypeDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import TransportTypeDTO +from .models import TransportType router = APIRouter(prefix="/transport-types") diff --git a/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py index 850dc680..c2215df0 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.transport_types.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py index 297fb569..00978582 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py @@ -1,5 +1,4 @@ -from pydantic import BaseModel, Field -from pydantic import ConfigDict +from pydantic import BaseModel, ConfigDict, Field class ValuationMethodDTO(BaseModel): diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py index 73fc9a81..b25c9457 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py @@ -1,6 +1,6 @@ -from sqlalchemy import String, PrimaryKeyConstraint -from sqlalchemy.orm import mapped_column, Mapped from core.database import Base +from sqlalchemy import PrimaryKeyConstraint, String +from sqlalchemy.orm import Mapped, mapped_column class ValuationMethod(Base): diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py index 614ef0a0..4742609d 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py @@ -1,11 +1,12 @@ -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from core.database import get_core_db -from core.security import get_current_user, has_role -from .models import ValuationMethod -from .dto import ValuationMethodDTO from typing import Any, Dict +from core.database import get_core_db +from core.security import get_current_user, has_role +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .dto import ValuationMethodDTO +from .models import ValuationMethod router = APIRouter(prefix="/valuation-methods") diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py index cf5f015a..9fb8384b 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py @@ -1,7 +1,7 @@ import pytest -from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.valuation_methods.routes import router from fastapi import FastAPI +from fastapi.testclient import TestClient app = FastAPI() app.include_router(router) diff --git a/backend/core/__init__.py b/backend/core/__init__.py index 840b9ef2..233883c3 100644 --- a/backend/core/__init__.py +++ b/backend/core/__init__.py @@ -5,18 +5,18 @@ Core module - Configuración y utilidades centrales de la aplicación from .config import settings from .database import ( Base, - get_core_db, get_async_core_db, + get_core_db, get_tenant_db, - init_db, init_async_db, + init_db, ) from .security import ( - verify_token, - get_current_user, get_current_active_user, - has_role, + get_current_user, get_tenant_from_token, + has_role, + verify_token, ) __all__ = [ diff --git a/backend/core/config.py b/backend/core/config.py index 887bf0b6..fb19d6ef 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -2,9 +2,10 @@ Configuración centralizada de la aplicación usando Pydantic Settings """ -from pydantic_settings import BaseSettings, SettingsConfigDict from typing import List +from pydantic_settings import BaseSettings, SettingsConfigDict + class Settings(BaseSettings): """Configuración de la aplicación""" diff --git a/backend/core/database.py b/backend/core/database.py index bd1ec4e2..d43e5641 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -4,12 +4,13 @@ Configuración de base de datos con soporte multi-tenant - Bases de datos dedicadas para clientes enterprise """ -from sqlalchemy import create_engine -from sqlalchemy.orm import declarative_base -from sqlalchemy.orm import sessionmaker, Session -from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker -from typing import Generator, Dict, Optional, AsyncGenerator from contextlib import contextmanager +from typing import AsyncGenerator, Dict, Generator, Optional + +from sqlalchemy import create_engine +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import Session, declarative_base, sessionmaker + from .config import settings # Base declarativa para modelos ORM @@ -21,7 +22,7 @@ core_engine = create_engine( pool_pre_ping=True, pool_size=10, max_overflow=20, - echo=settings.DEBUG, + echo=False, ) CoreSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=core_engine) diff --git a/backend/core/middleware.py b/backend/core/middleware.py index f57fc711..02731b37 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -5,16 +5,16 @@ Middleware personalizado para Anexo76 - Logging de requests """ -from fastapi import Request, HTTPException -from starlette.middleware.base import BaseHTTPMiddleware -from typing import Callable import logging import time -from datetime import datetime -from sqlalchemy.orm import Session -from .database import CoreSessionLocal -from .security import verify_token, get_tenant_from_token +from typing import Callable + +from fastapi import HTTPException, Request +from starlette.middleware.base import BaseHTTPMiddleware + from .config import settings +from .database import CoreSessionLocal +from .security import get_tenant_from_token, verify_token logger = logging.getLogger(__name__) @@ -158,6 +158,19 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next: Callable): start_time = time.time() + excluded_paths = [ + "/api/docs", + "/api/redoc", + "/openapi.json", + "/api/v1/status", + "/api/health", + ] + if any( + request.url.path == path or request.url.path.startswith(path + "/") + for path in excluded_paths + ): + return await call_next(request) + # Log request logger.info(f"Request: {request.method} {request.url.path}") diff --git a/backend/core/security.py b/backend/core/security.py index 0a1cc855..0b509d85 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -2,15 +2,16 @@ Utilidades de seguridad y autenticación con Keycloak """ -from fastapi import HTTPException, Security, Depends -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials -from keycloak import KeycloakOpenID -from jose import jwt, JWTError -from typing import Optional, Dict, Any -from sqlalchemy.orm import Session -from .config import settings import logging -from core.database import get_core_db +from typing import Any, Dict, Optional + +from fastapi import Depends, HTTPException, Security +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from jose import JWTError, jwt +from keycloak import KeycloakOpenID +from sqlalchemy.orm import Session + +from .config import settings logger = logging.getLogger(__name__) @@ -74,7 +75,7 @@ async def get_current_user( current_user: dict = Depends(get_current_user) """ token = credentials.credentials - user_info = verify_token(token) + user_info = verify_token(token) return user_info @@ -125,7 +126,7 @@ def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]: - En atributos del usuario """ # Intentar obtener de claims personalizados - tenant_id = user_info.get("tenant_id") + tenant_id = user_info.get("tenant_id") if not tenant_id: # Intentar obtener de atributos tenant_id = user_info.get("attributes", {}).get("tenant_id") @@ -136,7 +137,9 @@ def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]: return None -def validate_company_access(db: Session, company_id: int, current_user: Dict[str, Any]) -> bool: +def validate_company_access( + db: Session, company_id: int, current_user: Dict[str, Any] +) -> bool: """ Valida que el usuario tenga acceso a la compañía solicitada @@ -150,7 +153,7 @@ def validate_company_access(db: Session, company_id: int, current_user: Dict[str Nota: Verifica que la compañía pertenezca al tenant del usuario consultando la BD. """ - + tenant_id = get_tenant_from_token(current_user) # Si no hay tenant_id en el token, denegar acceso @@ -160,18 +163,21 @@ def validate_company_access(db: Session, company_id: int, current_user: Dict[str # Consultar si la compañía pertenece al tenant try: from api.v1.modules.a76.company.models import Company - - company = db.query(Company).filter( - Company.id == company_id, - Company.tenant_id == tenant_id - ).first() - + + company = ( + db.query(Company) + .filter(Company.id == company_id, Company.tenant_id == tenant_id) + .first() + ) + return company is not None finally: db.close() -def validate_access_to_resource(db: Session, company_id: int, current_user: Dict[str, Any]) -> int: +def validate_access_to_resource( + db: Session, company_id: int, current_user: Dict[str, Any] +) -> int: """ Valida que el usuario tenga acceso a un recurso específico basado en company_id y regresa el tenant_id @@ -196,23 +202,3 @@ def validate_access_to_resource(db: Session, company_id: int, current_user: Dict # Validar que el tenant_id del usuario coincida con el del recurso return tenant_id - - -class KeycloakClient: - """Cliente para interactuar con Keycloak Admin API""" - - def __init__(self): - self.openid = keycloak_openid - - def create_user(self, email: str, password: str, tenant_id: int, **kwargs): - """Crea un usuario en Keycloak""" - # Implementar lógica para crear usuario usando keycloak admin - pass - - def assign_role(self, user_id: str, role: str): - """Asigna un rol a un usuario""" - pass - - def create_tenant_realm(self, tenant_name: str): - """Crea un realm para un nuevo tenant""" - pass diff --git a/backend/main.py b/backend/main.py index 26ce5330..197a111b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,18 +3,18 @@ Anexo76 - Aplicación SaaS para gestión de comercio exterior Backend API con FastAPI + Keycloak + SQLAlchemy """ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware import logging +from api.v1.router import router as api_v1_router from core.config import settings +from core.database import init_db from core.middleware import ( - TenantMiddleware, LicenseValidationMiddleware, RequestLoggingMiddleware, + TenantMiddleware, ) -from core.database import init_db -from api.v1.router import router as api_v1_router +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware # Configurar logging logging.basicConfig( @@ -53,10 +53,10 @@ app.add_middleware( allow_headers=["*"], ) -logger.info(f"CORS configurado para orígenes: {settings.cors_origins_list}") - # Agregar middlewares personalizados -app.add_middleware(RequestLoggingMiddleware) +if settings.DEBUG: + app.add_middleware(RequestLoggingMiddleware) + app.add_middleware(LicenseValidationMiddleware) app.add_middleware(TenantMiddleware) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 095a23c2..f1d33f00 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -48,8 +48,7 @@ async function refreshToken(): Promise { }; refreshTokenValue = getCookie('refresh_token'); - if (refreshTokenValue) { - console.log('📝 [API] Refresh token encontrado en cookies, sincronizando a localStorage'); + if (refreshTokenValue) { localStorage.setItem('refresh_token', refreshTokenValue); } } @@ -57,9 +56,7 @@ async function refreshToken(): Promise { if (!refreshTokenValue) { console.error('❌ [API] No hay refresh token disponible'); return null; - } - - console.log('🔄 [API] Intentando refrescar token...'); + } try { const response = await fetch(`${API_BASE_URL}/v1/auth/refresh`, { @@ -88,8 +85,7 @@ async function refreshToken(): Promise { return null; } - const data = await response.json(); - console.log('✅ [API] Token refrescado correctamente'); + const data = await response.json(); // Guardar los nuevos tokens if (data.access_token) { @@ -136,8 +132,7 @@ async function fetchApi( retryCount = 0 ): Promise> { // Si ya estamos refrescando el token, esperar - if (isRefreshing && retryCount === 0) { - console.log('⏳ [API] Esperando refresh del token...'); + if (isRefreshing && retryCount === 0) { return new Promise((resolve) => { subscribeTokenRefresh((newToken) => { resolve(fetchApi(endpoint, options, 1)); @@ -168,16 +163,14 @@ async function fetchApi( }); // Si recibimos 401 o 403 y no es el endpoint de refresh, intentar refrescar el token - if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) { - console.log('🔄 [API] Recibido 401/403, intentando refrescar token...'); + if ((response.status === 401 || response.status === 403) && !endpoint.includes('/auth/refresh') && retryCount === 0) { isRefreshing = true; try { const newToken = await refreshToken(); if (newToken) { - // Token refrescado exitosamente - console.log('✅ [API] Token refrescado exitosamente'); + // Token refrescado exitosamente onTokenRefreshed(newToken); isRefreshing = false; // Reintentar la petición original con el nuevo token diff --git a/frontend/src/lib/components/login-form.svelte b/frontend/src/lib/components/login-form.svelte index 0ac8e091..11961b10 100644 --- a/frontend/src/lib/components/login-form.svelte +++ b/frontend/src/lib/components/login-form.svelte @@ -11,9 +11,10 @@ import { Button } from "$lib/components/ui/button/index.js"; import { cn } from "$lib/utils.js"; import type { HTMLAttributes } from "svelte/elements"; - import { page } from '$app/stores'; + import { page } from '$app/state'; import { enhance } from '$app/forms'; import { loginWithProvider } from '$lib/sso'; + import { onMount } from 'svelte'; let { class: className, ...restProps }: HTMLAttributes = $props(); @@ -25,23 +26,40 @@ let loading = $state(false); // Obtener el error del servidor si existe - const error = $derived($page.form?.error || ''); + const error = $derived(page.form?.error || ''); + + // Limpiar todo el localStorage y cookies al montar el componente de login + // Esto asegura que no queden datos del tenant anterior + onMount(() => { + clearAllData(); + }); // Función para limpiar cookies del cliente function clearClientCookies() { - if (typeof localStorage !== 'undefined') { - localStorage.removeItem('access_token'); - localStorage.removeItem('refresh_token'); - } - if (typeof document !== 'undefined') { const isSecure = window.location.protocol === 'https:'; const secureFlag = isSecure ? '; Secure' : ''; document.cookie = `access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`; + document.cookie = `refresh_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`; + document.cookie = `active_company_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax${secureFlag}`; } } + // Función para limpiar todo el localStorage y cookies + function clearAllData() { + if (typeof localStorage !== 'undefined') { + localStorage.removeItem('access_token'); + localStorage.removeItem('refresh_token'); + localStorage.removeItem('activeCompanyId'); + } + + clearClientCookies(); + } + function handleMicrosoftLogin() { + // Limpiar datos antes de iniciar SSO + clearAllData(); + // Guardar el tenant_slug en localStorage para recuperarlo después del callback if (tenantSlug) { localStorage.setItem('pending_tenant_slug', tenantSlug); @@ -50,6 +68,9 @@ } function handleGoogleLogin() { + // Limpiar datos antes de iniciar SSO + clearAllData(); + // Guardar el tenant_slug en localStorage para recuperarlo después del callback if (tenantSlug) { localStorage.setItem('pending_tenant_slug', tenantSlug); diff --git a/frontend/src/lib/stores/company.svelte.ts b/frontend/src/lib/stores/company.svelte.ts index 7933c59a..174c3ebf 100644 --- a/frontend/src/lib/stores/company.svelte.ts +++ b/frontend/src/lib/stores/company.svelte.ts @@ -15,6 +15,7 @@ class CompanyStore { private _activeCompany = $state(null); private _companies = $state([]); private _loading = $state(false); + private _currentTenantId = $state(null); get activeCompany() { return this._activeCompany; @@ -36,7 +37,22 @@ class CompanyStore { try { const response = await fetch('/api/company/my-companies'); if (response.ok) { - this._companies = await response.json(); + const newCompanies = await response.json(); + + // Detectar si el tenant ha cambiado + if (newCompanies.length > 0) { + const newTenantId = newCompanies[0].tenant_id; + + // Si el tenant cambió, limpiar el store primero + if (this._currentTenantId !== null && this._currentTenantId !== newTenantId) { + + this.clear(); + } + + this._currentTenantId = newTenantId; + } + + this._companies = newCompanies; // Si hay compañías y no hay una activa, seleccionar la primera if (this._companies.length > 0 && !this._activeCompany) { @@ -44,6 +60,10 @@ class CompanyStore { } } else { console.error('Error loading companies:', response.statusText); + // Si falla la carga (ej: 401), limpiar el store + if (response.status === 401) { + this.clear(); + } } } catch (error) { console.error('Error loading companies:', error); @@ -98,6 +118,7 @@ class CompanyStore { this._activeCompany = null; this._companies = []; this._loading = false; + this._currentTenantId = null; // Limpiar localStorage if (typeof window !== 'undefined') { diff --git a/frontend/src/routes/api/company/my-companies/+server.ts b/frontend/src/routes/api/company/my-companies/+server.ts index af6ec0fb..d50f3eec 100644 --- a/frontend/src/routes/api/company/my-companies/+server.ts +++ b/frontend/src/routes/api/company/my-companies/+server.ts @@ -8,6 +8,8 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { const token = cookies.get('access_token'); if (!token) { + // Limpiar cualquier cookie de compañía si no hay autenticación + cookies.delete('active_company_id', { path: '/' }); return json({ error: 'No authenticated' }, { status: 401 }); } @@ -31,9 +33,13 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { }); if (!response.ok) { + // Si la autenticación falló, limpiar la cookie de compañía + if (response.status === 401) { + cookies.delete('active_company_id', { path: '/' }); + } return json({ error: 'Failed to fetch companies' }, { status: response.status }); } - + console.log('✅ Token', token); const companies = await response.json(); return json(companies); } catch (error) { diff --git a/frontend/src/routes/login/+page.server.ts b/frontend/src/routes/login/+page.server.ts index cbd19011..ec187222 100644 --- a/frontend/src/routes/login/+page.server.ts +++ b/frontend/src/routes/login/+page.server.ts @@ -6,9 +6,16 @@ export const load: PageServerLoad = async ({ cookies, url }) => { if (url.searchParams.has('logout')) { cookies.delete('access_token', { path: '/' }); cookies.delete('refresh_token', { path: '/' }); + cookies.delete('active_company_id', { path: '/' }); return {}; } + // Limpiar siempre las cookies de sesión anterior al cargar login + // Esto evita que se queden datos del tenant anterior + cookies.delete('access_token', { path: '/' }); + cookies.delete('refresh_token', { path: '/' }); + cookies.delete('active_company_id', { path: '/' }); + // Permitir acceso al login sin redirigir automáticamente // Esto evita bucles de redirección cuando el token existe pero puede estar expirado return {}; diff --git a/frontend/src/routes/logout/+server.ts b/frontend/src/routes/logout/+server.ts index c18a53d2..c00e3e3b 100644 --- a/frontend/src/routes/logout/+server.ts +++ b/frontend/src/routes/logout/+server.ts @@ -6,6 +6,9 @@ export const POST: RequestHandler = async ({ cookies }) => { cookies.delete('access_token', { path: '/' }); cookies.delete('refresh_token', { path: '/' }); + // Eliminar la cookie de la compañía activa + cookies.delete('active_company_id', { path: '/' }); + // Redirigir al login throw redirect(303, '/login'); };