Traducción. Autoritativo: fs-loy-0011-expiring-points.md.
Contexto
La expiración es el mecanismo que mantiene acotado el pasivo y le da urgencia a los puntos. La expiración silenciosa es la versión que genera reclamos y atención regulatoria; la expiración avisada es una de las campañas de retención de mejor rendimiento que tiene un programa. La feature es aparte del libro (FS-LOY-0002) porque los modos de falla no tienen relación. El libro debe expirar puntos correctamente; esta feature debe encontrar a los members que están por perderlos y avisarles, a una escala donde un escaneo ingenuo sobre todo el libro cada noche deja de funcionar.Alcance (normativo)
- Escaneo programado de lotes que vencen dentro de un horizonte configurable.
loyalty.points.expiring_soonemitido una vez por member por cohorte de lotes por horizonte.- Configuración de horizontes por programa, con varios horizontes permitidos (30 y 7 días, digamos).
loyalty.expiry_scan_runsregistrando cada corrida, su cohorte y su resultado.- Fan-out por tenant para que un tenant grande no deje sin recursos al resto.
- Idempotencia que garantiza que a un member se le avisa una vez por cohorte, pase lo que pase con el job.
Fuera de alcance (normativo)
- Expirar los puntos. Eso lo hace el libro (FS-LOY-0002); esta feature solo avisa.
- La campaña y su contenido —
messagingconsume el evento. - La visualización de vencimientos en el portal, que lee la proyección.
Comportamiento (normativo)
- El escaneo corre por tenant y por programa, con fan-out por cron. Nunca como una consulta global.
- A un member se le avisa una vez por (cohorte de lotes, horizonte). Reejecutar el escaneo el mismo día, o reprocesar el job, no produce un segundo evento.
- Los horizontes son independientes: a un member se le puede avisar a 30 días y de nuevo a 7.
- El evento lleva el total por vencer, la moneda, la fecha de vencimiento y el id de cohorte —
suficiente para que
messagingrenderice sin volver a consultar. - Los avisos son de categoría
marketingy por lo tanto obedecen consentimiento, supresiones, quiet hours y frequency caps (ADR-019). PROHIBIDO: enviar un aviso de vencimiento como transaccional para saltarse un opt-out. Es un mensaje de retención, y el member dijo que no. - La frecuencia del escaneo sigue el tier de frescura del tenant (DEC-G5); el default es nocturno.
- Un member sin canal alcanzable igualmente queda registrado como escaneado. El escaneo reporta lo que encontró, con independencia de la entrega.
- El escaneo es de solo lectura contra el libro. Nunca muta lotes.
Datos (normativo)
API (normativo)
Eventos (normativo)
loyalty.points.expiring_soon, disponible como webhook saliente. Payload: contact_id,
points_amount, point_currency_id, expires_at, cohort_key, correlation_id.
Criterios de aceptación (normativo)
- Un member con un lote que vence en 30 días recibe aviso exactamente una vez cuando corre el horizonte de 30 días.
- Reejecutar el mismo escaneo el mismo día emite cero eventos adicionales.
- Dos horizontes producen dos eventos en sus días respectivos, y ninguno suprime al otro.
- El escaneo sobre 500 000 lotes termina dentro del presupuesto documentado sin exceder el presupuesto de consultas por tenant.
- Un member que optó por salir de marketing no recibe mensaje, mientras el evento igual se emite y el escaneo igual lo registra.
- Reprocesar el job tras una caída a media corrida se reanuda sin duplicar avisos.
- Negativo: el escaneo no realiza ninguna escritura contra
point_lotsniledger_transactions— probado con un rol de base de datos de solo lectura en el test.
Ejecución
Pipeline asíncrono. Fan-out programado enbackend/scheduler, el escaneo en backend/workers,
idempotente vía core.processed_jobs más la valla de expiry_scan_runs.
Preguntas abiertas
Ninguna pendiente. Toda pregunta que llevaba este spec quedó respondida en el registro consolidado (../../../../design/open-questions-v1.md, v1.1) y se
incorporó a las secciones normativas de arriba.