feat(web+api): tela de cadastro de cliente novo (CNPJ/CPF) com sync ERP
Web (NewClientPage):
- Segmentado PJ/CPF com campos específicos por categoria
- PJ: razão social, nome fantasia, CNPJ, IE, indicador IE, consumidor final
- PF: nome completo, CPF
- Endereço com auto-preenchimento via ViaCEP (CEP → rua/bairro/município)
- Município: Select com busca server-side (/catalog/municipios)
- Contato: DDD, telefone, email
- Comercial: forma pagamento, tabela de preços, limite crédito
- Feedback: código ERP exibido no sucesso; alerta de erro de sync sem bloquear
API:
- GET /catalog/municipios?q= — busca municipios da vw_municipios
- POST /clients/novo — INSERT em sar.clientes_novos; trigger sincroniza para sig.corrent
- Retorna { id, idCorrentErp, sincronizado, erroSync } após o trigger disparar
- Fix trigger fn_sync_cliente_to_erp: NULLIF para cgcpf vazio (evita violação
do unique index key_corrent_cgcpf quando cliente não tem CPF/CNPJ)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type {
|
||||
ActivityStatus,
|
||||
ClientDetail,
|
||||
ClientListQuery,
|
||||
ClientListResponse,
|
||||
ClientNovoResult,
|
||||
ClientSummary,
|
||||
ActivityStatus,
|
||||
CreateClientNovo,
|
||||
} from '@sar/api-interface';
|
||||
import type { WorkspaceClsStore } from '../workspace/workspace.types';
|
||||
|
||||
@@ -187,6 +189,65 @@ export class ClientsService {
|
||||
return { data: mapped, total, page, limit };
|
||||
}
|
||||
|
||||
async createNovo(dto: CreateClientNovo): Promise<ClientNovoResult> {
|
||||
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');
|
||||
const codVendedor = userId ? parseInt(userId, 10) : 0;
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
id_corrent_erp: number | null;
|
||||
sincronizado: boolean;
|
||||
erro_sync: string | null;
|
||||
}
|
||||
|
||||
const esc = (s: string) => s.replace(/'/g, "''");
|
||||
const opt = (v: string | undefined) => (v != null ? `'${esc(v)}'` : 'NULL');
|
||||
const optN = (v: number | undefined) => (v != null ? String(v) : 'NULL');
|
||||
|
||||
// INSERT retorna id — RETURNING lê valores pré-trigger, então
|
||||
// fazemos SELECT separado para capturar o que o trigger gravou
|
||||
const insertRows = await prisma.$queryRawUnsafe<[{ id: string }]>(`
|
||||
INSERT INTO sar.clientes_novos (
|
||||
id_empresa, cod_vendedor, pesso, consfinal,
|
||||
nome, razao, cgcpf, suf_cgcpf, inscr, indicador_ie,
|
||||
endereco, num_endereco, bairro, id_municipio, cep,
|
||||
ddd, telefone, email, limite_credito, cod_formapag, cod_pauta
|
||||
) VALUES (
|
||||
${idEmpresa}, ${codVendedor}, ${dto.pesso}, ${dto.consfinal},
|
||||
'${esc(dto.nome)}', '${esc(dto.razao)}',
|
||||
${opt(dto.cgcpf)}, '${esc(dto.sufCgcpf ?? '')}',
|
||||
'${esc(dto.inscr ?? '')}', ${optN(dto.indicadorIe)},
|
||||
'${esc(dto.endereco ?? '')}', '${esc(dto.numEndereco ?? '')}',
|
||||
'${esc(dto.bairro ?? '')}', ${dto.idMunicipio}, '${esc(dto.cep ?? '')}',
|
||||
'${esc(dto.ddd ?? '')}', '${esc(dto.telefone ?? '')}',
|
||||
'${esc(dto.email ?? '')}', ${dto.limiteCredito ?? 0},
|
||||
${optN(dto.codFormapag)}, ${optN(dto.codPauta)}
|
||||
)
|
||||
RETURNING id::text
|
||||
`);
|
||||
|
||||
const id = insertRows[0]?.id;
|
||||
if (!id) throw new Error('Falha ao inserir cliente');
|
||||
|
||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||
SELECT id::text, id_corrent_erp, sincronizado, erro_sync
|
||||
FROM sar.clientes_novos WHERE id = '${id}'
|
||||
`);
|
||||
|
||||
const r = rows[0];
|
||||
if (!r) throw new Error('Falha ao recuperar cliente inserido');
|
||||
|
||||
return {
|
||||
id: r.id,
|
||||
idCorrentErp: r.id_corrent_erp !== null ? Number(r.id_corrent_erp) : null,
|
||||
sincronizado: r.sincronizado,
|
||||
erroSync: r.erro_sync,
|
||||
};
|
||||
}
|
||||
|
||||
async findOne(idCliente: number): Promise<ClientDetail> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
Reference in New Issue
Block a user