Files
sar/libs/shared/api-interface/src/lib/client.contract.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

210 lines
8.3 KiB
TypeScript

import { z } from 'zod';
// Contratos canônicos de C2 — Consulta de Clientes.
// Consumidos pela API (output tipado) e pela Web (TanStack Query + parse).
// ADR 0006 revogado: id UUID → idCliente Int (sig.corrent.id_corrent)
// ─── Enums ────────────────────────────────────────────────────────────────────
export const ActivityStatusSchema = z.enum(['active', 'alert', 'inactive']);
export type ActivityStatus = z.infer<typeof ActivityStatusSchema>;
// ─── Client Summary (lista) ───────────────────────────────────────────────────
export const ClientSummarySchema = z.object({
idCliente: z.number().int(),
idEmpresa: z.number().int(),
nome: z.string(),
razao: z.string().nullable(),
cgcpf: z.string().nullable(),
email: z.string().nullable(),
telefone: z.string().nullable(),
codVendedor: z.number().int(),
nomeVendedor: z.string().nullable().optional(),
limiteCreditoStr: z.string().nullable(),
activityStatus: ActivityStatusSchema,
dtUltimaCompra: z.iso.datetime().nullable(),
});
export type ClientSummary = z.infer<typeof ClientSummarySchema>;
// ─── Client Detail (ficha) ───────────────────────────────────────────────────
export const ClientDetailSchema = ClientSummarySchema.extend({
ativo: z.number().int(),
pessoa: z.number().int().nullable(),
inscricaoEstadual: z.string().nullable(),
endereco: z.string().nullable(),
numEndereco: z.string().nullable(),
bairro: z.string().nullable(),
cep: z.string().nullable(),
ddd: z.string().nullable(),
obs: z.string().nullable(),
codPauta: z.number().int().nullable(),
dtCadastro: z.string().nullable(),
dtAtual: z.string().nullable(),
});
export type ClientDetail = z.infer<typeof ClientDetailSchema>;
// ─── List query + response ────────────────────────────────────────────────────
export const ClientListQuerySchema = z.object({
q: z.string().optional(),
status: ActivityStatusSchema.optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(200).default(50),
});
export type ClientListQuery = z.infer<typeof ClientListQuerySchema>;
export const ClientListResponseSchema = z.object({
data: z.array(ClientSummarySchema),
total: z.number().int().nonnegative(),
page: z.number().int().positive(),
limit: z.number().int().positive(),
});
export type ClientListResponse = z.infer<typeof ClientListResponseSchema>;
// ─── Contato ─────────────────────────────────────────────────────────────────
export const ContatoSchema = z.object({
idContato: z.number().int(),
idEmpresa: z.number().int(),
idCorrent: z.number().int(),
idChatwoot: z.number().int().nullable(),
nome: z.string(),
empresa: z.string().nullable(),
cargo: z.string().nullable(),
departamento: z.string().nullable(),
dtAniversario: z.string().nullable(),
telefone: z.string().nullable(),
ramal: z.string().nullable(),
celular: z.string().nullable(),
whatsapp: z.string().nullable(),
email: z.string().nullable(),
ativo: z.number().int(),
anotacoes: z.string().nullable(),
nomeCliente: z.string().nullable(),
razaoCliente: z.string().nullable(),
codVendedor: z.number().int().nullable(),
});
export type Contato = z.infer<typeof ContatoSchema>;
export const CreateContatoSchema = z.object({
nome: z.string().min(1).max(100),
empresa: z.string().max(100).optional(),
cargo: z.string().max(100).optional(),
departamento: z.string().max(100).optional(),
dtAniversario: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Data deve ser YYYY-MM-DD')
.optional(),
telefone: z.string().max(20).optional(),
ramal: z.string().max(10).optional(),
celular: z.string().max(20).optional(),
whatsapp: z.string().max(20).optional(),
email: z.string().max(150).optional(),
anotacoes: z.string().optional(),
});
export type CreateContato = z.infer<typeof CreateContatoSchema>;
export const ContatoResultSchema = z.object({
id: z.string().uuid(),
idContatoErp: z.number().int().nullable(),
sincronizado: z.boolean(),
erroSync: z.string().nullable(),
});
export type ContatoResult = z.infer<typeof ContatoResultSchema>;
// ─── Produtos mais comprados por cliente ─────────────────────────────────────
export const TopProdutoSchema = z.object({
idProduto: z.number().int(),
codProduto: z.string(),
descricao: z.string(),
unidade: z.string().nullable(),
qtdTotal: z.number(),
numPedidos: z.number().int(),
ultimaCompra: z.string(),
ultimoPreco: z.number(),
});
export type TopProduto = z.infer<typeof TopProdutoSchema>;
// ─── Contas a Receber (resumo por cliente) ───────────────────────────────────
export const CtrSummarySchema = z.object({
qtdAberto: z.number().int(),
totalAberto: z.number(),
qtdVencido: z.number().int(),
totalVencido: z.number(),
maiorAtrasoDias: z.number().int(),
});
export type CtrSummary = z.infer<typeof CtrSummarySchema>;
// ─── Contas a Receber (lista de títulos em aberto) ────────────────────────────
export const CtrTituloSchema = z.object({
idCtr: z.number().int(),
numero: z.string(),
prefixo: z.string(),
dtEmissao: z.string(),
dtVencimento: z.string(),
saldo: z.number(),
});
export type CtrTitulo = z.infer<typeof CtrTituloSchema>;
// ─── Notas Fiscais por cliente ───────────────────────────────────────────────
export const NotaFiscalSchema = z.object({
idNf: z.number().int(),
numero: z.number().int(),
serie: z.string(),
dtEmissao: z.string(),
vlNf: z.number(),
chaveAcessoNfe: z.string().nullable(),
});
export type NotaFiscal = z.infer<typeof NotaFiscalSchema>;
// ─── Municipio ───────────────────────────────────────────────────────────────
export const MunicipioSchema = z.object({
idMunicipio: z.number().int(),
nome: z.string(),
uf: z.string(),
codigoIbge: z.number().int().nullable(),
});
export type Municipio = z.infer<typeof MunicipioSchema>;
// ─── Criar Cliente Novo ───────────────────────────────────────────────────────
// pesso: 0=PJ (CNPJ), 1=PF (CPF) — campo do ERP, não renomear
// indicadorIe: 1=contribuinte, 2=isento, 9=não contribuinte
export const CreateClientNovoSchema = z.object({
pesso: z.union([z.literal(0), z.literal(1)]),
consfinal: z.union([z.literal(0), z.literal(1)]).default(0),
nome: z.string().min(1).max(40),
razao: z.string().min(1).max(65),
cgcpf: z.string().max(18).optional(),
sufCgcpf: z.string().max(3).default(''),
inscr: z.string().max(18).default(''),
indicadorIe: z.union([z.literal(1), z.literal(2), z.literal(9)]).optional(),
endereco: z.string().max(60).default(''),
numEndereco: z.string().max(10).default(''),
bairro: z.string().max(60).default(''),
idMunicipio: z.number().int().positive(),
cep: z.string().max(9).default(''),
ddd: z.string().max(4).default(''),
telefone: z.string().max(35).default(''),
email: z.string().max(80).default(''),
limiteCredito: z.number().nonnegative().default(0),
codFormapag: z.number().int().optional(),
codPauta: z.number().int().optional(),
});
export type CreateClientNovo = z.infer<typeof CreateClientNovoSchema>;
export const ClientNovoResultSchema = z.object({
id: z.string().uuid(),
idCorrentErp: z.number().int().nullable(),
sincronizado: z.boolean(),
erroSync: z.string().nullable(),
});
export type ClientNovoResult = z.infer<typeof ClientNovoResultSchema>;