From 44eef0844579afd4819e0d83b9ab18b99c367f15 Mon Sep 17 00:00:00 2001 From: acazares Date: Tue, 9 Dec 2025 11:51:18 -0600 Subject: [PATCH] feat: Implement license and tenant management APIs - Added endpoints for license management including creation, retrieval, update, validation, and usage tracking. - Developed service layer for business logic related to licenses. - Introduced tenant management APIs for creating, updating, listing, and deleting tenants. - Implemented user-tenant relationship management with endpoints for adding, removing, and updating user roles in tenants. - Created DTOs for data transfer between layers and models for ORM mapping. - Enhanced logging and error handling across services. --- backend/api/v1/common/base_models.py | 2 +- .../v1/modules/a76/general_catalogs/company/models.py | 2 +- backend/api/v1/modules/a76/router.py | 8 ++++---- backend/api/v1/modules/{a76 => core}/auth/__init__.py | 0 backend/api/v1/modules/{a76 => core}/auth/dto.py | 0 backend/api/v1/modules/{a76 => core}/auth/routes.py | 0 backend/api/v1/modules/{a76 => core}/auth/service.py | 8 ++++---- .../api/v1/modules/{a76 => core}/licenses/__init__.py | 0 backend/api/v1/modules/{a76 => core}/licenses/dto.py | 0 .../api/v1/modules/{a76 => core}/licenses/models.py | 8 ++++---- .../api/v1/modules/{a76 => core}/licenses/routes.py | 0 .../api/v1/modules/{a76 => core}/licenses/service.py | 0 .../api/v1/modules/{a76 => core}/tenants/__init__.py | 0 backend/api/v1/modules/{a76 => core}/tenants/dto.py | 0 backend/api/v1/modules/{a76 => core}/tenants/models.py | 4 ++-- backend/api/v1/modules/{a76 => core}/tenants/routes.py | 0 .../api/v1/modules/{a76 => core}/tenants/service.py | 0 .../api/v1/modules/{a76 => core}/user_tenant/dto.py | 0 .../api/v1/modules/{a76 => core}/user_tenant/models.py | 4 ++-- .../api/v1/modules/{a76 => core}/user_tenant/routes.py | 0 .../v1/modules/{a76 => core}/user_tenant/service.py | 0 scripts/init_first_time.sh | 10 +++++----- scripts/postgres-app-entrypoint.sh | 4 ++-- 23 files changed, 25 insertions(+), 25 deletions(-) rename backend/api/v1/modules/{a76 => core}/auth/__init__.py (100%) rename backend/api/v1/modules/{a76 => core}/auth/dto.py (100%) rename backend/api/v1/modules/{a76 => core}/auth/routes.py (100%) rename backend/api/v1/modules/{a76 => core}/auth/service.py (98%) rename backend/api/v1/modules/{a76 => core}/licenses/__init__.py (100%) rename backend/api/v1/modules/{a76 => core}/licenses/dto.py (100%) rename backend/api/v1/modules/{a76 => core}/licenses/models.py (91%) rename backend/api/v1/modules/{a76 => core}/licenses/routes.py (100%) rename backend/api/v1/modules/{a76 => core}/licenses/service.py (100%) rename backend/api/v1/modules/{a76 => core}/tenants/__init__.py (100%) rename backend/api/v1/modules/{a76 => core}/tenants/dto.py (100%) rename backend/api/v1/modules/{a76 => core}/tenants/models.py (93%) rename backend/api/v1/modules/{a76 => core}/tenants/routes.py (100%) rename backend/api/v1/modules/{a76 => core}/tenants/service.py (100%) rename backend/api/v1/modules/{a76 => core}/user_tenant/dto.py (100%) rename backend/api/v1/modules/{a76 => core}/user_tenant/models.py (92%) rename backend/api/v1/modules/{a76 => core}/user_tenant/routes.py (100%) rename backend/api/v1/modules/{a76 => core}/user_tenant/service.py (100%) diff --git a/backend/api/v1/common/base_models.py b/backend/api/v1/common/base_models.py index a3c321bd..ddfbafd6 100644 --- a/backend/api/v1/common/base_models.py +++ b/backend/api/v1/common/base_models.py @@ -20,7 +20,7 @@ class TimestampMixin: class TenantScopedMixin: """Mixin for tenant and company scoped entities""" - tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True) + tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("core.tenants.id"), nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.company.id"), nullable=False, index=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/company/models.py b/backend/api/v1/modules/a76/general_catalogs/company/models.py index 693a8801..b291dbb5 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/models.py @@ -31,7 +31,7 @@ class Company(Base, TimestampMixin): # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) - tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True) + tenant_id: Mapped[int] = mapped_column(Integer, ForeignKey("core.tenants.id"), nullable=False, index=True) # Información básica de la empresa name: Mapped[Optional[str]] = mapped_column(String(255)) diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 9729c23d..aed3aa5a 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -8,7 +8,7 @@ from fastapi import APIRouter from .customs_brokers.routes import router as customs_broker_router # Importar routers de módulos -from .auth import router as auth_router +from ..core.auth import router as auth_router from .classes import router as classes_router from .clients_and_providers import router as client_and_provider_router from .general_catalogs.company import router as company_router @@ -17,7 +17,7 @@ from .transportation.drivers.routes import router as drivers_router from .general_catalogs.exchange_rate.routes import router as exchange_rate_router from .general_catalogs.identifiers.routes import router as identifiers_router from .fraction_rule_octave.routes import router as fraction_rule_octave_router -from .licenses import router as licenses_router +from ..core.licenses import router as licenses_router from .general_catalogs.packages.routes import router as package_router from .general_catalogs.ports.routes import router as ports_router from .parts import router as parts_router @@ -38,10 +38,10 @@ from .general_catalogs.error_catalogs.routes import router as error_catalogs_rou from .general_catalogs.doda.routes import router as doda_router from .general_catalogs.prevalidators.routes import router as prevalidators_router from .general_catalogs.electronic_notices.routes import router as electronic_notices_router -from .tenants import router as tenants_router +from ..core.tenants import router as tenants_router from .transportation.trailers.routes import router as trailers_router from .transportation.transporters.routes import router as transporters_router -from .user_tenant.routes import router as user_tenant_router +from ..core.user_tenant.routes import router as user_tenant_router from .transportation.vehicles.routes import router as vehicles_router # Router principal diff --git a/backend/api/v1/modules/a76/auth/__init__.py b/backend/api/v1/modules/core/auth/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/auth/__init__.py rename to backend/api/v1/modules/core/auth/__init__.py diff --git a/backend/api/v1/modules/a76/auth/dto.py b/backend/api/v1/modules/core/auth/dto.py similarity index 100% rename from backend/api/v1/modules/a76/auth/dto.py rename to backend/api/v1/modules/core/auth/dto.py diff --git a/backend/api/v1/modules/a76/auth/routes.py b/backend/api/v1/modules/core/auth/routes.py similarity index 100% rename from backend/api/v1/modules/a76/auth/routes.py rename to backend/api/v1/modules/core/auth/routes.py diff --git a/backend/api/v1/modules/a76/auth/service.py b/backend/api/v1/modules/core/auth/service.py similarity index 98% rename from backend/api/v1/modules/a76/auth/service.py rename to backend/api/v1/modules/core/auth/service.py index c3b230e0..57293605 100644 --- a/backend/api/v1/modules/a76/auth/service.py +++ b/backend/api/v1/modules/core/auth/service.py @@ -4,8 +4,8 @@ Servicio de autenticación con Keycloak import logging -from api.v1.modules.a76.tenants.service import TenantService -from api.v1.modules.a76.user_tenant.service import UserTenantService +from api.v1.modules.core.tenants.service import TenantService +from api.v1.modules.core.user_tenant.service import UserTenantService from core.config import settings from fastapi import HTTPException from keycloak import KeycloakAdmin, KeycloakOpenID @@ -266,7 +266,7 @@ class AuthService: """ try: # Verificar que el tenant existe - from api.v1.modules.a76.tenants.service import TenantService + from api.v1.modules.core.tenants.service import TenantService tenant_service = TenantService(self.db) tenant = tenant_service.get_tenant_by_slug(register_data.tenant_slug) @@ -319,7 +319,7 @@ class AuthService: # Agregar el usuario al tenant en la base de datos try: - from api.v1.modules.a76.user_tenant.service import UserTenantService + from api.v1.modules.core.user_tenant.service import UserTenantService user_tenant_service = UserTenantService(self.db) user_tenant_service.add_user_to_tenant( diff --git a/backend/api/v1/modules/a76/licenses/__init__.py b/backend/api/v1/modules/core/licenses/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/licenses/__init__.py rename to backend/api/v1/modules/core/licenses/__init__.py diff --git a/backend/api/v1/modules/a76/licenses/dto.py b/backend/api/v1/modules/core/licenses/dto.py similarity index 100% rename from backend/api/v1/modules/a76/licenses/dto.py rename to backend/api/v1/modules/core/licenses/dto.py diff --git a/backend/api/v1/modules/a76/licenses/models.py b/backend/api/v1/modules/core/licenses/models.py similarity index 91% rename from backend/api/v1/modules/a76/licenses/models.py rename to backend/api/v1/modules/core/licenses/models.py index e2e0590f..2bf4bcb9 100644 --- a/backend/api/v1/modules/a76/licenses/models.py +++ b/backend/api/v1/modules/core/licenses/models.py @@ -36,11 +36,11 @@ class License(Base, TimestampMixin): """ __tablename__ = "licenses" - __table_args__ = {"schema": "a76"} + __table_args__ = {"schema": "core"} id = Column(Integer, primary_key=True, index=True) tenant_id = Column( - Integer, ForeignKey("a76.tenants.id"), nullable=False, unique=True, index=True + Integer, ForeignKey("core.tenants.id"), nullable=False, unique=True, index=True ) # Plan y características @@ -74,11 +74,11 @@ class LicenseUsage(Base, TimestampMixin): """ __tablename__ = "license_usage" - __table_args__ = {"schema": "a76"} + __table_args__ = {"schema": "core"} id = Column(Integer, primary_key=True, index=True) tenant_id = Column( - Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True + Integer, ForeignKey("core.tenants.id"), nullable=False, index=True ) # Métricas de uso diff --git a/backend/api/v1/modules/a76/licenses/routes.py b/backend/api/v1/modules/core/licenses/routes.py similarity index 100% rename from backend/api/v1/modules/a76/licenses/routes.py rename to backend/api/v1/modules/core/licenses/routes.py diff --git a/backend/api/v1/modules/a76/licenses/service.py b/backend/api/v1/modules/core/licenses/service.py similarity index 100% rename from backend/api/v1/modules/a76/licenses/service.py rename to backend/api/v1/modules/core/licenses/service.py diff --git a/backend/api/v1/modules/a76/tenants/__init__.py b/backend/api/v1/modules/core/tenants/__init__.py similarity index 100% rename from backend/api/v1/modules/a76/tenants/__init__.py rename to backend/api/v1/modules/core/tenants/__init__.py diff --git a/backend/api/v1/modules/a76/tenants/dto.py b/backend/api/v1/modules/core/tenants/dto.py similarity index 100% rename from backend/api/v1/modules/a76/tenants/dto.py rename to backend/api/v1/modules/core/tenants/dto.py diff --git a/backend/api/v1/modules/a76/tenants/models.py b/backend/api/v1/modules/core/tenants/models.py similarity index 93% rename from backend/api/v1/modules/a76/tenants/models.py rename to backend/api/v1/modules/core/tenants/models.py index 29453c61..e76fd34e 100644 --- a/backend/api/v1/modules/a76/tenants/models.py +++ b/backend/api/v1/modules/core/tenants/models.py @@ -13,7 +13,7 @@ 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 + from api.v1.modules.core.user_tenant.models import UserTenant class TenantType(enum.Enum): @@ -30,7 +30,7 @@ class Tenant(Base, TimestampMixin): """ __tablename__ = "tenants" - __table_args__ = {"schema": "a76", "extend_existing": True} + __table_args__ = {"schema": "core", "extend_existing": True} id = Column(Integer, primary_key=True, index=True) name = Column(String(255), nullable=False, index=True) diff --git a/backend/api/v1/modules/a76/tenants/routes.py b/backend/api/v1/modules/core/tenants/routes.py similarity index 100% rename from backend/api/v1/modules/a76/tenants/routes.py rename to backend/api/v1/modules/core/tenants/routes.py diff --git a/backend/api/v1/modules/a76/tenants/service.py b/backend/api/v1/modules/core/tenants/service.py similarity index 100% rename from backend/api/v1/modules/a76/tenants/service.py rename to backend/api/v1/modules/core/tenants/service.py diff --git a/backend/api/v1/modules/a76/user_tenant/dto.py b/backend/api/v1/modules/core/user_tenant/dto.py similarity index 100% rename from backend/api/v1/modules/a76/user_tenant/dto.py rename to backend/api/v1/modules/core/user_tenant/dto.py diff --git a/backend/api/v1/modules/a76/user_tenant/models.py b/backend/api/v1/modules/core/user_tenant/models.py similarity index 92% rename from backend/api/v1/modules/a76/user_tenant/models.py rename to backend/api/v1/modules/core/user_tenant/models.py index a88cc8ed..879f3dd1 100644 --- a/backend/api/v1/modules/a76/user_tenant/models.py +++ b/backend/api/v1/modules/core/user_tenant/models.py @@ -10,7 +10,7 @@ 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 + from api.v1.modules.core.tenants.models import Tenant class UserTenant(Base, TenantScopedMixin, TimestampMixin): @@ -26,7 +26,7 @@ class UserTenant(Base, TenantScopedMixin, TimestampMixin): UniqueConstraint( "keycloak_user_id", "tenant_id", "company_id", name="uq_user_tenant" ), - {"schema": "a76", "extend_existing": True}, + {"schema": "core", "extend_existing": True}, ) # Primary Key diff --git a/backend/api/v1/modules/a76/user_tenant/routes.py b/backend/api/v1/modules/core/user_tenant/routes.py similarity index 100% rename from backend/api/v1/modules/a76/user_tenant/routes.py rename to backend/api/v1/modules/core/user_tenant/routes.py diff --git a/backend/api/v1/modules/a76/user_tenant/service.py b/backend/api/v1/modules/core/user_tenant/service.py similarity index 100% rename from backend/api/v1/modules/a76/user_tenant/service.py rename to backend/api/v1/modules/core/user_tenant/service.py diff --git a/scripts/init_first_time.sh b/scripts/init_first_time.sh index 223908ab..4ef76185 100755 --- a/scripts/init_first_time.sh +++ b/scripts/init_first_time.sh @@ -502,17 +502,17 @@ fi # Insertar o actualizar tenant echo "Insertando tenant en PostgreSQL..." -docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "INSERT INTO a76.tenants (name, slug, type, keycloak_realm, contact_email, is_active, created_at, updated_at) VALUES ('${TENANT_NAME}', '${TENANT_SLUG}', 'SHARED'::tenanttype, '${KEYCLOAK_REALM}', '${DEMO_EMAIL}', true, now(), now()) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, contact_email = EXCLUDED.contact_email, updated_at = CURRENT_TIMESTAMP;" 2>&1 +docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "INSERT INTO core.tenants (name, slug, type, keycloak_realm, contact_email, is_active, created_at, updated_at) VALUES ('${TENANT_NAME}', '${TENANT_SLUG}', 'SHARED'::tenanttype, '${KEYCLOAK_REALM}', '${DEMO_EMAIL}', true, now(), now()) ON CONFLICT (slug) DO UPDATE SET name = EXCLUDED.name, contact_email = EXCLUDED.contact_email, updated_at = CURRENT_TIMESTAMP;" 2>&1 # Obtener el ID del tenant con mejor manejo de errores echo "Obteniendo ID del tenant..." -TENANT_ID_RESULT=$(docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -t -c "SELECT id FROM a76.tenants WHERE slug = '${TENANT_SLUG}';" 2>&1) +TENANT_ID_RESULT=$(docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -t -c "SELECT id FROM core.tenants WHERE slug = '${TENANT_SLUG}';" 2>&1) if [ $? -ne 0 ]; then echo -e "${RED}✗ Error al consultar el tenant:${NC}" echo "$TENANT_ID_RESULT" echo -e "${YELLOW}Verificando si la tabla existe...${NC}" - docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "\dt a76.tenants;" + docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "\dt core.tenants;" exit 1 fi @@ -522,7 +522,7 @@ if [ -z "$TENANT_ID" ]; then echo -e "${RED}✗ Error: No se pudo obtener el ID del tenant${NC}" echo "Resultado de la consulta: '$TENANT_ID_RESULT'" echo -e "${YELLOW}Intentando ver todos los tenants...${NC}" - docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "SELECT id, slug FROM a76.tenants LIMIT 10;" + docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "SELECT id, slug FROM core.tenants LIMIT 10;" exit 1 fi @@ -561,7 +561,7 @@ echo -e "${GREEN}✓ Atributo tenant_id asignado al usuario${NC}" # Agregar relación usuario-tenant en la base de datos echo -e "\n${YELLOW}Creando relación usuario-tenant en la base de datos...${NC}" -docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "INSERT INTO a76.user_tenants (keycloak_user_id, tenant_id, company_id, role, is_active, created_at, updated_at) VALUES ('${USER_ID}', ${TENANT_ID}, 1, 'admin', true, now(), now()) ON CONFLICT (keycloak_user_id, tenant_id, company_id) DO UPDATE SET is_active = true, updated_at = CURRENT_TIMESTAMP;" 2>&1 +docker exec -e PGPASSWORD="${POSTGRES_PASSWORD}" anexo76-postgres-a76 psql -h localhost -p 5432 -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c "INSERT INTO core.user_tenants (keycloak_user_id, tenant_id, company_id, role, is_active, created_at, updated_at) VALUES ('${USER_ID}', ${TENANT_ID}, 1, 'admin', true, now(), now()) ON CONFLICT (keycloak_user_id, tenant_id, company_id) DO UPDATE SET is_active = true, updated_at = CURRENT_TIMESTAMP;" 2>&1 echo -e "${GREEN}✓ Relación usuario-tenant creada en la base de datos${NC}" diff --git a/scripts/postgres-app-entrypoint.sh b/scripts/postgres-app-entrypoint.sh index 7b0dad73..8380bc0c 100755 --- a/scripts/postgres-app-entrypoint.sh +++ b/scripts/postgres-app-entrypoint.sh @@ -18,11 +18,11 @@ echo "Creando extensiones y esquemas..." psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS "pg_trgm"; + CREATE SCHEMA IF NOT EXISTS core; CREATE SCHEMA IF NOT EXISTS a76; CREATE SCHEMA IF NOT EXISTS a22; CREATE SCHEMA IF NOT EXISTS a24; - CREATE SCHEMA IF NOT EXISTS a30; - DROP SCHEMA IF EXISTS a31; + CREATE SCHEMA IF NOT EXISTS a30; EOSQL echo "✓ Extensiones y esquemas creados correctamente"