import { inputText } from './input-text.ts'; import { irpcInterestVersion, calculateIrpcInterest, validateIrpcInterestRule, type IrpcInterestRule, type IrpcInterestCalculation, } from './irpc-interest.ts'; import type { IrpcBook, IrpcResult, IrpcRow } from './irpc-withholdings.ts'; import { addFractions, decimalFraction, fraction, fractionPresentation, subtractFractions, type FiscalFraction, } from './fiscal-rational.ts'; export type MrftaInput = { version: 1; period: string; declaration: '' | 'initial' | 'replacement'; timing: '' | 'within' | 'late'; nuit: string; name: string; commercialName: string; taxArea: string; address: string; contact: string; completeness: '' | 'yes' | 'no'; completenessRef: string; otherRubrics: '' | 'none' | 'pending'; otherRubricsRef: string; codes: Array<{ rowId: string; cer: string; reference: string }>; interest: { mode: '' | 'assessment' | 'timely'; calculation?: 'automatic'; automatic?: IrpcInterestRule; period: string; amount: string; reference: string; date: string; dueDate: string; dueReference: string; }; }; type MrftaLine = { ids: string[]; documents: string[]; references: string[]; rubric: string; cer: string; rate: number; base: FiscalFraction; tax: FiscalFraction; paid: FiscalFraction | null; }; export type MrftaModel = { automatic?: IrpcInterestCalculation; period: string; input: MrftaInput; lines: MrftaLine[]; excluded: Array<{ id: string; reason: string }>; pending: string[]; principal: FiscalFraction; paid: FiscalFraction | null; interest: FiscalFraction | null; total: FiscalFraction | null; interestReason: string; source: string; }; export type MrftaRecord = { key: string; createdAt: string; model: MrftaModel }; export type MrftaStore = { mrfta?: MrftaInput; mrftaResult?: MrftaRecord; mrftaHistory?: MrftaRecord[]; }; export const newMrftaInput = (): MrftaInput => ({ version: 1, period: '', declaration: '', timing: '', nuit: '', name: '', commercialName: '', taxArea: '', address: '', contact: '', completeness: '', completenessRef: '', otherRubrics: '', otherRubricsRef: '', codes: [], interest: { mode: '', period: '', amount: '', reference: '', date: '', dueDate: '', dueReference: '', }, }); export const mrftaSource = 'AT MRFTA19/39,PDF1–2:quadros1–7;um mês/ano por declaração. Decreto9/2008 arts25/35,PDF6/8,p121/123:juros compensatórios separados. MAIBOR12meses+2p.p.na data da liquidação;publicação MAIBOR descontinuada segundo BM. Sem substituir por MIMO/Prime nem presumir zero. CER exige referência específica;remissões antigas do M/39 não renumeradas no original.'; const date = (s: string) => /^20\d{2}-\d{2}-\d{2}$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString().slice(0, 10) === s; const zero = () => fraction(0n, 1n); const isZero = (v: FiscalFraction) => BigInt(v.numerator) === 0n; const negative = (v: FiscalFraction) => BigInt(v.numerator) < 0n; const labels: Record = { period: 'mês/ano', declaration: 'tipo de declaração', timing: 'dentro/fora do prazo', nuit: 'NUIT', name: 'nome do sujeito passivo', taxArea: 'área fiscal', address: 'domicílio fiscal', }; export function validateMrfta(b: MrftaStore) { const i = b.mrfta; if ( i?.interest?.calculation !== undefined && i.interest.calculation !== 'automatic' ) throw Error('Método de cálculo de juros não recuperável.'); if (i?.interest?.automatic !== undefined) validateIrpcInterestRule(i.interest.automatic); if ( i && (!['', 'none', 'pending'].includes(i.otherRubrics) || typeof i.otherRubricsRef !== 'string') ) throw Error('Âmbito de outras rubricas MRFTA não recuperável.'); if ( i && (i.version !== 1 || !['', 'initial', 'replacement'].includes(i.declaration) || !['', 'within', 'late'].includes(i.timing) || !['', 'yes', 'no'].includes(i.completeness) || [ 'period', 'nuit', 'name', 'commercialName', 'taxArea', 'address', 'contact', 'completenessRef', ].some((k) => typeof i[k as keyof MrftaInput] !== 'string') || !Array.isArray(i.codes) || i.codes.some( (c) => !c || ['rowId', 'cer', 'reference'].some( (k) => typeof c[k as keyof typeof c] !== 'string', ), ) || new Set(i.codes.map((c) => c.rowId)).size !== i.codes.length || !i.interest || !['', 'assessment', 'timely'].includes(i.interest.mode) || [ 'period', 'amount', 'reference', 'date', 'dueDate', 'dueReference', ].some( (k) => typeof i.interest[k as keyof typeof i.interest] !== 'string', )) ) throw Error('Preparação MRFTA não recuperável;originais conservados.'); if ( (b.mrftaResult && (!b.mrftaResult.model || typeof b.mrftaResult.key !== 'string' || !Array.isArray(b.mrftaResult.model.lines) || !Array.isArray(b.mrftaResult.model.pending))) || (b.mrftaHistory !== undefined && !Array.isArray(b.mrftaHistory)) ) throw Error('Histórico MRFTA não recuperável.'); } function rubric(r: IrpcRow, v: IrpcResult) { if (v.nature === 'on_account') return r.kind === 'rent' ? 'Taxa normal — Rendimentos prediais' : r.kind === 'capital' ? 'Taxa normal — Rendimentos de capitais e mais valias' : null; if (v.nature !== 'definitive') return null; return ( ( { royalty: 'IRPC liberatório — Actividade intelectual ou industrial / prestação de informações', equipment: 'IRPC liberatório — Uso ou concessão do uso de equipamento', capital: 'IRPC liberatório — Aplicação de capitais e rendimentos prediais', services: 'IRPC liberatório — Comissões por intermediação de contratos ou prestação de serviços', } as Record )[r.kind] ?? null ); } function key(b: IrpcBook & MrftaStore, current: boolean) { return JSON.stringify([ 'MRFTA-L5/1', b.result, b.mrfta, current, ...(b.mrfta?.interest.mode === '' && b.mrfta?.interest.calculation === 'automatic' ? [irpcInterestVersion] : []), ]); } export function mrftaCurrent(b: IrpcBook & MrftaStore, current: boolean) { return !!b.mrftaResult && b.mrftaResult.key === key(b, current); } export function prepareMrfta( b: IrpcBook & MrftaStore, current: boolean, ): MrftaModel { validateMrfta(b); const i = b.mrfta ?? newMrftaInput(), pending: string[] = [], excluded: MrftaModel['excluded'] = [], lines: MrftaLine[] = []; for (const k of Object.keys(labels)) if (!inputText(i[k as keyof MrftaInput]).trim()) pending.push('Falta ' + labels[k] + '.'); if (!/^2026-(0[1-9]|1[0-2])$/.test(i.period)) pending.push('Mês/ano inválido ou fora do recorte2026.'); if (i.nuit && !/^\d{9}$/.test(i.nuit)) pending.push( 'NUIT deve ter nove algarismos;confira a identidade fiscal.', ); if (i.completeness !== 'yes' || !i.completenessRef.trim()) pending.push( 'Conferir completude das retenções deste sujeito passivo/período, incluindo os demais livros;este contexto não é automaticamente a declaração completa.', ); if (i.otherRubrics !== 'none' || !i.otherRubricsRef.trim()) pending.push( 'IRPS,tributação autónoma e outras rubricas fora deste recorte não são zero presumido;documente a ausência de outros valores devidos ou mantenha o total indeterminado.', ); if (!current || !b.result) pending.push( 'Cálculo das retenções desactualizado ou inexistente;guarde o cálculo actual antes da preparação.', ); for (const v of b.result?.rows ?? []) { const r = b.result!.inputs.find((r) => r.id === v.id); if (!r) { pending.push('Entrada do resultado ausente: ' + v.id); continue; } if (r.direction === 'received') { excluded.push({ id: r.id, reason: 'Retenção sofrida; não é imposto a entregar pelo titular.', }); continue; } if (v.period && v.period !== i.period) { excluded.push({ id: r.id, reason: 'Outro período: ' + v.period }); continue; } if (v.treatment === 'dispensed' && v.status === 'calculated') { excluded.push({ id: r.id, reason: 'Dispensa documentada;sem imposto a transcrever.', }); continue; } if ( v.status !== 'calculated' || !v.tax || !v.base || v.rate === null || !v.period ) { pending.push( `${r.documentRef}: rendimento indeterminado;${v.reason}`, ); continue; } const name = rubric(r, v); if (!name) { pending.push( `${r.documentRef}: rubrica MRFTA por corresponder;categoria digital/especial não copiada para serviços comuns.`, ); continue; } const code = i.codes.find((c) => c.rowId === r.id); if ( !code?.cer.trim() || !/^\d+$/.test(code.cer.trim()) || !code.reference.trim() ) pending.push( `${r.documentRef}: CER numérico e referência aplicável por identificar;sem código presumido.`, ); const cer = code?.cer.trim() ?? '', line = lines.find( (l) => l.rubric === name && l.rate === v.rate && l.cer === cer, ); const reference = `CER:${code?.reference ?? ''};contraparte:${r.counterparty};qualificação:${r.qualificationRef};benefício:${r.reliefPreparation?.evidence ?? r.reliefRef};residência fiscal:${r.reliefPreparation?.residenceDocument ?? ''}`; if (line) { line.ids.push(r.id); line.documents.push(r.documentRef); line.references.push(reference); line.base = addFractions([line.base, v.base]); line.tax = addFractions([line.tax, v.tax]); line.paid = line.paid && v.settled ? addFractions([line.paid, v.settled]) : null; } else lines.push({ ids: [r.id], documents: [r.documentRef], references: [reference], rubric: name, cer, rate: v.rate, base: v.base, tax: v.tax, paid: v.settled, }); } for (const line of lines) if ( lines.some( (other) => other !== line && other.rubric === line.rubric, ) ) pending.push( line.rubric + ': taxas ou CER distintos na mesma rubrica;conferir transcrição sem fundir taxas nem perder parcelas.', ); const principal = addFractions(lines.map((l) => l.tax)), paid = lines.every((l) => l.paid !== null) ? addFractions(lines.map((l) => l.paid!)) : null; if (paid === null) pending.push( 'Valor já pago não reconciliado;pagamento ao fornecedor não é entrega à AT.', ); let interest: FiscalFraction | null = null, interestReason = 'Juros compensatórios indeterminados: taxa aplicável, âmbito e período por resolver;sem MAIBOR2026 nem substituição automática por MIMO/Prime. Juros de mora e tributação autónoma não apurados.'; if (i.interest.mode === 'assessment') { try { const a = decimalFraction(i.interest.amount); if ( negative(a) || i.interest.period !== i.period || !i.interest.reference.trim() || !date(i.interest.date) || i.interest.date.slice(0, 7) < i.period ) throw Error(); interest = a; interestReason = `Juros compensatórios transcritos de liquidação documentada:${i.interest.reference};data:${i.interest.date};período:${i.interest.period};montante original em MZN:${i.interest.amount}. Não são juros calculados pelo portal.`; } catch { interestReason = 'Liquidação de juros incompleta,negativa ou incoerente;identifique montante,data e documento do mesmo período.'; } } else if (i.interest.mode === 'timely') { const ids = lines.flatMap((l) => l.ids), inputs = (b.result?.inputs ?? []).filter((r) => ids.includes(r.id)); const dates = inputs.flatMap((r) => r.allocations.map( (a) => b.result?.payments.find((p) => p.id === a.paymentId) ?.date ?? '', ), ); if ( i.interest.period === i.period && date(i.interest.dueDate) && i.interest.dueReference.trim() && i.interest.reference.trim() && paid !== null && isZero(subtractFractions(principal, paid)) && inputs.length > 0 && inputs.every( (r) => r.settlement === 'complete' && r.allocations.length > 0, ) && dates.every((d) => date(d) && d <= i.interest.dueDate) && i.interest.dueDate >= i.period + '-01' ) { interest = zero(); interestReason = `Entregas integralmente reconciliadas até ao prazo documentado:${i.interest.dueDate};${i.interest.dueReference};${i.interest.reference}. Sem atraso neste recorte;sem declarar cumprimento global de obrigações.`; } else interestReason = 'Ausência de atraso não demonstrada: confira prazo aplicável, comprovativos completos, datas e montante entregue. Sem zero de juros presumido.'; } let automatic: IrpcInterestCalculation | undefined; if (i.interest.mode === '' && i.interest.calculation === 'automatic') { if ( !i.interest.automatic || i.interest.period !== i.period || !i.interest.reference.trim() ) interestReason = 'Cálculo automático sem parâmetros/ocorrência e período documentados;juros continuam indeterminados.'; else { automatic = calculateIrpcInterest( b, current, i.period, i.interest.dueDate, i.interest.dueReference, i.interest.date, i.interest.automatic, ); interest = automatic.total; interestReason = automatic.pending.length ? automatic.pending.join(' | ') : 'Juros calculados por parâmetros documentados deste caso — simulação pelos pressupostos introduzidos, sem confirmação fiscal;sem taxa fiscal2026 pré-definida. Taxa anual:' + automatic.rule.annualRate + '%;fonte:' + automatic.rule.rateSource + ';fundamento:' + automatic.rule.fiscalBasis + ';vigência:' + automatic.rule.validFrom + '–' + automatic.rule.validTo + ';base anual:' + (automatic.rule.dayBasis === 'actual_year' ? 'dias efectivos de cada ano' : automatic.rule.dayBasis + ' dias') + ';fonte:' + automatic.rule.dayBasisRef + ';arredondamento do total aos cêntimos:' + (automatic.rule.rounding === 'total_cent_half_up' ? 'metade por excesso' : 'por defeito') + ';fonte:' + automatic.rule.roundingRef + ';ocorrência:' + automatic.rule.occurrenceRef + ';' + automatic.rows .map( (r) => r.document + ':' + r.segments .map( (s) => s.from + '–' + s.to + ':' + s.days + 'dias/' + s.denominator + ';principal ' + money(s.principal) + ';juros ' + money(s.interest), ) .join(','), ) .join(' | '); } } if (!interest) pending.push(interestReason); let total: FiscalFraction | null = null; if (pending.length === 0 && interest && paid) { const balance = subtractFractions( addFractions([principal, interest]), paid, ); if (negative(balance)) pending.push( 'Valor já pago excede imposto e juros;reconcilie sem crédito/reembolso automático.', ); else total = balance; } return { ...(automatic ? { automatic } : {}), period: i.period, input: structuredClone(i), lines, excluded, pending: [...new Set(pending)], principal, paid, interest, total, interestReason, source: mrftaSource, }; } export function recordMrfta( b: IrpcBook & MrftaStore, current: boolean, now = new Date().toISOString(), ): IrpcBook & MrftaStore { const model = prepareMrfta(b, current); return { ...b, mrftaHistory: b.mrftaResult ? [...(b.mrftaHistory ?? []), structuredClone(b.mrftaResult)] : (b.mrftaHistory ?? []), mrftaResult: { key: key(b, current), createdAt: now, model }, }; } const money = (f: FiscalFraction | null) => { if (!f) return 'Indeterminado'; const p = fractionPresentation(f); return (p.approximate ? '≈' : '') + p.decimal + ' MZN'; }; export function mrftaReportRows( b: IrpcBook & MrftaStore, current: boolean, ): Array<[string, string, string, string]> { const r = b.mrftaResult; if (!r) return []; const m = r.model; return [ [ 'IRPC — retenções', 'MRFTA — ' + m.period, `Subtotal das retenções correspondidas:${money(m.principal)};juros:${money(m.interest)};valor já pago:${money(m.paid)};total a pagar:${money(m.total)};${m.interestReason};pendências:${m.pending.join(' | ')}`, `${mrftaCurrent(b, current) ? 'Preparação correspondente' : 'Histórico;preparação desactualizada'};${r.createdAt};completude:${m.input.completenessRef};outras rubricas:${m.input.otherRubricsRef};${m.source}`, ], ...m.lines.map( (l) => [ 'IRPC — retenções', l.rubric, `${l.documents.join(';')};base:${money(l.base)};taxa:${l.rate}%;imposto:${money(l.tax)};CER:${l.cer || 'por corresponder'};código introduzido,sem validação por catálogo oficial`, `Período:${m.period};apenas rendimento pago;sem assinatura ou uso dos serviços;documentos:${l.ids.join(',')};suporte:${l.references.join(' | ')}`, ] as [string, string, string, string], ), ]; } export function mrftaHtml(b: IrpcBook & MrftaStore, current: boolean): string { if (!current || !mrftaCurrent(b, current)) throw Error( 'Calcule as retenções e prepare o MRFTA com as entradas actuais antes de exportar.', ); const m = b.mrftaResult!.model, esc = (v: unknown) => String(v).replace( /[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', })[c]!, ); return `MRFTA — preparação

MRFTA — preenchimento de preparação

Quadro1:retenção na fonte;${esc(m.input.declaration === 'initial' ? 'Inicial' : m.input.declaration === 'replacement' ? 'Substituição' : 'Tipo por identificar')}. Quadro2:${esc(m.period)};${esc(m.input.timing === 'within' ? 'Dentro do prazo' : m.input.timing === 'late' ? 'Fora do prazo' : 'Prazo por identificar')}.

Quadro3 NUIT:${esc(m.input.nuit)};área fiscal:${esc(m.input.taxArea)}. Quadro4:${esc(m.input.name)}. Quadro5:${esc(m.input.commercialName)}. Quadro6:${esc(m.input.address)};${esc(m.input.contact)}.

Conferência do período:${esc(m.input.completenessRef)}. Outras rubricas:${esc(m.input.otherRubricsRef)}.

CER introduzidos com referência do caso; sem validação automática por catálogo oficial. Códigos provisórios ou de simulação não são correspondências oficiais.

Quadro7

${m.lines.map((l) => ``).join('')}
CERRubricaBaseTaxaImpostoDocumentos
${esc(l.cer || 'Por corresponder')}${esc(l.rubric)}${esc(money(l.base))}${l.rate}%${esc(money(l.tax))}${esc(l.documents.join(';') + ';' + l.references.join(' | '))}

Subtotal correspondido:${esc(money(m.principal))};JUROS COMPENSATÓRIOS:${esc(money(m.interest))};VALOR JÁ PAGO:${esc(money(m.paid))};TOTAL A PAGAR:${esc(money(m.total))}.

${esc(m.interestReason)}

Casos pendentes

Casos separados

Quadros8/9 reservados,sem assinatura,carimbo,número de entrada ou aceitação AT. Tributação autónoma e juros de mora não apurados. Este documento de preparação não substitui o original oficial nem submete uma declaração.

${esc(m.source)}`; }