diff --git a/backend/api/v1/modules/crm/cases/service.py b/backend/api/v1/modules/crm/cases/service.py index 733c629..db76cc3 100644 --- a/backend/api/v1/modules/crm/cases/service.py +++ b/backend/api/v1/modules/crm/cases/service.py @@ -22,6 +22,27 @@ def create_case( ) db.add(case) db.flush() + + # ── Carril hacia EFC (T2026-08-046) ────────────────────────────────────────────────── + # EFC es la fuente única de los documentos del expediente. El reflejo allá —un pedimento + # provisional— se pide AQUÍ, en el instante en que nace el folio, porque el folio es + # precisamente la llave con la que las dos mitades se reconocen. + # + # El token de almacenamiento se fija al nacer y NO cambia nunca: es la carpeta de MinIO + # del lado de EFC. Que sea inmutable es lo que permite completar el provisional con la + # data aduanera real sin mover un solo archivo. + # + # Todo es best-effort y va en la MISMA transacción que el expediente: + # - con ``EFC_API_URL`` vacía es un no-op y el expediente vive igual, solo en el CRM; + # - si el encolado o el despacho fallan, no se propaga el error: el barrido del beat + # recoge lo pendiente. Un sistema de terceros caído no puede romper un alta. + # El import es diferido para no acoplar el arranque del módulo del expediente al carril. + from ..expediente_gateway import service as gateway + from ..expediente_gateway.storage import storage_token + + case.efc_storage_token = storage_token(company_id, case.reference) + db.flush() + gateway.replicate_expediente_best_effort(db, case) return case diff --git a/backend/api/v1/modules/crm/router.py b/backend/api/v1/modules/crm/router.py index 09353dc..c3c9823 100644 --- a/backend/api/v1/modules/crm/router.py +++ b/backend/api/v1/modules/crm/router.py @@ -17,6 +17,7 @@ from .cases.routes import router as cases_router from .catalogs.routes import router as catalogs_router from .contacts.routes import router as contacts_router from .documents.routes import router as documents_router +from .expediente_gateway.routes import router as expediente_gateway_router from .leads.routes import router as leads_router from .metrics.routes import router as metrics_router from .opportunities.routes import router as opportunities_router @@ -50,3 +51,7 @@ router.include_router(catalogs_router) router.include_router(uploads_router) router.include_router(rates_router) router.include_router(rates_cost_router) +# Tablero de operación del carril hacia EFC: cola pendiente, métricas y reintento manual. +# No es una ruta de negocio; existe para que una persona vea y desatore la entrega sin +# entrar a la base. Hereda el enforcement de crm.access del router agregador. +router.include_router(expediente_gateway_router) diff --git a/backend/tests/test_contrato_efc.py b/backend/tests/test_contrato_efc.py index 6346731..6deddfd 100644 --- a/backend/tests/test_contrato_efc.py +++ b/backend/tests/test_contrato_efc.py @@ -278,10 +278,14 @@ def test_el_formato_del_folio_es_el_del_contrato(db): es contrato, no detalle.""" import re - from api.v1.modules.crm.expedientes.folio import next_folio + # El generador es el del CRM (crm/common/folios.py), no uno del carril: el expediente y su + # consecutivo son del CRM y esta prueba solo afirma que la FORMA que produce es la que EFC + # espera. Por eso se llama a la implementación real y no se reimplementa el formato aquí. + from api.v1.modules.crm.common.folios import next_folio from tests.conftest import COMPANY_ID, TENANT_ID - folio, _, _, _ = next_folio(db, TENANT_ID, COMPANY_ID) + # with_direction=False: el expediente no lleva sufijo I/E, a diferencia de la solicitud. + folio = next_folio(db, TENANT_ID, COMPANY_ID, "EXP", None, with_direction=False) patron = ( API_CRM["folio"]["formato"]