Etapa 7 — Motor de alertas (BullMQ)
Adiciona um processo worker separado do processo web (src/worker.ts, WorkerModule) com dois crons diários e processors do BullMQ para alertas de mensalidade (master→tenant) e cobrança (tenant→customer): marca faturas/parcelas atrasadas, resolve template com fallback tenant→master por canal, renderiza placeholders, dispara WhatsApp/e-mail com retry e backoff exponencial, e grava alert_logs com idempotência por dia. Módulo alerts (web) traz o CRUD de alert_templates (master e tenant) e a consulta de alert_logs com filtros. alert_templates/alert_logs não têm RLS no schema, então o isolamento entre tenants é feito manualmente (PrismaMasterService + filtro por tenant_id), coberto por teste e2e dedicado. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { writeAuditLog } from '../../common/audit-log.util';
|
||||
import { PrismaMasterService } from '../../config/prisma-master.service';
|
||||
import { JwtPayload } from '../auth/types/jwt-payload.type';
|
||||
import { CreateMasterAlertTemplateDto } from './dto/create-master-alert-template.dto';
|
||||
import { ListAlertTemplatesQueryDto } from './dto/list-alert-templates.query.dto';
|
||||
import { UpdateMasterAlertTemplateDto } from './dto/update-master-alert-template.dto';
|
||||
|
||||
@Injectable()
|
||||
export class MasterAlertTemplatesService {
|
||||
constructor(private readonly prismaMaster: PrismaMasterService) {}
|
||||
|
||||
async create(dto: CreateMasterAlertTemplateDto, actor: JwtPayload, ip?: string) {
|
||||
return this.prismaMaster.$transaction(async (tx) => {
|
||||
const template = await tx.alert_templates.create({ data: { ...dto, tenant_id: null } });
|
||||
|
||||
await writeAuditLog(tx, {
|
||||
usuario_tipo: 'master',
|
||||
usuario_id: actor.sub,
|
||||
acao: 'criar_alert_template_master',
|
||||
entidade: 'alert_templates',
|
||||
entidade_id: template.id,
|
||||
detalhes: dto as unknown as Prisma.InputJsonValue,
|
||||
ip_origem: ip,
|
||||
});
|
||||
|
||||
return template;
|
||||
});
|
||||
}
|
||||
|
||||
findAll(query: ListAlertTemplatesQueryDto) {
|
||||
return this.prismaMaster.alert_templates.findMany({
|
||||
where: {
|
||||
tenant_id: null,
|
||||
...(query.tipo ? { tipo: query.tipo } : {}),
|
||||
...(query.canal ? { canal: query.canal } : {}),
|
||||
...(query.ativo === undefined ? {} : { ativo: query.ativo }),
|
||||
},
|
||||
orderBy: { criado_em: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: string) {
|
||||
const template = await this.prismaMaster.alert_templates.findFirst({
|
||||
where: { id, tenant_id: null },
|
||||
});
|
||||
if (!template) {
|
||||
throw new NotFoundException('Template não encontrado');
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateMasterAlertTemplateDto, actor: JwtPayload, ip?: string) {
|
||||
await this.findOne(id);
|
||||
|
||||
return this.prismaMaster.$transaction(async (tx) => {
|
||||
const atualizado = await tx.alert_templates.update({ where: { id }, data: dto });
|
||||
|
||||
await writeAuditLog(tx, {
|
||||
usuario_tipo: 'master',
|
||||
usuario_id: actor.sub,
|
||||
acao: 'atualizar_alert_template_master',
|
||||
entidade: 'alert_templates',
|
||||
entidade_id: id,
|
||||
detalhes: dto as unknown as Prisma.InputJsonValue,
|
||||
ip_origem: ip,
|
||||
});
|
||||
|
||||
return atualizado;
|
||||
});
|
||||
}
|
||||
|
||||
/** Soft delete — `alert_logs.template_id` referencia o template sem CASCADE; exclusão real quebraria o histórico. */
|
||||
async remove(id: string, actor: JwtPayload, ip?: string) {
|
||||
await this.findOne(id);
|
||||
|
||||
return this.prismaMaster.$transaction(async (tx) => {
|
||||
const desativado = await tx.alert_templates.update({ where: { id }, data: { ativo: false } });
|
||||
|
||||
await writeAuditLog(tx, {
|
||||
usuario_tipo: 'master',
|
||||
usuario_id: actor.sub,
|
||||
acao: 'desativar_alert_template_master',
|
||||
entidade: 'alert_templates',
|
||||
entidade_id: id,
|
||||
ip_origem: ip,
|
||||
});
|
||||
|
||||
return desativado;
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user