arcadiasuite/server/contabil/routes.ts

604 lines
19 KiB
TypeScript

import { Router, Request, Response } from "express";
import { db } from "../../db/index";
import {
contabilPlanoContas, contabilCentrosCusto, contabilLancamentos,
contabilPartidas, contabilPeriodos, contabilConfigLancamento, contabilSaldos,
insertContabilPlanoContasSchema, insertContabilCentroCustoSchema,
insertContabilLancamentoSchema, insertContabilPartidaSchema
} from "@shared/schema";
import { eq, and, desc, sql, like, gte, lte } from "drizzle-orm";
import { z } from "zod";
import multer from "multer";
import path from "path";
import fs from "fs";
import { PDFParse } from "pdf-parse";
const router = Router();
const uploadDir = path.join(process.cwd(), "uploads", "contabil");
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, uploadDir),
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + "-" + Math.round(Math.random() * 1e9);
cb(null, uniqueSuffix + "-" + file.originalname);
},
});
const upload = multer({
storage,
limits: { fileSize: 50 * 1024 * 1024 },
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (ext === ".pdf") {
cb(null, true);
} else {
cb(new Error("Apenas arquivos PDF são suportados"));
}
},
});
const CONTABIL_SERVICE_URL = process.env.CONTABIL_PYTHON_URL || "http://localhost:8003";
// ========== Proxy para serviço Python ==========
async function proxyToContabilService(path: string, method: string = "GET", body?: any) {
try {
const response = await fetch(`${CONTABIL_SERVICE_URL}${path}`, {
method,
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
return await response.json();
} catch (error) {
console.error("Erro ao conectar com serviço contábil:", error);
throw new Error("Serviço contábil indisponível");
}
}
// ========== Health Check ==========
router.get("/health", async (req: Request, res: Response) => {
try {
const pythonHealth = await proxyToContabilService("/health");
res.json({
status: "healthy",
database: "connected",
pythonService: pythonHealth
});
} catch (error) {
res.json({
status: "partial",
database: "connected",
pythonService: "unavailable"
});
}
});
// ========== Plano de Contas ==========
router.get("/plano-contas", async (req: Request, res: Response) => {
try {
const { tenantId, tipo, search } = req.query;
let query = db.select().from(contabilPlanoContas);
if (tenantId) {
query = query.where(eq(contabilPlanoContas.tenantId, Number(tenantId))) as any;
}
const contas = await query.orderBy(contabilPlanoContas.codigo);
res.json(contas);
} catch (error) {
console.error("Erro ao listar plano de contas:", error);
res.status(500).json({ error: "Erro ao listar plano de contas" });
}
});
router.get("/plano-contas/padrao", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/plano-contas/padrao");
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao obter plano de contas padrão" });
}
});
router.get("/plano-contas/:id", async (req: Request, res: Response) => {
try {
const conta = await db.select().from(contabilPlanoContas)
.where(eq(contabilPlanoContas.id, Number(req.params.id)))
.limit(1);
if (conta.length === 0) {
return res.status(404).json({ error: "Conta não encontrada" });
}
res.json(conta[0]);
} catch (error) {
res.status(500).json({ error: "Erro ao buscar conta" });
}
});
router.post("/plano-contas", async (req: Request, res: Response) => {
try {
const validatedData = insertContabilPlanoContasSchema.parse(req.body);
const [conta] = await db.insert(contabilPlanoContas)
.values(validatedData)
.returning();
res.status(201).json(conta);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Dados inválidos", details: error.errors });
}
res.status(500).json({ error: "Erro ao criar conta" });
}
});
router.post("/plano-contas/importar-padrao", async (req: Request, res: Response) => {
try {
const { tenantId } = req.body;
const padrao = await proxyToContabilService("/plano-contas/padrao");
const contasCriadas = [];
for (const conta of padrao.planoContas) {
const [novaConta] = await db.insert(contabilPlanoContas)
.values({
tenantId: tenantId || null,
codigo: conta.codigo,
descricao: conta.descricao,
tipo: conta.tipo,
natureza: conta.natureza,
nivel: conta.nivel,
aceitaLancamento: conta.nivel >= 4 ? 1 : 0,
})
.returning();
contasCriadas.push(novaConta);
}
res.status(201).json({
message: "Plano de contas importado",
total: contasCriadas.length
});
} catch (error) {
res.status(500).json({ error: "Erro ao importar plano de contas" });
}
});
router.put("/plano-contas/:id", async (req: Request, res: Response) => {
try {
const [conta] = await db.update(contabilPlanoContas)
.set({ ...req.body, updatedAt: new Date() })
.where(eq(contabilPlanoContas.id, Number(req.params.id)))
.returning();
res.json(conta);
} catch (error) {
res.status(500).json({ error: "Erro ao atualizar conta" });
}
});
router.delete("/plano-contas/:id", async (req: Request, res: Response) => {
try {
await db.delete(contabilPlanoContas)
.where(eq(contabilPlanoContas.id, Number(req.params.id)));
res.json({ message: "Conta excluída" });
} catch (error) {
res.status(500).json({ error: "Erro ao excluir conta" });
}
});
// ========== Centros de Custo ==========
router.get("/centros-custo", async (req: Request, res: Response) => {
try {
const { tenantId } = req.query;
let query = db.select().from(contabilCentrosCusto);
if (tenantId) {
query = query.where(eq(contabilCentrosCusto.tenantId, Number(tenantId))) as any;
}
const centros = await query.orderBy(contabilCentrosCusto.codigo);
res.json(centros);
} catch (error) {
res.status(500).json({ error: "Erro ao listar centros de custo" });
}
});
router.post("/centros-custo", async (req: Request, res: Response) => {
try {
const validatedData = insertContabilCentroCustoSchema.parse(req.body);
const [centro] = await db.insert(contabilCentrosCusto)
.values(validatedData)
.returning();
res.status(201).json(centro);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: "Dados inválidos", details: error.errors });
}
res.status(500).json({ error: "Erro ao criar centro de custo" });
}
});
router.put("/centros-custo/:id", async (req: Request, res: Response) => {
try {
const [centro] = await db.update(contabilCentrosCusto)
.set(req.body)
.where(eq(contabilCentrosCusto.id, Number(req.params.id)))
.returning();
res.json(centro);
} catch (error) {
res.status(500).json({ error: "Erro ao atualizar centro de custo" });
}
});
router.delete("/centros-custo/:id", async (req: Request, res: Response) => {
try {
await db.delete(contabilCentrosCusto)
.where(eq(contabilCentrosCusto.id, Number(req.params.id)));
res.json({ message: "Centro de custo excluído" });
} catch (error) {
res.status(500).json({ error: "Erro ao excluir centro de custo" });
}
});
// ========== Lançamentos Contábeis ==========
router.get("/lancamentos", async (req: Request, res: Response) => {
try {
const { tenantId, status, limit = 100 } = req.query;
const conditions = [];
if (tenantId) {
conditions.push(eq(contabilLancamentos.tenantId, Number(tenantId)));
}
if (status) {
conditions.push(eq(contabilLancamentos.status, String(status)));
}
let query = db.select().from(contabilLancamentos);
if (conditions.length > 0) {
query = query.where(and(...conditions)) as any;
}
const lancamentos = await query
.orderBy(desc(contabilLancamentos.dataLancamento))
.limit(Number(limit));
res.json(lancamentos);
} catch (error) {
res.status(500).json({ error: "Erro ao listar lançamentos" });
}
});
router.get("/lancamentos/:id", async (req: Request, res: Response) => {
try {
const [lancamento] = await db.select().from(contabilLancamentos)
.where(eq(contabilLancamentos.id, Number(req.params.id)))
.limit(1);
if (!lancamento) {
return res.status(404).json({ error: "Lançamento não encontrado" });
}
const partidas = await db.select().from(contabilPartidas)
.where(eq(contabilPartidas.lancamentoId, lancamento.id));
res.json({ ...lancamento, partidas });
} catch (error) {
res.status(500).json({ error: "Erro ao buscar lançamento" });
}
});
router.post("/lancamentos", async (req: Request, res: Response) => {
try {
const { partidas, ...lancamentoData } = req.body;
// Validar lançamento via serviço Python
const validacao = await proxyToContabilService("/validar-lancamento", "POST", {
...lancamentoData,
partidas
});
if (!validacao.valido) {
return res.status(400).json({
error: "Lançamento desbalanceado",
detalhes: validacao
});
}
// Calcular valor total
const valor = partidas
.filter((p: any) => p.tipo === "debito")
.reduce((acc: number, p: any) => acc + Number(p.valor), 0);
// Criar lançamento
const [lancamento] = await db.insert(contabilLancamentos)
.values({
...lancamentoData,
valor: valor.toString(),
dataLancamento: new Date(lancamentoData.dataLancamento),
dataCompetencia: lancamentoData.dataCompetencia ? new Date(lancamentoData.dataCompetencia) : null,
})
.returning();
// Criar partidas
for (const partida of partidas) {
await db.insert(contabilPartidas).values({
lancamentoId: lancamento.id,
contaId: partida.contaId,
centroCustoId: partida.centroCustoId || null,
tipo: partida.tipo,
valor: partida.valor.toString(),
historico: partida.historico || null,
});
}
res.status(201).json(lancamento);
} catch (error) {
console.error("Erro ao criar lançamento:", error);
res.status(500).json({ error: "Erro ao criar lançamento" });
}
});
router.delete("/lancamentos/:id", async (req: Request, res: Response) => {
try {
// Partidas são excluídas em cascata
await db.delete(contabilLancamentos)
.where(eq(contabilLancamentos.id, Number(req.params.id)));
res.json({ message: "Lançamento excluído" });
} catch (error) {
res.status(500).json({ error: "Erro ao excluir lançamento" });
}
});
// ========== Relatórios via Serviço Python ==========
router.post("/relatorios/dre", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/calcular-dre", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao calcular DRE" });
}
});
router.post("/relatorios/balanco", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/calcular-balanco", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao calcular balanço" });
}
});
router.post("/relatorios/balancete", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/calcular-balancete", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao calcular balancete" });
}
});
router.post("/relatorios/razao", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/gerar-razao", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao gerar razão" });
}
});
router.post("/relatorios/diario", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/gerar-diario", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao gerar diário" });
}
});
// ========== SPED ECD ==========
router.get("/sped/contas-referenciais", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/tabela-contas-referenciais");
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao obter contas referenciais" });
}
});
router.post("/sped/exportar-ecd", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/exportar-sped-ecd", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao exportar SPED ECD" });
}
});
// ========== Integrações ==========
router.post("/integrar/nfe", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/integrar-nfe", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao integrar NF-e" });
}
});
router.post("/integrar/folha", async (req: Request, res: Response) => {
try {
const resultado = await proxyToContabilService("/integrar-folha", "POST", req.body);
res.json(resultado);
} catch (error) {
res.status(500).json({ error: "Erro ao integrar folha" });
}
});
// ========== Importação de Balanço ==========
router.post("/extrair-pdf", upload.single("pdf"), async (req: Request, res: Response) => {
try {
if (!req.file) {
return res.status(400).json({ error: "Nenhum arquivo PDF enviado" });
}
const pdfBuffer = fs.readFileSync(req.file.path);
const parser = new PDFParse({ data: pdfBuffer });
const result = await parser.getText();
await parser.destroy();
fs.unlinkSync(req.file.path);
res.json({
success: true,
texto: result.text,
paginas: result.numPages || 1
});
} catch (error) {
console.error("Erro ao extrair texto do PDF:", error);
res.status(500).json({ error: "Erro ao processar PDF" });
}
});
router.post("/importar-balanco", async (req: Request, res: Response) => {
try {
const { texto, dataBalanco, tenantId, criarContas = true, criarAbertura = true } = req.body;
if (!texto) {
return res.status(400).json({ error: "Texto do balanço é obrigatório" });
}
const resultado = await proxyToContabilService("/importar-balanco", "POST", {
texto,
dataBalanco: dataBalanco || new Date().toISOString().split("T")[0],
criarContas,
criarAbertura
});
if (!resultado.success) {
return res.status(400).json({ error: resultado.detail || "Erro ao processar balanço" });
}
const contasCriadas = [];
const lancamentosCriados = [];
if (criarContas && resultado.planoContas) {
for (const conta of resultado.planoContas) {
try {
const [novaConta] = await db.insert(contabilPlanoContas)
.values({
tenantId: tenantId || null,
codigo: conta.codigo,
descricao: conta.descricao,
tipo: conta.tipo,
natureza: conta.natureza,
nivel: conta.nivel,
aceitaLancamento: conta.aceitaLancamento ? 1 : 0,
status: "ativo"
})
.returning();
contasCriadas.push(novaConta);
} catch (err) {
console.log(`Conta ${conta.codigo} já existe ou erro:`, err);
}
}
}
if (criarAbertura && resultado.lancamentosAbertura && resultado.lancamentosAbertura.length > 0) {
const dataAberturaStr = dataBalanco || new Date().toISOString().split("T")[0];
const dataAberturaDate = new Date(dataAberturaStr);
const [lancamento] = await db.insert(contabilLancamentos)
.values({
tenantId: tenantId || null,
dataLancamento: dataAberturaDate,
dataCompetencia: dataAberturaDate,
tipoDocumento: "ABERTURA",
numeroDocumento: `AB-${Date.now()}`,
historico: `Lançamento de abertura - Importação de balanço ${dataAberturaStr}`,
valor: String(resultado.resumo.totalDebito || 0),
status: "confirmado"
})
.returning();
for (const partida of resultado.lancamentosAbertura) {
const contaExistente = await db.select().from(contabilPlanoContas)
.where(eq(contabilPlanoContas.codigo, partida.conta))
.limit(1);
if (contaExistente.length > 0) {
await db.insert(contabilPartidas).values({
lancamentoId: lancamento.id,
contaId: contaExistente[0].id,
tipo: partida.tipo,
valor: String(partida.valor),
historico: partida.historico
});
}
}
lancamentosCriados.push(lancamento);
}
res.json({
success: true,
contasCriadas: contasCriadas.length,
lancamentosCriados: lancamentosCriados.length,
resumo: resultado.resumo
});
} catch (error) {
console.error("Erro ao importar balanço:", error);
res.status(500).json({ error: "Erro ao importar balanço" });
}
});
// ========== Estatísticas ==========
router.get("/stats/:tenantId?", async (req: Request, res: Response) => {
try {
const { tenantId } = req.params;
let contasQuery = db.select({ count: sql`count(*)` }).from(contabilPlanoContas);
let centrosQuery = db.select({ count: sql`count(*)` }).from(contabilCentrosCusto);
let lancamentosQuery = db.select({ count: sql`count(*)` }).from(contabilLancamentos);
if (tenantId) {
contasQuery = contasQuery.where(eq(contabilPlanoContas.tenantId, Number(tenantId))) as any;
centrosQuery = centrosQuery.where(eq(contabilCentrosCusto.tenantId, Number(tenantId))) as any;
lancamentosQuery = lancamentosQuery.where(eq(contabilLancamentos.tenantId, Number(tenantId))) as any;
}
const totalContas = await contasQuery;
const totalCentros = await centrosQuery;
const totalLancamentos = await lancamentosQuery;
res.json({
totalContas: Number(totalContas[0]?.count || 0),
totalCentrosCusto: Number(totalCentros[0]?.count || 0),
totalLancamentos: Number(totalLancamentos[0]?.count || 0),
});
} catch (error) {
res.status(500).json({ error: "Erro ao obter estatísticas" });
}
});
export default router;