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(!String(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)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

    ${m.pending.map(p=>'
  • '+esc(p)+'
  • ').join('')}

Casos separados

    ${m.excluded.map(p=>'
  • '+esc(p.id+':'+p.reason)+'
  • ').join('')}

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)}`; }