fix(api): restringe produtos e precos a pauta vinculada ao representante

Rep so enxerga e vende itens das pautas do cadastro dele (cod_pauta1..6):

- catalog: lista de produtos exige pauta e recusa pauta de terceiro
- orders: create/update validam pauta do pedido e todo item na pauta
  com preco > 0, fechando preco forjado e produto fora da pauta

Gestor/admin seguem navegando livremente. O teto de desconto continua
em assertAlcadaItens; aqui so ancora o preco de tabela.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 22:29:07 +00:00
parent 9b855f63b2
commit 9107b0a825
2 changed files with 112 additions and 0 deletions

View File

@@ -230,10 +230,22 @@ export class CatalogService {
const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS');
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
const role = this.cls.get('role') ?? 'rep';
const userId = this.cls.get('userId');
const codVendedor = userId ? parseInt(userId, 10) : 0;
const { q, codGrupo, idPauta, page, limit } = query;
const offset = (page - 1) * limit;
// PGD-AUTHZ: o rep só enxerga produtos/preços das pautas vinculadas ao
// cadastro dele (cod_pauta1..6). Sem pauta selecionada → lista vazia (precisa
// escolher a pauta primeiro); pauta de terceiro → recusa. Gestor/admin
// navegam livremente (preço base ou pauta que escolherem).
if (role === 'rep') {
if (idPauta == null) return { data: [], total: 0, page, limit };
await this.assertPautaDoRep(idPauta, codVendedor, idEmpresa);
}
const grupoFilter = codGrupo != null ? `AND p.cod_grupo = ${codGrupo}` : '';
const searchFilter = q
? `AND (p.descricao ILIKE '%${escSql(q)}%' OR p.codigo ILIKE '%${escSql(q)}%')`
@@ -310,6 +322,33 @@ export class CatalogService {
return { data: rows.map((p) => this.mapRow(p, p.vl_preco1)), total, page, limit };
}
// Confere que a pauta pertence às pautas do rep (cod_pauta1..6 do cadastro).
// codVendedor/idPauta são Int (JWT/query coercida) — interpolação segura.
private async assertPautaDoRep(
idPauta: number,
codVendedor: number,
idMatriz: number,
): Promise<void> {
const prisma = this.cls.get('prisma');
if (!prisma) throw new Error('prisma não disponível no CLS');
const rows = await prisma.$queryRawUnsafe<{ id_pauta: number }[]>(`
SELECT DISTINCT pa.id_pauta
FROM vw_pautas pa
JOIN vw_representantes r ON pa.codigo IN (
r.cod_pauta1, r.cod_pauta2, r.cod_pauta3,
r.cod_pauta4, r.cod_pauta5, r.cod_pauta6
)
WHERE pa.id_empresa = ${idMatriz}
AND pa.ativo = 1
AND r.codigo = ${codVendedor}
AND pa.id_pauta = ${Number(idPauta)}
LIMIT 1
`);
if (!rows[0]) {
throw new ForbiddenException('Pauta de preços não vinculada ao seu cadastro');
}
}
private mapRow(p: ProdutoRow, preco: string): ProdutoSummary {
return {
idErp: Number(p.id_erp),