import { inputText } from './input-text.ts'; import { closureBook, closureSources, closureFingerprint, type ClosureSource, type ClosurePayment, } from './iva-closure.ts'; import { addFractions, subtractFractions, positiveFraction, decimalFraction, fraction, fractionPresentation, type FiscalFraction, } from './fiscal-rational.ts'; export const substitutionSource = 'L3-C: VAT Return.pdf SHA2569c0b99a8,p2/quadro9;RCIVA15,Decreto7/2008 PDF3/p101.21=(17actual−18actual)−(17anterior−18anterior);devedor→22;credor só utilizável em declaração posterior em prazo após comunicação20. Sem repetir19/20 ou pagamentos anteriores. Exemplar recebido assinala substituiçãoModeloA;RCIVA15 refereModeloB:correspondência de formulário/submissão não automatizada.'; export type PriorDeclaration = { kind: 'AB' | 'C'; id: string; period: string; entityRef: string; origin: 'projected' | 'effective'; calculatedAt: string; declarationDate: string; declarationReference: string; fingerprint: string; debit: FiscalFraction; credit: FiscalFraction; paid: FiscalFraction; outstanding: FiscalFraction; carry: FiscalFraction; paymentRefs: string[]; }; export type SubstitutionInput = { id: string; prior: PriorDeclaration | null; source: ClosureSource | null; priorReference: string; priorDate: string; date: string; reference: string; reason: string; stage: 'preparation' | 'presented'; interest: '' | 'none' | 'documented'; interestAmount: string; interestRef: string; payments: ClosurePayment[]; }; export type SubstitutionResult = { id: string; status: 'determined' | 'indeterminate'; reason: string; inputKey: string; fields: Record; pendingCredit: FiscalFraction | null; paid: FiscalFraction | null; outstanding: FiscalFraction | null; overpayment: FiscalFraction | null; totalPaid: FiscalFraction | null; totalOutstanding: FiscalFraction | null; source: string; }; export type SubstitutionCalculation = { calculatedAt: string; key: string; inputs: SubstitutionInput[]; rows: SubstitutionResult[]; }; export type SubstitutionBook = { version: 1; rows: SubstitutionInput[]; result?: SubstitutionCalculation; history?: SubstitutionCalculation[]; }; const zero = () => fraction(0n), scope = (d: Record) => inputText(d.context_opportunity || 'general'), norm = (s: string) => s.trim().toLocaleLowerCase(); const date = (v: string) => /^20\d{2}-(0[1-9]|1[0-2])-\d{2}$/.test(v) && !Number.isNaN(Date.parse(v)) && new Date(v).toISOString().slice(0, 10) === v; function store(d: Record): Record { const s = JSON.parse( inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'), ); if (s.version !== 1 || !s.books) throw Error('Livros fiscais não recuperáveis;originais preservados.'); return s; } function maps(d: Record): Record { const s = store(d).ivaSubstitutions; if (s === undefined) return {}; if (!s || typeof s !== 'object' || Array.isArray(s)) throw Error('Substituições não recuperáveis;originais preservados.'); return s as Record; } function validate(b: SubstitutionBook): void { if ( !b || b.version !== 1 || !Array.isArray(b.rows) || (b.history !== undefined && !Array.isArray(b.history)) || (b.result !== undefined && (!Array.isArray(b.result.rows) || !Array.isArray(b.result.inputs))) ) throw Error('Livro de substituições não recuperável;sem o substituir.'); for (const r of b.rows) if ( !r || ![ 'id', 'priorReference', 'priorDate', 'date', 'reference', 'reason', 'interestAmount', 'interestRef', ].every( (k) => typeof r[k as keyof SubstitutionInput] === 'string', ) || !['preparation', 'presented'].includes(r.stage) || !['', 'none', 'documented'].includes(r.interest) || !Array.isArray(r.payments) || r.payments.some( (p) => !p || !['id', 'reference', 'date', 'amount'].every( (k) => typeof p[k as keyof ClosurePayment] === 'string', ), ) ) throw Error( 'Entradas da substituição não recuperáveis;originais preservados.', ); } export function substitutionBook(d: Record): SubstitutionBook { const b = maps(d)[scope(d)] ?? { version: 1, rows: [] }; validate(b); return b; } export function saveSubstitutionBook( d: Record, b: SubstitutionBook, ): string { validate(b); const s = store(d); return JSON.stringify({ ...s, ivaSubstitutions: { ...maps(d), [scope(d)]: b }, }); } export function newSubstitution(id: string): SubstitutionInput { return { id, prior: null, source: null, priorReference: '', priorDate: '', date: '', reference: '', reason: '', stage: 'preparation', interest: '', interestAmount: '', interestRef: '', payments: [], }; } export function previousDeclarations( d: Record, b = substitutionBook(d), ): PriorDeclaration[] { const ab = closureBook(d), out: PriorDeclaration[] = []; for (const calc of [ ...(ab.history ?? []), ...(ab.result ? [ab.result] : []), ]) for (const r of calc.rows) { const input = calc.inputs.find((i) => i.id === r.id); if ( r.status !== 'determined' || !input || !r.fields['17'] || !r.fields['18'] || !r.fields['26'] || !r.paid || !r.outstanding ) continue; out.push({ kind: 'AB', id: r.id, period: r.period, entityRef: r.entityRef, origin: r.origin, calculatedAt: calc.calculatedAt, declarationDate: input.submissionDate, declarationReference: '', fingerprint: closureFingerprint([input, r]), debit: r.fields['17'], credit: r.fields['18'], paid: r.paid, outstanding: r.outstanding, carry: r.fields['26'], paymentRefs: input.payments.map((p) => p.reference), }); } for (const calc of [...(b.history ?? []), ...(b.result ? [b.result] : [])]) for (const r of calc.rows) { const input = calc.inputs.find((i) => i.id === r.id); if ( r.status !== 'determined' || !input?.prior || !input.source || !r.totalPaid || !r.totalOutstanding ) continue; out.push({ kind: 'C', id: r.id, period: input.prior.period, entityRef: input.prior.entityRef, origin: input.prior.origin, calculatedAt: calc.calculatedAt, declarationDate: input.date, declarationReference: input.reference, fingerprint: closureFingerprint([input, r]), debit: input.source.debit!, credit: input.source.credit!, paid: r.totalPaid, outstanding: r.totalOutstanding, carry: input.prior.carry, paymentRefs: [ ...input.prior.paymentRefs, ...input.payments.map((p) => p.reference), ], }); } return out.filter( (r, i, a) => a.findIndex( (s) => s.kind === r.kind && s.id === r.id && s.fingerprint === r.fingerprint, ) === i, ); } function available( d: Record, b: SubstitutionBook, r: SubstitutionInput, ) { return [ previousDeclarations(d, b).some( (p) => JSON.stringify(p) === JSON.stringify(r.prior), ), closureSources(d).some( (s) => JSON.stringify(s) === JSON.stringify(r.source), ), ]; } function pastPaymentUses( d: Record, b: SubstitutionBook, ): Array<{ id: string; entity: string; origin: string; reference: string }> { return Object.entries({ ...maps(d), [scope(d)]: b }).flatMap( ([context, book]) => [ ...(book.history ?? []), ...(book.result ? [book.result] : []), ].flatMap((calc) => calc.inputs .filter( (r) => r.prior && (context === scope(d) || r.prior.origin === 'effective') && calc.rows.some( (v) => v.id === r.id && v.status === 'determined' && v.paid, ), ) .flatMap((r) => r.payments.map((p) => ({ id: r.id, entity: norm(r.prior!.entityRef), origin: r.prior!.origin, reference: norm(p.reference), })), ), ), ); } function abPaymentKeys(d: Record): string[] { const ab = store(d).ivaClosures as | Record> | undefined; return Object.entries(ab ?? {}).flatMap(([id, v]) => [ ...v.rows, ...[...(v.history ?? []), ...(v.result ? [v.result] : [])].flatMap( (c) => c.inputs, ), ] .filter((r) => id === scope(d) || r.origin === 'effective') .flatMap((r) => r.payments.map((p) => JSON.stringify([ norm(r.entityRef), r.origin, norm(p.reference), ]), ), ), ); } function key(d: Record, b: SubstitutionBook) { const ab = abPaymentKeys(d), past = pastPaymentUses(d, b); return closureFingerprint([ b.rows, b.rows.map((r) => available(d, b, r)), uses(d, b).map((r) => [ r.prior?.entityRef, r.prior?.origin, r.prior?.kind, r.prior?.id, r.payments.map((p) => norm(p.reference)), ]), b.rows.map((r) => r.payments.map( (p) => !!r.prior && (ab.includes( JSON.stringify([ norm(r.prior.entityRef), r.prior.origin, norm(p.reference), ]), ) || past.some( (v) => v.id !== r.id && v.entity === norm(r.prior!.entityRef) && v.origin === r.prior!.origin && v.reference === norm(p.reference), )), ), ), ]); } export function substitutionCurrent( d: Record, b: SubstitutionBook, ): boolean { return ( !!b.result && b.result.key === key(d, b) && b.result.rows.length === b.rows.length && b.rows.every((r) => b.result!.rows.some( (s) => s.id === r.id && s.inputKey === closureFingerprint(r), ), ) ); } function uses( d: Record, b: SubstitutionBook, ): SubstitutionInput[] { return [ ...b.rows, ...Object.entries(maps(d)) .filter(([id]) => id !== scope(d)) .flatMap(([, v]) => { validate(v); return v.rows.filter((r) => r.prior?.origin === 'effective'); }), ]; } export function recordSubstitution( d: Record, b: SubstitutionBook, now = new Date().toISOString(), ): SubstitutionBook { validate(b); if (new Set(b.rows.map((r) => r.id)).size !== b.rows.length) throw Error('Identidade de substituição repetida.'); const all = uses(d, b), paymentsAB = abPaymentKeys(d); const rows: SubstitutionResult[] = b.rows.map((row) => { const fields: Record = Object.fromEntries( [ '17', '18', '19', '20', '21', '22', '23', '24', '25', '26', '27', ].map((k) => [k, null]), ); const result: SubstitutionResult = { id: row.id, status: 'indeterminate', reason: '', inputKey: closureFingerprint(row), fields, pendingCredit: null, paid: null, outstanding: null, overpayment: null, totalPaid: null, totalOutstanding: null, source: substitutionSource, }; const stop = (reason: string) => ({ ...result, reason }); const prior = row.prior, source = row.source; if (!prior || !source || !available(d, b, row).every(Boolean)) return stop( 'Escolha declaração anterior e resultadoL2 congelados recuperáveis;sem fabricar ou recalcular origens.', ); if ( d.currency !== 'MZN' || source.currency !== 'MZN' || source.opportunity !== scope(d) || source.period !== prior.period || !/^2026-(0[1-9]|1[0-2])$/.test(prior.period) || !source.debit || !source.credit ) return stop( 'A substituição exige mesmo âmbito e período2026/MZN,com17/18 determinados.', ); if ( !prior.entityRef.trim() || (prior.origin === 'effective' && source.projected) ) return stop( 'Origem projectada não serve como declaração efectiva;conservar pressupostos identificados.', ); const seen = new Set([row.id]); let ancestor: PriorDeclaration | null = prior; while (ancestor?.kind === 'C') { if (seen.has(ancestor.id)) break; seen.add(ancestor.id); ancestor = b.rows.find((r) => r.id === ancestor!.id)?.prior ?? null; } if (ancestor?.kind === 'C' && seen.has(ancestor.id)) return stop( 'Sequência circular de substituições;ligue cada versão à anterior sem ciclos.', ); if ( all.filter( (r) => r.prior && norm(r.prior.entityRef) === norm(prior.entityRef) && r.prior.origin === prior.origin && r.prior.kind === prior.kind && r.prior.id === prior.id, ).length > 1 ) return stop( 'Declaração anterior já ligada a outra substituição;reconcilie a sequência sem duplicar o diferencial.', ); if ( !row.priorReference.trim() || row.priorDate !== prior.declarationDate || (prior.declarationReference && row.priorReference !== prior.declarationReference) || !date(row.priorDate) || row.priorDate.slice(0, 7) < prior.period || !date(row.date) || row.date < row.priorDate || !row.reference.trim() || !row.reason.trim() ) return stop( 'Reutilize a data/referência disponível da versão anterior;identifique referência/data da declaração anterior e da substituição,com motivo;preparação não equivale a submissão.', ); if ( BigInt(source.debit.numerator) < 0n || BigInt(source.credit.numerator) < 0n || (BigInt(source.debit.numerator) > 0n && BigInt(source.credit.numerator) > 0n) ) return stop('Sinais17/18 incompatíveis na origemL2.'); fields['17'] = source.debit; fields['18'] = source.credit; fields['21'] = subtractFractions( subtractFractions(source.debit, source.credit), subtractFractions(prior.debit, prior.credit), ); fields['22'] = positiveFraction(fields['21']); result.pendingCredit = positiveFraction( subtractFractions(zero(), fields['21']), ); if (row.interest === 'none' && row.interestRef.trim()) fields['23'] = zero(); else if (row.interest === 'documented' && row.interestRef.trim()) try { fields['23'] = decimalFraction(row.interestAmount); } catch { /* unknown preserved */ } if (!fields['23']) return stop( 'Juros adicionais por identificar;sem copiar juros anteriores ou presumir taxa/prazo.', ); fields['24'] = addFractions([fields['22'], fields['23']]); const paid: FiscalFraction[] = []; for (const p of row.payments) { const ident = JSON.stringify([ norm(prior.entityRef), prior.origin, norm(p.reference), ]); if ( !p.reference.trim() || !date(p.date) || p.date < row.date || prior.paymentRefs.some( (ref) => norm(ref) === norm(p.reference), ) || pastPaymentUses(d, b).some( (v) => v.id !== row.id && v.entity === norm(prior.entityRef) && v.origin === prior.origin && v.reference === norm(p.reference), ) || paymentsAB.includes(ident) || all.flatMap((r) => r.payments.filter( (p2) => r.prior && JSON.stringify([ norm(r.prior.entityRef), r.prior.origin, norm(p2.reference), ]) === ident, ), ).length > 1 ) return stop( 'Pagamento já registado na anterior/outra declaração,sem referência/data aplicável;registe só pagamentos adicionais.', ); try { paid.push(decimalFraction(p.amount)); } catch { return stop( 'Montante adicional não identificado;sem baixa presumida.', ); } } result.paid = addFractions(paid); result.outstanding = positiveFraction( subtractFractions(fields['24'], result.paid), ); result.overpayment = positiveFraction( subtractFractions(result.paid, fields['24']), ); result.totalPaid = addFractions([prior.paid, result.paid]); result.totalOutstanding = addFractions([ prior.outstanding, result.outstanding, ]); result.status = 'determined'; result.reason = 'Diferencial21 separado da declaração anterior;19/20 não reutilizados,25/26/27 não atribuídos por esta substituição. Crédito credor aguarda comunicação20;sem compensar dívida anterior automaticamente. Pagamentos adicionais e saldo anterior separados;sem submissãoAT ou certificação de cobrança. ' + (row.stage === 'preparation' ? 'Preparação/simulação;referências e datas declaradas não comprovam entrega.' : 'Apresentação registada pelo utilizador fora do InvesteMoz.'); return result; }); const result = { calculatedAt: now, key: key(d, b), inputs: structuredClone(b.rows), rows, }; return { ...b, ...(b.result ? { history: [...(b.history ?? []), structuredClone(b.result)] } : {}), result, }; } export function substitutionReportRows( d: Record, ): Array<[string, string, string, string]> { try { const b = substitutionBook(d); if (!b.rows.length) return []; const out: Array<[string, string, string, string]> = [ [ 'IVA mensal', 'L3-C — substituições', substitutionCurrent(d, b) ? 'Resultado correspondente às entradas' : 'Histórico preservado;rever entradas/origens', substitutionSource, ], ]; const fmt = (v: FiscalFraction | null | undefined) => v ? fractionPresentation(v).decimal : 'Indeterminado'; for (const r of b.result?.rows ?? []) { const input = b.result!.inputs.find((i) => i.id === r.id), p = input?.prior; out.push([ 'IVA mensal', `Substituição — ${p?.period ?? 'por identificar'}`, `21:${fmt(r.fields['21'])};22 adicional:${fmt(r.fields['22'])};23 adicional:${fmt(r.fields['23'])};24 adicional:${fmt(r.fields['24'])};crédito aguardando comunicação:${fmt(r.pendingCredit)}`, `${p?.entityRef ?? ''};${scope(d)};MZN;${p?.origin === 'effective' ? 'Dados efectivos declarados' : 'Pressuposto de simulação'};anterior ${input?.priorReference ?? ''},${input?.priorDate ?? ''};substituição ${input?.reference ?? ''},${input?.date ?? ''};${input?.reason ?? ''};${r.reason}`, ]); out.push([ 'IVA mensal', `Reconciliação da substituição — ${p?.period ?? ''}`, `Anterior:pago${fmt(p?.paid)},por liquidar${fmt(p?.outstanding)},reporte histórico26:${fmt(p?.carry)};adicional:pago${fmt(r.paid)},por liquidar${fmt(r.outstanding)},excedente${fmt(r.overpayment)};total registado${fmt(r.totalPaid)},total por liquidar sem compensação${fmt(r.totalOutstanding)}`, `Origem anterior${p?.kind}/${p?.id},versão${p?.calculatedAt};origemL2 revisão${input?.source?.revision},${input?.source?.calculatedAt};pagamentos adicionais:${input?.payments.map((p) => `${p.reference};${p.date};${p.amount}MZN`).join(' | ') || 'nenhum'};juros adicionais:${input?.interestRef ?? ''};histórico${b.result!.calculatedAt};${substitutionSource}`, ]); } return out; } catch { return [ [ 'IVA mensal', 'L3-C', 'Substituição não recuperável', 'Originais preservados;sem diferencial/crédito presumidos.', ], ]; } }