161 lines
6.3 KiB
TypeScript
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.';
|