import type { Acquisition, AcquisitionResult } from './iva-acquisitions.ts'; import { acquisitionScope } from './iva-acquisitions.ts'; import { evaluateAcquisitionBC } from './iva-acquisition-bc.ts'; import { reducedBaseBook, reducedBaseCurrent, reducedBaseTreatments, evaluateReducedBase, } from './iva-reduced-bases.ts'; import { decimalFraction, fraction, fractionLabel, fractionPresentation, subtractFractions, type FiscalFraction, } from './fiscal-rational.ts'; export const acquisitionAllocationVersion = 'acquisition-allocation/1'; export const acquisitionAllocationSource = 'L2-D: CIVA22(1–8),Lei13/2016 PDF16/p1426(274);Lei10/2025,18(6),PDF13/p2180(13).22(4)numerador anual semIVA com direito18/19;denominador anual semIVA de todas operações,incluindo fora de campo;22(5)exclui vendas de activo usado e operações imobiliárias/financeiras acessórias.22(8)quociente à centésima imediatamente superior.22(2)afectação real com comunicação prévia;22(6–7)provisório,regularização anual emE. Não substitui condições18–20 ou calcula restituiçãoL3.'; type Choice = '' | 'yes' | 'no'; export type AcquisitionAllocation = { version: 1; method: '' | 'actual' | 'pro_rata' | 'reduced'; numerator: string; denominator: string; unit: string; referenceYear: string; basis: '' | 'prior_year' | 'initial_changed'; scopeRef: string; source: string; communicationRef: string; declarationRef: string; actualRequired: Choice; noExcludedPayments: Choice; facts: Record; reducedId: string; reducedFingerprint: string; }; export function newAcquisitionAllocation( period: string, ): AcquisitionAllocation { return { version: 1, method: '', numerator: '', denominator: '', unit: '', referenceYear: String(Number(period.slice(0, 4)) - 1), basis: '', scopeRef: '', source: '', communicationRef: '', declarationRef: '', actualRequired: '', noExcludedPayments: '', facts: {}, reducedId: '', reducedFingerprint: '', }; } export function validAcquisitionAllocation( v: unknown, ): v is AcquisitionAllocation { if (!v || typeof v !== 'object' || Array.isArray(v)) return false; const d = v as AcquisitionAllocation; return ( d.version === 1 && ['', 'actual', 'pro_rata', 'reduced'].includes(d.method) && ['', 'prior_year', 'initial_changed'].includes(d.basis) && [ 'numerator', 'denominator', 'unit', 'referenceYear', 'scopeRef', 'source', 'communicationRef', 'declarationRef', 'reducedId', 'reducedFingerprint', ].every( (k) => typeof d[k as keyof AcquisitionAllocation] === 'string', ) && [d.actualRequired, d.noExcludedPayments].every((v) => ['', 'yes', 'no'].includes(v), ) && !!d.facts && typeof d.facts === 'object' && !Array.isArray(d.facts) && Object.values(d.facts).every((v) => ['', 'yes', 'no'].includes(v)) ); } export function allocationReducedChoices(draft: Record) { return reducedBaseBook(draft).rows.map((row) => ({ id: row.id, label: `${row.description || row.id} — ${row.period} — ${reducedBaseTreatments.find((t) => t.id === row.treatment)?.label ?? row.treatment}`, row, })); } export function adoptAllocationReduced( draft: Record, d: AcquisitionAllocation, id: string, ): AcquisitionAllocation { const source = allocationReducedChoices(draft).find((r) => r.id === id); if (!source) throw Error( 'Base reduzida de origem indisponível;originais preservados.', ); return { ...d, reducedId: id, reducedFingerprint: JSON.stringify(source.row), facts: { ...d.facts, scopeConfirmed: '' }, }; } export function allocationSourceKey( draft: Record, rows: Acquisition[], ): string { return rows.some((r) => r.d) ? JSON.stringify({ version: acquisitionAllocationVersion, reducedOperations: reducedBaseBook(draft).rows, sources: rows .filter((r) => r.d?.method === 'reduced') .map((r) => ({ row: allocationReducedChoices(draft).find( (s) => s.id === r.d?.reducedId, )?.row ?? null, current: reducedBaseCurrent( draft, reducedBaseBook(draft), ), })), }) : ''; } export function allocationHasUnconsolidatedOutputs( draft: Record, period: string, ): boolean { return reducedBaseBook(draft).rows.some((r) => r.period === period); } type Decision = { ratio: FiscalFraction | null; rawRatio?: FiscalFraction; reason: string; source: string; }; export function acquisitionAllocationDecision( draft: Record | undefined, row: Acquisition, ): Decision { const d = row.d; const stop = (reason: string): Decision => ({ ratio: null, reason, source: acquisitionAllocationSource, }); if (!d || !validAcquisitionAllocation(d)) return stop( 'Método/parâmetrosD não recuperáveis;sem percentagem por defeito.', ); if ( !draft || draft.currency !== 'MZN' || row.sourceCurrency !== 'MZN' || !/^2026-(0[1-9]|1[0-2])$/.test(row.period) ) return stop('ÂmbitoD2026/MZN ou contexto em falta.'); if ( !d.scopeRef.trim() || !d.source.trim() || d.facts.scopeConfirmed !== 'yes' ) return stop( 'Âmbito da aquisição/actividade,fonte e correspondência por confirmar;sem inferir da oportunidade.', ); if (d.method === 'reduced') { if (row.allocation !== 'taxable_only') return stop( '18(6) neste percurso exige afectação exclusiva à operação de base reduzida;combinação com afectação mista ainda não demonstrada.', ); const b = reducedBaseBook(draft), source = b.rows.find((r) => r.id === d.reducedId); if ( !source || !reducedBaseCurrent(draft, b) || JSON.stringify(source) !== d.reducedFingerprint ) return stop( 'Origem de base reduzida alterada/incompleta:rever e adoptar expressamente;histórico conservado.', ); const result = evaluateReducedBase(source, draft.currency), rule = reducedBaseTreatments.find((r) => r.id === source.treatment); if (!rule || result.status !== 'determined') return stop( 'Condições da base reduzida de origem ainda indeterminadas;não basta escolher percentagem.', ); return { ratio: fraction(BigInt(rule.percent), 100n), reason: `18(6):proporção específica${rule.percent}% reutilizada de${rule.branch}/${source.id};aplicada uma vez ao IVA elegível,sem converter percentagem de base em taxaIVA.`, source: acquisitionAllocationSource + ';' + b.result!.source, }; } if (row.allocation !== 'mixed') return stop( 'Método22 exige afectação mista expressamente identificada;conserve a afectação exclusiva quando aplicável.', ); if (d.facts.rightBasis !== 'yes') return stop( 'Identifique a parte/operações com direito18/19;isenção ou sector não estabelecem o direito.', ); if ( !/^\d{1,15}(\.\d{1,6})?$/.test(d.numerator) || !/^\d{1,15}(\.\d{1,6})?$/.test(d.denominator) ) return stop( 'Numerador/denominador em falta ou inválidos;sem percentagem presumida.', ); const n = decimalFraction(d.numerator), den = decimalFraction(d.denominator); if (BigInt(den.numerator) === 0n) return stop('Denominador zero:não existe quociente aplicável.'); const rawRatio = fraction( BigInt(n.numerator) * BigInt(den.denominator), BigInt(n.denominator) * BigInt(den.numerator), ); if (BigInt(rawRatio.numerator) > BigInt(rawRatio.denominator)) return stop( 'Numerador excede denominador;resolver bases sem limitar silenciosamente a100%.', ); if (d.method === 'actual') { if ( !d.unit.trim() || d.facts.actualScope !== 'yes' || d.facts.communication !== 'yes' || !d.communicationRef.trim() || d.facts.conditionsMet !== 'yes' ) return stop( 'Afectação real22(2):critério/unidade,repartição efectiva,comunicação prévia e condições concretamente aplicáveis por completar;sem criar aprovação adicional.', ); return { ratio: rawRatio, rawRatio, reason: `Afectação real22(2):${d.numerator}/${d.denominator} ${d.unit};critério e comunicação${d.communicationRef}. Fracção real exacta,sem arredondamento de pro rata.`, source: acquisitionAllocationSource, }; } if (d.method !== 'pro_rata') return stop( 'Escolha afectação real,pro rata provisório ou proporção específica18(6).', ); if (d.actualRequired === 'yes') return stop( 'Está identificada imposição de afectação real22(3);não aplicar pro rata em substituição.', ); if (d.facts.annualScope !== 'yes' || d.facts.netExclusions !== 'yes') return stop( 'Confirmar totais anuais semIVA e âmbito22(4–5):inclui fora de campo;exclui vendas de activo usado e imobiliárias/financeiras acessórias. Não usar só vendas de um mês como total anual.', ); const year = Number(row.period.slice(0, 4)); if (d.basis === 'prior_year') { if (d.referenceYear !== String(year - 1)) return stop( 'Pro rata provisório22(6) usa operações do ano anterior ao exercício;sem substituir pelo mês corrente.', ); } else if (d.basis === 'initial_changed') { if ( d.referenceYear !== String(year) || d.facts.initialChanged !== 'yes' || !d.declarationRef.trim() ) return stop( 'Estimativa22(7):início/alteração substancial,ano e referência da declaração de início/alterações por identificar.', ); } else return stop( 'Indique base provisória22(6) ou estimativa expressa22(7);regularização final22(6) segueE.', ); const numerator = BigInt(rawRatio.numerator), denominator = BigInt(rawRatio.denominator); const hundredths = (numerator * 100n + denominator - 1n) / denominator; return { ratio: fraction(hundredths, 100n), rawRatio, reason: `Pro rata provisório22(4/6/7/8):quociente${fractionLabel(rawRatio)}→${hundredths}% (quociente à centésima superior). Base anual${d.referenceYear},${d.basis === 'prior_year' ? 'ano anterior' : 'estimativa declarada'}. Acerto anual emE,sem recalcular históricos.`, source: acquisitionAllocationSource, }; } export function evaluateAcquisitionAllocation( row: Acquisition, currency: string, registered: Choice, draft: Record | undefined, raw: AcquisitionResult, ): AcquisitionResult { const decision = acquisitionAllocationDecision(draft, row), d = row.d!; const unknown = (reason: string): AcquisitionResult => ({ ...raw, deductible: null, reason, eligibility: { status: 'indeterminate', reason, source: decision.source, }, }); if (!decision.ratio) return unknown(decision.reason); if (!row.bc) return unknown( 'Complete a natureza/condiçõesB/C da aquisição antes da proporção;sem isenção de condições documentais.', ); const base = evaluateAcquisitionBC(row, currency, registered, draft, raw, { right: { status: 'eligible', reason: decision.reason, source: decision.source, scopeId: draft ? acquisitionScope(draft) : undefined, }, noExcludedPayments: d.noExcludedPayments, }); if (base.deductible === null || base.eligibility?.status !== 'eligible') return base; const before = base.deductibleExact ?? decimalFraction(base.deductible); const exact = fraction( BigInt(before.numerator) * BigInt(decision.ratio.numerator), BigInt(before.denominator) * BigInt(decision.ratio.denominator), ); const reason = base.reason + ' ' + decision.reason; return { ...base, deductible: fractionPresentation(exact).decimal, deductibleExact: exact, nonDeductibleExact: subtractFractions( decimalFraction(base.vat!), exact, ), reason, eligibility: { ...base.eligibility, status: 'eligible', reason }, allocationCalculation: { version: acquisitionAllocationVersion, ratio: decision.ratio, rawRatio: decision.rawRatio ?? decision.ratio, before, source: decision.source, }, }; }