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>
210 lines
8.3 KiB
TypeScript
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>;
|