feat(valuation): Módulo Consultivo de Valuation completo - Motor DCF/Múltiplos/Ativos, Governança 20 critérios, SWOT, Canvas Dual, PDCA, BI Dashboard, Relatórios, Agente IA

This commit is contained in:
jonaspachecoometas 2026-03-10 18:28:14 -03:00
parent f36238215a
commit 44dacedd90
7 changed files with 3324 additions and 1461 deletions

File diff suppressed because it is too large Load Diff

View File

@ -15,8 +15,8 @@ const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const ARCADIA_OWNER = process.env.GITHUB_OWNER || "JonasRodriguesPachceo";
const ARCADIA_REPO = process.env.GITHUB_REPO || "ArcadiaSuite-";
const ARCADIA_OWNER = process.env.GITHUB_OWNER || "jonaspachecoometas";
const ARCADIA_REPO = process.env.GITHUB_REPO || "arcadiasuite";
const DEFAULT_BRANCH = process.env.GITHUB_DEFAULT_BRANCH || "main";
interface FileContent {

View File

@ -0,0 +1,131 @@
export const CHECKLIST_ITEMS = [
{ code: "SOC-001", category: "Informações Gerais e Societárias", name: "Contrato Social e alterações", description: "Contrato social consolidado com todas as alterações contratuais", isBaseRequired: true },
{ code: "SOC-002", category: "Informações Gerais e Societárias", name: "Quadro societário atualizado", description: "Composição societária atual com percentuais", isBaseRequired: true },
{ code: "SOC-003", category: "Informações Gerais e Societárias", name: "Certidões negativas", description: "Certidões negativas federal, estadual e municipal", isBaseRequired: true },
{ code: "SOC-004", category: "Informações Gerais e Societárias", name: "Procurações vigentes", description: "Procurações em vigor com poderes e prazos", isBaseRequired: false },
{ code: "SOC-005", category: "Informações Gerais e Societárias", name: "Atas de assembleias/reuniões", description: "Atas dos últimos 3 anos", isBaseRequired: false },
{ code: "SOC-006", category: "Informações Gerais e Societárias", name: "Organograma da empresa", description: "Estrutura organizacional atualizada", isBaseRequired: false },
{ code: "SOC-007", category: "Informações Gerais e Societárias", name: "Histórico da empresa", description: "Breve histórico de fundação, marcos e evolução", isBaseRequired: true },
{ code: "FIN-001", category: "Dados Financeiros e Contábeis", name: "Balanço patrimonial (3 anos)", description: "Balanço patrimonial dos últimos 3 exercícios", isBaseRequired: true },
{ code: "FIN-002", category: "Dados Financeiros e Contábeis", name: "DRE (3 anos)", description: "Demonstração de resultados dos últimos 3 exercícios", isBaseRequired: true },
{ code: "FIN-003", category: "Dados Financeiros e Contábeis", name: "Fluxo de caixa (3 anos)", description: "Demonstração de fluxo de caixa dos últimos 3 exercícios", isBaseRequired: true },
{ code: "FIN-004", category: "Dados Financeiros e Contábeis", name: "Balancete mensal ano corrente", description: "Balancete mês a mês do exercício atual", isBaseRequired: true },
{ code: "FIN-005", category: "Dados Financeiros e Contábeis", name: "Relatório de contas a receber", description: "Aging de recebíveis com vencimentos", isBaseRequired: true },
{ code: "FIN-006", category: "Dados Financeiros e Contábeis", name: "Relatório de contas a pagar", description: "Aging de payables com vencimentos", isBaseRequired: true },
{ code: "FIN-007", category: "Dados Financeiros e Contábeis", name: "Posição de estoque", description: "Inventário valorizado e aging de estoque", isBaseRequired: false },
{ code: "FIN-008", category: "Dados Financeiros e Contábeis", name: "Contratos de empréstimo/financiamento", description: "Todos os contratos de dívida com condições", isBaseRequired: true },
{ code: "FIN-009", category: "Dados Financeiros e Contábeis", name: "Projeções financeiras (5 anos)", description: "Projeções de receita, custos e investimentos para 5 anos", isBaseRequired: true },
{ code: "ATF-001", category: "Ativos Físicos e Imóveis", name: "Relação de imóveis", description: "Lista de imóveis próprios com valores e documentação", isBaseRequired: false },
{ code: "ATF-002", category: "Ativos Físicos e Imóveis", name: "Laudo de avaliação de imóveis", description: "Laudos de avaliação de mercado dos imóveis", isBaseRequired: false },
{ code: "ATF-003", category: "Ativos Físicos e Imóveis", name: "Inventário de máquinas/equipamentos", description: "Lista completa de ativos fixos com valores", isBaseRequired: false },
{ code: "ATF-004", category: "Ativos Físicos e Imóveis", name: "Contratos de locação", description: "Contratos de aluguel vigentes com condições", isBaseRequired: false },
{ code: "ATI-001", category: "Ativos Intangíveis e PI", name: "Registros de marcas e patentes", description: "Certificados de registro de marcas e patentes", isBaseRequired: false },
{ code: "ATI-002", category: "Ativos Intangíveis e PI", name: "Contratos de licenciamento", description: "Contratos de licença de uso de tecnologia/software", isBaseRequired: false },
{ code: "ATI-003", category: "Ativos Intangíveis e PI", name: "Portfólio de softwares/sistemas", description: "Lista de sistemas proprietários e licenciados", isBaseRequired: false },
{ code: "ATI-004", category: "Ativos Intangíveis e PI", name: "Carteira de clientes valorizada", description: "Base de clientes com LTV e churn rate", isBaseRequired: false },
{ code: "RH-001", category: "Recursos Humanos", name: "Quadro de funcionários", description: "Lista completa de colaboradores com cargos e salários", isBaseRequired: true },
{ code: "RH-002", category: "Recursos Humanos", name: "Acordos coletivos vigentes", description: "Convenções e acordos coletivos de trabalho", isBaseRequired: false },
{ code: "RH-003", category: "Recursos Humanos", name: "Passivos trabalhistas", description: "Relatório de ações trabalhistas em curso", isBaseRequired: false },
{ code: "RH-004", category: "Recursos Humanos", name: "Plano de cargos e salários", description: "Estrutura de remuneração e benefícios", isBaseRequired: false },
{ code: "DIG-001", category: "Ativos Digitais e Sociais", name: "Métricas de redes sociais", description: "Seguidores, engajamento e alcance por plataforma", isBaseRequired: false },
{ code: "DIG-002", category: "Ativos Digitais e Sociais", name: "Analytics do site/app", description: "Métricas de tráfego, conversão e retenção", isBaseRequired: false },
{ code: "DIG-003", category: "Ativos Digitais e Sociais", name: "Base de leads/email marketing", description: "Tamanho e qualidade da base de contatos", isBaseRequired: false },
{ code: "DIG-004", category: "Ativos Digitais e Sociais", name: "Domínios e ativos digitais", description: "Lista de domínios, apps e propriedades digitais", isBaseRequired: false },
{ code: "GOV-001", category: "Governança e Compliance", name: "Políticas internas documentadas", description: "Manual de políticas e procedimentos internos", isBaseRequired: false },
{ code: "GOV-002", category: "Governança e Compliance", name: "Certificações (ISO, etc)", description: "Certificações de qualidade e compliance vigentes", isBaseRequired: false },
{ code: "GOV-003", category: "Governança e Compliance", name: "Mapa de riscos", description: "Matriz de riscos corporativos identificados", isBaseRequired: false },
{ code: "GOV-004", category: "Governança e Compliance", name: "Programa de compliance/LGPD", description: "Documentação do programa de conformidade", isBaseRequired: false },
{ code: "CTR-001", category: "Contratos e Obrigações", name: "Contratos com clientes principais", description: "Contratos dos 10 maiores clientes", isBaseRequired: true },
{ code: "CTR-002", category: "Contratos e Obrigações", name: "Contratos com fornecedores principais", description: "Contratos dos 10 maiores fornecedores", isBaseRequired: true },
{ code: "CTR-003", category: "Contratos e Obrigações", name: "Contingências judiciais", description: "Relatório de processos judiciais e contingências", isBaseRequired: true },
];
export const GOVERNANCE_CRITERIA = [
{ code: "GC-01", name: "Conselho de Administração", category: "Corporate", weight: 8, valuationImpactPct: 3.0, equityImpactPct: 2.0, roeImpactPct: 1.5 },
{ code: "GC-02", name: "Comitê de Auditoria", category: "Corporate", weight: 7, valuationImpactPct: 2.5, equityImpactPct: 1.8, roeImpactPct: 1.2 },
{ code: "GC-03", name: "Gestão de Riscos", category: "Corporate", weight: 8, valuationImpactPct: 3.0, equityImpactPct: 2.5, roeImpactPct: 2.0 },
{ code: "GC-04", name: "Código de Ética", category: "Corporate", weight: 5, valuationImpactPct: 1.5, equityImpactPct: 1.0, roeImpactPct: 0.5 },
{ code: "GC-05", name: "Transparência e Disclosure", category: "Corporate", weight: 7, valuationImpactPct: 2.5, equityImpactPct: 2.0, roeImpactPct: 1.5 },
{ code: "GC-06", name: "ISO 27001 / Segurança da Informação", category: "IT", weight: 6, valuationImpactPct: 2.0, equityImpactPct: 1.5, roeImpactPct: 1.0 },
{ code: "GC-07", name: "SOC 2 Type II", category: "IT", weight: 5, valuationImpactPct: 1.5, equityImpactPct: 1.0, roeImpactPct: 0.8 },
{ code: "GC-08", name: "Conformidade LGPD", category: "IT", weight: 7, valuationImpactPct: 2.5, equityImpactPct: 2.0, roeImpactPct: 1.0 },
{ code: "GC-09", name: "Plano BCDR (Continuidade)", category: "IT", weight: 6, valuationImpactPct: 2.0, equityImpactPct: 1.5, roeImpactPct: 1.0 },
{ code: "GC-10", name: "Pentests e Auditorias de SI", category: "IT", weight: 4, valuationImpactPct: 1.0, equityImpactPct: 0.8, roeImpactPct: 0.5 },
{ code: "GC-11", name: "Política Ambiental", category: "ESG", weight: 5, valuationImpactPct: 1.5, equityImpactPct: 1.0, roeImpactPct: 0.5 },
{ code: "GC-12", name: "Responsabilidade Social", category: "ESG", weight: 5, valuationImpactPct: 1.5, equityImpactPct: 1.0, roeImpactPct: 0.5 },
{ code: "GC-13", name: "Relatório ESG", category: "ESG", weight: 4, valuationImpactPct: 1.0, equityImpactPct: 0.8, roeImpactPct: 0.3 },
{ code: "GC-14", name: "Diversidade e Inclusão", category: "ESG", weight: 4, valuationImpactPct: 1.0, equityImpactPct: 0.8, roeImpactPct: 0.3 },
{ code: "GC-15", name: "Auditoria Externa", category: "Financial", weight: 8, valuationImpactPct: 3.0, equityImpactPct: 2.5, roeImpactPct: 2.0 },
{ code: "GC-16", name: "Controles Internos", category: "Financial", weight: 7, valuationImpactPct: 2.5, equityImpactPct: 2.0, roeImpactPct: 1.5 },
{ code: "GC-17", name: "Planejamento Financeiro Estruturado", category: "Financial", weight: 6, valuationImpactPct: 2.0, equityImpactPct: 1.5, roeImpactPct: 1.0 },
{ code: "GC-18", name: "Plano de Carreira", category: "HR", weight: 4, valuationImpactPct: 1.0, equityImpactPct: 0.8, roeImpactPct: 0.5 },
{ code: "GC-19", name: "Sistema de Avaliação de Desempenho", category: "HR", weight: 4, valuationImpactPct: 1.0, equityImpactPct: 0.8, roeImpactPct: 0.5 },
{ code: "GC-20", name: "Programa de Compliance", category: "Legal", weight: 7, valuationImpactPct: 2.5, equityImpactPct: 2.0, roeImpactPct: 1.5 },
];
export const CANVAS_BLOCKS = [
"key_partners",
"key_activities",
"key_resources",
"value_proposition",
"customer_relationships",
"channels",
"customer_segments",
"cost_structure",
"revenue_streams",
] as const;
export const CANVAS_BLOCK_LABELS: Record<string, string> = {
key_partners: "Parceiros-Chave",
key_activities: "Atividades-Chave",
key_resources: "Recursos-Chave",
value_proposition: "Proposta de Valor",
customer_relationships: "Relacionamento com Clientes",
channels: "Canais",
customer_segments: "Segmentos de Clientes",
cost_structure: "Estrutura de Custos",
revenue_streams: "Fontes de Receita",
};
export const SWOT_QUADRANTS = ["strengths", "weaknesses", "opportunities", "threats"] as const;
export const PDCA_PHASES = ["plan", "do", "check", "act"] as const;
export const PDCA_ORIGIN_AREAS = [
"governance",
"financial",
"operational",
"commercial",
"hr",
"technology",
"legal",
"esg",
] as const;
export const VALUATION_METHODS = {
dcf: "Fluxo de Caixa Descontado (DCF)",
ev_ebitda: "Múltiplo EV/EBITDA",
ev_revenue: "Múltiplo EV/Receita",
patrimonial: "Patrimonial (Book Value)",
assets: "Soma de Ativos",
} as const;
export const CALCULATION_WEIGHTS = {
simple: { dcf: 0.50, ev_ebitda: 0.30, ev_revenue: 0.20 },
governance: { dcf: 0.40, ev_ebitda: 0.25, ev_revenue: 0.15, patrimonial: 0.10, assets: 0.10 },
} as const;
export const SCENARIO_PROBABILITIES = {
conservative: 0.25,
base: 0.50,
optimistic: 0.25,
} as const;
export const SCENARIO_GROWTH_ADJUSTMENTS = {
conservative: -0.30,
base: 0,
optimistic: 0.30,
} as const;
export const MAX_GOVERNANCE_UPLIFT = 0.43;
export const MAX_WACC_REDUCTION = 0.025;

484
server/valuation/engine.ts Normal file
View File

@ -0,0 +1,484 @@
import {
CALCULATION_WEIGHTS,
SCENARIO_PROBABILITIES,
SCENARIO_GROWTH_ADJUSTMENTS,
MAX_GOVERNANCE_UPLIFT,
MAX_WACC_REDUCTION,
} from "./constants";
export interface FinancialData {
year: number;
isProjection: number;
revenue: number;
grossRevenue?: number;
cogs?: number;
grossProfit?: number;
operatingExpenses?: number;
ebitda: number;
ebit?: number;
netIncome?: number;
totalAssets?: number;
totalLiabilities?: number;
totalEquity?: number;
cash?: number;
debt?: number;
workingCapital?: number;
capex?: number;
depreciation?: number;
freeCashFlow?: number;
cashFlowOperations?: number;
headcount?: number;
growthRate?: number;
}
export interface AssumptionData {
riskFreeRate: number;
betaUnlevered: number;
marketPremium: number;
countryRisk: number;
sizePremium: number;
specificRisk: number;
costOfDebt: number;
taxRate: number;
equityRatio: number;
debtRatio: number;
terminalGrowth: number;
projectionYears: number;
}
export interface GovernanceCriterion {
currentScore: number;
targetScore: number;
weight: number;
valuationImpactPct: number;
equityImpactPct: number;
roeImpactPct: number;
}
export interface AssetData {
bookValue: number;
marketValue: number;
appraisedValue?: number;
}
export interface SectorMultiples {
evEbitda: number;
evRevenue: number;
}
export interface ValuationResult {
method: string;
enterpriseValue: number;
equityValue: number;
terminalValue?: number;
netDebt: number;
weight: number;
details: Record<string, any>;
}
export interface SensitivityCell {
wacc: number;
growth: number;
enterpriseValue: number;
equityValue: number;
}
function n(v: any): number {
const parsed = parseFloat(v);
return isNaN(parsed) ? 0 : parsed;
}
export function calculateWACC(assumptions: AssumptionData): number {
const ke =
assumptions.riskFreeRate +
assumptions.betaUnlevered * assumptions.marketPremium +
assumptions.countryRisk +
assumptions.sizePremium +
assumptions.specificRisk;
const kd = assumptions.costOfDebt * (1 - assumptions.taxRate);
const wacc = ke * assumptions.equityRatio + kd * assumptions.debtRatio;
return Math.max(wacc, 0.01);
}
export function calculateTerminalValue(
lastFCF: number,
wacc: number,
terminalGrowth: number,
): number {
if (wacc <= terminalGrowth) return lastFCF * 20;
return (lastFCF * (1 + terminalGrowth)) / (wacc - terminalGrowth);
}
export function calculateDCF(
projectedFCFs: number[],
wacc: number,
terminalGrowth: number,
netDebt: number,
): ValuationResult {
let pvFCFs = 0;
const discountedFCFs: number[] = [];
for (let i = 0; i < projectedFCFs.length; i++) {
const discountFactor = Math.pow(1 + wacc, i + 1);
const pv = projectedFCFs[i] / discountFactor;
discountedFCFs.push(pv);
pvFCFs += pv;
}
const lastFCF = projectedFCFs[projectedFCFs.length - 1] || 0;
const tv = calculateTerminalValue(lastFCF, wacc, terminalGrowth);
const pvTV = tv / Math.pow(1 + wacc, projectedFCFs.length);
const enterpriseValue = pvFCFs + pvTV;
const equityValue = enterpriseValue - netDebt;
return {
method: "dcf",
enterpriseValue,
equityValue,
terminalValue: tv,
netDebt,
weight: 0,
details: {
wacc,
terminalGrowth,
discountedFCFs,
pvFCFs,
pvTerminalValue: pvTV,
},
};
}
export function calculateMultiples(
ebitda: number,
revenue: number,
multiples: SectorMultiples,
netDebt: number,
): { evEbitda: ValuationResult; evRevenue: ValuationResult } {
const evByEbitda = ebitda * multiples.evEbitda;
const evByRevenue = revenue * multiples.evRevenue;
return {
evEbitda: {
method: "ev_ebitda",
enterpriseValue: evByEbitda,
equityValue: evByEbitda - netDebt,
netDebt,
weight: 0,
details: { ebitda, multiple: multiples.evEbitda },
},
evRevenue: {
method: "ev_revenue",
enterpriseValue: evByRevenue,
equityValue: evByRevenue - netDebt,
netDebt,
weight: 0,
details: { revenue, multiple: multiples.evRevenue },
},
};
}
export function calculatePatrimonial(
totalEquity: number,
netDebt: number,
): ValuationResult {
return {
method: "patrimonial",
enterpriseValue: totalEquity + netDebt,
equityValue: totalEquity,
netDebt,
weight: 0,
details: { totalEquity },
};
}
export function calculateAssetValue(
assets: AssetData[],
netDebt: number,
): ValuationResult {
const totalMarket = assets.reduce(
(sum, a) => sum + (a.appraisedValue || a.marketValue || a.bookValue || 0),
0,
);
return {
method: "assets",
enterpriseValue: totalMarket,
equityValue: totalMarket - netDebt,
netDebt,
weight: 0,
details: {
totalAssetValue: totalMarket,
assetCount: assets.length,
},
};
}
export function calculateGovernanceImpact(criteria: GovernanceCriterion[]): {
currentScore: number;
projectedScore: number;
valuationUplift: number;
waccReduction: number;
equityImpact: number;
roeImpact: number;
} {
if (!criteria.length)
return {
currentScore: 0,
projectedScore: 0,
valuationUplift: 0,
waccReduction: 0,
equityImpact: 0,
roeImpact: 0,
};
const totalWeight = criteria.reduce((s, c) => s + n(c.weight), 0);
if (totalWeight === 0)
return {
currentScore: 0,
projectedScore: 0,
valuationUplift: 0,
waccReduction: 0,
equityImpact: 0,
roeImpact: 0,
};
let currentWeighted = 0;
let projectedWeighted = 0;
let valuationImpact = 0;
let equityImpact = 0;
let roeImpact = 0;
for (const c of criteria) {
const w = n(c.weight) / totalWeight;
currentWeighted += n(c.currentScore) * w;
projectedWeighted += n(c.targetScore) * w;
const improvement = (n(c.targetScore) - n(c.currentScore)) / 10;
valuationImpact += improvement * (n(c.valuationImpactPct) / 100);
equityImpact += improvement * (n(c.equityImpactPct) / 100);
roeImpact += improvement * (n(c.roeImpactPct) / 100);
}
const uplift = Math.min(valuationImpact, MAX_GOVERNANCE_UPLIFT);
const scoreImprovement = (projectedWeighted - currentWeighted) / 10;
const waccRed = Math.min(scoreImprovement * MAX_WACC_REDUCTION, MAX_WACC_REDUCTION);
return {
currentScore: currentWeighted,
projectedScore: projectedWeighted,
valuationUplift: uplift,
waccReduction: waccRed,
equityImpact,
roeImpact,
};
}
export function generateProjections(
historicalData: FinancialData[],
assumptions: Partial<AssumptionData>,
years: number = 5,
): FinancialData[] {
const sorted = historicalData
.filter((d) => !d.isProjection)
.sort((a, b) => a.year - b.year);
if (sorted.length === 0) return [];
const last = sorted[sorted.length - 1];
let revenueGrowth = 0;
if (sorted.length >= 2) {
const first = sorted[0];
const n_years = last.year - first.year;
if (n_years > 0 && n(first.revenue) > 0) {
revenueGrowth = Math.pow(n(last.revenue) / n(first.revenue), 1 / n_years) - 1;
}
}
if (revenueGrowth <= 0) revenueGrowth = 0.05;
const ebitdaMargin = n(last.revenue) > 0 ? n(last.ebitda) / n(last.revenue) : 0.15;
const netMargin = n(last.revenue) > 0 ? n(last.netIncome) / n(last.revenue) : 0.08;
const capexRatio = n(last.revenue) > 0 ? Math.abs(n(last.capex)) / n(last.revenue) : 0.05;
const deprRatio = n(last.revenue) > 0 ? n(last.depreciation) / n(last.revenue) : 0.03;
const projections: FinancialData[] = [];
let prevRevenue = n(last.revenue);
for (let i = 1; i <= years; i++) {
const revenue = prevRevenue * (1 + revenueGrowth);
const ebitda = revenue * ebitdaMargin;
const depreciation = revenue * deprRatio;
const ebit = ebitda - depreciation;
const netIncome = revenue * netMargin;
const capex = revenue * capexRatio;
const wcChange = (revenue - prevRevenue) * 0.1;
const fcf = ebitda - capex - wcChange;
projections.push({
year: last.year + i,
isProjection: 1,
revenue,
ebitda,
ebit,
netIncome,
capex,
depreciation,
freeCashFlow: fcf,
totalEquity: n(last.totalEquity) * Math.pow(1.05, i),
totalAssets: n(last.totalAssets) * Math.pow(1.03, i),
totalLiabilities: n(last.totalLiabilities),
cash: n(last.cash) * Math.pow(1.02, i),
debt: n(last.debt) * 0.95,
workingCapital: n(last.workingCapital) + wcChange,
growthRate: revenueGrowth,
});
prevRevenue = revenue;
}
return projections;
}
export function sensitivityAnalysis(
baseFCFs: number[],
baseWACC: number,
baseGrowth: number,
netDebt: number,
gridSize: number = 5,
): SensitivityCell[] {
const cells: SensitivityCell[] = [];
const step = 0.01;
const halfGrid = Math.floor(gridSize / 2);
for (let wi = -halfGrid; wi <= halfGrid; wi++) {
for (let gi = -halfGrid; gi <= halfGrid; gi++) {
const wacc = baseWACC + wi * step;
const growth = baseGrowth + gi * step * 0.5;
if (wacc <= growth || wacc <= 0) continue;
const result = calculateDCF(baseFCFs, wacc, growth, netDebt);
cells.push({
wacc,
growth,
enterpriseValue: result.enterpriseValue,
equityValue: result.equityValue,
});
}
}
return cells;
}
export function runFullValuation(params: {
financials: FinancialData[];
assumptions: AssumptionData;
multiples: SectorMultiples;
assets?: AssetData[];
governanceCriteria?: GovernanceCriterion[];
projectType: "simple" | "governance";
scenario: "conservative" | "base" | "optimistic";
}): {
results: ValuationResult[];
weightedEV: number;
weightedEquity: number;
governanceImpact?: ReturnType<typeof calculateGovernanceImpact>;
} {
const {
financials,
assumptions,
multiples,
assets,
governanceCriteria,
projectType,
scenario,
} = params;
const projected = financials.filter((f) => f.isProjection).sort((a, b) => a.year - b.year);
const historical = financials.filter((f) => !f.isProjection).sort((a, b) => a.year - b.year);
const lastHistorical = historical[historical.length - 1];
const growthAdj = SCENARIO_GROWTH_ADJUSTMENTS[scenario];
const adjustedFCFs = projected.map((f) => {
const base = n(f.freeCashFlow);
return base * (1 + growthAdj);
});
const netDebt = lastHistorical
? n(lastHistorical.debt) - n(lastHistorical.cash)
: 0;
let wacc = calculateWACC(assumptions);
let govImpact: ReturnType<typeof calculateGovernanceImpact> | undefined;
if (projectType === "governance" && governanceCriteria?.length) {
govImpact = calculateGovernanceImpact(governanceCriteria);
wacc = Math.max(wacc - govImpact.waccReduction, 0.01);
}
const weights = CALCULATION_WEIGHTS[projectType];
const results: ValuationResult[] = [];
if (adjustedFCFs.length > 0) {
const dcf = calculateDCF(adjustedFCFs, wacc, assumptions.terminalGrowth, netDebt);
dcf.weight = weights.dcf;
results.push(dcf);
}
const lastEbitda = lastHistorical ? n(lastHistorical.ebitda) : 0;
const lastRevenue = lastHistorical ? n(lastHistorical.revenue) : 0;
if (lastEbitda > 0) {
const { evEbitda, evRevenue } = calculateMultiples(lastEbitda, lastRevenue, multiples, netDebt);
evEbitda.weight = weights.ev_ebitda;
evRevenue.weight = weights.ev_revenue;
results.push(evEbitda, evRevenue);
}
if (projectType === "governance") {
const gWeights = weights as typeof CALCULATION_WEIGHTS.governance;
if (lastHistorical) {
const patri = calculatePatrimonial(n(lastHistorical.totalEquity), netDebt);
patri.weight = gWeights.patrimonial;
results.push(patri);
}
if (assets?.length) {
const assetVal = calculateAssetValue(assets, netDebt);
assetVal.weight = gWeights.assets;
results.push(assetVal);
}
}
let weightedEV = 0;
let weightedEquity = 0;
const totalWeight = results.reduce((s, r) => s + r.weight, 0);
for (const r of results) {
const normalizedWeight = totalWeight > 0 ? r.weight / totalWeight : 0;
weightedEV += r.enterpriseValue * normalizedWeight;
weightedEquity += r.equityValue * normalizedWeight;
}
if (govImpact && govImpact.valuationUplift > 0) {
const projectedEV = weightedEV * (1 + govImpact.valuationUplift);
const projectedEquity = weightedEquity * (1 + govImpact.valuationUplift);
return {
results,
weightedEV: projectedEV,
weightedEquity: projectedEquity,
governanceImpact: govImpact,
};
}
return { results, weightedEV, weightedEquity, governanceImpact: govImpact };
}
export function calculateScenarioWeighted(
scenarioResults: { scenario: string; ev: number; equity: number }[],
): { weightedEV: number; weightedEquity: number } {
let wEV = 0;
let wEq = 0;
for (const s of scenarioResults) {
const prob = SCENARIO_PROBABILITIES[s.scenario as keyof typeof SCENARIO_PROBABILITIES] || 0;
wEV += s.ev * prob;
wEq += s.equity * prob;
}
return { weightedEV: wEV, weightedEquity: wEq };
}

View File

@ -13,7 +13,23 @@ import {
insertValuationDocumentSchema,
insertValuationCanvasSchema,
insertValuationAgentInsightSchema,
insertValuationGovernanceSchema,
insertValuationPdcaSchema,
insertValuationSwotSchema,
insertValuationAssetSchema,
} from "@shared/schema";
import {
runFullValuation,
sensitivityAnalysis,
calculateWACC,
calculateGovernanceImpact,
generateProjections,
calculateScenarioWeighted,
type FinancialData,
type AssumptionData,
type GovernanceCriterion,
} from "./engine";
import { GOVERNANCE_CRITERIA, CHECKLIST_ITEMS, CANVAS_BLOCKS } from "./constants";
import multer from "multer";
import * as XLSX from "xlsx";
import OpenAI from "openai";
@ -1290,4 +1306,912 @@ router.post("/projects/:id/canvas/snapshots", requireAuth, async (req, res) => {
}
});
// ========== CALCULATION ENGINE ==========
router.post("/projects/:id/calculate", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const inputs = await valuationStorage.getInputs(project.id);
const assumptions = await valuationStorage.getAssumptions(project.id);
const govCriteria = await valuationStorage.getGovernanceCriteria(project.id);
const assets = await valuationStorage.getAssets(project.id);
const financials: FinancialData[] = inputs.map((i) => ({
year: i.year,
isProjection: i.isProjection || 0,
revenue: parseFloat(i.revenue || "0"),
grossRevenue: parseFloat(i.grossRevenue || "0"),
cogs: parseFloat(i.cogs || "0"),
grossProfit: parseFloat(i.grossProfit || "0"),
operatingExpenses: parseFloat(i.operatingExpenses || "0"),
ebitda: parseFloat(i.ebitda || "0"),
ebit: parseFloat(i.ebit || "0"),
netIncome: parseFloat(i.netIncome || "0"),
totalAssets: parseFloat(i.totalAssets || "0"),
totalLiabilities: parseFloat(i.totalLiabilities || "0"),
totalEquity: parseFloat(i.totalEquity || "0"),
cash: parseFloat(i.cash || "0"),
debt: parseFloat(i.debt || "0"),
workingCapital: parseFloat(i.workingCapital || "0"),
capex: parseFloat(i.capex || "0"),
depreciation: parseFloat(i.depreciation || "0"),
freeCashFlow: parseFloat(i.freeCashFlow || "0"),
cashFlowOperations: parseFloat(i.cashFlowOperations || "0"),
headcount: i.headcount || 0,
growthRate: parseFloat(i.growthRate || "0"),
}));
const historical = financials.filter((f) => !f.isProjection);
let projected = financials.filter((f) => f.isProjection);
if (projected.length === 0 && historical.length > 0) {
projected = generateProjections(historical, {});
}
const allFinancials = [...historical, ...projected];
const firstAssumption = assumptions[0];
const assumptionData: AssumptionData = {
riskFreeRate: parseFloat(firstAssumption?.value || "0.1050"),
betaUnlevered: 0.8,
marketPremium: 0.065,
countryRisk: 0.025,
sizePremium: 0.035,
specificRisk: 0.02,
costOfDebt: 0.12,
taxRate: 0.34,
equityRatio: 0.7,
debtRatio: 0.3,
terminalGrowth: 0.035,
projectionYears: 5,
};
for (const a of assumptions) {
const key = a.key;
const val = parseFloat(a.value || "0");
if (key === "risk_free_rate") assumptionData.riskFreeRate = val;
if (key === "beta") assumptionData.betaUnlevered = val;
if (key === "market_premium") assumptionData.marketPremium = val;
if (key === "country_risk") assumptionData.countryRisk = val;
if (key === "size_premium") assumptionData.sizePremium = val;
if (key === "specific_risk") assumptionData.specificRisk = val;
if (key === "cost_of_debt") assumptionData.costOfDebt = val;
if (key === "tax_rate") assumptionData.taxRate = val;
if (key === "equity_ratio") assumptionData.equityRatio = val;
if (key === "debt_ratio") assumptionData.debtRatio = val;
if (key === "terminal_growth") assumptionData.terminalGrowth = val;
}
const sectorBenchmarks = await valuationStorage.getSectorBenchmarks(project.sector);
const evEbitdaBench = sectorBenchmarks.find((b) => b.indicatorCode === "ev_ebitda");
const evRevenueBench = sectorBenchmarks.find((b) => b.indicatorCode === "ev_revenue");
const multiples = {
evEbitda: parseFloat(evEbitdaBench?.median || "8"),
evRevenue: parseFloat(evRevenueBench?.median || "2"),
};
const projectType = (project.projectType as "simple" | "governance") || "simple";
const scenarios = ["conservative", "base", "optimistic"] as const;
const scenarioResults: { scenario: string; ev: number; equity: number }[] = [];
await valuationStorage.deleteResults(project.id);
for (const scenario of scenarios) {
const govCriteriaData: GovernanceCriterion[] = govCriteria.map((g) => ({
currentScore: g.currentScore || 0,
targetScore: g.targetScore || 10,
weight: parseFloat(g.weight || "5"),
valuationImpactPct: parseFloat(g.valuationImpactPct || "0"),
equityImpactPct: parseFloat(g.equityImpactPct || "0"),
roeImpactPct: parseFloat(g.roeImpactPct || "0"),
}));
const assetData = assets.map((a) => ({
bookValue: parseFloat(a.bookValue || "0"),
marketValue: parseFloat(a.marketValue || "0"),
appraisedValue: a.appraisedValue ? parseFloat(a.appraisedValue) : undefined,
}));
const result = runFullValuation({
financials: allFinancials,
assumptions: assumptionData,
multiples,
assets: assetData,
governanceCriteria: govCriteriaData,
projectType,
scenario,
});
for (const r of result.results) {
await valuationStorage.createResult({
projectId: project.id,
scenario,
method: r.method,
enterpriseValue: r.enterpriseValue.toFixed(2),
equityValue: r.equityValue.toFixed(2),
terminalValue: r.terminalValue?.toFixed(2),
netDebt: r.netDebt.toFixed(2),
weight: r.weight.toFixed(4),
calculationDetails: r.details,
});
}
scenarioResults.push({
scenario,
ev: result.weightedEV,
equity: result.weightedEquity,
});
}
const weighted = calculateScenarioWeighted(scenarioResults);
const wacc = calculateWACC(assumptionData);
const govImpact = govCriteria.length > 0
? calculateGovernanceImpact(
govCriteria.map((g) => ({
currentScore: g.currentScore || 0,
targetScore: g.targetScore || 10,
weight: parseFloat(g.weight || "5"),
valuationImpactPct: parseFloat(g.valuationImpactPct || "0"),
equityImpactPct: parseFloat(g.equityImpactPct || "0"),
roeImpactPct: parseFloat(g.roeImpactPct || "0"),
})),
)
: null;
await valuationStorage.updateProject(project.id, tenantId, {
currentValuation: weighted.weightedEV.toFixed(2),
projectedValuation: govImpact
? (weighted.weightedEV * (1 + govImpact.valuationUplift)).toFixed(2)
: weighted.weightedEV.toFixed(2),
governanceScore: govImpact?.currentScore?.toFixed(2),
});
await valuationStorage.createAiLog({
projectId: project.id,
eventType: "calculation",
triggerSource: "manual",
inputSummary: `Calculated ${scenarios.length} scenarios, ${projectType} mode`,
outputSummary: `EV: R$ ${(weighted.weightedEV / 1e6).toFixed(2)}M`,
fullResponse: { scenarioResults, weighted, wacc, govImpact },
confidence: "0.95",
});
res.json({
scenarioResults,
weighted,
wacc,
governanceImpact: govImpact,
projectType,
});
} catch (error: any) {
console.error("Calculation error:", error);
res.status(500).json({ error: "Failed to calculate valuation", details: error.message });
}
});
router.get("/projects/:id/results", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const results = await valuationStorage.getResults(project.id);
res.json(results);
} catch (error) {
res.status(500).json({ error: "Failed to fetch results" });
}
});
router.post("/projects/:id/sensitivity", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const inputs = await valuationStorage.getInputs(project.id);
const assumptions = await valuationStorage.getAssumptions(project.id);
const projected = inputs.filter((i) => i.isProjection);
const historical = inputs.filter((i) => !i.isProjection);
const lastHistorical = historical.sort((a, b) => a.year - b.year).pop();
const fcfs = projected.map((p) => parseFloat(p.freeCashFlow || "0"));
const netDebt = lastHistorical ? parseFloat(lastHistorical.debt || "0") - parseFloat(lastHistorical.cash || "0") : 0;
let baseWacc = 0.12;
let baseGrowth = 0.035;
for (const a of assumptions) {
if (a.key === "terminal_growth") baseGrowth = parseFloat(a.value || "0.035");
}
const assumptionData: AssumptionData = {
riskFreeRate: 0.1050,
betaUnlevered: 0.8,
marketPremium: 0.065,
countryRisk: 0.025,
sizePremium: 0.035,
specificRisk: 0.02,
costOfDebt: 0.12,
taxRate: 0.34,
equityRatio: 0.7,
debtRatio: 0.3,
terminalGrowth: baseGrowth,
projectionYears: 5,
};
for (const a of assumptions) {
const val = parseFloat(a.value || "0");
if (a.key === "risk_free_rate") assumptionData.riskFreeRate = val;
if (a.key === "beta") assumptionData.betaUnlevered = val;
if (a.key === "market_premium") assumptionData.marketPremium = val;
if (a.key === "country_risk") assumptionData.countryRisk = val;
if (a.key === "size_premium") assumptionData.sizePremium = val;
if (a.key === "cost_of_debt") assumptionData.costOfDebt = val;
if (a.key === "tax_rate") assumptionData.taxRate = val;
if (a.key === "equity_ratio") assumptionData.equityRatio = val;
if (a.key === "debt_ratio") assumptionData.debtRatio = val;
}
baseWacc = calculateWACC(assumptionData);
const gridSize = req.body.gridSize || 5;
const matrix = sensitivityAnalysis(fcfs, baseWacc, baseGrowth, netDebt, gridSize);
res.json({ matrix, baseWacc, baseGrowth });
} catch (error) {
res.status(500).json({ error: "Failed to run sensitivity analysis" });
}
});
router.post("/projects/:id/projections", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const inputs = await valuationStorage.getInputs(project.id);
const historical: FinancialData[] = inputs
.filter((i) => !i.isProjection)
.map((i) => ({
year: i.year,
isProjection: 0,
revenue: parseFloat(i.revenue || "0"),
ebitda: parseFloat(i.ebitda || "0"),
netIncome: parseFloat(i.netIncome || "0"),
totalEquity: parseFloat(i.totalEquity || "0"),
totalAssets: parseFloat(i.totalAssets || "0"),
totalLiabilities: parseFloat(i.totalLiabilities || "0"),
cash: parseFloat(i.cash || "0"),
debt: parseFloat(i.debt || "0"),
capex: parseFloat(i.capex || "0"),
depreciation: parseFloat(i.depreciation || "0"),
workingCapital: parseFloat(i.workingCapital || "0"),
freeCashFlow: parseFloat(i.freeCashFlow || "0"),
}));
const projections = generateProjections(historical, {}, req.body.years || 5);
for (const p of projections) {
await valuationStorage.createInput({
projectId: project.id,
year: p.year,
isProjection: 1,
revenue: p.revenue?.toFixed(2),
ebitda: p.ebitda?.toFixed(2),
ebit: p.ebit?.toFixed(2),
netIncome: p.netIncome?.toFixed(2),
totalEquity: p.totalEquity?.toFixed(2),
totalAssets: p.totalAssets?.toFixed(2),
cash: p.cash?.toFixed(2),
debt: p.debt?.toFixed(2),
capex: p.capex?.toFixed(2),
depreciation: p.depreciation?.toFixed(2),
freeCashFlow: p.freeCashFlow?.toFixed(2),
workingCapital: p.workingCapital?.toFixed(2),
growthRate: p.growthRate?.toFixed(4),
source: "auto",
});
}
res.json(projections);
} catch (error) {
res.status(500).json({ error: "Failed to generate projections" });
}
});
// ========== GOVERNANCE ==========
router.get("/projects/:id/governance", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const criteria = await valuationStorage.getGovernanceCriteria(project.id);
res.json(criteria);
} catch (error) {
res.status(500).json({ error: "Failed to fetch governance criteria" });
}
});
router.post("/projects/:id/governance/initialize", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const criteriaData = GOVERNANCE_CRITERIA.map((c) => ({
projectId: project.id,
criterionCode: c.code,
criterionName: c.name,
category: c.category,
currentScore: 0,
targetScore: 10,
weight: c.weight.toString(),
valuationImpactPct: c.valuationImpactPct.toString(),
equityImpactPct: c.equityImpactPct.toString(),
roeImpactPct: c.roeImpactPct.toString(),
}));
const result = await valuationStorage.initializeGovernance(project.id, criteriaData);
res.json(result);
} catch (error) {
res.status(500).json({ error: "Failed to initialize governance" });
}
});
router.patch("/projects/:id/governance/:criterionId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationGovernanceSchema.partial().parse(req.body);
const updated = await valuationStorage.updateGovernanceCriterion(
Number(req.params.criterionId),
project.id,
data,
);
if (!updated) return res.status(404).json({ error: "Criterion not found" });
res.json(updated);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update governance criterion" });
}
});
router.get("/projects/:id/governance/impact", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const criteria = await valuationStorage.getGovernanceCriteria(project.id);
const impact = calculateGovernanceImpact(
criteria.map((c) => ({
currentScore: c.currentScore || 0,
targetScore: c.targetScore || 10,
weight: parseFloat(c.weight || "5"),
valuationImpactPct: parseFloat(c.valuationImpactPct || "0"),
equityImpactPct: parseFloat(c.equityImpactPct || "0"),
roeImpactPct: parseFloat(c.roeImpactPct || "0"),
})),
);
res.json(impact);
} catch (error) {
res.status(500).json({ error: "Failed to calculate governance impact" });
}
});
// ========== PDCA ==========
router.get("/projects/:id/pdca", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const items = await valuationStorage.getPdcaItems(project.id);
res.json(items);
} catch (error) {
res.status(500).json({ error: "Failed to fetch PDCA items" });
}
});
router.post("/projects/:id/pdca", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationPdcaSchema.parse({ ...req.body, projectId: project.id });
const item = await valuationStorage.createPdcaItem(data);
res.status(201).json(item);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create PDCA item" });
}
});
router.patch("/projects/:id/pdca/:itemId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationPdcaSchema.partial().parse(req.body);
const updated = await valuationStorage.updatePdcaItem(Number(req.params.itemId), project.id, data);
if (!updated) return res.status(404).json({ error: "PDCA item not found" });
res.json(updated);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update PDCA item" });
}
});
router.delete("/projects/:id/pdca/:itemId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const deleted = await valuationStorage.deletePdcaItem(Number(req.params.itemId), project.id);
if (!deleted) return res.status(404).json({ error: "PDCA item not found" });
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to delete PDCA item" });
}
});
// ========== SWOT ==========
router.get("/projects/:id/swot", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const items = await valuationStorage.getSwotItems(project.id);
res.json(items);
} catch (error) {
res.status(500).json({ error: "Failed to fetch SWOT items" });
}
});
router.post("/projects/:id/swot", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationSwotSchema.parse({ ...req.body, projectId: project.id });
const item = await valuationStorage.createSwotItem(data);
res.status(201).json(item);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create SWOT item" });
}
});
router.patch("/projects/:id/swot/:itemId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationSwotSchema.partial().parse(req.body);
const updated = await valuationStorage.updateSwotItem(Number(req.params.itemId), project.id, data);
if (!updated) return res.status(404).json({ error: "SWOT item not found" });
res.json(updated);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update SWOT item" });
}
});
router.delete("/projects/:id/swot/:itemId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const deleted = await valuationStorage.deleteSwotItem(Number(req.params.itemId), project.id);
if (!deleted) return res.status(404).json({ error: "SWOT item not found" });
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to delete SWOT item" });
}
});
router.post("/projects/:id/swot/generate", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const inputs = await valuationStorage.getInputs(project.id);
const govCriteria = await valuationStorage.getGovernanceCriteria(project.id);
const prompt = `Analise a empresa "${project.companyName}" no setor "${project.sector}" (modelo: ${project.businessModel || "N/A"}, porte: ${project.size}).
Dados financeiros: ${JSON.stringify(inputs.slice(-3).map((i) => ({ ano: i.year, receita: i.revenue, ebitda: i.ebitda, lucro: i.netIncome })))}
Governança: ${govCriteria.length} critérios avaliados, score médio: ${govCriteria.length > 0 ? (govCriteria.reduce((s, c) => s + (c.currentScore || 0), 0) / govCriteria.length).toFixed(1) : "N/A"}
Gere uma análise SWOT com exatamente 3 itens por quadrante (Strengths, Weaknesses, Opportunities, Threats).
Para cada item, indique: item (texto), impact (low/medium/high), valuationRelevance (0-10), governanceRelevance (0-10).
Responda em JSON: { strengths: [...], weaknesses: [...], opportunities: [...], threats: [...] }`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
response_format: { type: "json_object" },
});
const swotData = JSON.parse(completion.choices[0].message.content || "{}");
const created = [];
for (const quadrant of ["strengths", "weaknesses", "opportunities", "threats"]) {
const items = swotData[quadrant] || [];
for (let i = 0; i < items.length; i++) {
const item = await valuationStorage.createSwotItem({
projectId: project.id,
quadrant,
item: items[i].item,
impact: items[i].impact || "medium",
valuationRelevance: items[i].valuationRelevance || 5,
governanceRelevance: items[i].governanceRelevance || 5,
orderIndex: i,
});
created.push(item);
}
}
await valuationStorage.createAiLog({
projectId: project.id,
eventType: "swot_generation",
triggerSource: "manual",
inputSummary: `Generated SWOT for ${project.companyName}`,
outputSummary: `${created.length} items created`,
fullResponse: swotData,
confidence: "0.85",
tokensUsed: completion.usage?.total_tokens,
});
res.json(created);
} catch (error: any) {
console.error("SWOT generation error:", error);
res.status(500).json({ error: "Failed to generate SWOT" });
}
});
// ========== ASSETS ==========
router.get("/projects/:id/assets", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const assets = await valuationStorage.getAssets(project.id);
res.json(assets);
} catch (error) {
res.status(500).json({ error: "Failed to fetch assets" });
}
});
router.post("/projects/:id/assets", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationAssetSchema.parse({ ...req.body, projectId: project.id });
const asset = await valuationStorage.createAsset(data);
res.status(201).json(asset);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to create asset" });
}
});
router.patch("/projects/:id/assets/:assetId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const data = insertValuationAssetSchema.partial().parse(req.body);
const updated = await valuationStorage.updateAsset(Number(req.params.assetId), project.id, data);
if (!updated) return res.status(404).json({ error: "Asset not found" });
res.json(updated);
} catch (error) {
if (error instanceof z.ZodError) return res.status(400).json({ error: error.errors });
res.status(500).json({ error: "Failed to update asset" });
}
});
router.delete("/projects/:id/assets/:assetId", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const deleted = await valuationStorage.deleteAsset(Number(req.params.assetId), project.id);
if (!deleted) return res.status(404).json({ error: "Asset not found" });
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: "Failed to delete asset" });
}
});
// ========== AI LOG / FEED ==========
router.get("/projects/:id/ai-feed", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const logs = await valuationStorage.getAiLogs(project.id, 20);
res.json(logs);
} catch (error) {
res.status(500).json({ error: "Failed to fetch AI feed" });
}
});
router.post("/projects/:id/ai-chat", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const { message } = req.body;
if (!message) return res.status(400).json({ error: "Message required" });
const inputs = await valuationStorage.getInputs(project.id);
const govCriteria = await valuationStorage.getGovernanceCriteria(project.id);
const results = await valuationStorage.getResults(project.id);
const swot = await valuationStorage.getSwotItems(project.id);
const systemPrompt = `Você é um consultor especialista em Valuation e M&A da Arcádia Suite.
Empresa: ${project.companyName} | Setor: ${project.sector} | Porte: ${project.size}
Status: ${project.status} | Tipo: ${project.projectType || "simple"}
Valuation Atual: R$ ${project.currentValuation || "N/A"} | Projetado: R$ ${project.projectedValuation || "N/A"}
Dados financeiros (últimos anos): ${JSON.stringify(inputs.slice(-5).map((i) => ({ ano: i.year, receita: i.revenue, ebitda: i.ebitda, lucro: i.netIncome, fcf: i.freeCashFlow })))}
Governança: ${govCriteria.length} critérios, score médio ${govCriteria.length > 0 ? (govCriteria.reduce((s, c) => s + (c.currentScore || 0), 0) / govCriteria.length).toFixed(1) : "N/A"}
Resultados: ${results.length > 0 ? results.map((r) => `${r.method}/${r.scenario}: EV=${r.enterpriseValue}`).join("; ") : "Nenhum cálculo realizado"}
SWOT: ${swot.length} itens
Responda de forma consultiva, em português, com foco em recomendações acionáveis.`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: message },
],
});
const reply = completion.choices[0].message.content || "";
await valuationStorage.createAiLog({
projectId: project.id,
eventType: "chat",
triggerSource: "user",
inputSummary: message.substring(0, 200),
outputSummary: reply.substring(0, 200),
fullResponse: { message, reply },
tokensUsed: completion.usage?.total_tokens,
});
res.json({ reply });
} catch (error: any) {
console.error("AI chat error:", error);
res.status(500).json({ error: "Failed to process chat" });
}
});
// ========== REPORTS ==========
router.get("/projects/:id/reports", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const reports = await valuationStorage.getReports(project.id);
res.json(reports);
} catch (error) {
res.status(500).json({ error: "Failed to fetch reports" });
}
});
router.post("/projects/:id/reports/generate", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const { reportType = "executive", format = "html" } = req.body;
const inputs = await valuationStorage.getInputs(project.id);
const results = await valuationStorage.getResults(project.id);
const govCriteria = await valuationStorage.getGovernanceCriteria(project.id);
const swot = await valuationStorage.getSwotItems(project.id);
const pdca = await valuationStorage.getPdcaItems(project.id);
const assets = await valuationStorage.getAssets(project.id);
const govImpact = govCriteria.length > 0
? calculateGovernanceImpact(
govCriteria.map((c) => ({
currentScore: c.currentScore || 0,
targetScore: c.targetScore || 10,
weight: parseFloat(c.weight || "5"),
valuationImpactPct: parseFloat(c.valuationImpactPct || "0"),
equityImpactPct: parseFloat(c.equityImpactPct || "0"),
roeImpactPct: parseFloat(c.roeImpactPct || "0"),
})),
)
: null;
const prompt = `Gere um relatório ${reportType === "executive" ? "executivo" : "técnico"} de valuation para:
Empresa: ${project.companyName} | Setor: ${project.sector} | Porte: ${project.size}
Valuation: R$ ${project.currentValuation || "N/A"} (atual) R$ ${project.projectedValuation || "N/A"} (projetado)
Dados financeiros: ${JSON.stringify(inputs.slice(-5).map((i) => ({ ano: i.year, receita: i.revenue, ebitda: i.ebitda })))}
Resultados por método: ${results.map((r) => `${r.method} (${r.scenario}): EV R$ ${r.enterpriseValue}`).join("; ")}
Governança: Score ${govImpact?.currentScore?.toFixed(1) || "N/A"}/10, uplift potencial ${((govImpact?.valuationUplift || 0) * 100).toFixed(1)}%
SWOT: ${swot.length} itens | PDCA: ${pdca.length} ações | Ativos: ${assets.length}
Gere em formato HTML com seções claras. Use formatação profissional.`;
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
});
const content = completion.choices[0].message.content || "";
const report = await valuationStorage.createReport({
projectId: project.id,
reportType,
format,
fileUrl: null,
generatedBy: req.user?.id,
});
await valuationStorage.createAiLog({
projectId: project.id,
eventType: "report_generation",
triggerSource: "manual",
inputSummary: `Generated ${reportType} report in ${format}`,
outputSummary: `Report #${report.id} created`,
fullResponse: { reportId: report.id, content },
tokensUsed: completion.usage?.total_tokens,
});
res.json({ report, content });
} catch (error: any) {
console.error("Report generation error:", error);
res.status(500).json({ error: "Failed to generate report" });
}
});
// ========== PROJECT SUMMARY ==========
router.get("/projects/:id/summary", requireAuth, async (req, res) => {
try {
const tenantId = await getUserTenantId(req);
if (!tenantId) return res.status(403).json({ error: "Tenant not found" });
const project = await valuationStorage.getProject(Number(req.params.id), tenantId);
if (!project) return res.status(404).json({ error: "Project not found" });
const [inputs, results, govCriteria, swot, pdca, assets, checklistProgress, aiLogs] =
await Promise.all([
valuationStorage.getInputs(project.id),
valuationStorage.getResults(project.id),
valuationStorage.getGovernanceCriteria(project.id),
valuationStorage.getSwotItems(project.id),
valuationStorage.getPdcaItems(project.id),
valuationStorage.getAssets(project.id),
valuationStorage.getChecklistProgress(project.id),
valuationStorage.getAiLogs(project.id, 5),
]);
const checklistTotal = checklistProgress.length;
const checklistCompleted = checklistProgress.filter((p) => p.status === "uploaded" || p.status === "completed").length;
const govImpact = govCriteria.length > 0
? calculateGovernanceImpact(
govCriteria.map((c) => ({
currentScore: c.currentScore || 0,
targetScore: c.targetScore || 10,
weight: parseFloat(c.weight || "5"),
valuationImpactPct: parseFloat(c.valuationImpactPct || "0"),
equityImpactPct: parseFloat(c.equityImpactPct || "0"),
roeImpactPct: parseFloat(c.roeImpactPct || "0"),
})),
)
: null;
const baseResults = results.filter((r) => r.scenario === "base");
const pdcaCompleted = pdca.filter((p) => p.status === "completed").length;
res.json({
project,
financials: {
historicalYears: inputs.filter((i) => !i.isProjection).length,
projectedYears: inputs.filter((i) => i.isProjection).length,
latestRevenue: inputs.filter((i) => !i.isProjection).sort((a, b) => b.year - a.year)[0]?.revenue,
latestEbitda: inputs.filter((i) => !i.isProjection).sort((a, b) => b.year - a.year)[0]?.ebitda,
},
valuation: {
currentEV: project.currentValuation,
projectedEV: project.projectedValuation,
creationOfValue: project.currentValuation && project.projectedValuation
? (parseFloat(project.projectedValuation) - parseFloat(project.currentValuation)).toFixed(2)
: null,
creationPct: project.currentValuation && project.projectedValuation && parseFloat(project.currentValuation) > 0
? (((parseFloat(project.projectedValuation) - parseFloat(project.currentValuation)) / parseFloat(project.currentValuation)) * 100).toFixed(1)
: null,
resultsByMethod: baseResults.map((r) => ({ method: r.method, ev: r.enterpriseValue, equity: r.equityValue })),
},
governance: govImpact
? {
currentScore: govImpact.currentScore.toFixed(1),
projectedScore: govImpact.projectedScore.toFixed(1),
uplift: (govImpact.valuationUplift * 100).toFixed(1),
waccReduction: (govImpact.waccReduction * 100).toFixed(2),
criteriaCount: govCriteria.length,
}
: null,
checklist: {
total: checklistTotal,
completed: checklistCompleted,
progress: checklistTotal > 0 ? Math.round((checklistCompleted / checklistTotal) * 100) : 0,
},
swot: {
total: swot.length,
byQuadrant: {
strengths: swot.filter((s) => s.quadrant === "strengths").length,
weaknesses: swot.filter((s) => s.quadrant === "weaknesses").length,
opportunities: swot.filter((s) => s.quadrant === "opportunities").length,
threats: swot.filter((s) => s.quadrant === "threats").length,
},
},
pdca: {
total: pdca.length,
completed: pdcaCompleted,
byPhase: {
plan: pdca.filter((p) => p.phase === "plan").length,
do: pdca.filter((p) => p.phase === "do").length,
check: pdca.filter((p) => p.phase === "check").length,
act: pdca.filter((p) => p.phase === "act").length,
},
},
assets: {
total: assets.length,
totalBookValue: assets.reduce((s, a) => s + parseFloat(a.bookValue || "0"), 0).toFixed(2),
totalMarketValue: assets.reduce((s, a) => s + parseFloat(a.marketValue || "0"), 0).toFixed(2),
},
recentAiActions: aiLogs.map((l) => ({
type: l.eventType,
summary: l.outputSummary,
timestamp: l.createdAt,
})),
});
} catch (error) {
res.status(500).json({ error: "Failed to fetch project summary" });
}
});
export default router;

View File

@ -21,6 +21,13 @@ import {
valuationSectorScores,
valuationCanvasBlocks,
valuationCanvasSnapshots,
valuationGovernance,
valuationPdca,
valuationSwot,
valuationResults,
valuationAssets,
valuationReports,
valuationAiLog,
crmClients,
InsertValuationProject,
InsertValuationInput,
@ -39,6 +46,13 @@ import {
InsertValuationSectorBenchmark,
InsertValuationSectorScore,
InsertValuationCanvasSnapshot,
InsertValuationGovernance,
InsertValuationPdca,
InsertValuationSwot,
InsertValuationResult,
InsertValuationAsset,
InsertValuationReport,
InsertValuationAiLog,
} from "@shared/schema";
export const valuationStorage = {
@ -706,4 +720,226 @@ export const valuationStorage = {
.where(eq(valuationCanvasSnapshots.id, id));
return snapshot;
},
// ========== GOVERNANCE ==========
async getGovernanceCriteria(projectId: number) {
return await db
.select()
.from(valuationGovernance)
.where(eq(valuationGovernance.projectId, projectId))
.orderBy(valuationGovernance.criterionCode);
},
async getGovernanceCriterion(id: number, projectId: number) {
const [c] = await db
.select()
.from(valuationGovernance)
.where(and(eq(valuationGovernance.id, id), eq(valuationGovernance.projectId, projectId)));
return c;
},
async createGovernanceCriterion(data: InsertValuationGovernance) {
const [c] = await db.insert(valuationGovernance).values(data).returning();
return c;
},
async updateGovernanceCriterion(id: number, projectId: number, data: Partial<InsertValuationGovernance>) {
const [c] = await db
.update(valuationGovernance)
.set(data)
.where(and(eq(valuationGovernance.id, id), eq(valuationGovernance.projectId, projectId)))
.returning();
return c;
},
async deleteGovernanceCriterion(id: number, projectId: number) {
const r = await db
.delete(valuationGovernance)
.where(and(eq(valuationGovernance.id, id), eq(valuationGovernance.projectId, projectId)))
.returning();
return r.length > 0;
},
async initializeGovernance(projectId: number, criteria: InsertValuationGovernance[]) {
const existing = await this.getGovernanceCriteria(projectId);
if (existing.length > 0) return existing;
const results = [];
for (const c of criteria) {
const [created] = await db.insert(valuationGovernance).values({ ...c, projectId }).returning();
results.push(created);
}
return results;
},
// ========== PDCA ==========
async getPdcaItems(projectId: number) {
return await db
.select()
.from(valuationPdca)
.where(eq(valuationPdca.projectId, projectId))
.orderBy(desc(valuationPdca.createdAt));
},
async getPdcaItem(id: number, projectId: number) {
const [item] = await db
.select()
.from(valuationPdca)
.where(and(eq(valuationPdca.id, id), eq(valuationPdca.projectId, projectId)));
return item;
},
async createPdcaItem(data: InsertValuationPdca) {
const [item] = await db.insert(valuationPdca).values(data).returning();
return item;
},
async updatePdcaItem(id: number, projectId: number, data: Partial<InsertValuationPdca>) {
const [item] = await db
.update(valuationPdca)
.set({ ...data, updatedAt: new Date() })
.where(and(eq(valuationPdca.id, id), eq(valuationPdca.projectId, projectId)))
.returning();
return item;
},
async deletePdcaItem(id: number, projectId: number) {
const r = await db
.delete(valuationPdca)
.where(and(eq(valuationPdca.id, id), eq(valuationPdca.projectId, projectId)))
.returning();
return r.length > 0;
},
// ========== SWOT ==========
async getSwotItems(projectId: number) {
return await db
.select()
.from(valuationSwot)
.where(eq(valuationSwot.projectId, projectId))
.orderBy(valuationSwot.orderIndex);
},
async getSwotItem(id: number, projectId: number) {
const [item] = await db
.select()
.from(valuationSwot)
.where(and(eq(valuationSwot.id, id), eq(valuationSwot.projectId, projectId)));
return item;
},
async createSwotItem(data: InsertValuationSwot) {
const [item] = await db.insert(valuationSwot).values(data).returning();
return item;
},
async updateSwotItem(id: number, projectId: number, data: Partial<InsertValuationSwot>) {
const [item] = await db
.update(valuationSwot)
.set(data)
.where(and(eq(valuationSwot.id, id), eq(valuationSwot.projectId, projectId)))
.returning();
return item;
},
async deleteSwotItem(id: number, projectId: number) {
const r = await db
.delete(valuationSwot)
.where(and(eq(valuationSwot.id, id), eq(valuationSwot.projectId, projectId)))
.returning();
return r.length > 0;
},
// ========== RESULTS ==========
async getResults(projectId: number) {
return await db
.select()
.from(valuationResults)
.where(eq(valuationResults.projectId, projectId))
.orderBy(desc(valuationResults.calculatedAt));
},
async createResult(data: InsertValuationResult) {
const [r] = await db.insert(valuationResults).values(data).returning();
return r;
},
async deleteResults(projectId: number) {
await db.delete(valuationResults).where(eq(valuationResults.projectId, projectId));
},
// ========== ASSETS ==========
async getAssets(projectId: number) {
return await db
.select()
.from(valuationAssets)
.where(eq(valuationAssets.projectId, projectId))
.orderBy(valuationAssets.name);
},
async getAsset(id: number, projectId: number) {
const [a] = await db
.select()
.from(valuationAssets)
.where(and(eq(valuationAssets.id, id), eq(valuationAssets.projectId, projectId)));
return a;
},
async createAsset(data: InsertValuationAsset) {
const [a] = await db.insert(valuationAssets).values(data).returning();
return a;
},
async updateAsset(id: number, projectId: number, data: Partial<InsertValuationAsset>) {
const [a] = await db
.update(valuationAssets)
.set(data)
.where(and(eq(valuationAssets.id, id), eq(valuationAssets.projectId, projectId)))
.returning();
return a;
},
async deleteAsset(id: number, projectId: number) {
const r = await db
.delete(valuationAssets)
.where(and(eq(valuationAssets.id, id), eq(valuationAssets.projectId, projectId)))
.returning();
return r.length > 0;
},
// ========== REPORTS ==========
async getReports(projectId: number) {
return await db
.select()
.from(valuationReports)
.where(eq(valuationReports.projectId, projectId))
.orderBy(desc(valuationReports.generatedAt));
},
async createReport(data: InsertValuationReport) {
const [r] = await db.insert(valuationReports).values(data).returning();
return r;
},
async deleteReport(id: number, projectId: number) {
const r = await db
.delete(valuationReports)
.where(and(eq(valuationReports.id, id), eq(valuationReports.projectId, projectId)))
.returning();
return r.length > 0;
},
// ========== AI LOG ==========
async getAiLogs(projectId: number, limit: number = 20) {
return await db
.select()
.from(valuationAiLog)
.where(eq(valuationAiLog.projectId, projectId))
.orderBy(desc(valuationAiLog.createdAt))
.limit(limit);
},
async createAiLog(data: InsertValuationAiLog) {
const [log] = await db.insert(valuationAiLog).values(data).returning();
return log;
},
};

View File

@ -2488,6 +2488,7 @@ export const valuationProjects = pgTable("valuation_projects", {
businessModel: text("business_model"),
stage: text("stage").notNull(),
size: text("size").notNull(),
projectType: text("project_type").default("simple"),
status: text("status").default("draft"),
consultantId: varchar("consultant_id").references(() => users.id),
clientUserId: varchar("client_user_id").references(() => users.id),
@ -2495,9 +2496,20 @@ export const valuationProjects = pgTable("valuation_projects", {
valuationRangeMin: numeric("valuation_range_min"),
valuationRangeMax: numeric("valuation_range_max"),
finalValue: numeric("final_value"),
currentValuation: numeric("current_valuation"),
projectedValuation: numeric("projected_valuation"),
governanceScore: numeric("governance_score"),
checklistProgress: numeric("checklist_progress"),
currency: text("currency").default("BRL"),
baseDate: timestamp("base_date"),
valuationObjective: text("valuation_objective"),
foundingYear: integer("founding_year"),
city: text("city"),
state: text("state"),
legalNature: text("legal_nature"),
reportUrl: text("report_url"),
notes: text("notes"),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
@ -2508,8 +2520,12 @@ export const valuationInputs = pgTable("valuation_inputs", {
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
year: integer("year").notNull(),
isProjection: integer("is_projection").default(0),
periodType: text("period_type").default("annual"),
grossRevenue: numeric("gross_revenue"),
revenue: numeric("revenue"),
cogs: numeric("cogs"),
grossProfit: numeric("gross_profit"),
operatingExpenses: numeric("operating_expenses"),
ebitda: numeric("ebitda"),
ebit: numeric("ebit"),
netIncome: numeric("net_income"),
@ -2521,7 +2537,10 @@ export const valuationInputs = pgTable("valuation_inputs", {
workingCapital: numeric("working_capital"),
capex: numeric("capex"),
depreciation: numeric("depreciation"),
cashFlowOperations: numeric("cash_flow_operations"),
freeCashFlow: numeric("free_cash_flow"),
headcount: integer("headcount"),
source: text("source").default("manual"),
arr: numeric("arr"),
mrr: numeric("mrr"),
churnRate: numeric("churn_rate"),
@ -2664,7 +2683,151 @@ export const valuationAgentInsights = pgTable("valuation_agent_insights", {
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// Insert Schemas - Valuation
// ========== VALUATION GOVERNANCE ==========
export const valuationGovernance = pgTable("valuation_governance", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
criterionCode: text("criterion_code").notNull(),
criterionName: text("criterion_name").notNull(),
category: text("category").notNull(),
currentScore: integer("current_score").default(0),
targetScore: integer("target_score").default(10),
weight: numeric("weight"),
valuationImpactPct: numeric("valuation_impact_pct"),
equityImpactPct: numeric("equity_impact_pct"),
roeImpactPct: numeric("roe_impact_pct"),
priority: text("priority").default("medium"),
implementationQuarter: text("implementation_quarter"),
implementationCost: numeric("implementation_cost"),
status: text("status").default("not_started"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// ========== VALUATION PDCA ==========
export const valuationPdca = pgTable("valuation_pdca", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
governanceCriterionId: integer("governance_criterion_id").references(() => valuationGovernance.id),
title: text("title").notNull(),
originArea: text("origin_area").notNull(),
phase: text("phase").notNull().default("plan"),
status: text("status").default("not_started"),
priority: text("priority").default("medium"),
description: text("description"),
expectedResult: text("expected_result"),
actualResult: text("actual_result"),
improvementScore: integer("improvement_score"),
heatMapValue: numeric("heat_map_value"),
responsible: text("responsible"),
startDate: timestamp("start_date"),
endDate: timestamp("end_date"),
completedAt: timestamp("completed_at"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
updatedAt: timestamp("updated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// ========== VALUATION SWOT ==========
export const valuationSwot = pgTable("valuation_swot", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
quadrant: text("quadrant").notNull(),
item: text("item").notNull(),
impact: text("impact").default("medium"),
valuationRelevance: integer("valuation_relevance").default(0),
governanceRelevance: integer("governance_relevance").default(0),
linkedPdcaId: integer("linked_pdca_id").references(() => valuationPdca.id),
valuationImpactPct: numeric("valuation_impact_pct"),
orderIndex: integer("order_index").default(0),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// ========== VALUATION RESULTS ==========
export const valuationResults = pgTable("valuation_results", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
scenario: text("scenario").notNull(),
method: text("method").notNull(),
enterpriseValue: numeric("enterprise_value"),
equityValue: numeric("equity_value"),
terminalValue: numeric("terminal_value"),
netDebt: numeric("net_debt"),
roe: numeric("roe"),
roa: numeric("roa"),
weight: numeric("weight"),
calculationDetails: jsonb("calculation_details"),
calculatedAt: timestamp("calculated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// ========== VALUATION ASSETS ==========
export const valuationAssets = pgTable("valuation_assets", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
assetType: text("asset_type").notNull(),
name: text("name").notNull(),
description: text("description"),
bookValue: numeric("book_value"),
marketValue: numeric("market_value"),
appraisedValue: numeric("appraised_value"),
depreciationRate: numeric("depreciation_rate"),
acquisitionDate: timestamp("acquisition_date"),
status: text("status").default("active"),
notes: text("notes"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// ========== VALUATION REPORTS ==========
export const valuationReports = pgTable("valuation_reports", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
reportType: text("report_type").notNull(),
format: text("format").notNull(),
fileUrl: text("file_url"),
generatedAt: timestamp("generated_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
generatedBy: varchar("generated_by").references(() => users.id),
version: integer("version").default(1),
isCurrent: integer("is_current").default(1),
});
// ========== VALUATION AI LOG ==========
export const valuationAiLog = pgTable("valuation_ai_log", {
id: serial("id").primaryKey(),
projectId: integer("project_id").notNull().references(() => valuationProjects.id, { onDelete: "cascade" }),
eventType: text("event_type").notNull(),
triggerSource: text("trigger_source").notNull(),
inputSummary: text("input_summary"),
outputSummary: text("output_summary"),
fullResponse: jsonb("full_response"),
confidence: numeric("confidence"),
tokensUsed: integer("tokens_used"),
createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`).notNull(),
});
// Insert Schemas - Valuation (new tables)
export const insertValuationGovernanceSchema = createInsertSchema(valuationGovernance).omit({ id: true, createdAt: true });
export const insertValuationPdcaSchema = createInsertSchema(valuationPdca).omit({ id: true, createdAt: true, updatedAt: true });
export const insertValuationSwotSchema = createInsertSchema(valuationSwot).omit({ id: true, createdAt: true });
export const insertValuationResultSchema = createInsertSchema(valuationResults).omit({ id: true, calculatedAt: true });
export const insertValuationAssetSchema = createInsertSchema(valuationAssets).omit({ id: true, createdAt: true });
export const insertValuationReportSchema = createInsertSchema(valuationReports).omit({ id: true, generatedAt: true });
export const insertValuationAiLogSchema = createInsertSchema(valuationAiLog).omit({ id: true, createdAt: true });
// Types - Valuation (new tables)
export type ValuationGovernanceEntry = typeof valuationGovernance.$inferSelect;
export type InsertValuationGovernance = z.infer<typeof insertValuationGovernanceSchema>;
export type ValuationPdcaEntry = typeof valuationPdca.$inferSelect;
export type InsertValuationPdca = z.infer<typeof insertValuationPdcaSchema>;
export type ValuationSwotEntry = typeof valuationSwot.$inferSelect;
export type InsertValuationSwot = z.infer<typeof insertValuationSwotSchema>;
export type ValuationResultEntry = typeof valuationResults.$inferSelect;
export type InsertValuationResult = z.infer<typeof insertValuationResultSchema>;
export type ValuationAssetEntry = typeof valuationAssets.$inferSelect;
export type InsertValuationAsset = z.infer<typeof insertValuationAssetSchema>;
export type ValuationReportEntry = typeof valuationReports.$inferSelect;
export type InsertValuationReport = z.infer<typeof insertValuationReportSchema>;
export type ValuationAiLogEntry = typeof valuationAiLog.$inferSelect;
export type InsertValuationAiLog = z.infer<typeof insertValuationAiLogSchema>;
// Insert Schemas - Valuation (original tables)
export const insertValuationProjectSchema = createInsertSchema(valuationProjects).omit({ id: true, createdAt: true, updatedAt: true });
export const insertValuationInputSchema = createInsertSchema(valuationInputs).omit({ id: true, createdAt: true, updatedAt: true });
export const insertValuationAssumptionSchema = createInsertSchema(valuationAssumptions).omit({ id: true, createdAt: true });