arcadia-suite-sv/server/fisco/routes.ts

1140 lines
38 KiB
TypeScript

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<number>`count(*)` })
.from(fiscalGruposTributacao)
.where(eq(fiscalGruposTributacao.tenantId, tenantId));
const naturezasCount = await db.select({ count: sql<number>`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;