Cierra el ciclo de la factura: construcción del comprobante, sellado con el CSD de la
empresa emisora y transmisión al PAC.
- cfdi_builder: XML 4.0 de ingreso en el orden de atributos del XSD, del que depende la
cadena original y con ella el sello. Todo el dinero con Decimal.
- sealer: cadena original vía el XSLT oficial del SAT y firma con la llave del CSD.
- pac_comercio_digital: cliente de timbrarV5. Conserva el código y el saldo de folios que
el legado leía en una variable que descartaba (CFDI.cs:19324-19336).
- csd_service y core/crypto: CSD por empresa, con la contraseña cifrada en la base. Antes
el certificado había que dejarlo a mano en el almacenamiento y su contraseña era una
variable de entorno global, lo que no funciona con varias empresas emisoras.
- Cada intento —también los rechazados— guarda el XML que se transmitió y el que contestó
el PAC: sin ese par no hay forma de reconstruir un rechazo cuando termina la petición.
La declaración XML se escribe a mano con comillas dobles. lxml la emite con comillas
simples, que es XML válido, pero Comercio Digital compara la cadena literal version="1.0"
y responde 642 "la versión del XML no es 1.0".
El modo (pruebas o producción) sale de invoices.stamping_mode y no se puede pasar por la
API: es lo único que separa un timbre de prueba de un CFDI con validez fiscal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cierra la decisión pendiente 7. create_item ya no copia solo la descripción del
concepto: también hereda product_service_id, unit_of_measure_id y tax_object_id
cuando el cliente no los envía, para que la partida capturada por catálogo quede
completa para el CFDI. Lo que el cliente sí manda gana sobre el catálogo, para
poder facturar con una unidad distinta a la del concepto.
update_item pasa por la misma resolución cuando cambia concept_id: revalida que
el concepto sea de la empresa (antes el PATCH no lo validaba y admitía apuntar a
un concepto de otro tenant) y vuelve a heredar del concepto nuevo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
El selector de concepto de la partida deja de ser una lista fija en el código y
se alimenta del catálogo de conceptos de la empresa: al elegir uno se manda
concept_id y el backend copia la descripción a la columna de texto libre que
consume el PDF. Si el concepto trae precio unitario, se precarga en la partida.
Las claves genéricas anteriores quedan en un segundo grupo del mismo selector,
marcadas como "sin clave del SAT", para no bloquear a las empresas que aún no
tienen catálogo; si está vacío se enlaza al alta de conceptos.
El listado de partidas etiqueta con la clave y descripción del catálogo cuando
la partida lo referencia, y cae al texto libre para las facturas anteriores.
Los tipos de Invoice e InvoiceItem se completan con las claves fiscales que el
backend ya devuelve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Backend: los 8 endpoints de catálogo responden 200 con las semillas exactas y
filtran por búsqueda; tax-regimes acota por tipo de persona; ninguna ruta de
catálogo acepta escritura (405); sync_catalogs es idempotente. CRUD de
conceptos, conflicto 409 por clave ProdServ repetida en la misma empresa,
la misma clave permitida en otra empresa, la baja lógica liberándola,
aislamiento multi-tenant, upsert del emisor sin duplicar filas y RFC inválido
rechazado. También que una partida con concept_id hereda la descripción y que
las facturas sin claves del SAT siguen listándose y generando PDF.
El fixture de pruebas siembra los catálogos con la misma función que usa la
migración, sobre el schema sat mapeado a SQLite.
Frontend: prueba del cacheo del cliente de catálogos.
RFC dummy XAXX010101000 en todas las pruebas: sin datos reales.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Tabla crm.cases (expediente) con folio EXP2026-08-001 (next_folio entidad EXP,
sin dirección). Nace al crear la Oportunidad y se hereda vía case_id a
solicitud → cotización → operación → factura. advance_stage solo avanza.
- case_id (FK a crm.cases) en crm.opportunities/service_requests/quotes,
ops.shipments y fin.invoices; propagación en sus create_*. Migración
d4e5f6a7b8c9 reversible.
- Endpoints GET /v1/crm/cases, /cases/{id}, /cases/by-ref/{ref} con timeline
(historia completa para UI y otros sistemas).
- Frontend: casesAPI, ruta /dashboard/crm/expedientes (lista + timeline vertical),
chip "📁 Expediente" en solicitud/cotización, "Expedientes" en el sidebar.
- Consecutivo de folios sin tope (soporta >10,000,000/mes).
- 4 pruebas de expediente (minteo, propagación, timeline, no-retroceso). Suite en verde (113).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catálogos y selects:
- Incoterm como catálogo (nuevo catálogo global 'incoterm') en solicitud y modal
de conversión de oportunidad.
- Moneda como catálogo en Cotizaciones (nuevo/detalle) y Facturación.
- "Tipo de transporte" desde catálogo medio_transporte (antes lista fija).
- Cotizador: origen/destino como selects alineados a las rutas de los tarifarios
activos (endpoint /rate-locations), para que el costeo siempre encuentre ruta.
Bugs de la sesión:
- Direcciones no guardaban: DTO country String(2)→String(3) (ISO alfa-3); se
amplía accounts.country y se normaliza 'MX'→'MEX' (migración).
- Contacto de proveedor mal filtrado: contacts.ts ahora envía supplier_id.
- Selects ilegibles en modo oscuro: regla global select option en app.css.
- Formas de pago SAT a 2 dígitos (01/04/08) en catálogo y valores guardados.
- RelatedManager: editar direcciones/contactos/documentos (antes solo eliminar).
Oportunidades:
- Se quitan etapas Prospecto/Contactado del embudo semilla.
- Fechas separadas won_date/lost_date + motivo de pérdida, con modal al mover a
Ganada/Perdida (migración).
Facturación:
- Folio automático F{AAAA}-{MM}-{NNN} (next_folio entidad F, sin dirección).
- Moneda como catálogo.
UI:
- Giro "otro" habilita campo para especificar (accounts.industry_other, migración).
- Lista de contactos muestra a quién pertenece (cliente/prospecto/proveedor).
- Proveedores: países/puertos/aeropuertos/aduanas por catálogo (select + chips).
Migraciones reversibles (c2d3e4f5a6b7 ya existía; d3e4f5a6b7c8, e4f5a6b7c8d9).
Suite backend en verde (109). svelte-check sin errores nuevos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Modalidad AÉREO (4ª opción en "¿Cómo desea cotizar?"): en la solicitud
muestra la sección aérea con el cálculo en vivo P/Vol = (L×A×H cm × bultos)/6000
y el peso a cobrar = max(peso bruto, P/Vol). Fija el transporte en aéreo.
- Utilidad compartida crm/common/pricing.py (air_volumetric_kg / air_chargeable_kg,
factor internacional 6000).
- Motor de costeo (rates): la rama aérea usa el P/Vol por dimensiones si vienen
(CostRequest ahora acepta length/width/height_cm); respaldo m³×167 cuando no.
- Cotizador: captura por dimensiones (L×A×H + bultos) en modo aéreo y muestra el P/Vol.
- Solicitud→Cotización: si es AÉREO, siembra el concepto de flete con cantidad =
peso a cobrar (P/Vol) para capturar la tarifa por kg.
- Pruebas: test_pricing (ejemplo del doc → 720; max bruto/volumétrico) + cotización
aérea desde solicitud. Suite en verde (108).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Solicitud: al marcar un servicio adicional se habilita su costo estimado
(columna JSON additional_service_costs). Al cotizar, cada servicio marcado se
siembra como concepto de la cotización con ese costo de partida (costo=venta).
- Folios visibles: se muestran en la tarjeta de Oportunidad del kanban y se aclara
en el formulario que el folio se asigna al guardar (las listas ya lo mostraban).
- Sidebar CRM reordenado por flujo comercial (captación → embudo → solicitud →
cotización → catálogos de apoyo).
- Migración c2d3e4f5a6b7 aditiva y reversible. Suite backend en verde (102).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Desacopla la sesión de la app del access token de Keycloak (60s). Tras el SSO/
refresh, el backend emite un JWT de sesión local (HS256) con vida por inactividad
(SESSION_IDLE_MINUTES, cap SESSION_MAX_HOURS) y guarda los tokens KC en valkey.
La app usa esa sesión local como bearer (cookie access_token); el token KC vive en
kc_access_token solo para llamadas directas al Hub. Así el refresh KC contra el Hub
solo se intenta al expirar la sesión local, no cada ~60s → se elimina el bucle
causado por el bug "Token is not active" del relay.
Se RESPETA la revocación central de Keycloak: si el Hub rechaza el refresh, la
sesión termina (401). Sin re-emisión de fallback → sin bypass de revocación.
Todo detrás de SESSION_STORE_ENABLED (default False = comportamiento idéntico).
Backend: core/local_session.py, core/session_store.py (valkey), verify_token acepta
la sesión local, refresh la emite/actualiza; DTOs con session_token/session_id;
test unitario de local_session.
Frontend: access_token=sesión local, kc_access_token para el Hub; sso/refresh/
silent-refresh/switch-tenant y pantallas de workspace cableadas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cierra los huecos de la auditoría contra "SOFTWARE PARA AGENTES DE CARGA":
- ops (Diag. 2/3): bitácora con puntos de decisión (kind=decision) y ciclo de
corrección (parent_event_id/attempt) para ¿Cut Off? y ¿despacho autorizado?
(R-E-05/13, R-I-06). Reprogramación de salida (previous_etd, R-E-06). Hitos
operativos completos export/import. Cierre operativo con costos finales
(close_shipment, R-E-22).
- fin (Diag. 4): facturación con gate por cierre operativo y sin duplicar
(R-F-01), costos de operación arrastrados (ops_cost_total, R-F-02), envío con
PDF generado y guardado en MinIO (send_invoice + pdf.py sin dependencias,
R-F-05) y revisión del cliente (en_revision_cliente + aprobación, R-F-06).
- crm (Diag. 1): opportunity_id enlaza embudo→RFQ (R-C-02), contacto como etapa
(first_contact_at, R-C-04), re-cotización (clone_quote + reopen, R-C-12).
- transversal: catálogo de Incoterms y participantes/actores incl. autoridad
aduanera (R-T-01/10), enforcement de permisos por carril (RBAC) con roles
sembrados y dependencias dev-safe (R-T-07).
- Migración d5e6f7a8b9c0 con downgrade. Seed extendido. 70 tests (12 nuevos).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- schema fin: fin.invoices + fin.invoice_items + fin.payments; totales con IVA,
estados borrador→emitida→enviada→pagada, cobranza (pagos) y saldo automático
- generar-factura-desde-embarque (toma conceptos de venta de la cotización)
- ops.shipment_events: bitácora/hitos del embarque con secuencia por defecto
según operación (importación/exportación) — cubre Diagrama 3
- subida de documentos a MinIO: POST /crm/uploads (multipart) + URL firmada
- migración c4d5e6f7a8b9, routers/permisos (fin), seed del flujo hasta factura
- 58 tests pytest en verde
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diagrama 1 (CRM comercial) y Diagrama 2 (Operaciones) del spec de agente de carga:
- crm.service_requests (RFQ) + crm.rate_requests (solicitud de tarifas a proveedores)
- crm.quotes + crm.quote_items: conceptos costo/venta/margen, totales automáticos,
estados borrador→enviada→aceptada/rechazada
- schema ops: ops.shipments (booking, Cut Off, ETD/ETA, naviera/agente aduanal/destino)
y ops.shipment_documents (MBL/HBL, MAWB/HAWB, CMR…)
- liberar-a-operaciones: crea el embarque desde la cotización aceptada
- migración b3c4d5e6f7a8, routers/permisos, seed del flujo completo
- 52 tests pytest en verde
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- convert_lead valida embudo/etapa en el scope tenant/company (evita fuga multi-tenant y 500)
- oportunidades: estado/probabilidad/cierre se derivan de la etapa también en create/update (no solo move)
- oportunidades: valida que la etapa pertenezca al embudo indicado
- cuentas: country por defecto 'MX' (el server_default no aplicaba con NULL explícito)
- convert_lead: contact_name se normaliza (evita first_name vacío)
- +4 tests que fijan el comportamiento corregido (28 en verde)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Nuevo schema `crm` con 7 tablas multi-tenant (TenantScopedMixin + soft delete)
- Módulos FastAPI por dominio: models/dto/service/routes (patrón example)
- Métricas del dashboard (KPIs + embudo por etapa)
- Conversión de prospecto → cuenta/contacto/oportunidad (idempotente)
- Movimiento de oportunidad entre etapas (Kanban) con estado/probabilidad derivados
- 25 permisos registrados en PermissionRegistry
- Migración Alembic con upgrade/downgrade completos
- 24 tests de servicios (pytest) en verde
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>