SICOP — Corrección del scaffolding V1 contra Prompt Maestro v2.0

Fecha: 2026-08-25 · Proyecto: mvp-crm · Estado: aplicado, suite verde (52 ejemplos)

Decisiones aplicadas

  1. Bug en el brief, no en el código: la implementación literal de LeadAssignment.next_business_start del prompt maestro NO avanzaba de día cuando el lead llegaba después del cierre en día laboral (sáb 21:00 → devolvía sáb 08:00, en el pasado). Se corrigió añadiendo t += 1.day if t.hour > BUSINESS_HOURS.end. → Actualizar el prompt maestro v2.0 con esta versión corregida.

  2. Enum Lead.status renumerado: se insertó reserved: 6 (negotiating → reserved → won) y se agregó invalid: 10 (faltaba; la Regla 3 escala wrong_number → invalid). won/lost/recycled cambiaron de valor entero. Seguro porque no había datos en ningún ambiente (repo sin commits, PG nunca migrado en prod).

  3. Migración original editada, no apilada: repo sin commits ni deploy; había que tocarla de todos modos por jsonb → json (SQLite dev/test). Se añadieron en la misma migración: sla_starts_at, contact_score, sla_breached, recycle_date, campaign_id en leads; channel, outcome, to_status nullable en lead_events; índices únicos parciales contacts.phone y leads[campaign_id, contact_id].

  4. Stack realineado: SQLite dev/test (PG no corría en la máquina local; el stack lo mandata), pg al group :production, sqlite3 dev/test, pundit añadido. config.time_zone = "America/Mexico_City". Solid Queue conectado en development.

  5. Webhook de Facebook: payload simplificado firmado con HMAC (X-Hub-Signature-256) + GET hub.challenge. El mapeo del payload real de leadgen queda PENDIENTE del acceso a Meta Business (pregunta abierta #6 del brief). ENV: SICOP_FACEBOOK_APP_SECRET, SICOP_FACEBOOK_VERIFY_TOKEN.

  6. Regla 1 implementada: SLA corre desde sla_starts_at en horario lun–sáb 08:00–19:59 CDMX. Lead fuera de horario → score abre el día en 100; monitor job se agenda para sla_starts_at. Score y breach persisten en columnas (calculados por LeadSlaMonitorJob, no en la vista).

  7. Idempotencia: EscalateSlaBreachService retorna early si sla_breached ya es true; CreateLeadService dedup por campaign_id+phone y rescata RecordNotUnique devolviendo el lead existente (webhook reintenta seguro).

Aplazado conscientemente (siguientes vertical slices)

  • Namespacing de controllers (bdc/, seller/, management/) — estructural, no urgente.
  • ContactLeadService + Regla 3 completa (marcar contactado con outcome answered).
  • AssignToSellerService (reloj atómico con lock de fila).
  • Reciclaje (recycle_date existe; falta el job que reaparece el lead).
  • AppointmentPolicy, UserPolicy, AssignmentQueuePolicy (se crean con sus controllers).
  • Tailwind (stack lo lista; el scaffold usa application.css — decidir antes de UI seria).

Comandos

bundle config set --local without production && bundle install
bin/rails db:migrate && bin/rails db:test:prepare
bundle exec rspec        # 52 ejemplos, 0 fallos
bundle exec rubocop      # sin ofensas