From 236dd69eba05167650b6039e54025be9cea15eb4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 24 Jun 2026 17:06:27 +0000 Subject: [PATCH] feat(web+api): tela de cadastro de cliente novo (CNPJ/CPF) com sync ERP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../api/src/app/catalog/catalog.controller.ts | 6 + apps/api/src/app/catalog/catalog.service.ts | 30 ++ .../api/src/app/clients/clients.controller.ts | 12 +- apps/api/src/app/clients/clients.service.ts | 63 ++- apps/web/src/cockpits/rep/ClientsPage.tsx | 6 +- apps/web/src/cockpits/rep/NewClientPage.tsx | 430 ++++++++++++++++++ apps/web/src/lib/queries/catalog.ts | 15 + apps/web/src/lib/queries/clients.ts | 26 +- apps/web/src/lib/router.tsx | 8 + .../api-interface/src/lib/client.contract.ts | 45 ++ scripts/sar-triggers.sql | 6 +- 11 files changed, 638 insertions(+), 9 deletions(-) create mode 100644 apps/web/src/cockpits/rep/NewClientPage.tsx diff --git a/apps/api/src/app/catalog/catalog.controller.ts b/apps/api/src/app/catalog/catalog.controller.ts index 73bce34..2e0f8ae 100644 --- a/apps/api/src/app/catalog/catalog.controller.ts +++ b/apps/api/src/app/catalog/catalog.controller.ts @@ -4,6 +4,7 @@ import { ProdutoListQuerySchema, type EmpresaInfo, type FormaPagamento, + type Municipio, type Pauta, type ProdutoDetail, type ProdutoListQuery, @@ -19,6 +20,11 @@ class ProdutoListQueryDto extends createZodDto(ProdutoListQuerySchema) {} export class CatalogController { constructor(private readonly catalog: CatalogService) {} + @Get('municipios') + municipios(@Query('q') q?: string): Promise { + return this.catalog.municipios(q); + } + @Get('pautas') pautas(): Promise { return this.catalog.pautas(); diff --git a/apps/api/src/app/catalog/catalog.service.ts b/apps/api/src/app/catalog/catalog.service.ts index a9b913d..4d076c5 100644 --- a/apps/api/src/app/catalog/catalog.service.ts +++ b/apps/api/src/app/catalog/catalog.service.ts @@ -3,6 +3,7 @@ import { ClsService } from 'nestjs-cls'; import type { EmpresaInfo, FormaPagamento, + Municipio, Pauta, ProdutoDetail, ProdutoListQuery, @@ -122,6 +123,35 @@ export class CatalogService { }; } + async municipios(q?: string): Promise { + const prisma = this.cls.get('prisma'); + if (!prisma) throw new Error('prisma não disponível no CLS'); + + interface Row { + id_municipio: number; + nome: string; + uf: string; + codigo_ibge: number | null; + } + + const searchFilter = q ? `WHERE UPPER(nome) LIKE UPPER('%${escSql(q)}%')` : ''; + + const rows = await prisma.$queryRawUnsafe(` + SELECT id_municipio, TRIM(nome) AS nome, uf::text AS uf, codigo_ibge + FROM sar.vw_municipios + ${searchFilter} + ORDER BY nome + LIMIT 100 + `); + + return rows.map((r) => ({ + idMunicipio: Number(r.id_municipio), + nome: r.nome, + uf: r.uf, + codigoIbge: r.codigo_ibge !== null ? Number(r.codigo_ibge) : null, + })); + } + async pautas(): Promise { const prisma = this.cls.get('prisma'); if (!prisma) throw new Error('prisma não disponível no CLS'); diff --git a/apps/api/src/app/clients/clients.controller.ts b/apps/api/src/app/clients/clients.controller.ts index 2716464..52172c6 100644 --- a/apps/api/src/app/clients/clients.controller.ts +++ b/apps/api/src/app/clients/clients.controller.ts @@ -1,14 +1,18 @@ -import { Controller, Get, Param, ParseIntPipe, Query } from '@nestjs/common'; +import { Body, Controller, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common'; import { createZodDto } from 'nestjs-zod'; import { ClientListQuerySchema, + CreateClientNovoSchema, type ClientDetail, type ClientListQuery, type ClientListResponse, + type ClientNovoResult, + type CreateClientNovo, } from '@sar/api-interface'; import { ClientsService } from './clients.service'; class ClientListQueryDto extends createZodDto(ClientListQuerySchema) {} +class CreateClientNovoDto extends createZodDto(CreateClientNovoSchema) {} @Controller({ path: 'clients' }) export class ClientsController { @@ -20,6 +24,12 @@ export class ClientsController { return this.clients.list(parsed); } + @Post('novo') + createNovo(@Body() body: CreateClientNovoDto): Promise { + const parsed = CreateClientNovoSchema.parse(body) as CreateClientNovo; + return this.clients.createNovo(parsed); + } + @Get(':id') findOne(@Param('id', ParseIntPipe) id: number): Promise { return this.clients.findOne(id); diff --git a/apps/api/src/app/clients/clients.service.ts b/apps/api/src/app/clients/clients.service.ts index b2fd730..758aa01 100644 --- a/apps/api/src/app/clients/clients.service.ts +++ b/apps/api/src/app/clients/clients.service.ts @@ -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 { + 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(` + 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 { const prisma = this.cls.get('prisma'); if (!prisma) throw new Error('prisma não disponível no CLS'); diff --git a/apps/web/src/cockpits/rep/ClientsPage.tsx b/apps/web/src/cockpits/rep/ClientsPage.tsx index c83c844..0c35a09 100644 --- a/apps/web/src/cockpits/rep/ClientsPage.tsx +++ b/apps/web/src/cockpits/rep/ClientsPage.tsx @@ -1291,7 +1291,7 @@ export function ClientsPage() { icon={} size="large" style={{ borderRadius: 8, fontWeight: 600 }} - onClick={() => void msg.info('Cadastro de clientes em breve.')} + onClick={() => void navigate({ to: '/clientes/novo' })} > Cadastrar Cliente @@ -1318,7 +1318,7 @@ export function ClientsPage() { type="primary" icon={} style={{ borderRadius: 8, fontWeight: 600 }} - onClick={() => void msg.info('Cadastro de clientes em breve.')} + onClick={() => void navigate({ to: '/clientes/novo' })} > Cadastrar Cliente @@ -1549,7 +1549,7 @@ export function ClientsPage() { shape="circle" icon={} size="large" - onClick={() => void msg.info('Cadastro de clientes em breve.')} + onClick={() => void navigate({ to: '/clientes/novo' })} style={{ position: 'fixed', bottom: 24, diff --git a/apps/web/src/cockpits/rep/NewClientPage.tsx b/apps/web/src/cockpits/rep/NewClientPage.tsx new file mode 100644 index 0000000..b870dc8 --- /dev/null +++ b/apps/web/src/cockpits/rep/NewClientPage.tsx @@ -0,0 +1,430 @@ +import { useState } from 'react'; +import { + Alert, + App, + Button, + Card, + Col, + Form, + Input, + InputNumber, + Row, + Segmented, + Select, + Spin, + Switch, + Typography, +} from 'antd'; +import { + ArrowLeftOutlined, + BankOutlined, + CheckCircleOutlined, + EnvironmentOutlined, + PhoneOutlined, + ShopOutlined, + UserOutlined, +} from '@ant-design/icons'; +import { useNavigate } from '@tanstack/react-router'; +import type { CreateClientNovo } from '@sar/api-interface'; +import { useMunicipios, usePautas, useFormasPagamento } from '../../lib/queries/catalog'; +import { useCreateClientNovo } from '../../lib/queries/clients'; + +const { Title, Text } = Typography; + +type Pessoa = 'PJ' | 'PF'; + +function Section({ + icon, + title, + children, +}: { + icon: React.ReactNode; + title: string; + children: React.ReactNode; +}) { + return ( +
+
+ {icon} + + {title} + +
+ {children} +
+ ); +} + +export function NewClientPage() { + const navigate = useNavigate(); + const { message } = App.useApp(); + const [form] = Form.useForm(); + const [pessoa, setPessoa] = useState('PJ'); + const [municipioSearch, setMunicipioSearch] = useState(''); + const [syncError, setSyncError] = useState(null); + + const { data: municipios = [], isFetching: loadingMun } = useMunicipios( + municipioSearch || undefined, + ); + const { data: pautas = [] } = usePautas(); + const { data: formas = [] } = useFormasPagamento(); + const createMutation = useCreateClientNovo(); + + async function handleCepBlur(cep: string) { + const digits = cep.replace(/\D/g, ''); + if (digits.length !== 8) return; + try { + const res = await fetch(`https://viacep.com.br/ws/${digits}/json/`); + const data = (await res.json()) as { + erro?: boolean; + logradouro?: string; + bairro?: string; + localidade?: string; + uf?: string; + }; + if (data.erro) return; + if (data.logradouro) form.setFieldValue('endereco', data.logradouro); + if (data.bairro) form.setFieldValue('bairro', data.bairro); + if (data.localidade && data.uf) { + setMunicipioSearch(data.localidade); + } + } catch { + // silencioso — CEP inválido não bloqueia + } + } + + async function onFinish(values: Record) { + setSyncError(null); + + const payload: CreateClientNovo = { + pesso: pessoa === 'PJ' ? 0 : 1, + consfinal: values.consfinal ? 1 : 0, + nome: String(values.nome ?? '').slice(0, 40), + razao: String(values.razao ?? values.nome ?? '').slice(0, 65), + cgcpf: values.cgcpf ? String(values.cgcpf) : undefined, + sufCgcpf: '', + inscr: values.inscr ? String(values.inscr) : '', + indicadorIe: values.indicadorIe as 1 | 2 | 9 | undefined, + endereco: String(values.endereco ?? '').slice(0, 60), + numEndereco: String(values.numEndereco ?? '').slice(0, 10), + bairro: String(values.bairro ?? '').slice(0, 60), + idMunicipio: Number(values.idMunicipio), + cep: String(values.cep ?? '') + .replace(/\D/g, '') + .slice(0, 9), + ddd: String(values.ddd ?? '').slice(0, 4), + telefone: String(values.telefone ?? '').slice(0, 35), + email: String(values.email ?? '').slice(0, 80), + limiteCredito: Number(values.limiteCredito ?? 0), + codFormapag: values.codFormapag ? Number(values.codFormapag) : undefined, + codPauta: values.codPauta ? Number(values.codPauta) : undefined, + }; + + const result = await createMutation.mutateAsync(payload); + + if (!result.sincronizado && result.erroSync) { + setSyncError(result.erroSync); + void message.warning('Cliente cadastrado, mas houve erro na sincronização com o ERP.'); + } else { + void message.success( + `Cliente cadastrado com sucesso! Código ERP: ${result.idCorrentErp ?? '—'}`, + ); + void navigate({ to: '/clientes' }); + } + } + + const isPJ = pessoa === 'PJ'; + + return ( +
+ {/* Cabeçalho */} +
+ +
+ + Cadastrar Cliente + + + Após salvar, o cliente é criado automaticamente no ERP. + +
+
+ + {/* Seletor PJ / PF */} +
+ { + setPessoa(v as Pessoa); + form.resetFields(['cgcpf', 'inscr', 'indicadorIe', 'consfinal', 'razao']); + }} + options={[ + { value: 'PJ', icon: , label: 'CNPJ — Pessoa Jurídica' }, + { value: 'PF', icon: , label: 'CPF — Pessoa Física' }, + ]} + style={{ borderRadius: 10 }} + /> +
+ +
void onFinish(v as Record)} + requiredMark="optional" + initialValues={{ consfinal: false, limiteCredito: 0 }} + > + + {/* ── Identificação ─────────────────────────────────────────── */} +
: } title="Identificação"> + + {isPJ ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + + + + + + + + + + + + )} + +
+ + {/* ── Endereço ──────────────────────────────────────────────── */} +
} title="Endereço"> + + + + void handleCepBlur(e.target.value)} + /> + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* ── Contato ───────────────────────────────────────────────── */} +
} title="Contato"> + + + + + + + + + + + + + + + + + +
+ + {/* ── Comercial ─────────────────────────────────────────────── */} +
} title="Comercial"> + + + + + + + + + + + + + + `R$ ${v ?? 0}`.replace(/\B(?=(\d{3})+(?!\d))/g, '.')} + parser={(v) => Number((v ?? '').replace(/R\$\s?\.*/g, '').replace(',', '.'))} + placeholder="0,00" + /> + + + +
+ + {syncError && ( + + )} +
+ + {/* Rodapé sticky */} +
+ + +
+ +
+ ); +} diff --git a/apps/web/src/lib/queries/catalog.ts b/apps/web/src/lib/queries/catalog.ts index e7c9954..861fcf7 100644 --- a/apps/web/src/lib/queries/catalog.ts +++ b/apps/web/src/lib/queries/catalog.ts @@ -1,10 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import { FormaPagamentoSchema, + MunicipioSchema, PautaSchema, ProdutoListResponseSchema, ProdutoDetailSchema, type FormaPagamento, + type Municipio, type ProdutoListQuery, type ProdutoListResponse, type ProdutoDetail, @@ -13,6 +15,19 @@ import { import { z } from 'zod'; import { apiFetch } from '../api-client'; +export function useMunicipios(q?: string) { + return useQuery({ + queryKey: ['catalog', 'municipios', q ?? ''], + queryFn: async () => { + const qs = q ? `?q=${encodeURIComponent(q)}` : ''; + const res = await apiFetch(`/catalog/municipios${qs}`); + return z.array(MunicipioSchema).parse(res); + }, + staleTime: 24 * 60 * 60 * 1000, + enabled: !q || q.length >= 2, + }); +} + export function usePautas() { return useQuery({ queryKey: ['catalog', 'pautas'], diff --git a/apps/web/src/lib/queries/clients.ts b/apps/web/src/lib/queries/clients.ts index 635b009..9ab89f1 100644 --- a/apps/web/src/lib/queries/clients.ts +++ b/apps/web/src/lib/queries/clients.ts @@ -1,10 +1,13 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { - ClientListResponseSchema, ClientDetailSchema, + ClientListResponseSchema, + ClientNovoResultSchema, + type ClientDetail, type ClientListQuery, type ClientListResponse, - type ClientDetail, + type ClientNovoResult, + type CreateClientNovo, } from '@sar/api-interface'; import { apiFetch } from '../api-client'; @@ -42,3 +45,20 @@ export function useClientDetail(id: number | string | undefined) { enabled: !!id, }); } + +export function useCreateClientNovo() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const res = await apiFetch('/clients/novo', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return ClientNovoResultSchema.parse(res); + }, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: CLIENT_KEYS.all }); + }, + }); +} diff --git a/apps/web/src/lib/router.tsx b/apps/web/src/lib/router.tsx index d43526d..aa2c8d4 100644 --- a/apps/web/src/lib/router.tsx +++ b/apps/web/src/lib/router.tsx @@ -13,6 +13,7 @@ import { ClientDetailPage } from '../cockpits/rep/ClientDetailPage'; import { OrdersPage } from '../cockpits/rep/OrdersPage'; import { OrderDetailPage } from '../cockpits/rep/OrderDetailPage'; import { OrderPrintPage } from '../cockpits/rep/OrderPrintPage'; +import { NewClientPage } from '../cockpits/rep/NewClientPage'; import { NewOrderPage } from '../cockpits/rep/NewOrderPage'; import { CatalogPage } from '../cockpits/rep/CatalogPage'; import { ApprovalQueuePage } from '../cockpits/supervisor/ApprovalQueuePage'; @@ -75,6 +76,12 @@ const clientesRoute = createRoute({ component: ClientsPage, }); +const novoClienteRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/clientes/novo', + component: NewClientPage, +}); + const clienteDetailRoute = createRoute({ getParentRoute: () => rootRoute, path: '/clientes/$id', @@ -121,6 +128,7 @@ const routeTree = rootRoute.addChildren([ indexRoute, rafaelRoute, clientesRoute, + novoClienteRoute, clienteDetailRoute, pedidosRoute, novoOrderRoute, diff --git a/libs/shared/api-interface/src/lib/client.contract.ts b/libs/shared/api-interface/src/lib/client.contract.ts index 545cfe2..0d09841 100644 --- a/libs/shared/api-interface/src/lib/client.contract.ts +++ b/libs/shared/api-interface/src/lib/client.contract.ts @@ -62,3 +62,48 @@ export const ClientListResponseSchema = z.object({ limit: z.number().int().positive(), }); export type ClientListResponse = z.infer; + +// ─── 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; + +// ─── 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; + +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; diff --git a/scripts/sar-triggers.sql b/scripts/sar-triggers.sql index b9d25ba..0a64e92 100644 --- a/scripts/sar-triggers.sql +++ b/scripts/sar-triggers.sql @@ -366,8 +366,12 @@ CREATE OR REPLACE FUNCTION sar.fn_sync_cliente_to_erp() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE v_id_corrent INTEGER; + v_cgcpf CHAR(18); BEGIN BEGIN + -- NULLIF evita conflito no unique index key_corrent_cgcpf quando cgcpf é vazio + v_cgcpf := NULLIF(TRIM(COALESCE(NEW.cgcpf, '')), ''); + INSERT INTO sig.corrent ( ativo, pesso, consfinal, cgcpf, suf_cgcpf, inscr, nome, razao, endereco, cxpostal, compl, bairr, id_municipio, cep, @@ -391,7 +395,7 @@ BEGIN limcred, indicador_ie, cod_formapag, cod_pauta ) VALUES ( 1, NEW.pesso, NEW.consfinal, - COALESCE(NEW.cgcpf, ''), COALESCE(NEW.suf_cgcpf, ''), COALESCE(NEW.inscr, ''), + v_cgcpf, COALESCE(NEW.suf_cgcpf, ''), COALESCE(NEW.inscr, ''), COALESCE(LEFT(NEW.nome, 40), ''), LEFT(NEW.razao, 65), COALESCE(LEFT(NEW.endereco, 60), ''), 0, '', COALESCE(LEFT(NEW.bairro, 60), ''), NEW.id_municipio, COALESCE(NEW.cep, ''),