marcos 192a2d9d89 fix(crm): el worker no podia entregar, y el carril encolaba lo que EFC rechaza siempre
Tres defectos que solo aparecieron al probar con los dos sistemas cableados. Ninguno lo
habrian encontrado las pruebas: usan SQLite con su propio registro de modelos y no
ejercitan el proceso del worker.

1. REGISTRO DE MODELOS EN EL WORKER. Celery no carga la app: importa el modulo de la
   tarea y nada mas. SQLAlchemy resuelve las ForeignKey por nombre de tabla contra su
   registro global, asi que sin la clase del otro extremo importada la configuracion de
   mappers moria con "Foreign key associated with column 'cases.account_id' could not
   find table 'crm.accounts'" y la tarea con PendingRollbackError.
   El sintoma era cruel: la fila del outbox se quedaba en pending con attempts=0 y SIN
   last_error --el fallo ocurre antes de poder registrarlo--, asi que el carril se veia
   encolando bien y no entregaba nunca. En la app web no pasa porque main.py monta todos
   los routers. Se importan los cuatro modelos del juego minimo verificado con
   configure_mappers() en un proceso limpio.

2. LA MIGRACION NO RELLENABA LAS FILAS PREVIAS. Los expedientes creados antes del carril
   quedaban con efc_storage_token NULL; el barrido de reconciliacion los encolaba, EFC los
   rechazaba con {'storage_token': ['This field may not be null.']} y agotaban sus 8
   intentos hasta failed. Ruido permanente por un dato derivable. La migracion ahora
   rellena 'CRM-'||company_id||'-'||reference, con la misma condicion de longitud que la
   guarda de storage_token: lo que no cabe en los 25 de pedimento_app se queda NULL a
   proposito, porque un token recortado apuntaria a la carpeta de otro expediente.

3. SIN GUARDA DE FOLIO NULO. crm.cases.reference es nullable, y ni el encolado ni el
   barrido de huecos lo comprobaban. Ahora los dos saltan lo que no tiene folio o token,
   y el encolado lo avisa en WARNING: es una omision silenciosa --el expediente vive en el
   CRM y sus documentos no llegaran a EFC-- y merece dejar rastro.

Verificado de punta a punta. Los TRES expedientes del CRM estan en EFC, leido desde EFC:

  EXP2026-08-001 -> CRM-2-EXP2026-08-001  provisional
  EXP2026-08-002 -> CRM-2-EXP2026-08-002  provisional
  EXP2026-08-003 -> CRM-2-EXP2026-08-003  provisional

y los tres en LINKED con su outbox en sent. El 001 nacio antes del enganche y se recupero
por el camino del relleno + reintento, que es el que usaria una persona desde el tablero.

Ref: T2026-08-046

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 12:44:12 -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%