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

274 lines
10 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 { canonicalParcel } from './fiscal-proof-parcel.ts';
import type { NormalBook } from './irpc-normal.ts';
import {
economicTaxProof,
economicDistributionIdentity,
economicIncome,
validateEconomic,
type EconomicPosition,
} from './irpc-economic-credit.ts';
import {
creditMoney,
creditLimit,
creditCompare,
} from './irpc-international-credit.ts';
import {
addFractions,
fractionPresentation,
subtractFractions,
fraction,
type FiscalFraction,
} from './fiscal-rational.ts';
export type EconomicInclusion = {
source: EconomicPosition;
profitAlreadyIncluded: string;
creditAlreadyIncluded: string;
reference: string;
};
export type EconomicInclusions = { version: 1; rows: EconomicInclusion[] };
export const economicInclusionVersion = 'IRPC-economic-inclusion/1';
export function validateEconomicInclusions(input: EconomicInclusions) {
if (
input?.version !== 1 ||
!Array.isArray(input.rows) ||
input.rows.some(
(r) =>
!r ||
typeof r.profitAlreadyIncluded !== 'string' ||
typeof r.creditAlreadyIncluded !== 'string' ||
typeof r.reference !== 'string',
)
)
throw Error('Inclusões económicas não recuperáveis.');
validateEconomic({
version: 1,
state: 'known',
reference: economicInclusionVersion,
rows: input.rows.map((r) => r.source),
});
}
export function economicInclusionMoney(value: FiscalFraction): string {
const presentation = fractionPresentation(value);
return presentation.approximate
? value.numerator + '/' + value.denominator
: presentation.decimal;
}
export function calculateEconomicInclusions(
input: EconomicInclusions,
subject: string,
year: string,
) {
validateEconomicInclusions(input);
const pending: string[] = [];
const effects: {
id: string;
topic: string;
amount: FiscalFraction | null;
effect: FiscalFraction | null;
reason: string;
}[] = [];
const seen = new Set<string>();
for (const row of input.rows) {
const r = row.source;
try {
if (
!r ||
!['ordinary_profit', 'liquidation', 'association'].includes(
r.kind,
) ||
!r.id.trim() ||
!/^\d{9}$/.test(r.payerNuit) ||
r.subjectNuit !== subject ||
r.year !== year ||
year !== '2026' ||
r.recipientResident !== 'yes' ||
r.payerResident !== 'yes' ||
r.payerIrpc !== 'yes' ||
r.payerExempt !== 'no' ||
r.article40 !== 'no' ||
!r.qualificationReference?.trim() ||
!r.article40Reference?.trim() ||
!row.reference?.trim() ||
!r.distributionDocument?.trim() ||
!r.distributionParcel?.trim() ||
!/^2026-\d{2}-\d{2}$/.test(r.distributionDate) ||
new Date(r.distributionDate).toISOString().slice(0, 10) !==
r.distributionDate
)
throw Error(
'Falta qualificação, origem, titular/exercício ou correspondência da inclusão; art40 e outros âmbitos separados.',
);
const identity =
r.distributionDocument.trim().toUpperCase() +
'|' +
r.distributionParcel.trim().toUpperCase();
const taxIdentity = JSON.stringify([
canonicalParcel(r.taxIssuer),
canonicalParcel(r.taxDocument),
canonicalParcel(r.taxParcel),
]);
const distributionIdentity = economicDistributionIdentity(r);
if (
seen.has(distributionIdentity) ||
seen.has('tax|' + taxIdentity)
)
throw Error(
'Origem/parcela económica repetida; consolide antes de corrigir a base.',
);
seen.add(distributionIdentity);
seen.add('tax|' + taxIdentity);
const qualification = economicIncome(r);
const tax = economicTaxProof(r).value;
const nominal = fraction(
BigInt(tax.numerator) * 3n,
BigInt(tax.denominator) * 5n,
);
if (
creditCompare(qualification.income, fraction(0n)) === 0n &&
creditCompare(nominal, fraction(0n)) > 0n
)
throw Error(
'Imposto subjacente positivo sem rendimento elegível para crédito económico.',
);
const specialReference = qualification.liquidation
? `${qualification.liquidation.version}; rendimento elegível ${economicInclusionMoney(qualification.liquidation.eligibleIncome)}; excesso com natureza de mais-valia ${economicInclusionMoney(qualification.liquidation.excessCapitalGain)} incluído no rendimento total a reconciliar. ${qualification.liquidation.reference}`
: qualification.association
? `${qualification.association.version}; ${qualification.association.kind}; ${qualification.association.reference}`
: '';
const targets = [creditMoney(r.income), nominal];
const included = [
creditMoney(row.profitAlreadyIncluded),
creditLimit(row.creditAlreadyIncluded),
];
if (
r.incomeIncluded === 'yes' &&
creditCompare(included[0], targets[0]) < 0n
)
throw Error(
'A origem L8 declara os lucros já incluídos; o montante anterior indicado contradiz essa declaração. Compare e corrija a origem antes de acrescentar novamente.',
);
if (
r.grossUpIncluded.trim() &&
creditCompare(creditLimit(r.grossUpIncluded), included[1]) !==
0n
)
throw Error(
'O crédito já acrescido indicado não corresponde à origem L8; reconcilie a diferença antes de alterar a base.',
);
targets.forEach((target, index) => {
const effect = subtractFractions(target, included[index]);
economicInclusionMoney(effect);
effects.push({
id: identity + ':' + index,
topic: 'incomeCorrections',
amount: fraction(
BigInt(effect.numerator) < 0n
? -BigInt(effect.numerator)
: BigInt(effect.numerator),
BigInt(effect.denominator),
),
effect,
reason: `${economicInclusionVersion}${[target, included[index], effect].some((v) => (BigInt(v.numerator) * 100n) % BigInt(v.denominator) !== 0n) ? '; IRPC-economic-inclusion-exact/1: fracções conservadas, sem arredondamento fiscal' : ''}; CIRPC64(1–2)/53(1)a; ${index ? 'acréscimo nominal' : 'lucros'}: alvo ${economicInclusionMoney(target)}; já considerado ${economicInclusionMoney(included[index])}; diferença ${economicInclusionMoney(effect)}. ${row.reference}${specialReference ? ' ' + specialReference : ''}`,
});
});
} catch (e) {
pending.push(String(e));
}
}
return {
version: economicInclusionVersion,
input: structuredClone(input),
effects,
pending,
net: addFractions(effects.flatMap((e) => (e.effect ? [e.effect] : []))),
};
}
export function prepareEconomicInclusion(
book: NormalBook,
row: EconomicInclusion,
): NormalBook {
if (
book.special ||
book.periodsFx ||
(book.schedules && book.schedules.period.kind !== 'calendar') ||
book.scope !== 'resident_general' ||
book.year !== '2026' ||
!book.base ||
!book.profit
)
throw Error(
'Prepare primeiro o lucro e a base L6 ordinária do sujeito/ano civil2026/MZN; períodos especiais e conversões têm correspondência própria.',
);
const old = book.economicInclusions ?? { version: 1 as const, rows: [] };
const rows = old.rows.filter(
(p) =>
economicDistributionIdentity(p.source) !==
economicDistributionIdentity(row.source),
);
const next: EconomicInclusions = {
version: 1,
rows: [...rows, structuredClone(row)],
};
const previous = calculateEconomicInclusions(
old,
book.base.subjectNuit,
book.year,
),
prepared = calculateEconomicInclusions(
next,
book.base.subjectNuit,
book.year,
);
if (previous.pending.length || prepared.pending.length)
throw Error([...previous.pending, ...prepared.pending].join(' '));
const topic = book.profit.topics.incomeCorrections;
if (
!topic.reference.trim() ||
!['known', 'absent'].includes(topic.state) ||
(topic.state === 'absent' && topic.amount.trim())
)
throw Error(
'Reconcilie as outras correcções aos proveitos antes de substituir parcelas.',
);
const current =
topic.state === 'absent'
? fraction(0n)
: old.rows.length
? creditLimit(topic.amount)
: creditMoney(topic.amount);
const remaining = subtractFractions(
current,
addFractions(
previous.effects.flatMap((e) => (e.amount ? [e.amount] : [])),
),
);
if (BigInt(remaining.numerator) < 0n)
throw Error(
'Montante de correcções inferior às parcelas anteriores; compare antes de substituir.',
);
const amount = addFractions([
remaining,
addFractions(
prepared.effects.flatMap((e) => (e.amount ? [e.amount] : [])),
),
]);
return {
...book,
economicInclusions: next,
profit: {
...book.profit,
topics: {
...book.profit.topics,
incomeCorrections: {
state: 'known',
amount: economicInclusionMoney(amount),
reference: topic.reference.includes(row.reference)
? topic.reference
: topic.reference + '\n' + row.reference,
},
},
},
};
}