- Nova tabela sar.regras_desconto (grupo, subgrupo, representante, % e vigencia; campos nulos = todos) com migration idempotente, espelho no sar-erp-schema.sql e GRANT no provision-client - CRUD manager-only em /politicas/regras + aba "Regras de Desconto" na tela de Politicas Comerciais (selects com nomes de rep/grupo/subgrupo via /politicas/grupos-produto) - GET /politicas/regras/vigentes (rep ve so as que o alcancam) e GET /politicas/promocoes/vigentes para o catalogo - Regra vigente pre-aplica o desconto no carrinho do novo pedido - Selos "Promocao"/"Cond. especial" na busca de produto, catalogo do pedido e pagina de catalogo; secao "Condicoes Comerciais" no modal de detalhe do produto (descricao, % e validade) - Empty imageStyle deprecado -> styles.image no carrinho Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
383 lines
14 KiB
TypeScript
383 lines
14 KiB
TypeScript
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<WorkspaceClsStore>) {}
|
|
|
|
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<AlcadaDescontosResponse> {
|
|
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<void> {
|
|
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<PromocoesResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
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<GruposProdutoResponse> {
|
|
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<RegrasDescontoResponse> {
|
|
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<void> {
|
|
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<void> {
|
|
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<PromocoesVigentesResponse> {
|
|
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<RegrasVigentesResponse> {
|
|
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),
|
|
})),
|
|
};
|
|
}
|
|
}
|