import { Router, Request, Response, NextFunction } from "express"; import { db } from "../../db/index"; import { eq, like, desc, asc, and, or, sql, SQL } from "drizzle-orm"; import { z } from "zod"; import { fiscalNcms, fiscalCests, fiscalCfops, fiscalGruposTributacao, fiscalNaturezaOperacao, fiscalCertificados, fiscalConfiguracoes, fiscalNotas, fiscalNotaItens, fiscalEventos, fiscalIbpt, } from "@shared/schema"; const router = Router(); function ensureAuthenticated(req: Request, res: Response, next: NextFunction) { if (!req.isAuthenticated || !req.isAuthenticated()) { return res.status(401).json({ error: "Não autenticado" }); } next(); } router.use(ensureAuthenticated); const ncmSchema = z.object({ codigo: z.string().min(1), descricao: z.string().min(1), aliqIpi: z.string().optional(), aliqPis: z.string().optional(), aliqCofins: z.string().optional(), cest: z.string().optional(), unidadeTributavel: z.string().optional(), exTipi: z.string().optional(), exNcm: z.string().optional(), }); const cestSchema = z.object({ codigo: z.string().min(1), descricao: z.string().min(1), ncm: z.string().optional(), segmento: z.string().optional(), }); const cfopSchema = z.object({ codigo: z.string().min(1), descricao: z.string().min(1), tipo: z.enum(["entrada", "saida"]), natureza: z.string().optional(), aplicacao: z.string().optional(), indNfe: z.boolean().optional().default(true), indComunica: z.boolean().optional().default(false), indTransp: z.boolean().optional().default(false), indDevol: z.boolean().optional().default(false), }); const grupoTributacaoSchema = z.object({ tenantId: z.number(), nome: z.string().min(1), ncm: z.string().min(1), cest: z.string().optional(), cfopVendaInterna: z.string(), cfopVendaInterestadual: z.string(), cfopDevolucaoInterna: z.string(), cfopDevolucaoInterestadual: z.string(), cstIcms: z.string(), csosnIcms: z.string().optional(), percIcms: z.string().optional(), percReducaoBaseIcms: z.string().optional(), cstPis: z.string(), percPis: z.string().optional(), cstCofins: z.string(), percCofins: z.string().optional(), cstIpi: z.string(), percIpi: z.string().optional(), percIbsUf: z.string().optional(), percIbsMun: z.string().optional(), percCbs: z.string().optional(), cstIbsCbs: z.string().optional(), observacoes: z.string().optional(), ativo: z.boolean().optional().default(true), }); const naturezaOperacaoSchema = z.object({ tenantId: z.number(), codigo: z.string().min(1), descricao: z.string().min(1), tipo: z.string(), cfopInterno: z.string(), cfopInterestadual: z.string(), cfopExportacao: z.string().optional(), movimentaEstoque: z.boolean().optional().default(true), geraFinanceiro: z.boolean().optional().default(true), destacaIpi: z.boolean().optional().default(false), destacaIcmsSt: z.boolean().optional().default(false), finalidade: z.string(), observacoes: z.string().optional(), ativo: z.boolean().optional().default(true), }); const certificadoSchema = z.object({ tenantId: z.number(), nome: z.string().min(1), tipo: z.enum(["A1", "A3"]), cnpj: z.string().optional(), razaoSocial: z.string().optional(), validoAte: z.string().optional(), certificadoBase64: z.string().optional(), senha: z.string().optional(), ambiente: z.enum(["homologacao", "producao"]).default("homologacao"), status: z.enum(["ativo", "expirado", "revogado"]).default("ativo"), }); const configuracaoFiscalSchema = z.object({ tenantId: z.number(), ambiente: z.enum(["homologacao", "producao"]).optional().default("homologacao"), regimeTributario: z.enum(["1", "2", "3"]).optional(), ufEmitente: z.string().optional(), cnpjEmitente: z.string().optional(), ieEmitente: z.string().optional(), razaoSocialEmitente: z.string().optional(), nomeFantasiaEmitente: z.string().optional(), serieNfe: z.number().optional(), proximoNumeroNfe: z.number().optional(), serieNfce: z.number().optional(), proximoNumeroNfce: z.number().optional(), cscId: z.string().optional(), cscToken: z.string().optional(), habilitarIbsCbs: z.boolean().optional().default(false), }); const notaItemSchema = z.object({ produtoCodigo: z.string().optional(), produtoDescricao: z.string().min(1), ncm: z.string().optional(), cfop: z.string().optional(), unidade: z.string().optional(), quantidade: z.string().optional(), valorUnitario: z.string().optional(), valorTotal: z.string().optional(), cstIcms: z.string().optional(), percIcms: z.string().optional(), valorIcms: z.string().optional(), cstPis: z.string().optional(), percPis: z.string().optional(), valorPis: z.string().optional(), cstCofins: z.string().optional(), percCofins: z.string().optional(), valorCofins: z.string().optional(), cstIpi: z.string().optional(), percIpi: z.string().optional(), valorIpi: z.string().optional(), percIbsUf: z.string().optional(), percIbsMun: z.string().optional(), percCbs: z.string().optional(), }); const notaFiscalSchema = z.object({ tenantId: z.number(), modelo: z.enum(["55", "65"]), serie: z.number().optional(), numero: z.number().optional(), naturezaOperacao: z.string(), tipoOperacao: z.enum(["0", "1"]).optional(), destinatarioNome: z.string().optional(), destinatarioCnpjCpf: z.string().optional(), destinatarioIe: z.string().optional(), destinatarioEndereco: z.string().optional(), destinatarioMunicipio: z.string().optional(), destinatarioUf: z.string().optional(), destinatarioCep: z.string().optional(), valorProdutos: z.string().optional(), valorDesconto: z.string().optional(), valorFrete: z.string().optional(), valorSeguro: z.string().optional(), valorOutros: z.string().optional(), valorTotal: z.string().optional(), baseIcms: z.string().optional(), valorIcms: z.string().optional(), valorPis: z.string().optional(), valorCofins: z.string().optional(), valorIpi: z.string().optional(), informacoesAdicionais: z.string().optional(), status: z.enum(["rascunho", "pendente", "autorizada", "cancelada", "rejeitada", "denegada"]).optional().default("rascunho"), }); const notaFiscalComItensSchema = notaFiscalSchema.extend({ itens: z.array(notaItemSchema).optional(), }); const eventoFiscalSchema = z.object({ tipoEvento: z.string().min(1), descricaoEvento: z.string().optional(), justificativa: z.string().optional(), correcao: z.string().optional(), }); const ibptImportSchema = z.object({ dados: z.array(z.object({ ncm: z.string(), exTipi: z.string().optional(), tipo: z.string().optional(), descricao: z.string().optional(), aliqNacional: z.string().optional(), aliqImportado: z.string().optional(), aliqEstadual: z.string().optional(), aliqMunicipal: z.string().optional(), vigenciaInicio: z.string().optional(), vigenciaFim: z.string().optional(), versao: z.string().optional(), fonte: z.string().optional(), })), }); // ========== NCM ========== router.get("/ncm", async (req: Request, res: Response) => { try { const { search, limit = "100", offset = "0" } = req.query; const conditions: SQL[] = []; if (search) { conditions.push( or( sql`${fiscalNcms.codigo} ILIKE ${'%' + search + '%'}`, sql`${fiscalNcms.descricao} ILIKE ${'%' + search + '%'}` )! ); } const ncms = await db.select().from(fiscalNcms) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(asc(fiscalNcms.codigo)) .limit(parseInt(limit as string)) .offset(parseInt(offset as string)); res.json(ncms); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/ncm/:id", async (req: Request, res: Response) => { try { const [ncm] = await db.select().from(fiscalNcms).where(eq(fiscalNcms.id, parseInt(req.params.id))); if (!ncm) return res.status(404).json({ error: "NCM não encontrado" }); res.json(ncm); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/ncm", async (req: Request, res: Response) => { try { const validated = ncmSchema.parse(req.body); const [ncm] = await db.insert(fiscalNcms).values(validated).returning(); res.status(201).json(ncm); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/ncm/:id", async (req: Request, res: Response) => { try { const validated = ncmSchema.partial().parse(req.body); const [ncm] = await db.update(fiscalNcms) .set({ ...validated, updatedAt: new Date() }) .where(eq(fiscalNcms.id, parseInt(req.params.id))) .returning(); if (!ncm) return res.status(404).json({ error: "NCM não encontrado" }); res.json(ncm); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/ncm/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalNcms).where(eq(fiscalNcms.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "NCM não encontrado" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CEST ========== router.get("/cest", async (req: Request, res: Response) => { try { const { search, limit = "100" } = req.query; const conditions: SQL[] = []; if (search) { conditions.push( or( sql`${fiscalCests.codigo} ILIKE ${'%' + search + '%'}`, sql`${fiscalCests.descricao} ILIKE ${'%' + search + '%'}` )! ); } const cests = await db.select().from(fiscalCests) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(asc(fiscalCests.codigo)) .limit(parseInt(limit as string)); res.json(cests); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/cest/:id", async (req: Request, res: Response) => { try { const [cest] = await db.select().from(fiscalCests).where(eq(fiscalCests.id, parseInt(req.params.id))); if (!cest) return res.status(404).json({ error: "CEST não encontrado" }); res.json(cest); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/cest", async (req: Request, res: Response) => { try { const validated = cestSchema.parse(req.body); const [cest] = await db.insert(fiscalCests).values(validated).returning(); res.status(201).json(cest); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/cest/:id", async (req: Request, res: Response) => { try { const validated = cestSchema.partial().parse(req.body); const [cest] = await db.update(fiscalCests) .set(validated) .where(eq(fiscalCests.id, parseInt(req.params.id))) .returning(); if (!cest) return res.status(404).json({ error: "CEST não encontrado" }); res.json(cest); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/cest/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalCests).where(eq(fiscalCests.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "CEST não encontrado" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CFOP ========== router.get("/cfop", async (req: Request, res: Response) => { try { const { tipo, search } = req.query; const conditions: SQL[] = []; if (tipo) conditions.push(eq(fiscalCfops.tipo, tipo as string)); if (search) { conditions.push( or( sql`${fiscalCfops.codigo} ILIKE ${'%' + search + '%'}`, sql`${fiscalCfops.descricao} ILIKE ${'%' + search + '%'}` )! ); } const cfops = await db.select().from(fiscalCfops) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(asc(fiscalCfops.codigo)); res.json(cfops); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/cfop/:id", async (req: Request, res: Response) => { try { const [cfop] = await db.select().from(fiscalCfops).where(eq(fiscalCfops.id, parseInt(req.params.id))); if (!cfop) return res.status(404).json({ error: "CFOP não encontrado" }); res.json(cfop); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/cfop", async (req: Request, res: Response) => { try { const validated = cfopSchema.parse(req.body); const [cfop] = await db.insert(fiscalCfops).values(validated).returning(); res.status(201).json(cfop); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/cfop/:id", async (req: Request, res: Response) => { try { const validated = cfopSchema.partial().parse(req.body); const [cfop] = await db.update(fiscalCfops) .set(validated) .where(eq(fiscalCfops.id, parseInt(req.params.id))) .returning(); if (!cfop) return res.status(404).json({ error: "CFOP não encontrado" }); res.json(cfop); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/cfop/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalCfops).where(eq(fiscalCfops.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "CFOP não encontrado" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== GRUPOS DE TRIBUTAÇÃO ========== router.get("/grupos-tributacao", async (req: Request, res: Response) => { try { const { tenantId, search, ncm } = req.query; const conditions: SQL[] = []; if (tenantId) conditions.push(eq(fiscalGruposTributacao.tenantId, parseInt(tenantId as string))); if (ncm) conditions.push(eq(fiscalGruposTributacao.ncm, ncm as string)); if (search) { conditions.push(sql`${fiscalGruposTributacao.nome} ILIKE ${'%' + search + '%'}`); } const grupos = await db.select().from(fiscalGruposTributacao) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(asc(fiscalGruposTributacao.nome)); res.json(grupos); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/grupos-tributacao/:id", async (req: Request, res: Response) => { try { const [grupo] = await db.select().from(fiscalGruposTributacao) .where(eq(fiscalGruposTributacao.id, parseInt(req.params.id))); if (!grupo) return res.status(404).json({ error: "Grupo não encontrado" }); res.json(grupo); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/grupos-tributacao", async (req: Request, res: Response) => { try { const validated = grupoTributacaoSchema.parse(req.body); const [grupo] = await db.insert(fiscalGruposTributacao).values(validated).returning(); res.status(201).json(grupo); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/grupos-tributacao/:id", async (req: Request, res: Response) => { try { const validated = grupoTributacaoSchema.partial().parse(req.body); const [grupo] = await db.update(fiscalGruposTributacao) .set({ ...validated, updatedAt: new Date() }) .where(eq(fiscalGruposTributacao.id, parseInt(req.params.id))) .returning(); if (!grupo) return res.status(404).json({ error: "Grupo não encontrado" }); res.json(grupo); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/grupos-tributacao/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalGruposTributacao).where(eq(fiscalGruposTributacao.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "Grupo não encontrado" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== NATUREZA DE OPERAÇÃO ========== router.get("/natureza-operacao", async (req: Request, res: Response) => { try { const { tenantId, search } = req.query; const conditions: SQL[] = []; if (tenantId) conditions.push(eq(fiscalNaturezaOperacao.tenantId, parseInt(tenantId as string))); if (search) { conditions.push(sql`${fiscalNaturezaOperacao.descricao} ILIKE ${'%' + search + '%'}`); } const naturezas = await db.select().from(fiscalNaturezaOperacao) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(asc(fiscalNaturezaOperacao.descricao)); res.json(naturezas); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/natureza-operacao/:id", async (req: Request, res: Response) => { try { const [natureza] = await db.select().from(fiscalNaturezaOperacao) .where(eq(fiscalNaturezaOperacao.id, parseInt(req.params.id))); if (!natureza) return res.status(404).json({ error: "Natureza não encontrada" }); res.json(natureza); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/natureza-operacao", async (req: Request, res: Response) => { try { const validated = naturezaOperacaoSchema.parse(req.body); const [natureza] = await db.insert(fiscalNaturezaOperacao).values(validated).returning(); res.status(201).json(natureza); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/natureza-operacao/:id", async (req: Request, res: Response) => { try { const validated = naturezaOperacaoSchema.partial().parse(req.body); const [natureza] = await db.update(fiscalNaturezaOperacao) .set({ ...validated, updatedAt: new Date() }) .where(eq(fiscalNaturezaOperacao.id, parseInt(req.params.id))) .returning(); if (!natureza) return res.status(404).json({ error: "Natureza não encontrada" }); res.json(natureza); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/natureza-operacao/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalNaturezaOperacao).where(eq(fiscalNaturezaOperacao.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "Natureza não encontrada" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CERTIFICADOS DIGITAIS ========== router.get("/certificados", async (req: Request, res: Response) => { try { const { tenantId } = req.query; const conditions: SQL[] = []; if (tenantId) { conditions.push(eq(fiscalCertificados.tenantId, parseInt(tenantId as string))); } const certificados = await db.select({ id: fiscalCertificados.id, tenantId: fiscalCertificados.tenantId, nome: fiscalCertificados.nome, tipo: fiscalCertificados.tipo, cnpj: fiscalCertificados.cnpj, razaoSocial: fiscalCertificados.razaoSocial, validoAte: fiscalCertificados.validoAte, ambiente: fiscalCertificados.ambiente, status: fiscalCertificados.status, createdAt: fiscalCertificados.createdAt, }).from(fiscalCertificados) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(desc(fiscalCertificados.createdAt)); res.json(certificados); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/certificados", async (req: Request, res: Response) => { try { const validated = certificadoSchema.parse(req.body); const [certificado] = await db.insert(fiscalCertificados).values(validated).returning(); res.status(201).json(certificado); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/certificados/:id", async (req: Request, res: Response) => { try { const [deleted] = await db.delete(fiscalCertificados).where(eq(fiscalCertificados.id, parseInt(req.params.id))).returning(); if (!deleted) return res.status(404).json({ error: "Certificado não encontrado" }); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== CONFIGURAÇÕES FISCAIS ========== router.get("/configuracoes/:tenantId", async (req: Request, res: Response) => { try { const [config] = await db.select().from(fiscalConfiguracoes) .where(eq(fiscalConfiguracoes.tenantId, parseInt(req.params.tenantId))); res.json(config || null); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/configuracoes", async (req: Request, res: Response) => { try { const validated = configuracaoFiscalSchema.parse(req.body); const existing = await db.select().from(fiscalConfiguracoes) .where(eq(fiscalConfiguracoes.tenantId, validated.tenantId)); if (existing.length > 0) { const [config] = await db.update(fiscalConfiguracoes) .set({ ...validated, updatedAt: new Date() }) .where(eq(fiscalConfiguracoes.tenantId, validated.tenantId)) .returning(); return res.json(config); } const [config] = await db.insert(fiscalConfiguracoes).values(validated).returning(); res.status(201).json(config); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); // ========== NOTAS FISCAIS ========== router.get("/notas", async (req: Request, res: Response) => { try { const { tenantId, modelo, status, limit = "50", offset = "0" } = req.query; const conditions: SQL[] = []; if (tenantId) conditions.push(eq(fiscalNotas.tenantId, parseInt(tenantId as string))); if (modelo) conditions.push(eq(fiscalNotas.modelo, modelo as string)); if (status) conditions.push(eq(fiscalNotas.status, status as string)); const notas = await db.select().from(fiscalNotas) .where(conditions.length > 0 ? and(...conditions) : undefined) .orderBy(desc(fiscalNotas.dataEmissao)) .limit(parseInt(limit as string)) .offset(parseInt(offset as string)); res.json(notas); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.get("/notas/:id", async (req: Request, res: Response) => { try { const [nota] = await db.select().from(fiscalNotas) .where(eq(fiscalNotas.id, parseInt(req.params.id))); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); const itens = await db.select().from(fiscalNotaItens) .where(eq(fiscalNotaItens.notaId, nota.id)) .orderBy(asc(fiscalNotaItens.ordem)); const eventos = await db.select().from(fiscalEventos) .where(eq(fiscalEventos.notaId, nota.id)) .orderBy(desc(fiscalEventos.createdAt)); res.json({ ...nota, itens, eventos }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/notas", async (req: Request, res: Response) => { try { const validated = notaFiscalComItensSchema.parse(req.body); const { itens, ...notaData } = validated; const [nota] = await db.insert(fiscalNotas).values(notaData).returning(); if (itens && itens.length > 0) { const itensComNotaId = itens.map((item, index) => ({ ...item, notaId: nota.id, ordem: index + 1, })); await db.insert(fiscalNotaItens).values(itensComNotaId); } res.status(201).json(nota); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.put("/notas/:id", async (req: Request, res: Response) => { try { const validated = notaFiscalComItensSchema.partial().parse(req.body); const { itens, ...notaData } = validated; const [nota] = await db.update(fiscalNotas) .set({ ...notaData, updatedAt: new Date() }) .where(eq(fiscalNotas.id, parseInt(req.params.id))) .returning(); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); if (itens) { await db.delete(fiscalNotaItens).where(eq(fiscalNotaItens.notaId, nota.id)); if (itens.length > 0) { const itensComNotaId = itens.map((item, index) => ({ ...item, notaId: nota.id, ordem: index + 1, })); await db.insert(fiscalNotaItens).values(itensComNotaId); } } res.json(nota); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); router.delete("/notas/:id", async (req: Request, res: Response) => { try { const [nota] = await db.select().from(fiscalNotas) .where(eq(fiscalNotas.id, parseInt(req.params.id))); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); if (nota.status !== 'rascunho') { return res.status(400).json({ error: "Apenas notas em rascunho podem ser excluídas" }); } await db.delete(fiscalNotaItens).where(eq(fiscalNotaItens.notaId, nota.id)); await db.delete(fiscalNotas).where(eq(fiscalNotas.id, parseInt(req.params.id))); res.json({ success: true }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== EVENTOS FISCAIS ========== router.post("/notas/:id/eventos", async (req: Request, res: Response) => { try { const notaId = parseInt(req.params.id); const validated = eventoFiscalSchema.parse(req.body); const [nota] = await db.select().from(fiscalNotas).where(eq(fiscalNotas.id, notaId)); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); const existingEvents = await db.select().from(fiscalEventos) .where(and( eq(fiscalEventos.notaId, notaId), eq(fiscalEventos.tipoEvento, validated.tipoEvento) )); const [evento] = await db.insert(fiscalEventos).values({ ...validated, notaId, sequencia: existingEvents.length + 1, dataEvento: new Date(), }).returning(); res.status(201).json(evento); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== IBPT ========== router.get("/ibpt/:ncm", async (req: Request, res: Response) => { try { const [ibpt] = await db.select().from(fiscalIbpt) .where(eq(fiscalIbpt.ncm, req.params.ncm)) .orderBy(desc(fiscalIbpt.vigenciaInicio)) .limit(1); res.json(ibpt || null); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/ibpt/importar", async (req: Request, res: Response) => { try { const validated = ibptImportSchema.parse(req.body); let importados = 0; for (const item of validated.dados) { try { await db.insert(fiscalIbpt).values(item); importados++; } catch (e) { } } res.json({ success: true, importados }); } catch (error: any) { if (error instanceof z.ZodError) { return res.status(400).json({ error: "Dados inválidos", details: error.errors }); } res.status(500).json({ error: error.message }); } }); // ========== ESTATÍSTICAS ========== router.get("/stats/:tenantId", async (req: Request, res: Response) => { try { const tenantId = parseInt(req.params.tenantId); const notasResult = await db.execute(sql` SELECT COUNT(*) FILTER (WHERE status = 'autorizada') as autorizadas, COUNT(*) FILTER (WHERE status = 'rascunho') as rascunhos, COUNT(*) FILTER (WHERE status = 'cancelada') as canceladas, COUNT(*) FILTER (WHERE status = 'rejeitada') as rejeitadas, COALESCE(SUM(valor_total) FILTER (WHERE status = 'autorizada'), 0) as valor_total FROM fiscal_notas WHERE tenant_id = ${tenantId} AND EXTRACT(MONTH FROM data_emissao) = EXTRACT(MONTH FROM CURRENT_DATE) AND EXTRACT(YEAR FROM data_emissao) = EXTRACT(YEAR FROM CURRENT_DATE) `); const gruposCount = await db.select({ count: sql`count(*)` }) .from(fiscalGruposTributacao) .where(eq(fiscalGruposTributacao.tenantId, tenantId)); const naturezasCount = await db.select({ count: sql`count(*)` }) .from(fiscalNaturezaOperacao) .where(eq(fiscalNaturezaOperacao.tenantId, tenantId)); res.json({ notas: notasResult.rows?.[0] || {}, grupos: gruposCount[0]?.count || 0, naturezas: naturezasCount[0]?.count || 0, }); } catch (error: any) { res.status(500).json({ error: error.message }); } }); // ========== INTEGRAÇÃO COM SERVIÇO PYTHON NF-e ========== const FISCO_PYTHON_URL = process.env.FISCO_PYTHON_URL || "http://localhost:8002"; async function callFiscoPython(endpoint: string, method: string = "GET", body?: any) { const fetch = (await import("node-fetch")).default; const url = `${FISCO_PYTHON_URL}${endpoint}`; const options: any = { method, headers: { "Content-Type": "application/json" }, }; if (body) { options.body = JSON.stringify(body); } const response = await fetch(url, options); return response.json(); } router.get("/nfe/service-status", async (req: Request, res: Response) => { try { const result = await callFiscoPython("/"); res.json(result); } catch (error: any) { res.status(503).json({ error: "Serviço Python não disponível", details: error.message, hint: "Verifique se o serviço fisco_service.py está rodando na porta 8002" }); } }); router.post("/nfe/validar-certificado", async (req: Request, res: Response) => { try { const result = await callFiscoPython("/certificado/validar", "POST", req.body); res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/nfe/gerar-xml", async (req: Request, res: Response) => { try { const result = await callFiscoPython("/nfe/gerar-xml", "POST", req.body); res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/nfe/emitir", async (req: Request, res: Response) => { try { const { notaId, certificadoId } = req.body; const [nota] = await db.select().from(fiscalNotas).where(eq(fiscalNotas.id, notaId)); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); const [certificado] = await db.select().from(fiscalCertificados).where(eq(fiscalCertificados.id, certificadoId)); if (!certificado) return res.status(404).json({ error: "Certificado não encontrado" }); const itens = await db.select().from(fiscalNotaItens).where(eq(fiscalNotaItens.notaId, notaId)); const [config] = await db.select().from(fiscalConfiguracoes) .where(eq(fiscalConfiguracoes.tenantId, nota.tenantId)); const payload = { dados: { modelo: nota.modelo, serie: nota.serie || 1, numero: nota.numero || 1, natureza_operacao: nota.naturezaOperacao || "VENDA", tipo_operacao: nota.tipoOperacao || "1", ambiente: config?.ambiente || "2", emitente: { cnpj: config?.cnpjEmitente || "", ie: config?.ieEmitente || "", razao_social: config?.razaoSocialEmitente || "", nome_fantasia: config?.nomeFantasiaEmitente || "", endereco: config?.enderecoEmitente || "", numero: config?.numeroEmitente || "", bairro: config?.bairroEmitente || "", municipio: config?.municipioEmitente || "", cod_municipio: config?.codMunicipioEmitente || "", uf: config?.ufEmitente || "", cep: config?.cepEmitente || "", crt: config?.crt || "3", }, destinatario: { cpf_cnpj: nota.destinatarioCnpjCpf || "", razao_social: nota.destinatarioNome || "", ie: nota.destinatarioIe || "", endereco: nota.destinatarioEndereco || "", municipio: nota.destinatarioMunicipio || "", uf: nota.destinatarioUf || "", cep: nota.destinatarioCep || "", ind_ie_dest: "9", }, itens: itens.map((item, idx) => ({ numero: idx + 1, codigo: item.produtoCodigo || "", descricao: item.produtoDescricao || "", ncm: item.ncm || "", cfop: item.cfop || "", unidade: item.unidade || "UN", quantidade: parseFloat(item.quantidade || "1"), valor_unitario: parseFloat(item.valorUnitario || "0"), valor_total: parseFloat(item.valorTotal || "0"), cst_icms: item.cstIcms || "00", aliq_icms: parseFloat(item.percIcms || "0"), valor_icms: parseFloat(item.valorIcms || "0"), cst_pis: item.cstPis || "01", aliq_pis: parseFloat(item.percPis || "0"), valor_pis: parseFloat(item.valorPis || "0"), cst_cofins: item.cstCofins || "01", aliq_cofins: parseFloat(item.percCofins || "0"), valor_cofins: parseFloat(item.valorCofins || "0"), })), valor_produtos: parseFloat(nota.valorProdutos || "0"), valor_total: parseFloat(nota.valorTotal || "0"), valor_desconto: parseFloat(nota.valorDesconto || "0"), valor_frete: parseFloat(nota.valorFrete || "0"), valor_seguro: parseFloat(nota.valorSeguro || "0"), valor_outros: parseFloat(nota.valorOutros || "0"), info_complementar: nota.informacoesAdicionais || "", }, certificado: { arquivo_base64: certificado.arquivoBase64 || "", senha: certificado.senha || "", }, }; const result: any = await callFiscoPython("/nfe/emitir", "POST", payload); if (result.sucesso) { await db.update(fiscalNotas) .set({ status: "autorizada", chaveAcesso: result.chave_nfe, protocolo: result.protocolo, xmlAutorizado: result.xml_autorizado, dataAutorizacao: new Date(), updatedAt: new Date(), }) .where(eq(fiscalNotas.id, notaId)); } res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/nfe/consultar", async (req: Request, res: Response) => { try { const result = await callFiscoPython("/nfe/consultar", "POST", req.body); res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/nfe/cancelar", async (req: Request, res: Response) => { try { const { notaId, justificativa, certificadoId } = req.body; const [nota] = await db.select().from(fiscalNotas).where(eq(fiscalNotas.id, notaId)); if (!nota) return res.status(404).json({ error: "Nota não encontrada" }); const [certificado] = await db.select().from(fiscalCertificados).where(eq(fiscalCertificados.id, certificadoId)); if (!certificado) return res.status(404).json({ error: "Certificado não encontrado" }); const [config] = await db.select().from(fiscalConfiguracoes) .where(eq(fiscalConfiguracoes.tenantId, nota.tenantId)); const payload = { chave_nfe: nota.chaveAcesso, protocolo_autorizacao: nota.protocolo, justificativa, ambiente: config?.ambiente || "2", certificado: { arquivo_base64: certificado.arquivoBase64 || "", senha: certificado.senha || "", }, }; const result: any = await callFiscoPython("/nfe/cancelar", "POST", payload); if (result.sucesso) { await db.update(fiscalNotas) .set({ status: "cancelada", updatedAt: new Date(), }) .where(eq(fiscalNotas.id, notaId)); await db.insert(fiscalEventos).values({ notaId, tipoEvento: "110111", descricaoEvento: "Cancelamento", sequencia: 1, dataEvento: new Date(), protocolo: result.protocolo, justificativa, status: "registrado", }); } res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); router.post("/nfe/inutilizar", async (req: Request, res: Response) => { try { const result = await callFiscoPython("/nfe/inutilizar", "POST", req.body); res.json(result); } catch (error: any) { res.status(500).json({ error: error.message }); } }); export default router;