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): { amounts: Array; 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, 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, ): 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 { 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.';