diff --git a/apps/api/src/app/catalog/catalog.service.ts b/apps/api/src/app/catalog/catalog.service.ts index 2e85663..c1f10cf 100644 --- a/apps/api/src/app/catalog/catalog.service.ts +++ b/apps/api/src/app/catalog/catalog.service.ts @@ -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 { + 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), diff --git a/apps/api/src/app/orders/orders.service.ts b/apps/api/src/app/orders/orders.service.ts index d517620..a3bcf4d 100644 --- a/apps/api/src/app/orders/orders.service.ts +++ b/apps/api/src/app/orders/orders.service.ts @@ -315,6 +315,69 @@ export class OrdersService { return this.mapDetail(o); } + // PGD-AUTHZ: o rep só vende produtos/preços das pautas vinculadas ao cadastro + // dele (cod_pauta1..6). Valida que (a) a pauta do pedido é do rep e (b) todo + // item existe naquela pauta com preço > 0 — fecha preço forjado (ex.: 0,01) e + // produto fantasma/fora da pauta. O TETO de desconto (alçada + promoção/regra + // ativa da política comercial) continua em assertAlcadaItens; aqui só ancora o + // preço de tabela na pauta do rep para que aquele cálculo nunca caia no + // fallback de "sem preço de referência". + private async assertPautaDoRep( + codVendedor: number, + idPauta: number | null | undefined, + itens: { ordem: number; idProduto: number; codProduto?: string | null }[], + ): Promise { + 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 idMatriz = idEmpresa > 9000 ? idEmpresa - 9000 : idEmpresa; + + if (idPauta == null) { + throw new BadRequestException('Selecione a pauta de preços para lançar o pedido.'); + } + + // codVendedor (JWT) e idPauta/idProduto (payload validado como Int) — seguros. + const pautaRows = 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 (!pautaRows[0]) { + throw new BadRequestException('Pauta de preços não vinculada ao seu cadastro.'); + } + + const prodIds = [...new Set(itens.map((i) => i.idProduto))]; + if (prodIds.length === 0) return; + const okRows = await prisma.$queryRawUnsafe<{ id_produto: number }[]>(` + SELECT pp.id_produto + FROM vw_pauta_produtos pp + JOIN vw_produtos p ON p.id_erp = pp.id_produto + AND p.id_empresa = ${idMatriz} + AND p.ativo = 1 + WHERE pp.id_pauta = ${Number(idPauta)} + AND pp.preco1 > 0 + AND pp.id_produto IN (${prodIds.join(',')}) + `); + const validos = new Set(okRows.map((r) => Number(r.id_produto))); + const foraDaPauta = itens.filter((it) => !validos.has(it.idProduto)); + if (foraDaPauta.length > 0) { + const lista = foraDaPauta + .map((it) => `item ${it.ordem} (${it.codProduto ?? it.idProduto})`) + .join(', '); + throw new BadRequestException( + `Produto fora da pauta selecionada: ${lista}. Só é possível vender itens da sua pauta de preços.`, + ); + } + } + // Valida a alçada de um payload de criação/edição (mesma regra do transmit): // desconto global e por item contra alcada + promoção/regra do dono do pedido. private async assertAlcadaDto( @@ -398,6 +461,11 @@ export class OrdersService { if (existing) return this.mapDetail(existing); } + // Rep: pauta do pedido e itens têm que ser da pauta vinculada ao cadastro + if (role === 'rep') { + await this.assertPautaDoRep(codVendedor, dto.idPauta, dto.itens); + } + // Alçada: bloqueia a criação do orçamento se algum desconto passa do teto await this.assertAlcadaDto(codVendedor, dto); @@ -709,6 +777,11 @@ export class OrdersService { } } + // Rep: pauta do pedido e itens têm que ser da pauta vinculada ao cadastro + if (role === 'rep') { + await this.assertPautaDoRep(pedido.codVendedor, dto.idPauta, dto.itens); + } + // Alçada do DONO do pedido (supervisor editando usa o limite do rep) await this.assertAlcadaDto(pedido.codVendedor, dto);