Files
Investidor-V2/resources/js/lib/irpc-normal-profit.ts
T
2026-11-10 13:54:06 +02:00

375 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { creditLimit, creditCompare } from './irpc-international-credit.ts';
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, exact = false) {
if (exact) {
const v = creditLimit(s.startsWith('-') ? s.slice(1) : s);
return s.startsWith('-')
? fraction(-BigInt(v.numerator), BigInt(v.denominator))
: v;
}
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,
economic?: {
effects: {
id: string;
topic: string;
amount: FiscalFraction | null;
effect: FiscalFraction | null;
reason: string;
}[];
net: FiscalFraction;
pending: string[];
},
): 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,
key === 'incomeCorrections' && !!economic,
);
if (BigInt(value.numerator) !== 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 ?? []),
...(economic?.effects ?? []),
].filter((e) => e.topic === key);
const mappedAmount = addFractions(
mapped.flatMap((e) => (e.amount ? [e.amount] : [])),
);
const covered =
(schedules || costs || economic) &&
mapped.length > 0 &&
mapped.every((e) => e.effect !== null) &&
value !== null &&
t.state === 'known' &&
(economic && key === 'incomeCorrections'
? creditCompare(mappedAmount, value) === 0n
: units(mappedAmount) === units(value));
if (covered)
reason =
'Montante reconciliado com as regras do mapa complementar; efeito fiscal discriminado por linha.';
if (
value === null ||
(BigInt(value.numerator) !== 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 (economic) pending.push(...economic.pending);
if (depreciation && !schedules) pending.push(...depreciation.pending);
return {
input: structuredClone(input),
lines,
topics,
pending,
taxableProfit:
pending.length === 0 && accounting
? addFractions([
accounting,
additions,
...(economic ? [economic.net] : []),
...(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.',
};
}