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:
parent
f36238215a
commit
44dacedd90
File diff suppressed because it is too large
Load Diff
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
165
shared/schema.ts
165
shared/schema.ts
|
|
@ -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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue