Cada renovacao que sai do orgao vira ciclo observado (protocolo -> emissao); a mediana por orgao/tipo estica o prazo de inicio de renovacao quando o orgao esta comprovadamente mais lento que o catalogo. O catalogo e piso legal: os 120 dias da LO nunca encolhem. Campos aditivos effectiveLeadDays/observed no upcoming e nos alertas; GET /compliance/lead-times; seed deterministico. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
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<IDueAlert[]> {
|
|
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;
|
|
}
|
|
}
|