import { Injectable } from '@nestjs/common'; import { ComplianceService } from '../compliance/compliance.service'; import { UpcomingExpirationDto } from '../compliance/dto/response/compliance.dto'; import { StoreDocumentStatus } from '../common/enum/document.enum'; export interface IDueAlert { typeCode: string; typeName: string; /** Qual degrau da régua disparou (lead | 30 | 15 | 7 | 1 | vencido). */ trigger: 'lead' | 'd30' | 'd15' | 'd7' | 'd1' | 'expired'; daysToExpiry: number; expiryDate: string; /** O prazo que disparou o degrau `lead` — dinâmico quando há amostra. */ effectiveLeadDays: number; /** Amostra observada do órgão que esticou o prazo; nulo = só catálogo. */ observed?: { count: number; medianDays: number } | null; } /** * Régua de alertas de vencimento. * * Degraus fixos (30/15/7/1/vencido) + o degrau específico do tipo * (`renewalLeadDays` — na LO, 120 dias, porque protocolar no prazo prorroga a * validade). Documento com renovação já protocolada NÃO alerta: a pendência * do gestor acabou; acompanhar o órgão é outro fluxo (v3). * * v1.2 (TODO): envio de verdade — WhatsApp como canal principal (o dono de * posto vive nele; reusar a infraestrutura de WhatsApp das campanhas), * e-mail como secundário, escalonamento para o superior perto do vencimento * e pendência rastreável que só fecha com documento novo anexado. Este * service já entrega a lista pronta para o job diário consumir. */ @Injectable() export class AlertsService { constructor(private readonly complianceService: ComplianceService) {} async computeDueAlerts(params: { programId: string; storeId: string; }): Promise { const upcoming = await this.complianceService.getUpcoming({ ...params, days: 365, }); return upcoming .filter(item => item.status !== StoreDocumentStatus.InRenewal) .map(item => this.toAlert(item)) .filter((alert): alert is IDueAlert => alert !== null); } private toAlert(item: UpcomingExpirationDto): IDueAlert | null { // Prazo dinâmico (Onda 1.2): quando o órgão está comprovadamente mais // lento que o prazo do catálogo, o degrau `lead` dispara mais cedo. const lead = item.effectiveLeadDays ?? item.renewalLeadDays; const trigger = this.resolveTrigger(item.daysToExpiry, lead); if (!trigger) return null; return { typeCode: item.typeCode, typeName: item.typeName, trigger, daysToExpiry: item.daysToExpiry, expiryDate: item.expiryDate, effectiveLeadDays: lead, observed: item.observed ?? null, }; } /** O degrau mais urgente que já foi atingido; nenhum → sem alerta hoje. */ private resolveTrigger( daysToExpiry: number, renewalLeadDays: number, ): IDueAlert['trigger'] | null { if (daysToExpiry < 0) return 'expired'; if (daysToExpiry <= 1) return 'd1'; if (daysToExpiry <= 7) return 'd7'; if (daysToExpiry <= 15) return 'd15'; if (daysToExpiry <= 30) return 'd30'; if (daysToExpiry <= renewalLeadDays) return 'lead'; return null; } }