import { inputText } from './input-text.ts'; import type { AcquisitionCalculation, Acquisition, AcquisitionResult, } from './iva-acquisitions.ts'; import { addFractions, decimalFraction, fraction, fractionLabel, subtractFractions, positiveFraction, type FiscalFraction, } from './fiscal-rational.ts'; export const regularizationSource = 'L2-E: CIVA22(4–8),Lei13/2016 PDF16/p1426(274),acerto anual no último período do ano;CIVA51(1/4),PDF21/p1426(279),rectificação da dedução pelo adquirente até ao fim do período seguinte à recepção. Decreto7/2008,RCIVA27,BR16 PDF5/p103,registo individual de activos: data,IVA suportado e deduzido. Caixas ModeloA12/13;divergência de remissões textuais11/12 conservada,sem alterar P001/05–06.'; type Choice = '' | 'yes' | 'no'; export type RegularizationEvent = { id: string; kind: 'annual' | 'purchase_correction'; period: string; source: AcquisitionCalculation | null; rowIds: string[]; numerator: string; denominator: string; reference: string; receivedDate: string; correctedDeductible: string; reason: string; facts: Record; }; export type RegularizationResult = { id: string; period: string; status: 'determined' | 'indeterminate'; reason: string; delta: FiscalFraction | null; rawRatio?: FiscalFraction; ratio?: FiscalFraction; details: string; source: string; }; type Calculation = { version: 1; calculatedAt: string; key: string; events: RegularizationResult[]; }; export type RegularizationBook = { version: 1; events: RegularizationEvent[]; completePeriods: string[]; result?: Calculation; history?: Calculation[]; }; type FrozenLine = { input: Acquisition; result: AcquisitionResult }; const scope = (draft: Record) => inputText(draft.context_opportunity || 'general'); function store(draft: Record): Record { const s = JSON.parse( inputText(draft.modelo_a_snapshot || '{"version":1,"books":{}}'), ); if (s.version !== 1 || !s.books) throw Error('Origem fiscal não recuperável;conserve os originais.'); return s; } export function validateRegularizationStore(s: Record): void { if (s.regularizations === undefined) return; if ( !s.regularizations || typeof s.regularizations !== 'object' || Array.isArray(s.regularizations) ) throw Error( 'Mapa de regularizações não recuperável;originais preservados.', ); for (const value of Object.values(s.regularizations)) assertRegularizationBook(value as RegularizationBook); } function assertRegularizationBook(value: RegularizationBook): void { const badSource = (s: AcquisitionCalculation | null) => s !== null && (!s || typeof s !== 'object' || !Array.isArray(s.inputs) || !Array.isArray(s.rows) || !Array.isArray(s.periods) || !Number.isInteger(s.revision) || !['currency', 'opportunity', 'calculatedAt', 'sourceKey'].every( (k) => typeof s[k as keyof AcquisitionCalculation] === 'string', )); if ( !value || value.version !== 1 || !Array.isArray(value.events) || !Array.isArray(value.completePeriods) || value.completePeriods.some((p) => typeof p !== 'string') || (value.history !== undefined && !Array.isArray(value.history)) || value.events.some( (e) => !e || !['annual', 'purchase_correction'].includes(e.kind) || ![ 'id', 'period', 'numerator', 'denominator', 'reference', 'receivedDate', 'correctedDeductible', 'reason', ].every( (k) => typeof e[k as keyof RegularizationEvent] === 'string', ) || !Array.isArray(e.rowIds) || e.rowIds.some((id) => typeof id !== 'string') || !e.facts || typeof e.facts !== 'object' || Array.isArray(e.facts) || Object.values(e.facts).some( (v) => !['', 'yes', 'no'].includes(v), ) || badSource(e.source), ) ) throw Error( 'Regularizações não recuperáveis;originais preservados,sem substituir por zero.', ); if ( value.result && (!Array.isArray(value.result.events) || value.result.version !== 1 || typeof value.result.key !== 'string' || typeof value.result.calculatedAt !== 'string' || value.result.events.some( (e) => !e || typeof e.id !== 'string' || typeof e.period !== 'string' || !['determined', 'indeterminate'].includes(e.status) || (e.delta !== null && (!e.delta || !/^-?\d{1,80}$/.test(e.delta.numerator) || !/^\d{1,80}$/.test(e.delta.denominator) || BigInt(e.delta.denominator) === 0n)), )) ) throw Error( 'Resultado de regularizações não recuperável;originais preservados.', ); } export function regularizationBook( draft: Record, ): RegularizationBook { const value = ( store(draft).regularizations as | Record | undefined )?.[scope(draft)]; if (!value) return { version: 1, events: [], completePeriods: [] }; assertRegularizationBook(value); return value; } export function saveRegularizationBook( draft: Record, book: RegularizationBook, ): string { const s = store(draft); return JSON.stringify({ ...s, regularizations: { ...(s.regularizations as object), [scope(draft)]: book, }, }); } export function regularizationSources( draft: Record, ): AcquisitionCalculation[] { const b = ( store(draft).books as Record< string, { result?: AcquisitionCalculation; history?: AcquisitionCalculation[]; } > )[scope(draft)]; return b ? [...(b.history ?? []), ...(b.result ? [b.result] : [])].filter( (v, i, a) => a.findIndex( (x) => JSON.stringify(x) === JSON.stringify(v), ) === i, ) : []; } export function newRegularization( id: string, kind: RegularizationEvent['kind'], source: AcquisitionCalculation | null, ): RegularizationEvent { return { id, kind, period: source ? source.inputs[0]?.period.slice(0, 4) + '-12' : '', source: source ? structuredClone(source) : null, rowIds: source?.inputs .filter((r) => kind === 'annual' ? r.d?.method === 'pro_rata' : true, ) .map((r) => r.id) ?? [], numerator: '', denominator: '', reference: '', receivedDate: '', correctedDeductible: '', reason: '', facts: {}, }; } function frozenLines(e: RegularizationEvent): FrozenLine[] { return e.rowIds.map((id) => ({ input: e.source!.inputs.find((r) => r.id === id)!, result: e.source!.rows.find((r) => r.id === id)!, })); } function sourceAvailable( draft: Record, e: RegularizationEvent, ): boolean { return ( !!e.source && regularizationSources(draft).some( (s) => JSON.stringify(s) === JSON.stringify(e.source), ) ); } function multiply(a: FiscalFraction, b: FiscalFraction) { return fraction( BigInt(a.numerator) * BigInt(b.numerator), BigInt(a.denominator) * BigInt(b.denominator), ); } const validDate = (s: string) => /^2026-\d{2}-\d{2}$/.test(s) && !Number.isNaN(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; function identity(e: RegularizationEvent, id: string): string { const r = e.source?.inputs.find((r) => r.id === id); return JSON.stringify([ e.kind, r?.supplier.trim().toLowerCase(), r?.documentType, r?.documentRef.trim().toLowerCase(), r?.documentDate, r?.documentLine.trim(), e.kind === 'annual' ? e.period.slice(0, 4) : '', ]); } export function evaluateRegularization( draft: Record, e: RegularizationEvent, ): RegularizationResult { const stop = (reason: string): RegularizationResult => ({ id: e.id, period: e.period, status: 'indeterminate', reason, delta: null, details: 'Histórico preservado;sem lançamento automático.', source: regularizationSource, }); if ( !/^2026-(0[1-9]|1[0-2])$/.test(e.period) || draft.currency !== 'MZN' || e.source?.currency !== 'MZN' || e.source.opportunity !== scope(draft) || !sourceAvailable(draft, e) ) return stop( 'Âmbito2026/MZN e resultado original congelado na mesma oportunidade por identificar;uma edição actual não substitui o resultado original.', ); if (!e.rowIds.length || new Set(e.rowIds).size !== e.rowIds.length) return stop('Seleccione parcelas originais distintas.'); const lines = frozenLines(e); if ( lines.some( (l) => !l.input || !l.result || l.result.deductibleExact === undefined || l.result.eligibility?.status !== 'eligible' || !l.input.documentRef.trim(), ) ) return stop( 'Parcela original sem dedução exacta/elegível/documento;não presumir dedução inicial.', ); if ( !e.reference.trim() || !e.reason.trim() || e.facts.scope !== 'yes' || e.facts.recorded !== 'yes' ) return stop( 'Identifique documento/fonte,motivo,âmbito e montante originalmente deduzido (efectivo ou pressuposto da projecção).', ); let delta: FiscalFraction, details: string, rawRatio: FiscalFraction | undefined, ratio: FiscalFraction | undefined; if (e.kind === 'annual') { if ( !e.period.endsWith('-12') || lines.some( (l) => l.input.period.slice(0, 4) !== e.period.slice(0, 4) || l.input.d?.method !== 'pro_rata' || !l.result.allocationCalculation, ) ) return stop( 'Acerto22(6):apenas pro rata provisório do mesmo ano,no último período;afectação real e18(6) têm regras próprias.', ); if (e.facts.annual !== 'yes' || e.facts.exclusions !== 'yes') return stop( 'Confirme bases efectivas anuais22(4),semIVA,incluindo fora de campo,e exclusões22(5);não usar apenas vendas do mês.', ); if ( !/^\d{1,15}(\.\d{1,6})?$/.test(e.numerator) || !/^\d{1,15}(\.\d{1,6})?$/.test(e.denominator) ) return stop('Bases anuais finais em falta ou inválidas.'); const n = decimalFraction(e.numerator), d = decimalFraction(e.denominator); if (BigInt(d.numerator) === 0n) return stop('Denominador anual zero;sem percentagem por defeito.'); rawRatio = fraction( BigInt(n.numerator) * BigInt(d.denominator), BigInt(n.denominator) * BigInt(d.numerator), ); if (BigInt(rawRatio.numerator) > BigInt(rawRatio.denominator)) return stop('Numerador anual excede denominador.'); ratio = fraction( (BigInt(rawRatio.numerator) * 100n + BigInt(rawRatio.denominator) - 1n) / BigInt(rawRatio.denominator), 100n, ); delta = addFractions( lines.map((l) => subtractFractions( multiply(l.result.allocationCalculation!.before, ratio!), l.result.deductibleExact!, ), ), ); details = lines .map( (l) => `${l.input.documentRef}/linha${l.input.documentLine || 'única'}:IVA elegível antesD${fractionLabel(l.result.allocationCalculation!.before)},deduzido${fractionLabel(l.result.deductibleExact!)},final${fractionLabel(multiply(l.result.allocationCalculation!.before, ratio!))}`, ) .join('; ') + `;quociente final${fractionLabel(rawRatio)};aplicado${fractionLabel(ratio)}`; } else { if (lines.length !== 1 || !validDate(e.receivedDate)) return stop( 'Rectificação51(4):seleccione uma linha e a data de recepção do documento rectificativo.', ); const received = e.receivedDate.slice(0, 7), next = new Date(e.receivedDate + 'T00:00:00Z'); next.setUTCMonth(next.getUTCMonth() + 1, 1); const last = next.toISOString().slice(0, 7); if ( e.receivedDate < lines[0].input.documentDate || e.period < received || e.period > last || e.period < lines[0].input.period ) return stop( 'Período fora da recepção/seguinte51(4),ou anterior à dedução original;sem alteração retroactiva.', ); if ( e.facts.supplierCorrection !== 'yes' || e.facts.rightUnchanged !== 'yes' ) return stop( 'Confirme rectificação pelo fornecedor e manutenção do âmbito/condições da dedução. Mudança de direito exige enquadramento próprio.', ); let corrected: FiscalFraction; try { corrected = decimalFraction(e.correctedDeductible); } catch { return stop( 'Indique a dedução corrigida documentada,sem taxa ou dedução presumida.', ); } if ( BigInt( subtractFractions(corrected, lines[0].result.deductibleExact!) .numerator, ) > 0n ) return stop( 'Este percurso51(4) trata anulação/redução da dedução pelo adquirente;acréscimos ou erros têm fundamento/prazo próprios.', ); delta = subtractFractions(corrected, lines[0].result.deductibleExact!); details = `${lines[0].input.documentRef};deduzido${fractionLabel(lines[0].result.deductibleExact!)};corrigido${fractionLabel(corrected)};recebido${e.receivedDate};limite${last}`; } const sign = BigInt(delta.numerator); return { id: e.id, period: e.period, status: 'determined', delta, ...(rawRatio && ratio ? { rawRatio, ratio } : {}), reason: sign > 0n ? 'A favor do sujeito passivo — campo12' : sign < 0n ? 'A favor do Estado — campo13' : 'Sem diferença;evento conservado,sem nova dedução', details: `${details};revisão original${e.source.revision}/${e.source.calculatedAt};${lines.some((l) => l.input.origin === 'projected') ? 'Pressuposto de simulação;sem declaração fiscal submetida' : 'Dados efectivos declarados;submissão fora do portal'};${e.reference};${e.reason}`, source: regularizationSource, }; } // Change detection only. Keep frozen source values, without embedding them again in every calculation key. function fingerprint(value: unknown): string { const raw = JSON.stringify(value); let hash = 14695981039346656037n; for (let i = 0; i < raw.length; i++) hash = BigInt.asUintN( 64, (hash ^ BigInt(raw.charCodeAt(i))) * 1099511628211n, ); return `${raw.length}:${hash.toString(16)}`; } function key(book: RegularizationBook) { return fingerprint([book.version, book.events, book.completePeriods]); } export function recordRegularizations( draft: Record, book: RegularizationBook, now = new Date().toISOString(), ): RegularizationBook { if (new Set(book.events.map((e) => e.id)).size !== book.events.length) throw Error('Evento repetido;sem dupla regularização.'); const identities = book.events.flatMap((e) => e.rowIds.map((id) => identity(e, id)), ); if (new Set(identities).size !== identities.length) throw Error( 'Mesma aquisição/ano ou documento já incluído noutra regularização deste motivo;consolide o evento original.', ); // Mixing an annual correction and a credit note on the same deduction needs sequencing, not two independent deltas. const seen = new Map(); for (const e of book.events) for (const id of e.rowIds) { const k = identity({ ...e, kind: 'purchase_correction' }, id), prior = seen.get(k); if (prior && prior !== e.kind) throw Error( 'Acerto anual e documento rectificativo sobre a mesma parcela exigem reconciliação sequencial;sem dupla correcção.', ); seen.set(k, e.kind); } const result: Calculation = { version: 1, calculatedAt: now, key: key(book), events: book.events.map((e) => evaluateRegularization(draft, e)), }; return { ...book, ...(book.result ? { history: [ ...(book.history ?? []), structuredClone(book.result), ], } : {}), result, }; } export function regularizationsCurrent( draft: Record, b: RegularizationBook, ): boolean { return ( !!b.result && b.result.version === 1 && b.result.key === key(b) && b.result.events.length === b.events.length && b.events.every( (e) => sourceAvailable(draft, e) && b.result!.events.filter( (r) => r.id === e.id && r.period === e.period, ).length === 1, ) ); } export function regularizationFields( draft: Record, period: string, currentRows?: Acquisition[], ): { fields: Record; reasons: string[] } | null { const b = regularizationBook(draft); if ( !b.events.some((e) => e.period === period) && !b.completePeriods.includes(period) ) return null; const events = b.result?.events.filter((e) => e.period === period) ?? []; if ( currentRows && b.events.some( (e) => e.period === period && e.source && frozenLines(e).some( (l) => l.input && currentRows.some( (r) => r.id === l.input.id && JSON.stringify(r) !== JSON.stringify(l.input), ), ), ) ) return { fields: { '12': null, '13': null }, reasons: [ 'A aquisição actual difere da parcela congelada usada emE;reconcilie a diferença antes de somar a regularização,sem recalcular o histórico.', ], }; if ( !b.completePeriods.includes(period) || !regularizationsCurrent(draft, b) || events.some((e) => !e.delta) ) return { fields: { '12': null, '13': null }, reasons: [ 'RegularizaçõesE:complete/reconcilie o período e guarde o cálculo correspondente;sem zero presumido nem uso de resultado histórico.', ], }; return { fields: { '12': addFractions(events.map((e) => positiveFraction(e.delta!))), '13': addFractions( events.map((e) => positiveFraction( fraction( -BigInt(e.delta!.numerator), BigInt(e.delta!.denominator), ), ), ), ), }, reasons: [ 'E:apenas eventos documentados declarados completos;12/13 separados,sem recalcular aquisições históricas.', ], }; } export function regularizationSourceKey( draft: Record, ): string { const b = regularizationBook(draft); const reduced = ( store(draft).regulatedReducedBases as | Record | undefined )?.[scope(draft)]?.rows ?? []; return b.events.length || b.completePeriods.length ? fingerprint([b, regularizationsCurrent(draft, b), reduced]) : ''; } export function regularizationReportRows( draft: Record, ): Array<[string, string, string, string]> { const b = regularizationBook(draft); if (!b.events.length) return []; return [ [ 'IVA mensal', 'RegularizaçõesE', regularizationsCurrent(draft, b) ? 'Resultado correspondente às entradas' : 'Histórico preservado;rever entradas/origem', regularizationSource, ], ...(b.result?.events ?? []).map( (e) => [ 'IVA mensal', `Regularização ${e.id} — ${e.period}`, `${e.status === 'determined' ? e.reason : 'Indeterminado: ' + e.reason};delta${e.delta ? fractionLabel(e.delta) : 'indeterminado'}`, e.details + ';' + e.source, ] as [string, string, string, string], ), ]; } export function frozenAssetRows( draft: Record, ): Array<[string, string, string, string]> { const sources = regularizationSources(draft), seen = new Set(), out: Array<[string, string, string, string]> = []; for (const s of [...sources].reverse()) for (const r of s.inputs.filter( (r) => r.field === '08' || r.investmentAsset === true, )) { const k = JSON.stringify([ r.supplier, r.documentRef, r.documentDate, r.documentLine, ]); if (seen.has(k)) continue; seen.add(k); const v = s.rows.find((v) => v.id === r.id); out.push([ 'IVA mensal', `Activo — ${r.description || r.id}`, `${r.documentRef};linha${r.documentLine || 'única'};aquisição${r.documentDate};IVA suportado${v?.vat ?? 'indeterminado'};IVA deduzido${v?.deductibleExact ? fractionLabel(v.deductibleExact) : (v?.deductible ?? 'indeterminado')}`, `RCIVA27,Decreto7/2008 PDF5/p103;${s.currency};revisão${s.revision}/${s.calculatedAt};${r.origin === 'projected' ? 'Pressuposto de simulação' : 'Dados efectivos declarados'};sem amortização fiscal ou regularização plurianual presumidas.`, ]); } return out; }