import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common'; import { ClsService } from 'nestjs-cls'; import type { AlcadaDescontosResponse, UpsertDescontoBody, PromocoesResponse, CreatePromocaoBody, UpdatePromocaoBody, RegrasDescontoResponse, CreateRegraDescontoBody, UpdateRegraDescontoBody, RegrasVigentesResponse, PromocoesVigentesResponse, GruposProdutoResponse, } from '@sar/api-interface'; import type { WorkspaceClsStore } from '../workspace/workspace.types'; @Injectable() export class PoliticasService { constructor(private readonly cls: ClsService) {} private assertManager(): void { const role = this.cls.get('role') ?? 'rep'; if (role !== 'manager' && role !== 'admin') { throw new ForbiddenException('Apenas gerentes podem gerenciar políticas comerciais'); } } async listDescontos(): Promise { this.assertManager(); 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 rows = await prisma.alcadaDesconto.findMany({ where: { idEmpresa }, orderBy: [{ codVendedor: 'asc' }, { codGrupo: 'asc' }], }); // Busca nomes dos representantes const codigos = [...new Set(rows.map((r) => r.codVendedor))]; const repRows = codigos.length > 0 ? await prisma.$queryRawUnsafe<{ codigo: number; nome: string | null }[]>( `SELECT codigo, nome FROM vw_representantes WHERE codigo IN (${codigos.join(',')})`, ) : []; const repNameMap = new Map(repRows.map((r) => [Number(r.codigo), r.nome])); return { descontos: rows.map((r) => ({ codVendedor: r.codVendedor, codGrupo: r.codGrupo, limitePerc: Number(r.limitePerc), nomeVendedor: repNameMap.get(r.codVendedor) ?? null, })), }; } async upsertDesconto(body: UpsertDescontoBody): Promise { this.assertManager(); const prisma = this.cls.get('prisma'); if (!prisma) throw new Error('prisma não disponível no CLS'); const idEmpresa = this.cls.get('idEmpresa'); await prisma.alcadaDesconto.upsert({ where: { codVendedor_idEmpresa_codGrupo: { codVendedor: body.codVendedor, idEmpresa, codGrupo: body.codGrupo ?? 0, }, }, create: { codVendedor: body.codVendedor, idEmpresa, codGrupo: body.codGrupo ?? 0, limitePerc: body.limitePerc, }, update: { limitePerc: body.limitePerc }, }); } async listPromocoes(): Promise { this.assertManager(); 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 rows = await prisma.promocao.findMany({ where: { idEmpresa }, orderBy: [{ dataFim: 'desc' }, { id: 'desc' }], }); return { promocoes: rows.map((p) => ({ id: p.id, descricao: p.descricao, codProduto: p.codProduto, grpProd: p.grpProd, descPct: Number(p.descPct), dataInicio: p.dataInicio.toISOString().slice(0, 10), dataFim: p.dataFim.toISOString().slice(0, 10), ativa: p.ativa, createdAt: p.createdAt.toISOString(), createdBy: p.createdBy, })), }; } async createPromocao(body: CreatePromocaoBody): Promise<{ id: number }> { this.assertManager(); 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 userId = this.cls.get('userId') ?? 'system'; const p = await prisma.promocao.create({ data: { idEmpresa, descricao: body.descricao, codProduto: body.codProduto ?? null, grpProd: body.grpProd ?? null, descPct: body.descPct, dataInicio: new Date(body.dataInicio), dataFim: new Date(body.dataFim), createdBy: userId, }, }); return { id: p.id }; } async updatePromocao(id: number, body: UpdatePromocaoBody): Promise { this.assertManager(); 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 exists = await prisma.promocao.findFirst({ where: { id, idEmpresa } }); if (!exists) throw new NotFoundException('Promoção não encontrada'); await prisma.promocao.update({ where: { id }, data: { ...(body.descricao !== undefined && { descricao: body.descricao }), ...(body.codProduto !== undefined && { codProduto: body.codProduto }), ...(body.grpProd !== undefined && { grpProd: body.grpProd }), ...(body.descPct !== undefined && { descPct: body.descPct }), ...(body.dataInicio !== undefined && { dataInicio: new Date(body.dataInicio) }), ...(body.dataFim !== undefined && { dataFim: new Date(body.dataFim) }), ...(body.ativa !== undefined && { ativa: body.ativa }), }, }); } async deletePromocao(id: number): Promise { this.assertManager(); 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 exists = await prisma.promocao.findFirst({ where: { id, idEmpresa } }); if (!exists) throw new NotFoundException('Promoção não encontrada'); await prisma.promocao.delete({ where: { id } }); } // ─── Regras de Desconto ───────────────────────────────────────────────────── // Grupos/subgrupos distintos de vw_produtos (empresa matriz), para combos e // resolução de nomes na listagem — UI sempre mostra o nome, nunca só o código. private async loadGruposProduto(): Promise< { cod_grupo: number | null; grupo: string | null; cod_subgrupo: number | null; subgrupo: string | null; }[] > { 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; return prisma.$queryRawUnsafe( `SELECT DISTINCT cod_grupo, TRIM(grupo) AS grupo, cod_subgrupo, TRIM(subgrupo) AS subgrupo FROM vw_produtos WHERE id_empresa = ${idMatriz} AND (cod_grupo IS NOT NULL OR cod_subgrupo IS NOT NULL) ORDER BY grupo, subgrupo`, ); } async listGruposProduto(): Promise { this.assertManager(); const rows = await this.loadGruposProduto(); return { grupos: rows.map((r) => ({ codGrupo: r.cod_grupo != null ? Number(r.cod_grupo) : null, grupo: r.grupo, codSubgrupo: r.cod_subgrupo != null ? Number(r.cod_subgrupo) : null, subgrupo: r.subgrupo, })), }; } async listRegras(): Promise { this.assertManager(); 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 rows = await prisma.regraDesconto.findMany({ where: { idEmpresa }, orderBy: [{ dataFim: 'desc' }, { id: 'desc' }], }); const codigos = [ ...new Set(rows.map((r) => r.codVendedor).filter((c): c is number => c != null)), ]; const repRows = codigos.length > 0 ? await prisma.$queryRawUnsafe<{ codigo: number; nome: string | null }[]>( `SELECT codigo, nome FROM vw_representantes WHERE codigo IN (${codigos.join(',')})`, ) : []; const repNameMap = new Map(repRows.map((r) => [Number(r.codigo), r.nome])); const grupos = rows.some((r) => r.codGrupo != null || r.codSubgrupo != null) ? await this.loadGruposProduto() : []; const grupoNameMap = new Map( grupos.filter((g) => g.cod_grupo != null).map((g) => [Number(g.cod_grupo), g.grupo]), ); const subgrupoNameMap = new Map( grupos.filter((g) => g.cod_subgrupo != null).map((g) => [Number(g.cod_subgrupo), g.subgrupo]), ); return { regras: rows.map((r) => ({ id: r.id, descricao: r.descricao, codGrupo: r.codGrupo, grupo: r.codGrupo != null ? (grupoNameMap.get(r.codGrupo) ?? null) : null, codSubgrupo: r.codSubgrupo, subgrupo: r.codSubgrupo != null ? (subgrupoNameMap.get(r.codSubgrupo) ?? null) : null, codVendedor: r.codVendedor, nomeVendedor: r.codVendedor != null ? (repNameMap.get(r.codVendedor) ?? null) : null, descPct: Number(r.descPct), dataInicio: r.dataInicio.toISOString().slice(0, 10), dataFim: r.dataFim.toISOString().slice(0, 10), ativa: r.ativa, createdAt: r.createdAt.toISOString(), createdBy: r.createdBy, })), }; } async createRegra(body: CreateRegraDescontoBody): Promise<{ id: number }> { this.assertManager(); 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 userId = this.cls.get('userId') ?? 'system'; const r = await prisma.regraDesconto.create({ data: { idEmpresa, descricao: body.descricao, codGrupo: body.codGrupo ?? null, codSubgrupo: body.codSubgrupo ?? null, codVendedor: body.codVendedor ?? null, descPct: body.descPct, dataInicio: new Date(body.dataInicio), dataFim: new Date(body.dataFim), createdBy: userId, }, }); return { id: r.id }; } async updateRegra(id: number, body: UpdateRegraDescontoBody): Promise { this.assertManager(); 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 exists = await prisma.regraDesconto.findFirst({ where: { id, idEmpresa } }); if (!exists) throw new NotFoundException('Regra de desconto não encontrada'); await prisma.regraDesconto.update({ where: { id }, data: { ...(body.descricao !== undefined && { descricao: body.descricao }), ...(body.codGrupo !== undefined && { codGrupo: body.codGrupo }), ...(body.codSubgrupo !== undefined && { codSubgrupo: body.codSubgrupo }), ...(body.codVendedor !== undefined && { codVendedor: body.codVendedor }), ...(body.descPct !== undefined && { descPct: body.descPct }), ...(body.dataInicio !== undefined && { dataInicio: new Date(body.dataInicio) }), ...(body.dataFim !== undefined && { dataFim: new Date(body.dataFim) }), ...(body.ativa !== undefined && { ativa: body.ativa }), }, }); } async deleteRegra(id: number): Promise { this.assertManager(); 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 exists = await prisma.regraDesconto.findFirst({ where: { id, idEmpresa } }); if (!exists) throw new NotFoundException('Regra de desconto não encontrada'); await prisma.regraDesconto.delete({ where: { id } }); } // Promoções vigentes hoje — qualquer usuário autenticado. Usadas pelo rep para // sinalizar no catálogo que o produto tem condição comercial diferenciada. async listPromocoesVigentes(): 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; const hoje = new Date(); const rows = await prisma.promocao.findMany({ where: { idEmpresa: { in: [idEmpresa, idMatriz] }, ativa: true, dataInicio: { lte: hoje }, dataFim: { gte: hoje }, }, orderBy: [{ descPct: 'desc' }], }); return { promocoes: rows.map((p) => ({ id: p.id, descricao: p.descricao, codProduto: p.codProduto, grpProd: p.grpProd, descPct: Number(p.descPct), dataFim: p.dataFim.toISOString().slice(0, 10), })), }; } // Regras vigentes hoje para o usuário logado. Rep recebe só as regras que o // alcançam (cod_vendedor nulo = todos, ou o próprio código); gerente vê todas. async listRegrasVigentes(): 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; const role = this.cls.get('role') ?? 'rep'; const codVendedor = parseInt(this.cls.get('userId') ?? '0', 10); const hoje = new Date(); const rows = await prisma.regraDesconto.findMany({ where: { idEmpresa: { in: [idEmpresa, idMatriz] }, ativa: true, dataInicio: { lte: hoje }, dataFim: { gte: hoje }, ...(role === 'rep' ? { OR: [{ codVendedor: null }, { codVendedor }] } : {}), }, orderBy: [{ descPct: 'desc' }], }); return { regras: rows.map((r) => ({ id: r.id, descricao: r.descricao, codGrupo: r.codGrupo, codSubgrupo: r.codSubgrupo, descPct: Number(r.descPct), dataFim: r.dataFim.toISOString().slice(0, 10), })), }; } }