Files
Investidor-V2/resources/js/lib/monthly-iva.ts
T

52 lines
4.5 KiB
TypeScript

import { monthlyProjectedSales, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
import { restoreCashflowDraftSnapshot } from './monthly-cashflow.ts';
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from './simulation-contract.ts';
export type MonthlyIvaInput = {
scenario_kind: 'fiscal_rule_published';
monthly_sales: Array<{ date: string; amount: string | null }>;
sales_vat_basis: 'net' | 'included' | 'unknown';
currency: string;
scope_declared: boolean;
};
export type MonthlyIvaSettings = { start: string; basis: MonthlyIvaInput['sales_vat_basis']; declared: boolean; auto: boolean };
export type MonthlyIvaRecord = { input: MonthlyIvaInput; execution: ApiSimulationExecutionV1 };
export function readMonthlyIvaSettings(raw?: string): MonthlyIvaSettings {
try { const value = JSON.parse(raw ?? '');
if (typeof value.start === 'string' && ['net', 'included', 'unknown'].includes(value.basis) && typeof value.declared === 'boolean' && typeof value.auto === 'boolean') return value;
} catch { /* An invalid setting is not an invented business assumption. */ }
return {start: '', basis: 'unknown', declared: false, auto: false};
}
export function monthlySalesFromDraft(draft: Record<string, unknown>): { amounts: Array<string | null>; source: string } {
if (draft.business_plan_snapshot) {
const plan = restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot));
if (!plan) throw new Error('Não foi possível recuperar a projecção de vendas. Os dados guardados não foram substituídos.');
if (draft.currency && plan.currency && draft.currency !== plan.currency) throw new Error(`A projecção de vendas está em ${plan.currency}, mas o projecto está em ${String(draft.currency)}. Reveja a moeda da projecção; os valores não foram convertidos nem reclassificados.`);
return {amounts: monthlyProjectedSales(plan), source: 'Vendas por produto e mês do plano empresarial; mesmos pressupostos e representação monetária da projecção de vendas.'};
}
if (draft.cashflow_snapshot) {
const saved = restoreCashflowDraftSnapshot(String(draft.cashflow_snapshot));
if (!saved) throw new Error('Não foi possível recuperar a receita projectada.');
return {amounts: Array.from({length: 12}, () => saved.inputs.monthlyRevenue.trim() === '' ? null : saved.inputs.monthlyRevenue), source: 'Receita mensal constante já declarada na projecção de tesouraria; confirme que corresponde a vendas tributáveis.'};
}
return {amounts: Array.from({length: 12}, () => null), source: 'Sem projecção de vendas guardada. Prepare as vendas no plano empresarial; não é necessário voltar a introduzi-las aqui.'};
}
export function monthlyIvaInput(draft: Record<string, unknown>, settings: MonthlyIvaSettings): MonthlyIvaInput {
const sales = monthlySalesFromDraft(draft);
if (!/^2026-(0[1-9]|1[0-2])$/.test(settings.start)) throw new Error('Indique o primeiro mês real da projecção. Este lote abrange apenas 2026.');
const start = Number(settings.start.slice(5));
if (sales.amounts.length > 12 || start + sales.amounts.length > 13) throw new Error('O horizonte ultrapassa a cobertura de 2026. Esta versão não calcula os meses fora desse período.');
return {scenario_kind: 'fiscal_rule_published', monthly_sales: sales.amounts.map((amount, index) => ({date: `2026-${String(start + index).padStart(2, '0')}-01`, amount})),
sales_vat_basis: settings.basis, currency: String(draft.currency ?? ''), scope_declared: settings.declared};
}
export function monthlyIvaKey(input: Omit<MonthlyIvaInput, 'scenario_kind'>): string {
return JSON.stringify([input.currency, input.sales_vat_basis, input.scope_declared, input.monthly_sales.map((row) => [row.date, row.amount])]);
}
export function readMonthlyIvaRecord(raw?: string): MonthlyIvaRecord | null {
try { const value = JSON.parse(raw ?? '');
if (value?.input?.scenario_kind === 'fiscal_rule_published' && Array.isArray(value.input.monthly_sales) && isApiSimulationReadResponseV1({data: value.execution})) return value;
} catch { /* Keep the stored value intact; no fallback fiscal result. */ }
return null;
}
export const monthlyIvaCashTreatment = 'IVA apresentado separadamente: não é somado novamente aos recebimentos declarados nem deduzido como custo ou imposto final a pagar. A tesouraria conserva os recebimentos e pagamentos introduzidos, antes de uma liquidação fiscal não apurada; não se presume cobrança integral das vendas no mesmo mês.';