65 lines
9.1 KiB
TypeScript
65 lines
9.1 KiB
TypeScript
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<ProfitTopic,ProfitEvidence>};
|
||
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<ProfitEvidence&{key:ProfitTopic;label:string;value:FiscalFraction|null;reason:string}>;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.'};
|
||
}
|