Files
CRM_AGENTES_CARGA/backend/tests/conftest.py
Ernesto Herrera 915bdd19fe feat(crm): ampliar solicitud de servicio y encadenar el ciclo comercial
Solicitud de servicio:
- Campos del documento maestro de cotización (ruta estructurada por país,
  mercancía, dimensiones/bultos, FCL/LCL, servicios adicionales, notas).
- Origen/Destino seleccionables por catálogo de país (seed ya poblado).
- Validación de contacto asociado (422 si no existe).

Ciclo Oportunidad -> Solicitud -> Cotización -> Operación:
- Dirección impo/expo se captura en la Oportunidad y se hereda al ciclo.
- Conversión Oportunidad->Solicitud idempotente con back-link.
- Endpoint Solicitud->Cotización; "Ambas" genera 2 cotizaciones (FCL/LCL).
- Liberación a Operaciones confirma IMPO/EXPO (prefijado) y siembra los hitos.
- Fecha de la cotización (issue_date) por defecto hoy, editable y en el PDF.

Folios auto-generados {LETRA}{AAAA}-{MM}-{NNN}-{DIR} para Oportunidad (O),
Solicitud (S), Cotización (C) y Operación (OP); consecutivo mensual por
compañía y entidad (crm.folio_counters + helper next_folio con bloqueo de fila).

Catálogos: 9 nuevos (tipo_operacion, medio_transporte, tipo_servicio, prioridad,
tipo_mercancia, unidad_medida, tipo_embalaje, servicio_adicional, tipo_documento).

Migración b1c2d3e4f5a6 reversible (upgrade->downgrade->upgrade verificado en PG).
25 pruebas unitarias nuevas (folios, catálogos, solicitudes, cotizaciones,
embarques); suite completa en verde (101 pruebas).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-03 17:51:40 -06:00

85 lines
3.0 KiB
Python

"""Fixtures de pruebas del módulo CRM.
Las pruebas de servicios corren contra SQLite en memoria usando
``schema_translate_map`` para mapear los schemas ``crm``/``core`` al schema
principal de SQLite. Esto permite ejercitar la lógica de negocio sin depender
de PostgreSQL/Docker en el entorno de desarrollo local.
Nota: la validación estructural del esquema (FKs cross-schema, DDL) se hace
con la migración Alembic contra PostgreSQL en CI (``TEST_DATABASE_URL``).
"""
import os
import sys
from datetime import datetime
import pytest
from sqlalchemy import Column, Integer, String, Table, create_engine, event
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool
BACKEND_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if BACKEND_DIR not in sys.path:
sys.path.insert(0, BACKEND_DIR)
from core.database import Base # noqa: E402
# Importar los modelos registra sus tablas en Base.metadata
import api.v1.modules.crm.accounts.models # noqa: E402,F401
import api.v1.modules.crm.activities.models # noqa: E402,F401
import api.v1.modules.crm.addresses.models # noqa: E402,F401
import api.v1.modules.crm.catalogs.models # noqa: E402,F401
import api.v1.modules.crm.common.folios # noqa: E402,F401
import api.v1.modules.crm.contacts.models # noqa: E402,F401
import api.v1.modules.crm.documents.models # noqa: E402,F401
import api.v1.modules.crm.leads.models # noqa: E402,F401
import api.v1.modules.crm.opportunities.models # noqa: E402,F401
import api.v1.modules.crm.pipelines.models # noqa: E402,F401
import api.v1.modules.crm.quotes.models # noqa: E402,F401
import api.v1.modules.crm.service_requests.models # noqa: E402,F401
import api.v1.modules.crm.suppliers.models # noqa: E402,F401
import api.v1.modules.ops.shipments.models # noqa: E402,F401
import api.v1.modules.fin.invoices.models # noqa: E402,F401
_SCHEMA_MAP = {"crm": None, "core": None, "ops": None, "fin": None}
# Tabla mínima core.tenants para resolver la FK tenant_id de las tablas crm.
# En CI (PostgreSQL) la tabla real la crea la migración inicial del core.
if "core.tenants" not in Base.metadata.tables:
Table(
"tenants",
Base.metadata,
Column("id", Integer, primary_key=True),
Column("name", String(255)),
schema="core",
)
TENANT_ID = 1
COMPANY_ID = 1
@pytest.fixture()
def db():
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
future=True,
).execution_options(schema_translate_map=_SCHEMA_MAP)
@event.listens_for(engine, "connect")
def _register_now(dbapi_conn, _record):
# Soporta server_default text("now()") de los mixins de timestamp
dbapi_conn.create_function(
"now", 0, lambda: datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")
)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine, future=True)
session = session_factory()
try:
yield session
finally:
session.close()
engine.dispose()