Files
Investidor-V2/resources/js/lib/financial-fiscal-bridge.ts
T
nala e65cb26901
tests / ci (push) Canceled after 0s
varias alteracoes
2026-10-06 14:50:12 +02:00

284 lines
10 KiB
TypeScript

import type { forecastBusinessPlan } from '@/lib/business-financial-plan';
type Forecast = ReturnType<typeof forecastBusinessPlan>;
export type FinancialFiscalLine = {
code:
| 'revenue'
| 'cost_of_sales'
| 'operating_expenses'
| 'personnel'
| 'depreciation'
| 'interest_expense';
origin: string;
accountingEffect: 'income' | 'expense';
taxTreatment: 'unclassified';
total: string;
monthlyAmounts: Array<{ period: number; amount: string }>;
};
function cents(value: string): bigint {
const match = /^(-?)(\d+)(?:\.(\d{1,2}))?$/.exec(value);
if (!match)
throw new Error('O resultado financeiro contém um montante inválido.');
const amount =
BigInt(match[2]) * 100n +
BigInt((match[3] ?? '').padEnd(2, '0') || '0');
return match[1] ? -amount : amount;
}
function formatCents(value: bigint): string {
const absolute = value < 0n ? -value : value;
return `${value < 0n ? '-' : ''}${absolute / 100n}.${String(absolute % 100n).padStart(2, '0')}`;
}
function assertForecastReconciles(forecast: Forecast): void {
if (forecast.rows.length === 0)
throw new Error(
'A previsão financeira não contém períodos para mapear.',
);
let accountingProfit = 0n;
let totalRevenue = 0n;
let totalCollections = 0n;
let cumulativeNetCashFlow = 0n;
let previousClosingCash = cents(forecast.openingCashAfterInitialUses);
let previousOpeningReceivables = cents(forecast.openingReceivables);
let totalOpeningReceivablesCollections = 0n;
let previousPeriod = 0;
for (const row of forecast.rows) {
if (!Number.isSafeInteger(row.month) || row.month <= previousPeriod)
throw new Error(
'Os períodos da previsão financeira não estão em ordem.',
);
previousPeriod = row.month;
const revenue = cents(String(row.revenue));
totalRevenue += revenue;
accountingProfit += revenue;
for (const field of [
'variableCost',
'operatingExpenses',
'payrollExpense',
'depreciation',
'interestExpense',
] as const) {
accountingProfit -= cents(String(row[field]));
}
const salesCollections =
row.salesCollections === null
? 0n
: cents(String(row.salesCollections));
const openingReceivablesCollections = cents(
String(row.openingReceivablesCollections),
);
const openingReceivablesBalance = cents(
String(row.openingReceivablesBalance),
);
if (
openingReceivablesBalance !==
previousOpeningReceivables - openingReceivablesCollections
)
throw new Error(
'O saldo de recebíveis de abertura não reconcilia com as cobranças do período.',
);
previousOpeningReceivables = openingReceivablesBalance;
totalOpeningReceivablesCollections += openingReceivablesCollections;
const otherCollections =
row.otherCollections === null
? cents(String(row.cashCollections)) -
salesCollections -
openingReceivablesCollections
: cents(String(row.otherCollections));
if (otherCollections < 0n)
throw new Error(
'A previsão financeira contém cobranças reconciliadas inválidas.',
);
const collections = cents(String(row.cashCollections));
if (
collections !==
salesCollections + openingReceivablesCollections + otherCollections
)
throw new Error(
'As cobranças mensais não reconciliam com as suas origens.',
);
totalCollections += collections;
const netCashFlow =
collections -
cents(String(row.variableCostPayments)) -
cents(String(row.operatingPayments)) -
cents(String(row.payrollPayments)) -
cents(String(row.interestPayment)) -
cents(String(row.principalRepayment));
if (netCashFlow !== cents(String(row.netCashFlow)))
throw new Error(
'O fluxo líquido mensal não reconcilia com recebimentos e pagamentos.',
);
const closingCash = cents(String(row.closingCash));
if (closingCash !== previousClosingCash + netCashFlow)
throw new Error(
'O saldo de caixa mensal não reconcilia com o período anterior.',
);
previousClosingCash = closingCash;
cumulativeNetCashFlow += netCashFlow;
}
if (accountingProfit !== cents(forecast.totalProfitBeforeTax))
throw new Error(
'O resultado contabilístico não reconcilia com as linhas mapeadas.',
);
if (totalRevenue !== cents(forecast.totalRevenue))
throw new Error(
'A facturação total não reconcilia com os períodos mapeados.',
);
if (totalCollections !== cents(forecast.totalCashCollections))
throw new Error(
'As cobranças totais não reconciliam com o calendário de caixa.',
);
if (
totalOpeningReceivablesCollections !==
cents(forecast.totalOpeningReceivablesCollections) ||
previousOpeningReceivables !== cents(forecast.openingReceivablesAtEnd)
)
throw new Error(
'Os recebíveis de abertura não reconciliam com a previsão final.',
);
if (
previousClosingCash !== cents(forecast.endingCash) ||
cumulativeNetCashFlow !==
cents(forecast.endingCash) -
cents(forecast.openingCashAfterInitialUses)
)
throw new Error(
'O saldo final de caixa não reconcilia com o calendário mensal.',
);
}
/** Maps accounting figures into a reviewable tax-input draft without deciding tax treatment. */
export function buildFinancialFiscalBridge(forecast: Forecast) {
assertForecastReconciles(forecast);
const definitions: Array<
Pick<
FinancialFiscalLine,
'code' | 'origin' | 'accountingEffect' | 'taxTreatment'
> & { field: keyof Forecast['rows'][number] }
> = [
{
code: 'revenue',
origin: 'Produtos/serviços facturados',
accountingEffect: 'income',
taxTreatment: 'unclassified',
field: 'revenue',
},
{
code: 'cost_of_sales',
origin: 'Custo variável dos produtos',
accountingEffect: 'expense',
taxTreatment: 'unclassified',
field: 'variableCost',
},
{
code: 'operating_expenses',
origin: 'Despesas operacionais reconhecidas',
accountingEffect: 'expense',
taxTreatment: 'unclassified',
field: 'operatingExpenses',
},
{
code: 'personnel',
origin: 'Despesas com pessoal reconhecidas',
accountingEffect: 'expense',
taxTreatment: 'unclassified',
field: 'payrollExpense',
},
{
code: 'depreciation',
origin: 'Depreciação contabilística',
accountingEffect: 'expense',
taxTreatment: 'unclassified',
field: 'depreciation',
},
{
code: 'interest_expense',
origin: 'Juros reconhecidos',
accountingEffect: 'expense',
taxTreatment: 'unclassified',
field: 'interestExpense',
},
];
const accountingLines: FinancialFiscalLine[] = definitions.map(
({ field, ...definition }) => {
const monthlyAmounts = forecast.rows.map((row) => ({
period: row.month,
amount: String(row[field]),
}));
return {
...definition,
total: formatCents(
monthlyAmounts.reduce(
(sum, row) => sum + cents(row.amount),
0n,
),
),
monthlyAmounts,
};
},
);
return {
status: 'incomplete' as const,
currency: forecast.currency,
accountingProfitBeforeTax: forecast.totalProfitBeforeTax,
taxableBase: {
status: 'indeterminate' as const,
amount: null,
missingInputs: [
'expense_deductibility_classification',
'approved_tax_adjustments',
'applicable_tax_period',
'approved_effective_rule',
],
},
accountingLines,
cashSchedule: forecast.rows.map((row) => ({
period: row.month,
collectionMode:
row.salesCollections === null
? ('manual' as const)
: ('scheduled' as const),
salesCollections: row.salesCollections,
openingReceivablesCollections: row.openingReceivablesCollections,
openingReceivablesBalance: row.openingReceivablesBalance,
otherCollections:
row.otherCollections ??
formatCents(
cents(row.cashCollections) -
(row.salesCollections === null
? 0n
: cents(row.salesCollections)) -
cents(row.openingReceivablesCollections),
),
collections: row.cashCollections,
collectionsTotal: row.cashCollections,
salesReceivablesBalance: row.salesReceivablesClosing,
supplierPayments: row.variableCostPayments,
operatingPayments: row.operatingPayments,
personnelPayments: row.payrollPayments,
interestPayments: row.interestPayment,
principalRepayments: row.principalRepayment,
netCashFlow: row.netCashFlow,
closingCash: row.closingCash,
})),
notes: [
'A classificação fiscal das linhas contabilísticas está pendente; este mapeamento não afirma dedutibilidade.',
'Cobranças e pagamentos são fluxos de caixa separados das receitas e despesas reconhecidas.',
'Reembolsos de capital aparecem apenas no calendário de caixa e não como despesa contabilística.',
'Nenhum imposto, contribuição, retenção ou pagamento fiscal é somado como zero ou estimado nesta ponte.',
],
};
}