Files
sar/apps/api/src/app/notifications/notifications.service.ts
julian 518769218f fix(api,web): corrige autorização da área do rep e falhas silenciosas no front
Auditoria de segurança + robustez da área do representante antes de produção.

API (autorização e injeção):
- IDOR em clients: endpoints de detalhe (findOne, notas, ctr, top-produtos,
  histórico, contatos) validam carteira do rep via assertClienteDaCarteira;
  cliente alheio retorna 404. orders.create idem antes de aceitar idCliente.
- Role guard em dashboards supervisor/manager e ger/chamados (rep -> 403).
- Datas em $queryRawUnsafe agora exigem YYYY-MM-DD (DateOnlySchema + revalidação);
  dtAniversario de contato validado e escapado.
- id_empresa de CTR/NF usa matrizEmpresa() em vez de 1/9001 hardcoded.
- Alçada de desconto: transmit valida desconto efetivo por item (preço vs tabela
  pauta>promo>base + promoção ativa), não só o desconto global.
- Idempotency-Key escopado a empresa+vendedor; unsubscribe com DTO e dono.

Web (erros nunca silenciosos):
- Remove dupla serialização (apiFetch já faz JSON.stringify) em funil, sac e
  politicas — corrige criar oportunidade, abrir chamado e editar políticas.
- Handler global de erro no QueryClient (query e mutation viram toast).
- Error boundary por rota (payload divergente não derruba o app em branco).
- Alert usa message= em vez de title= (AntD 6); sw.js usa globalThis.

Qualidade:
- Corrige ping.contract.spec (idEmpresa) e zera erros de lint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:40:19 +00:00

98 lines
3.2 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { ClsService } from 'nestjs-cls';
import type { SubscribePayload, PendingCountResponse } from '@sar/api-interface';
import type { WorkspaceClsStore } from '../workspace/workspace.types';
import { PushService, type PushPayload } from './push.service';
// Situa: 1=Pendente Aprovação
const SITUA_PENDENTE = 1;
@Injectable()
export class NotificationsService {
constructor(
private readonly cls: ClsService<WorkspaceClsStore>,
private readonly push: PushService,
) {}
async subscribe(userId: string, role: string, dto: SubscribePayload): Promise<void> {
const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS');
const idEmpresa = this.cls.get('idEmpresa');
const codVendedor = userId ? parseInt(userId, 10) : null;
await prisma.pushSubscription.upsert({
where: { endpoint: dto.endpoint },
update: {
codVendedor,
idEmpresa,
role,
p256dh: dto.keys.p256dh,
auth: dto.keys.auth,
},
create: {
codVendedor,
idEmpresa,
role,
endpoint: dto.endpoint,
p256dh: dto.keys.p256dh,
auth: dto.keys.auth,
},
});
}
// Escopado ao dono: só remove subscription do próprio usuário — quem souber o
// endpoint de terceiro não consegue desregistrá-lo.
async unsubscribe(userId: string, endpoint: string): Promise<void> {
const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS');
const idEmpresa = this.cls.get('idEmpresa');
const codVendedor = userId ? parseInt(userId, 10) : null;
await prisma.pushSubscription.deleteMany({ where: { endpoint, idEmpresa, codVendedor } });
}
async pendingCount(userId: string, role: string): Promise<PendingCountResponse> {
const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS');
const idEmpresa = this.cls.get('idEmpresa');
if (role === 'supervisor' || role === 'manager' || role === 'admin') {
const count = await prisma.pedido.count({
where: { situa: SITUA_PENDENTE, idEmpresa },
});
return { count };
}
return { count: 0 };
}
// Envia push para todos os supervisores/managers/admin da empresa.
async notifySupervisors(payload: PushPayload): Promise<void> {
const prisma = this.cls.get('prisma');
if (!prisma) return;
const idEmpresa = this.cls.get('idEmpresa');
const subs = await prisma.pushSubscription.findMany({
where: {
idEmpresa,
role: { in: ['supervisor', 'manager', 'admin'] },
},
});
await Promise.allSettled(subs.map((s) => this.push.send(s, payload)));
}
// Envia push para um codVendedor específico (todos os dispositivos registrados).
async notifyUser(userId: string, payload: PushPayload): Promise<void> {
const prisma = this.cls.get('prisma');
if (!prisma) return;
const idEmpresa = this.cls.get('idEmpresa');
const codVendedor = parseInt(userId, 10);
const subs = await prisma.pushSubscription.findMany({
where: { idEmpresa, codVendedor },
});
await Promise.allSettled(subs.map((s) => this.push.send(s, payload)));
}
}