marcos 02feb973c9 feat(crm): carril con reintentos que entrega los documentos del CRM a EFC
Clon del gateway Anexo22 -> EFC que ya esta en produccion, con los nombres
cambiados. No es una reinterpretacion: la maquina de reintentos de tres capas,
el corte en 4xx y las cuatro guardas de idempotencia se conservan tal cual.

Sin esto, "EFC es la fuente unica" obligaria a llamar a EFC dentro del request
del usuario, y un EFC caido le haria perder su trabajo. Con outbox + Celery, la
subida responde 201 siempre y el sistema entrega cuando EFC vuelve, sin
duplicar: el crm_document_ref viaja con la subida y EFC devuelve 200 con el
documento que ya existia en vez de crear otro. Es lo que cubre el timeout
ambiguo -EFC commiteo y contesto tarde-, donde el CRM no puede saber si entro.

TRES DESVIACIONES DELIBERADAS DEL ORIGINAL, las tres con su razon en el codigo:

1. SAVEPOINT en vez de db.rollback() en el except del encolado. En Anexo22 el
   outbox vive en OTRA base que el pedimento, asi que su rollback solo revertia
   la sesion del outbox. El CRM es mono-base y el encolado corre DENTRO de la
   transaccion del usuario: heredar ese rollback tumbaba la solicitud y el
   expediente recien creados -exactamente lo contrario de best-effort, y en
   silencio-. Lo destapo un test y asi se manifestaba:
   "InvalidRequestError: Instance '<ServiceRequest>' is not persistent within
   this Session". Con el savepoint el fallo deshace solo la fila del outbox.

2. source_table junto a source_id en la guarda _ya_entregado. El CRM tiene DOS
   tablas de documentos con secuencias independientes: crm.documents.id = 5 y
   ops.shipment_documents.id = 5 son documentos distintos. Con el id solo, haber
   entregado el primero haria que el segundo se saltara para siempre sin un solo
   error visible. Hay test que lo fija.

3. EFC_UPLOAD_TIMEOUT_MS aparte de EFC_API_TIMEOUT_MS. Los 8 s de los metadatos
   no alcanzan para un archivo de 25 MB, y el timeout debe quedar POR DEBAJO del
   proxy_read_timeout del nginx de EFC: si el CRM esperara mas, veria un 504
   opaco sin saber si el documento entro.

El cliente HTTP llega con las pruebas que el carril de referencia NO tiene
-verificado: en Anexo22 no hay ni un test de EfcClient._request-, asi que alli
el bucle de reintentos, el backoff y el corte en 4xx nunca se ejercitan. Ese
hueco no se clona: 16 casos contra httpx.MockTransport, sin tocar la red.

Las migraciones se GENERAN y se dejan SIN aplicar.

BLOQUEADO: docker-compose.prod.yml no se toco. El ticket pide las 8 variables en
api, worker y beat, pero Orquestacion.md 13.14 y 4.5 lo prohiben expresamente
("ni tocarlo"), y el orquestador manda sobre el ticket. Queda como paso manual
en el reporte; sin el, worker y beat no ven EFC_API_URL y el carril queda
apagado en produccion, que es degradar limpio y no romper.

Verificacion: pytest tests/ EXIT=0, 151 passed 1 skipped (baseline 70 passed).
Cuatro roturas deliberadas y restauradas: quitando source_table de la guarda el
test de la ambiguedad se puso rojo; reintentando los 4xx los tres tests del
corte dieron "assert 3 == 1"; borrando el objeto local antes de subir cayeron
los tres del corte directo; y disparando el ensure por cualquier 404 se rompio
el test del code.

Ticket: T2026-08-046 (fase 6 de 7)
2026-08-07 18:05:48 -06:00

CRM — Aduanasoft

Sistema CRM construido sobre la plantilla Workspace de Aduanasoft (SvelteKit 5 + FastAPI + PostgreSQL, multi-tenant con Keycloak/Hub). Gestiona cuentas, contactos, prospectos, oportunidades (pipeline Kanban) y actividades comerciales.

Base: plantillas-proyectos. Este repo conserva el core de la plantilla (auth, tenants, permisos, licencias) y agrega el dominio CRM en backend y frontend.


Stack

Capa Tecnología
Frontend SvelteKit 5 (runes) + Tailwind + shadcn-svelte
Backend FastAPI + SQLAlchemy 2.0 + Pydantic v2
Auth Keycloak (OIDC) vía Workspace Hub
Base de datos PostgreSQL (schema crm)
Cache / Queue Valkey (Redis) + Celery
Storage MinIO (S3-compatible)
Contenedores Docker Compose

Módulo CRM

Esquema dedicado crm con 7 tablas multi-tenant (tenant_id + company_id, soft delete):

Entidad Tabla Descripción
Cuentas crm.accounts Empresas cliente/prospecto (importador, IMMEX, agencia aduanal, transportista). Incluye RFC y patente aduanal.
Contactos crm.contacts Personas asociadas a una cuenta.
Prospectos crm.leads Leads sin calificar; se convierten en cuenta + contacto + oportunidad.
Embudos crm.pipelines Embudos de venta por compañía.
Etapas crm.pipeline_stages Columnas del Kanban (con probabilidad y etapas terminales ganada/perdida).
Oportunidades crm.opportunities Negocios que avanzan por el embudo.
Actividades crm.activities Llamadas, reuniones, tareas, correos y notas.

Endpoints (prefijo /v1/crm)

Todos reciben company_id como query param y validan permisos vía Keycloak/PermissionService.

  • GET|POST /accounts, GET|PATCH|DELETE /accounts/{id}
  • GET|POST /contacts, GET|PATCH|DELETE /contacts/{id}
  • GET|POST /leads, GET|PATCH|DELETE /leads/{id}, POST /leads/{id}/convert
  • GET|POST /pipelines, PATCH|DELETE /pipelines/{id}
  • GET|POST /stages, PATCH|DELETE /stages/{id}
  • GET|POST /opportunities, GET|PATCH|DELETE /opportunities/{id}, PATCH /opportunities/{id}/move
  • GET|POST /activities, GET|PATCH|DELETE /activities/{id}, PATCH /activities/{id}/complete
  • GET /metrics — KPIs y embudo por etapa para el dashboard

Permisos

Se registran en el PermissionRegistry al arrancar (25 permisos: crm.access + crm.{account,contact,lead,opportunity,pipeline,activity}.{view,create,edit,delete}). Para persistirlos en BD: POST /v1/core/permissions/sync (o el CLI de sincronización).


Inicio rápido (dev)

cp .env.example .env          # ajustar CORE_DB_NAME, Keycloak, etc.
./scripts/auth-mode.sh local  # login local sin workspace
docker compose up -d

Abre http://localhost:5173CRM en el sidebar.

Migraciones

cd backend
alembic upgrade head          # crea el schema crm y sus tablas (revisión f1a2b3c4d5e6)
alembic downgrade -1          # revierte el CRM (down() probado)

Pruebas

Backend (servicios CRM):

cd backend
pytest tests/ -q              # 24 pruebas de servicios (SQLite en memoria)

En CI/PostgreSQL, define TEST_DATABASE_URL para ejercitar el esquema real y las políticas RLS (ver docs/ARCHITECTURE.md).


Estructura del CRM

backend/api/v1/modules/crm/
├── router.py            # agrega submódulos bajo /crm (y registra permisos)
├── permissions.py       # alta de permisos CRM en el registry
├── accounts/  contacts/  leads/  pipelines/  opportunities/  activities/  metrics/
│   └── models.py · dto.py · service.py · routes.py
backend/alembic/versions/f1a2b3c4d5e6_crm_schema.py

frontend/src/
├── lib/api/crm/         # clientes API tipados por entidad
├── lib/components/crm/  # helpers de formato/etiquetas
└── routes/dashboard/crm/
    ├── +page.svelte             # panel (KPIs + embudo)
    ├── cuentas/  contactos/  prospectos/  actividades/   # CRUD
    └── oportunidades/           # Kanban con drag & drop

Convenciones

  • Commits: Conventional Commits (feat:, fix:, refactor:, chore:)
  • Ramas: feature/AS-###-desc, fix/AS-###-desc
  • Código: nombres en inglés, comentarios de negocio en español
  • Backend: Pydantic v2, routers por dominio, filtros multi-tenant explícitos
  • Frontend: runes ($state, $derived, $effect, $props), Tailwind utility-first
Description
No description provided
Readme 2.1 MiB
Languages
Python 50.9%
Svelte 23.3%
TypeScript 13.6%
HTML 11.4%
Shell 0.4%
Other 0.2%