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

161 lines
6.3 KiB
TypeScript

import { inputText } from './input-text.ts';
import {
monthlyProjectedSales,
restoreBusinessPlanSnapshot,
} from './business-financial-plan.ts';
import {
cashflowCurrency,
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(
inputText(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 ${inputText(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(
inputText(draft.cashflow_snapshot),
);
if (!saved)
throw new Error('Não foi possível recuperar a receita projectada.');
const origin = cashflowCurrency(saved, inputText(draft.currency ?? ''));
if (!origin.matchesProject) throw new Error(origin.notice);
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: inputText(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;
}
/** Read an admitted execution through the existing ownership-protected endpoint; never create one. */
export async function recoverMonthlyIvaRecord(
record: MonthlyIvaRecord,
request: typeof fetch = fetch,
): Promise<MonthlyIvaRecord> {
const response = await request(
`/api/v1/simulations/${encodeURIComponent(record.execution.id)}`,
{ credentials: 'same-origin', headers: { Accept: 'application/json' } },
);
const value: unknown = await response.json();
if (
!response.ok ||
!isApiSimulationReadResponseV1(value) ||
value.data.id !== record.execution.id
)
throw new Error(
'Não foi possível recuperar a execução mensal guardada. Os dados foram preservados e não foi criada uma execução alternativa.',
);
return { input: record.input, execution: value.data };
}
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.';