Appearance
Cobranza — Estado actual
El worker que ya corre
DunningWhatsAppJob vive en crm-backend/api/internal/jobs/dunning_whatsapp_job.go. Se registra en el scheduler global (internal/jobs/scheduler.go) con una expresión cron 0 * * * * (cada hora en punto).
Lo que hace, paso a paso
1. Resuelve lista de tenants activos (hoy: Semtec).
2. Para cada tenant:
a. GET billing /invoices?company_id=…&status=overdue
b. Dedupe por contact_id (un contacto puede tener varias facturas)
c. Para cada contacto:
- Carga contact de Mongo (wa_id, preferred_channel)
- Skip si preferred_channel != "whatsapp"
- Lee last_dunning_sent_at del contact
- Elige template según days_overdue
- Check cooldown (48h mismo template)
- Encola job a whatsapp-outbound-jobs
- Actualiza last_dunning_sent_at + contador intentos
3. Publica métrica dunning_messages_sent_total{template}Configuración
Vars de entorno (deploy-dev/.env):
DUNNING_ENABLED=true
DUNNING_WINDOW_START=08
DUNNING_WINDOW_END=20
DUNNING_COOLDOWN_HOURS=48
DUNNING_MAX_ATTEMPTS=3Métricas
Expuestas en /metrics del crm-api:
dunning_cycle_duration_seconds(histogram).dunning_invoices_scanned_total.dunning_messages_sent_total{template="payment_reminder|payment_overdue|service_suspended"}.dunning_skipped_total{reason="cooldown|no_wa_id|out_of_window|not_preferred_channel"}.
Por qué no es "billing-driven"
El worker pregunta a billing cada hora. Lo que queremos es que billing avise cuando algo pasa. Esa diferencia es el próximo paso.
Ver gaps para saber qué falta.