import type {NormalDepreciationModel} from './irpc-normal-depreciation.ts'; import type {NormalCostsModel} from './irpc-normal-costs.ts'; import type {SchedulesModel} from './irpc-normal-schedules.ts'; import {addFractions,decimalFraction,fraction,type FiscalFraction} from './fiscal-rational.ts'; export const normalProfitVersion='IRPC-normal-adjustments/2'; export type ProfitEvidence={state:''|'known'|'absent';amount:string;reference:string}; export const profitTopics=[ ['positivePatrimonial','Variações patrimoniais positivas fora do resultado'], ['negativePatrimonial','Variações patrimoniais negativas fora do resultado'], ['incomeCorrections','Outras correcções aos proveitos, incluindo exclusões'], ['expenseCorrections','Outras correcções aos custos, além dos acréscimos registados'], ['timing','Diferenças de periodização e de critérios contabilísticos/fiscais'], ['special','Operações especiais, benefícios e regimes fora deste recorte'], ]as const; export type ProfitTopic=typeof profitTopics[number][0]; export type NormalProfitInput={version:1;completenessReference:string;lines:Array<{code:string;state:''|'known'|'absent';reference:string}>;topics:Record}; export const newNormalProfitInput=():NormalProfitInput=>({version:1,completenessReference:'',lines:[],topics:Object.fromEntries(profitTopics.map(([k])=>[k,{state:'',amount:'',reference:''}]))as NormalProfitInput['topics']}); export function validateNormalProfit(i:NormalProfitInput){ if(!i||i.version!==1||typeof i.completenessReference!=='string'||!Array.isArray(i.lines)||i.lines.some(l=>!l||typeof l.code!=='string'||typeof l.reference!=='string'||!['','known','absent'].includes(l.state))||new Set(i.lines.map(l=>l.code)).size!==i.lines.length||!i.topics||typeof i.topics!=='object'||Array.isArray(i.topics))throw Error('Conferência do lucro tributável não recuperável.'); for(const[k]of profitTopics){const t=i.topics[k];if(!t||!['','known','absent'].includes(t.state)||typeof t.amount!=='string'||typeof t.reference!=='string')throw Error('Estado documental do lucro tributável não recuperável.');} } type Source={currency:string;periods:number;accountingLines:Array<{code:string;origin:string;accountingEffect:'income'|'expense';total:string}>}; export type NormalProfitModel={input:NormalProfitInput;lines:Array<{code:string;label:string;amount:string;addedBack:FiscalFraction;remainder:FiscalFraction|null;state:ProfitEvidence['state'];reference:string;reason:string}>;topics:Array;pending:string[];taxableProfit:FiscalFraction|null;source:string}; const zero=()=>fraction(0n,1n); function money(s:string){if(!/^-?\d+(?:\.\d{1,2})?$/.test(s))throw Error('Montante inválido');const v=decimalFraction(s.startsWith('-')?s.slice(1):s);return s.startsWith('-')?fraction(-BigInt(v.numerator),BigInt(v.denominator)):v;} const units=(f:FiscalFraction)=>BigInt(f.numerator)*100n/BigInt(f.denominator); const difference=(a:FiscalFraction,b:FiscalFraction)=>fraction(BigInt(a.numerator)*BigInt(b.denominator)-BigInt(b.numerator)*BigInt(a.denominator),BigInt(a.denominator)*BigInt(b.denominator)); export function closeNormalProfit(input:NormalProfitInput,source:Source|null,rows:Array<{sourceCode:string;adjustment:FiscalFraction|null;status:string}>,accounting:FiscalFraction|null,additions:FiscalFraction,contextReady:boolean,schedules?:SchedulesModel,costs?:NormalCostsModel,depreciation?:NormalDepreciationModel):NormalProfitModel{ validateNormalProfit(input);const pending:string[]=[],lines:NormalProfitModel['lines']=[],topics:NormalProfitModel['topics']=[]; if(!contextReady)pending.push('Âmbito,exercício,moeda ou correspondência anual ainda insuficientes.'); if(!input.completenessReference.trim())pending.push('Documente que o plano e os ajustamentos abrangem o sujeito passivo/exercício completo;uma oportunidade isolada não equivale automaticamente ao conjunto da empresa.'); if(rows.some(r=>r.status!=='calculated'))pending.push('Existem ajustamentos registados por resolver;acréscimos independentes conservados.'); if(!source)pending.push('Falta resultado financeiro reconciliado.'); for(const l of source?.accountingLines??[]){ const review=input.lines.find(r=>r.code===l.code),state=review?.state??'',reference=review?.reference??''; const addedBack=addFractions(rows.filter(r=>r.sourceCode===l.code&&r.adjustment).map(r=>r.adjustment!));let remainder:FiscalFraction|null=null;const issues:string[]=[]; try{const amount=money(l.total);remainder=difference(amount,addedBack);if(units(amount)<0n||units(remainder)<0n)issues.push('Valor negativo ou acréscimos superiores à origem;reconcilie a linha sem compensação presumida.'); if(!state||!reference.trim())issues.push('Conferência fiscal e suporte da linha em falta.'); if(state==='absent'&&(units(amount)!==0n||units(addedBack)!==0n))issues.push('Ausência declarada contradiz o montante conhecido da origem.'); if(state==='known'&&l.code==='depreciation'&&units(amount)!==0n&&(depreciation?depreciation.adjustment:schedules?.depreciation)==null)issues.push('Depreciação contabilística conhecida;amortização fiscal por corresponder,não presumir igualdade.'); if(state==='known'&&l.code==='interest_expense'&&units(remainder)!==0n&&schedules?.interest==null)issues.push('Juros financeiros remanescentes conhecidos;dedutibilidade e eventuais limites por corresponder. Só os juros compensatórios já acrescidos estão cobertos.'); if(!['revenue','cost_of_sales','operating_expenses','personnel','depreciation','interest_expense'].includes(l.code))issues.push('Linha financeira sem correspondência neste recorte.'); }catch{issues.push('Montante da linha por reconciliar.');} const reason=issues.join(' ')|| (state==='absent'?'Ausência expressamente declarada e compatível com a origem zero.':units(remainder??zero())===0n?'Valor conhecido,integralmente corrigido ou igual a zero;sem presumir ausência.':l.accountingEffect==='income'?'Proveito ordinário reconhecido no exercício,qualificado e documentado nos termos dos artigos18/20.':'Parcela remanescente qualificada como custo ordinário indispensável e documentado do artigo22,sem exclusões/limites adicionais declarados.'); if(issues.length)pending.push(l.origin+': '+reason); lines.push({code:l.code,label:l.origin,amount:l.total,addedBack,remainder,state,reference,reason}); } for(const l of input.lines)if(!source?.accountingLines.some(s=>s.code===l.code))pending.push('Conferência sem linha de origem actual: '+l.code); for(const[key,label]of profitTopics){const t=input.topics[key];let value:FiscalFraction|null=null,reason=''; if(!t.state||!t.reference.trim())reason='Informação em falta;sem zero presumido.'; else if(t.state==='absent'){if(t.amount.trim())reason='Ausência declarada com montante preenchido;reconcilie os dois estados.';else{value=zero();reason='Ausência expressamente declarada com suporte;parcela zero apenas por esta declaração.';}} else{try{value=money(t.amount);if(units(value)!==0n)reason='Montante conhecido,mas tratamento específico ainda por corresponder;conservado sem aplicação presumida.';else reason='Valor conhecido zero com suporte;distinto de ausência declarada.';}catch{reason='Montante conhecido por preencher ou inválido.';}} const mapped=[...(schedules?.effects??[]),...(costs?.effects??[])].filter(e=>e.topic===key); const covered=(schedules||costs)&&mapped.length>0&&mapped.every(e=>e.effect!==null)&&value!==null&&t.state==='known'&&units(addFractions(mapped.flatMap(e=>e.amount?[e.amount]:[])))===units(value); if(covered)reason='Montante reconciliado com as regras do mapa complementar; efeito fiscal discriminado por linha.'; if(value===null||units(value)!==0n&&!covered||t.state==='absent'&&!!t.amount.trim()||mapped.length>0&&!covered)pending.push(label+': '+(mapped.length&&!covered?'Mapa complementar não reconciliado com este estado/montante. ': '')+reason); topics.push({...structuredClone(t),key,label,value,reason}); } if(schedules)pending.push(...schedules.pending); if(costs)pending.push(...costs.pending); if(depreciation&&!schedules)pending.push(...depreciation.pending); return{input:structuredClone(input),lines,topics,pending,taxableProfit:pending.length===0&&accounting?addFractions([accounting,additions,...(schedules?[schedules.net]:[]),...(costs?[costs.net]:[]),...(depreciation&&!schedules&&depreciation.adjustment?[depreciation.adjustment]:[])]):null,source:schedules?'CIRPC17/18/20/21/22/24/36; Decreto72/2013 art3–7. Mapa complementar de efeitos com sinal reconciliado por tema, juros financeiros ordinários e quotas anuais constantes documentadas. Simulação emMZN/2026; benefícios e casos especiais separados emL7, saldo anual emL8.':'CIRPC17(1)/18/20/22/36(1)c;recorte anual2026,residente geral,MZN. Resultado antes de imposto + acréscimos36(1)c;demais correcções/variações sem efeito apenas quando ausência ou zero conhecidos expressamente suportados. Receita/custos ordinários classificados pelo utilizador;depreciação fiscal e juros financeiros remanescentes fora deste percurso. Lucro tributável simulado,não matéria colectável,colecta ou declaraçãoM20/22.'}; }