52 lines
4.5 KiB
TypeScript
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.';
|