feat: enhance audit log functionality with tenant_id handling and optional seed data in initialization script

This commit is contained in:
2026-02-18 12:58:11 -06:00
parent bbd0b7ad90
commit d4b52b32ed
8 changed files with 341 additions and 63 deletions

View File

@@ -75,7 +75,9 @@ class FaLineItem(Base, TenantScopedMixin, TimestampMixin):
omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31 omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31
# --- RELACIÓN --- # --- RELACIÓN ---
master_info: Mapped["LineItem"] = relationship("LineItem", back_populates="fa_data") master_info: Mapped["LineItem"] = relationship(
"api.v1.modules.a76.items.models.LineItem", back_populates="fa_data"
)
def __repr__(self) -> str: def __repr__(self) -> str:
return f"<FaLineItem(id={self.id}, asset_number='{self.asset_number}')>" return f"<FaLineItem(id={self.id}, asset_number='{self.asset_number}')>"

View File

@@ -1,11 +1,14 @@
""" """
Audit Log Events Audit Log Events
""" """
from sqlalchemy import event, inspect from sqlalchemy import event, inspect
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from .services.service import AuditService from .services.service import AuditService
from .utils.serialization import serialize_for_json
from core.context import get_user_context from core.context import get_user_context
def register_audit_listeners(models_to_audit): def register_audit_listeners(models_to_audit):
""" """
Register SQLAlchemy listeners for given models Register SQLAlchemy listeners for given models
@@ -15,16 +18,23 @@ def register_audit_listeners(models_to_audit):
event.listen(model, "after_update", after_update_listener) event.listen(model, "after_update", after_update_listener)
event.listen(model, "after_delete", after_delete_listener) event.listen(model, "after_delete", after_delete_listener)
def _get_current_username(): def _get_current_username():
try: try:
context = get_user_context() context = get_user_context()
if context: if context:
# Token usually has 'preferred_username' or 'name' or 'sub' # Token usually has 'preferred_username' or 'name' or 'sub'
return context.get("preferred_username") or context.get("email") or context.get("sub") or "System" return (
context.get("preferred_username")
or context.get("email")
or context.get("sub")
or "System"
)
except: except:
pass pass
return "System" return "System"
def after_insert_listener(mapper, connection, target): def after_insert_listener(mapper, connection, target):
""" """
Listener for INSERT operations Listener for INSERT operations
@@ -33,7 +43,8 @@ def after_insert_listener(mapper, connection, target):
record_data = {c.name: getattr(target, c.name) for c in mapper.columns} record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
username = _get_current_username() username = _get_current_username()
company_id = getattr(target, "company_id", None) company_id = getattr(target, "company_id", None)
tenant_id = getattr(target, "tenant_id", None)
# Create a session bound to the connection # Create a session bound to the connection
session = Session(bind=connection) session = Session(bind=connection)
try: try:
@@ -44,24 +55,26 @@ def after_insert_listener(mapper, connection, target):
record_data=record_data, record_data=record_data,
username=username, username=username,
record_id=str(getattr(target, "id", "")), record_id=str(getattr(target, "id", "")),
company_id=company_id company_id=company_id,
tenant_id=tenant_id,
) )
except Exception as e: except Exception as e:
print(f"Error logging insert: {e}") print(f"Error logging insert: {e}")
finally: finally:
session.close() session.close()
def after_update_listener(mapper, connection, target): def after_update_listener(mapper, connection, target):
""" """
Listener for UPDATE operations Listener for UPDATE operations
""" """
table_name = target.__tablename__ table_name = target.__tablename__
state = inspect(target) state = inspect(target)
changes = {} changes = {}
old_values = {} old_values = {}
new_values = {} new_values = {}
for attr in state.attrs: for attr in state.attrs:
hist = attr.history hist = attr.history
if hist.has_changes(): if hist.has_changes():
@@ -74,6 +87,8 @@ def after_update_listener(mapper, connection, target):
record_data = {c.name: getattr(target, c.name) for c in mapper.columns} record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
username = _get_current_username() username = _get_current_username()
company_id = getattr(target, "company_id", None)
tenant_id = getattr(target, "tenant_id", None)
session = Session(bind=connection) session = Session(bind=connection)
try: try:
@@ -84,13 +99,16 @@ def after_update_listener(mapper, connection, target):
record_data=record_data, record_data=record_data,
username=username, username=username,
record_id=str(getattr(target, "id", "")), record_id=str(getattr(target, "id", "")),
old_values=old_values, old_values=serialize_for_json(old_values),
new_values=new_values new_values=serialize_for_json(new_values),
company_id=company_id,
tenant_id=tenant_id,
) )
except Exception as e: except Exception as e:
print(f"Error logging update: {e}") print(f"Error logging update: {e}")
finally: finally:
session.close() session.close()
def after_delete_listener(mapper, connection, target): def after_delete_listener(mapper, connection, target):
""" """
@@ -99,7 +117,9 @@ def after_delete_listener(mapper, connection, target):
table_name = target.__tablename__ table_name = target.__tablename__
record_data = {c.name: getattr(target, c.name) for c in mapper.columns} record_data = {c.name: getattr(target, c.name) for c in mapper.columns}
username = _get_current_username() username = _get_current_username()
company_id = getattr(target, "company_id", None)
tenant_id = getattr(target, "tenant_id", None)
session = Session(bind=connection) session = Session(bind=connection)
try: try:
AuditService.log_crud_operation( AuditService.log_crud_operation(
@@ -108,9 +128,11 @@ def after_delete_listener(mapper, connection, target):
operation_type="DELETE", operation_type="DELETE",
record_data=record_data, record_data=record_data,
username=username, username=username,
record_id=str(getattr(target, "id", "")) record_id=str(getattr(target, "id", "")),
company_id=company_id,
tenant_id=tenant_id,
) )
except Exception as e: except Exception as e:
print(f"Error logging delete: {e}") print(f"Error logging delete: {e}")
finally: finally:
session.close() session.close()

View File

@@ -4,10 +4,18 @@ Audit Log Models
from sqlalchemy import Column, Integer, String, Date, Time, DateTime, Text, Index, func from sqlalchemy import Column, Integer, String, Date, Time, DateTime, Text, Index, func
from sqlalchemy.dialects.postgresql import JSONB, ARRAY from sqlalchemy.dialects.postgresql import JSONB, ARRAY
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base from core.database import Base
class AuditLog(Base): class AuditLog(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "audit_logs" __tablename__ = "audit_logs"
__table_args__ = (
Index('idx_audit_username_date', 'username', 'date'),
Index('idx_audit_procedure_date', 'procedure', 'date'),
Index('idx_audit_system_timestamp', 'system', 'timestamp'),
Index('idx_audit_table_record', 'table_name', 'record_id'),
{"schema": "a76"} # Use the a76 schema for audit logs
)
# Primary Key # Primary Key
spec_id = Column(Integer, primary_key=True, autoincrement=True) spec_id = Column(Integer, primary_key=True, autoincrement=True)
@@ -23,8 +31,6 @@ class AuditLog(Base):
# Technical Columns # Technical Columns
timestamp = Column(DateTime(timezone=True), nullable=False, index=True) # Combined for queries timestamp = Column(DateTime(timezone=True), nullable=False, index=True) # Combined for queries
system = Column(String(20), nullable=False, index=True, default="SCAF") system = Column(String(20), nullable=False, index=True, default="SCAF")
company_id = Column(Integer, nullable=True, index=True)
tenant_id = Column(Integer, nullable=True, index=True)
# Traceability # Traceability
table_name = Column(String(100), nullable=True, index=True) table_name = Column(String(100), nullable=True, index=True)
@@ -42,15 +48,4 @@ class AuditLog(Base):
endpoint = Column(String(500), nullable=True) endpoint = Column(String(500), nullable=True)
request_method = Column(String(10), nullable=True) request_method = Column(String(10), nullable=True)
session_id = Column(String(50), nullable=True, index=True) session_id = Column(String(50), nullable=True, index=True)
execution_time_ms = Column(Integer, nullable=True) execution_time_ms = Column(Integer, nullable=True)
# Metadata
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
# Composite Indexes for common filters
__table_args__ = (
Index('idx_audit_username_date', 'username', 'date'),
Index('idx_audit_procedure_date', 'procedure', 'date'),
Index('idx_audit_system_timestamp', 'system', 'timestamp'),
Index('idx_audit_table_record', 'table_name', 'record_id'),
)

View File

@@ -86,7 +86,8 @@ class AuditService:
# Context # Context
ip_address: Optional[str] = None, ip_address: Optional[str] = None,
user_agent: Optional[str] = None, user_agent: Optional[str] = None,
company_id: Optional[int] = None company_id: Optional[int] = None,
tenant_id: Optional[int] = None
): ):
""" """
High-level wrapper to log CRUD operations automatically mapping to Legacy format High-level wrapper to log CRUD operations automatically mapping to Legacy format
@@ -149,7 +150,8 @@ class AuditService:
changed_fields=changed_fields, changed_fields=changed_fields,
ip_address=ip_address, ip_address=ip_address,
user_agent=user_agent, user_agent=user_agent,
company_id=company_id company_id=company_id,
tenant_id=tenant_id
) )
@staticmethod @staticmethod

View File

@@ -0,0 +1,7 @@
"""
Audit Log Utilities
"""
from .serialization import serialize_for_json
__all__ = ["serialize_for_json"]

View File

@@ -0,0 +1,44 @@
"""
Serialization utilities for audit logs
"""
from datetime import date, datetime, time
from decimal import Decimal
from uuid import UUID
from typing import Any, Dict
def serialize_value(value: Any) -> Any:
"""
Convert a Python value to a JSON-serializable type
"""
if value is None:
return None
elif isinstance(value, (date, datetime)):
return value.isoformat()
elif isinstance(value, time):
return value.isoformat()
elif isinstance(value, Decimal):
return float(value)
elif isinstance(value, UUID):
return str(value)
elif isinstance(value, bytes):
return value.decode("utf-8", errors="replace")
elif isinstance(value, (list, tuple)):
return [serialize_value(item) for item in value]
elif isinstance(value, dict):
return {key: serialize_value(val) for key, val in value.items()}
else:
# For any other type, try to return as-is (str, int, float, bool, None)
# If it fails JSON serialization later, at least we tried
return value
def serialize_for_json(data: Dict[str, Any]) -> Dict[str, Any]:
"""
Recursively serialize a dictionary for JSON storage
"""
if not data:
return data
return {key: serialize_value(value) for key, value in data.items()}

View File

@@ -6,14 +6,7 @@ SQLAlchemy v2 - Annex 24 Compliance
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from core.database import Base from core.database import Base
from decimal import Decimal from decimal import Decimal
from sqlalchemy import ( from sqlalchemy import Boolean, String, Integer, Numeric, SmallInteger, ForeignKey
Boolean,
String,
Integer,
Numeric,
SmallInteger,
ForeignKey
)
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base from core.database import Base
@@ -21,7 +14,7 @@ from core.database import Base
from api.v1.modules.a76.classes.models import Class from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
if TYPE_CHECKING: if TYPE_CHECKING:
from .line_financials.models import LineFinancial from .line_financials.models import LineFinancial
from .line_quantities.models import LineQuantity from .line_quantities.models import LineQuantity
from .line_customs.models import LineCustom from .line_customs.models import LineCustom
@@ -41,17 +34,20 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
Unified item header table for all import/export operations Unified item header table for all import/export operations
Consolidates headers from both SCAF and SCAII systems Consolidates headers from both SCAF and SCAII systems
""" """
__tablename__ = "item_lines" __tablename__ = "item_lines"
__table_args__ = { __table_args__ = {
"schema": "a76", "schema": "a76",
} }
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id")) # CONSECUTIVO invoice_id: Mapped[int] = mapped_column(
ForeignKey("a76.invoice_header.id")
) # CONSECUTIVO
line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA
# Part identification # Part identification
part_number_id: Mapped[Optional[int]] = mapped_column( part_number_id: Mapped[Optional[int]] = mapped_column(
Integer, ForeignKey("a76.parts.id") Integer, ForeignKey("a76.parts.id")
) # NUMPARTE ) # NUMPARTE
@@ -170,30 +166,28 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
pallet2: Mapped[Optional[int]] = mapped_column(SmallInteger) # PALLET2 pallet2: Mapped[Optional[int]] = mapped_column(SmallInteger) # PALLET2
# Wildcard field # Wildcard field
wildcard_field: Mapped[Optional[str]] = mapped_column(String(100)) # CAMPOCOMODIN wildcard_field: Mapped[Optional[str]] = mapped_column(String(100)) # CAMPOCOMODIN
# Item references # Item references
reference_number: Mapped[Optional[str]] = mapped_column( reference_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMREFERENCIA
String(20)) # NUMREFERENCIA order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA / ORDENVENTA
order: Mapped[Optional[str]] = mapped_column(
String(50)) # ORDENCOMPRA / ORDENVENTA
guide_number: Mapped[Optional[str]] = mapped_column( guide_number: Mapped[Optional[str]] = mapped_column(
String(50)) # NUMEROGUIA/NUMERODEGUIA String(50)
) # NUMEROGUIA/NUMERODEGUIA
# Dates # Dates
depreciation_date: Mapped[Optional[int]] = mapped_column( depreciation_date: Mapped[Optional[int]] = mapped_column(
Integer) # FECHADEPRECIACION Integer
) # FECHADEPRECIACION
# Administrative fields # Administrative fields
rectification: Mapped[Optional[bool]] = mapped_column( rectification: Mapped[Optional[bool]] = mapped_column(Boolean) # RECTIFICACION
Boolean) # RECTIFICACION
warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA
location: Mapped[Optional[str]] = mapped_column( location: Mapped[Optional[str]] = mapped_column(String(200)) # LOCALIZACION
String(200)) # LOCALIZACION
invoice: Mapped["InvoiceHeader"] = relationship("InvoiceHeader") invoice: Mapped["InvoiceHeader"] = relationship("InvoiceHeader")
# Relationships # Relationships
financial: Mapped[Optional["LineFinancial"]] = relationship( financial: Mapped[Optional["LineFinancial"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False back_populates="line", cascade="all, delete-orphan", uselist=False
) )
@@ -220,7 +214,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
viewonly=True, viewonly=True,
) )
fa_data: Mapped[Optional["FaLineItem"]] = relationship( fa_data: Mapped[Optional["FaLineItem"]] = relationship(
"FaLineItem", "api.v1.modules.a24.fa.fa_item_lines.models.FaLineItem",
back_populates="master_info", back_populates="master_info",
cascade="all, delete-orphan", cascade="all, delete-orphan",
uselist=False, uselist=False,
@@ -231,6 +225,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
viewonly=True, viewonly=True,
) )
# ============================================================================ # ============================================================================
# SUPPORTING TABLES # SUPPORTING TABLES
# ============================================================================ # ============================================================================
@@ -241,6 +236,7 @@ class PackingList(Base, TenantScopedMixin, TimestampMixin):
Packing list items Packing list items
From: SPartidasPackingList From: SPartidasPackingList
""" """
__tablename__ = "packing_lists" __tablename__ = "packing_lists"
__table_args__ = { __table_args__ = {
"schema": "a76", "schema": "a76",
@@ -249,7 +245,8 @@ class PackingList(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
item_line_id: Mapped[int] = mapped_column(Integer) # LINEA item_line_id: Mapped[int] = mapped_column(Integer) # LINEA
packing_list_number: Mapped[Optional[str]] = mapped_column( packing_list_number: Mapped[Optional[str]] = mapped_column(
String(100)) # NUMPACKINGLIST String(100)
) # NUMPACKINGLIST
class CTMReceipt(Base, TenantScopedMixin, TimestampMixin): class CTMReceipt(Base, TenantScopedMixin, TimestampMixin):
@@ -257,6 +254,7 @@ class CTMReceipt(Base, TenantScopedMixin, TimestampMixin):
CTM Receipt lines (temporary manufacturing) CTM Receipt lines (temporary manufacturing)
From: SPartidasReciboCTM From: SPartidasReciboCTM
""" """
__tablename__ = "ctm_receipts" __tablename__ = "ctm_receipts"
__table_args__ = { __table_args__ = {
"schema": "a76", "schema": "a76",
@@ -264,11 +262,11 @@ class CTMReceipt(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
receipt_line: Mapped[int] = mapped_column( receipt_line: Mapped[int] = mapped_column(
ForeignKey("a76.item_lines.id")) # LINEARECIBO ForeignKey("a76.item_lines.id")
) # LINEARECIBO
option: Mapped[Optional[str]] = mapped_column(String(3)) # OPCION option: Mapped[Optional[str]] = mapped_column(String(3)) # OPCION
exit_invoice: Mapped[Optional[str]] = mapped_column( exit_invoice: Mapped[Optional[str]] = mapped_column(String(19)) # FACTURASALIDA
String(19)) # FACTURASALIDA
class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin): class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
@@ -276,6 +274,7 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
Subassembly/Submanufacturing Entry lines Subassembly/Submanufacturing Entry lines
From: SPartidasEntradaSM From: SPartidasEntradaSM
""" """
__tablename__ = "subassembly_entries" __tablename__ = "subassembly_entries"
__table_args__ = { __table_args__ = {
"schema": "a76", "schema": "a76",
@@ -283,10 +282,10 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION
exit_invoice: Mapped[Optional[str]] = mapped_column( exit_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURASALIDA
String(15)) # FACTURASALIDA
exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA
# ============================================================================ # ============================================================================
# INDEXES AND CONSTRAINTS # INDEXES AND CONSTRAINTS
# ============================================================================ # ============================================================================

View File

@@ -12,6 +12,11 @@
# 6. Relación usuario-tenant en tabla user_tenants # 6. Relación usuario-tenant en tabla user_tenants
# 7. Actualización del tenant_id del usuario con el valor real # 7. Actualización del tenant_id del usuario con el valor real
# 8. Licencia Enterprise para el tenant (ilimitada, 1 año de vigencia) # 8. Licencia Enterprise para el tenant (ilimitada, 1 año de vigencia)
# 9. [OPCIONAL] Datos iniciales de ejemplo si se pasa --seed-data
#
# Uso:
# ./init_first_time.sh # Solo configuración básica
# ./init_first_time.sh --seed-data # Configuración + datos de ejemplo
# #
# Requisitos: # Requisitos:
# - Keycloak corriendo en http://localhost:8080 # - Keycloak corriendo en http://localhost:8080
@@ -28,6 +33,22 @@ set -euo pipefail # Modo strict: exit on error, undefined vars, pipe failures
# Trap para cleanup en caso de error # Trap para cleanup en caso de error
trap 'echo -e "\n${RED}✗ Error en línea $LINENO. Script abortado.${NC}" >&2' ERR trap 'echo -e "\n${RED}✗ Error en línea $LINENO. Script abortado.${NC}" >&2' ERR
# Parsear argumentos
SEED_DATA=false
while [[ $# -gt 0 ]]; do
case $1 in
--seed-data)
SEED_DATA=true
shift
;;
*)
echo "Uso: $0 [--seed-data]"
echo " --seed-data: Carga datos de ejemplo en las tablas"
exit 1
;;
esac
done
# Colores para output # Colores para output
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
@@ -112,6 +133,171 @@ create_tenant_mapper() {
fi fi
} }
###############################################################################
# Funciones de seed data
###############################################################################
# Insertar datos de ejemplo para customs_brokers
seed_customs_brokers() {
echo " → Insertando customs brokers..."
exec_pg_sql "
INSERT INTO a76.customs_brokers (tenant_id, company_id, type, broker_key, name, address, postal_code, city, state, phone, email, country, tax_id, license, company, contact, created_at, updated_at)
VALUES
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB001', 'Agente Aduanal García', 'Av. Reforma 123', '01000', 'Ciudad de México', 'CDMX', '5555555555', 'garcia@aduanas.com', 'MEX', 'GAAR800101ABC', '1234', 'García y Asociados', 'Juan García', now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB002', 'Agente Aduanal López', 'Blvd. Díaz Ordaz 456', '22000', 'Tijuana', 'BC', '6641234567', 'lopez@customs.com', 'MEX', 'LOPL750505XYZ', '2345', 'López Customs', 'María López', now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB003', 'Agente Aduanal Martínez', 'Calle Industria 789', '45000', 'Guadalajara', 'JAL', '3339876543', 'martinez@broker.com', 'MEX', 'MARM850315DEF', '3456', 'Martínez Brokerage', 'Pedro Martínez', now(), now())
ON CONFLICT (broker_key, tenant_id, company_id) DO NOTHING;
" >/dev/null 2>&1
}
# Insertar datos de ejemplo para clients_and_providers
seed_clients_and_providers() {
echo " → Insertando clientes y proveedores..."
exec_pg_sql "
INSERT INTO a76.clients_and_providers (tenant_id, company_id, type_nat_foreign, name, short_name, rfc, client_or_provider, web_key, is_active, created_at, updated_at)
VALUES
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Proveedor Tecnológico SA de CV', 'PROVTECH', 'PTE901201ABC', 'BOTH', 'PROV001', true, now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Cliente Industrial del Norte SA', 'CINORTE', 'CIN850615XYZ', 'BOTH', 'CLI001', true, now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'E', 'Global Supplies Inc', 'GLOBSUP', 'GSI123456789', 'BOTH', 'BOTH001', true, now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'N', 'Manufacturas del Bajío SA', 'MANBAJIO', 'MDB920310DEF', 'BOTH', 'CLI002', true, now(), now())
RETURNING id;
" >/dev/null 2>&1
}
# Insertar datos de ejemplo para packages
seed_packages() {
echo " → Insertando tipos de paquete..."
exec_pg_sql "
INSERT INTO a76.packages (tenant_id, company_id, key, description_es, description_en, weight_unit, plurals, plural_in, code_ace, code_aamex, created_at, updated_at)
VALUES
(${TENANT_ID}, ${COMPANY_ID}, 'PK01', 'Caja de Cartón', 'Cardboard Box', 0.5, 'CAJS', 'BOXS', 'CB01', 'CAJA001', now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'PK02', 'Pallet de Madera', 'Wooden Pallet', 15.0, 'PLTS', 'PLTS', 'WP01', 'PALL001', now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'PK03', 'Tambor Metálico', 'Metal Drum', 10.0, 'TMBS', 'DRMS', 'MD01', 'TAMB001', now(), now()),
(${TENANT_ID}, ${COMPANY_ID}, 'PK04', 'Contenedor', 'Container', 2000.0, 'CONT', 'CONT', 'CT01', 'CONT001', now(), now())
ON CONFLICT (tenant_id, company_id, key) DO NOTHING;
" >/dev/null 2>&1
}
# Insertar datos de ejemplo para classes
seed_classes() {
echo " → Insertando clases..."
exec_pg_sql "INSERT INTO a76.classes (tenant_id, company_id, class_code, description_es, description_en, material_key, unit_of_measure, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, 'CLS001', 'Componentes Electrónicos', 'Electronic Components', 'MP', 'PZA', '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS002', 'Partes Automotrices', 'Automotive Parts', 'MP', 'KGS', '8708.29.99', '8708.29.9900', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS003', 'Textiles y Telas', 'Textiles and Fabrics', 'MP', 'MT', '5407.20.01', '5407.20.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS004', 'Equipo de Computación', 'Computer Equipment', 'MP', 'PZA', '8471.30.01', '8471.30.0100', now(), now()) ON CONFLICT (tenant_id, company_id, class_code) DO NOTHING;" >/dev/null 2>&1
}
# Insertar datos de ejemplo para parts
seed_parts() {
echo " → Insertando partes/componentes..."
# Obtener un client_id para asociar las partes
local client_id
client_id=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} LIMIT 1;" | xargs)
if [ -n "$client_id" ]; then
exec_pg_sql "INSERT INTO a76.parts (tenant_id, company_id, client_id, part_number, description_spanish, description_english, part_class, currency_key, unit_of_measure, unit_cost, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-001', 'Microcontrolador ARM Cortex-M4', 'ARM Cortex-M4 Microcontroller', 'CLS001', 'USD', 'PZA', 15.50, '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-002', 'Filtro de Aceite Automotriz', 'Automotive Oil Filter', 'CLS002', 'USD', 'PZA', 8.75, '8421.23.01', '8421.23.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-003', 'Tela de Algodón para Tapicería', 'Cotton Upholstery Fabric', 'CLS003', 'USD', 'MT', 12.00, '5208.31.01', '5208.31.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-004', 'Disco Duro SSD 500GB', '500GB SSD Hard Drive', 'CLS004', 'USD', 'PZA', 65.00, '8471.70.01', '8471.70.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-005', 'Sensor de Temperatura Digital', 'Digital Temperature Sensor', 'CLS001', 'USD', 'PZA', 5.25, '9025.19.01', '9025.19.0100', now(), now()) ON CONFLICT (tenant_id, company_id, part_number) DO NOTHING;" >/dev/null 2>&1
fi
}
# Insertar datos de ejemplo para pedimentos
seed_pedimentos() {
echo " → Insertando pedimentos..."
# Primero obtener IDs de clientes
local client_ids
client_ids=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND client_or_provider IN ('CLIENT', 'BOTH') LIMIT 2;")
local client_id_1=$(echo "$client_ids" | sed -n '1p' | xargs)
local client_id_2=$(echo "$client_ids" | sed -n '2p' | xargs)
if [ -n "$client_id_1" ]; then
exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001234', ${client_id_1}, 'imp', 'normal', 'V1', 'ITE', 'draft', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001235', ${client_id_1}, 'exp', 'normal', 'V1', 'ETE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$client_id_2" ]; then
exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001236', ${client_id_2}, 'imp', 'consolidated', 'V1', 'ITE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1
fi
}
# Insertar datos de ejemplo para invoices
seed_invoices() {
echo " → Insertando facturas..."
# Obtener IDs de clientes/proveedores
local provider_ids
provider_ids=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND client_or_provider IN ('PROVIDER', 'BOTH') LIMIT 2;")
local provider_id_1=$(echo "$provider_ids" | sed -n '1p' | xargs)
local provider_id_2=$(echo "$provider_ids" | sed -n '2p' | xargs)
# Insertar facturas en invoice_header (mínimo requerido)
exec_pg_sql "INSERT INTO a76.invoice_header (tenant_id, company_id, system, operation_type, invoice_type, invoice_number, invoice_date, is_updated, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'INV-2024-001', '2024-01-10', false, now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'INV-2024-002', '2024-02-15', false, now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'INV-2024-003', '2024-03-05', false, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
# Obtener IDs de las facturas recién creadas
local invoice_ids
invoice_ids=$(exec_pg_sql "SELECT id FROM a76.invoice_header WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND invoice_number IN ('INV-2024-001', 'INV-2024-002', 'INV-2024-003') ORDER BY invoice_number;")
local invoice_id_1=$(echo "$invoice_ids" | sed -n '1p' | xargs)
local invoice_id_2=$(echo "$invoice_ids" | sed -n '2p' | xargs)
local invoice_id_3=$(echo "$invoice_ids" | sed -n '3p' | xargs)
# Insertar datos financieros para las facturas
if [ -n "$invoice_id_1" ]; then
exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, 'foreign', 17.50, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_2" ]; then
exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, 'foreign', 17.45, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_3" ]; then
exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, 'local', 1.00, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
# Insertar datos de compliance mexicano con proveedores
if [ -n "$invoice_id_1" ] && [ -n "$provider_id_1" ]; then
exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, ${provider_id_1}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_2" ] && [ -n "$provider_id_1" ]; then
exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, ${provider_id_1}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_3" ] && [ -n "$provider_id_2" ]; then
exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, ${provider_id_2}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
# Insertar datos de logística con incoterm
if [ -n "$invoice_id_1" ]; then
exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, 'none', 'kgs', 'FOB', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_2" ]; then
exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, 'none', 'kgs', 'CIF', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
if [ -n "$invoice_id_3" ]; then
exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, 'none', 'kgs', 'EXW', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1
fi
}
# Ejecutar todas las funciones de seed
execute_seed_data() {
echo -e "\n${YELLOW}[SEED] Cargando datos de ejemplo...${NC}"
seed_customs_brokers
seed_clients_and_providers
seed_packages
seed_classes
seed_parts
seed_pedimentos
seed_invoices
echo -e "${GREEN}✓ Datos de ejemplo cargados exitosamente${NC}"
echo -e "${YELLOW} • 3 Agentes aduanales${NC}"
echo -e "${YELLOW} • 4 Clientes/Proveedores${NC}"
echo -e "${YELLOW} • 4 Tipos de paquete${NC}"
echo -e "${YELLOW} • 4 Clases${NC}"
echo -e "${YELLOW} • 5 Parts/Componentes${NC}"
echo -e "${YELLOW} • 3 Pedimentos${NC}"
echo -e "${YELLOW} • 3 Facturas${NC}"
}
# Variables de configuración # Variables de configuración
KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}" KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}"
KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}" KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}"
@@ -559,8 +745,10 @@ fi
# Obtener información de la company # Obtener información de la company
COMPANY_INFO=$(exec_pg_sql "SELECT id, name FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;") COMPANY_INFO=$(exec_pg_sql "SELECT id, name FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;")
COMPANY_ID=$(exec_pg_sql "SELECT id FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;" | xargs)
echo -e "${GREEN}✓ Company: ${COMPANY_INFO}${NC}" echo -e "${GREEN}✓ Company: ${COMPANY_INFO}${NC}"
echo -e "${GREEN}✓ Company ID: ${COMPANY_ID}${NC}"
# Agregar tenant_id al usuario demo en Keycloak # Agregar tenant_id al usuario demo en Keycloak
echo -e "\n${YELLOW}Asignando tenant_id al usuario demo...${NC}" echo -e "\n${YELLOW}Asignando tenant_id al usuario demo...${NC}"
@@ -635,6 +823,13 @@ else
fi fi
fi fi
###############################################################################
# 9. Cargar datos de ejemplo (opcional)
###############################################################################
if [ "$SEED_DATA" = true ]; then
execute_seed_data
fi
############################################################################### ###############################################################################
# Resumen final # Resumen final
############################################################################### ###############################################################################
@@ -673,6 +868,18 @@ echo -e " ${GREEN}✓${NC} Plan: Enterprise (ilimitado)"
echo -e " ${GREEN}${NC} Status: Activa" echo -e " ${GREEN}${NC} Status: Activa"
echo -e " ${GREEN}${NC} Features: API, Reportes Avanzados, Integraciones, Soporte Dedicado" echo -e " ${GREEN}${NC} Features: API, Reportes Avanzados, Integraciones, Soporte Dedicado"
echo -e " ${GREEN}${NC} Vigencia: 1 año" echo -e " ${GREEN}${NC} Vigencia: 1 año"
if [ "$SEED_DATA" = true ]; then
echo -e "\n${YELLOW}Datos de ejemplo:${NC}"
echo -e " ${GREEN}${NC} Agentes aduanales: 3"
echo -e " ${GREEN}${NC} Clientes/Proveedores: 4"
echo -e " ${GREEN}${NC} Tipos de paquete: 4"
echo -e " ${GREEN}${NC} Clases: 4"
echo -e " ${GREEN}${NC} Parts/Componentes: 5"
echo -e " ${GREEN}${NC} Pedimentos: 3"
echo -e " ${GREEN}${NC} Facturas: 3"
fi
echo -e "\n${YELLOW}Puedes acceder al sistema en:${NC}" echo -e "\n${YELLOW}Puedes acceder al sistema en:${NC}"
echo -e " ${GREEN}http://localhost:5173${NC}" echo -e " ${GREEN}http://localhost:5173${NC}"
echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}\n" echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}\n"