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

510 lines
20 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 {
addFractions,
fraction,
subtractFractions,
type FiscalFraction,
} from './fiscal-rational.ts';
import {
creditCompare,
creditMoney,
creditLimit,
type CreditEvidence,
type CreditState,
} from './irpc-international-credit.ts';
import { canonicalParcel } from './fiscal-proof-parcel.ts';
export const economicVersion = 'IRPC-economic-credit/1';
export const economicSource =
'CIRPC64(1–2), Lei34/2007, BR52, PDF18/p768-(72):60% do IRPC correspondente aos lucros distribuídos incluídos na base, limitado à colecta proporcional após acréscimo53(1)a (PDF15/p768-(69)). Distinto de65 e da exclusão40(1), na redacçãoLei20/2009,PDF4/p272-(16). RIRPC21(2/5),PDF6/p121:económico antes do internacional/benefícios/PEC,até à colecta,sem reembolso. Recorte2026,residente geral,ano civil,MZN,lucros ordinários,qualificação e quota proporcional documentadas na colecta corrente. Sem alterar a base por presunção, transporte ou consumo de saldos oficiais.';
type Choice = '' | 'yes' | 'no';
export type EconomicPosition = {
id: string;
subjectNuit: string;
year: string;
assessmentKey: string;
kind: '' | 'ordinary_profit' | 'liquidation' | 'association';
payerNuit: string;
recipientResident: Choice;
payerResident: Choice;
payerIrpc: Choice;
payerExempt: Choice;
article40: Choice;
qualificationReference: string;
article40Reference: string;
distributionDocument: string;
distributionParcel: string;
distributionDate: string;
income: string;
incomeIncluded: Choice;
incomeReference: string;
taxIssuer: string;
taxDocument: string;
taxParcel: string;
parcelReference: string;
currency: string;
proofAmount: string;
taxAmount: string;
attributionReference: string;
previouslyAttributed: string;
priorAttributionReference: string;
grossUpIncluded: string;
grossUpReference: string;
nationalLimit: string;
nationalLimitReference: string;
association?: {
kind: '' | 'participation' | 'quota';
effectivelyTaxed: Choice;
reference: string;
};
liquidation?: {
attributed: string;
paidInCapital: string;
reference: string;
};
nationalMethod?: 'documented' | 'proportional';
nationalBasisReference?: string;
previousUse: CreditEvidence;
};
export type EconomicCredits = {
version: 1;
state: CreditState;
reference: string;
rows: EconomicPosition[];
};
export const newEconomicCredits = (): EconomicCredits => ({
version: 1,
state: '',
reference: '',
rows: [],
});
export const newEconomicPosition = (id: string): EconomicPosition => ({
id,
subjectNuit: '',
year: '',
assessmentKey: '',
kind: '',
payerNuit: '',
recipientResident: '',
payerResident: '',
payerIrpc: '',
payerExempt: '',
article40: '',
qualificationReference: '',
article40Reference: '',
distributionDocument: '',
distributionParcel: '',
distributionDate: '',
income: '',
incomeIncluded: '',
incomeReference: '',
taxIssuer: '',
taxDocument: '',
taxParcel: '',
parcelReference: '',
currency: 'MZN',
proofAmount: '',
taxAmount: '',
attributionReference: '',
previouslyAttributed: '',
priorAttributionReference: '',
grossUpIncluded: '',
grossUpReference: '',
nationalLimit: '',
nationalLimitReference: '',
previousUse: { state: '', amount: '', reference: '' },
});
export function validateEconomic(c: EconomicCredits) {
if (
!c ||
c.version !== 1 ||
!['', 'known', 'absent'].includes(c.state) ||
typeof c.reference !== 'string' ||
!Array.isArray(c.rows) ||
new Set(c.rows.map((r) => r?.id)).size !== c.rows.length
)
throw Error(
'Crédito económico não recuperável; originais conservados.',
);
for (const r of c.rows)
if (
r?.liquidation !== undefined &&
(!r.liquidation ||
['attributed', 'paidInCapital', 'reference'].some(
(k) =>
typeof (
r.liquidation as unknown as Record<string, unknown>
)[k] !== 'string',
))
)
throw Error('Partilha não recuperável; originais conservados.');
for (const r of c.rows)
if (
r?.association !== undefined &&
(!r.association ||
!['', 'participation', 'quota'].includes(r.association.kind) ||
!['', 'yes', 'no'].includes(r.association.effectivelyTaxed) ||
typeof r.association.reference !== 'string')
)
throw Error('Associação não recuperável; originais conservados.');
const keys = Object.keys(newEconomicPosition('')).filter(
(k) => k !== 'previousUse',
);
for (const r of c.rows)
if (
!r ||
(r.nationalMethod !== undefined &&
!['documented', 'proportional'].includes(r.nationalMethod)) ||
(r.nationalBasisReference !== undefined &&
typeof r.nationalBasisReference !== 'string') ||
keys.some(
(k) =>
typeof (r as unknown as Record<string, unknown>)[k] !==
'string',
) ||
!['', 'ordinary_profit', 'liquidation', 'association'].includes(
r.kind,
) ||
[
'recipientResident',
'payerResident',
'payerIrpc',
'payerExempt',
'article40',
'incomeIncluded',
].some((k) => !['', 'yes', 'no'].includes(r[k as 'payerIrpc'])) ||
!r.previousUse ||
!['', 'known', 'absent'].includes(r.previousUse.state) ||
typeof r.previousUse.amount !== 'string' ||
typeof r.previousUse.reference !== 'string'
)
throw Error(
'Posição económica não recuperável; originais conservados.',
);
}
const zero = () => fraction(0n);
const minimum = (...v: FiscalFraction[]) =>
v.reduce((a, b) => (creditCompare(a, b) < 0n ? a : b));
const validDate = (s: string) =>
/^20\d{2}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s;
export const economicDistributionIdentity = (r: EconomicPosition) =>
JSON.stringify([
r.payerNuit,
canonicalParcel(r.distributionDocument),
canonicalParcel(r.distributionParcel),
]);
export function economicTaxProof(r: EconomicPosition) {
if (
!r.taxIssuer.trim() ||
!r.taxDocument.trim() ||
!r.taxParcel.trim() ||
!r.parcelReference.trim() ||
!r.attributionReference.trim() ||
!r.priorAttributionReference.trim() ||
r.currency !== 'MZN'
)
throw Error(
'IRPC correspondente exige documento, emitente/parcela, suporte da atribuição aos lucros e montante MZN; não é retenção do beneficiário nem imposto estrangeiro.',
);
const proofAmount = creditMoney(r.proofAmount),
value = creditMoney(r.taxAmount),
previouslyUsed = creditMoney(r.previouslyAttributed);
if (creditCompare(addFractions([value, previouslyUsed]), proofAmount) > 0n)
throw Error(
'IRPC atribuído aos lucros mais parcelas já atribuídas ultrapassam o comprovado disponível.',
);
return { proofAmount, value, previouslyUsed };
}
export function economicGroupPending(c: EconomicCredits) {
const pending: string[] = [];
if (!c.state || !c.reference.trim())
pending.push(
'Crédito económico: inventário conhecido/ausente e suporte em falta.',
);
if (c.state === 'absent' && c.rows.length)
pending.push(
'Ausência de crédito económico contradiz lucros registados; conserve e reconcilie.',
);
if (c.state === 'known' && !c.rows.length)
pending.push(
'Crédito económico conhecido sem lucros/IRPC discriminados; zero não é ausência presumida.',
);
if (
new Set(c.rows.map(economicDistributionIdentity)).size !== c.rows.length
)
pending.push(
'Mesma parcela de lucros distribuídos repetida; consolide a origem, mesmo que o documento de imposto ou a data tenham sido alterados.',
);
return pending;
}
export function economicIncome(r: EconomicPosition) {
const income = creditMoney(r.income);
if (r.kind === 'ordinary_profit') return { income };
if (r.kind === 'association') {
const a = r.association;
if (!a?.kind || !a.reference.trim())
throw Error(
'Identifique associação em participação ou à quota e o suporte da adaptação64(4).',
);
if (a.kind === 'participation' && a.effectivelyTaxed !== 'yes')
throw Error(
'Associação em participação exige rendimentos distribuídos efectivamente tributados; desconhecido não significa sim.',
);
return {
income,
association: {
version: 'IRPC-economic-association/1',
...structuredClone(a),
},
};
}
if (r.kind !== 'liquidation')
throw Error('Natureza económica por identificar.');
if (!r.liquidation?.reference.trim())
throw Error(
'Partilha64(3): identifique o valor atribuído, entradas efectivas para capital e classificação documental do rendimento.',
);
const attributed = creditMoney(r.liquidation.attributed),
paidInCapital = creditMoney(r.liquidation.paidInCapital);
if (creditCompare(income, attributed) > 0n)
throw Error(
'Rendimento da partilha não pode exceder o valor atribuído.',
);
const difference = subtractFractions(attributed, paidInCapital);
const ceiling =
creditCompare(difference, zero()) > 0n ? difference : zero();
const eligibleIncome = minimum(income, ceiling);
return {
income: eligibleIncome,
liquidation: {
version: 'IRPC-economic-liquidation/1',
attributed,
paidInCapital,
capitalIncome: income,
ceiling,
eligibleIncome,
excessCapitalGain: subtractFractions(income, eligibleIncome),
reference: r.liquidation.reference,
},
};
}
export function economicPosition(
r: EconomicPosition,
subjectNuit: string,
year: string,
assessmentKey: string,
colecta: FiscalFraction | null,
proofIssues: string[],
basis?: { ordinary: boolean; taxableBase: FiscalFraction | null },
) {
const pending = [...proofIssues];
let nominal: FiscalFraction | null = null,
nationalLimit: FiscalFraction | null = null,
eligible: FiscalFraction | null = null,
previousUse: FiscalFraction | null = null,
available: FiscalFraction | null = null,
included: FiscalFraction | null = null;
let liquidation: ReturnType<typeof economicIncome>['liquidation'];
let association: ReturnType<typeof economicIncome>['association'];
let nationalDerivation: {
version: string;
included: FiscalFraction;
taxableBase: FiscalFraction;
colecta: FiscalFraction;
reference: string;
} | null = null;
let status: 'qualified' | 'outside_scope' | 'indeterminate' =
'indeterminate';
try {
if (
!r.id.trim() ||
!/^\d{9}$/.test(r.payerNuit) ||
r.subjectNuit !== subjectNuit ||
year !== '2026' ||
r.year !== year ||
r.currency !== 'MZN'
)
throw Error(
'Titular, entidade distribuidora, exercício2026 e MZN por corresponder.',
);
if (
!['ordinary_profit', 'liquidation', 'association'].includes(r.kind)
) {
status = r.kind ? 'outside_scope' : 'indeterminate';
throw Error(
'Identifique lucros ordinários64(1–2), partilha64(3) ou associação64(4), com correspondência própria; não converter a parcela em crédito internacional.',
);
}
if (
r.recipientResident !== 'yes' ||
r.payerResident !== 'yes' ||
r.payerIrpc !== 'yes' ||
r.payerExempt !== 'no' ||
!r.qualificationReference.trim()
)
throw Error(
'Exige sede/direcção efectiva em Moçambique de beneficiário e distribuidor, distribuidor sujeito a IRPC e não isento, com suporte.',
);
if (r.article40 !== 'no' || !r.article40Reference.trim()) {
status = r.article40 === 'yes' ? 'outside_scope' : 'indeterminate';
throw Error(
r.article40 === 'yes'
? 'Exclusão40(1) identificada: tratar na base; não somar crédito64 à mesma parcela excluída.'
: 'Falta corresponder a não aplicação de40(1), incluindo a redacção20/2009; desconhecido não significa não aplicável.',
);
}
if (
!validDate(r.distributionDate) ||
!r.distributionDate.startsWith(year + '-') ||
!r.distributionDocument.trim() ||
!r.distributionParcel.trim()
)
throw Error(
'Documento/parcela e data da distribuição do exercício por identificar.',
);
if (
!r.assessmentKey ||
r.assessmentKey !== assessmentKey ||
r.incomeIncluded !== 'yes' ||
!r.incomeReference.trim()
)
throw Error(
'Confirme a inclusão dos lucros ilíquidos de retenção e a correspondência com a colecta corrente; a origem pode ter mudado.',
);
const qualified = economicIncome(r);
liquidation = qualified.liquidation;
association = qualified.association;
const tax = economicTaxProof(r),
income = qualified.income;
nominal = fraction(
BigInt(tax.value.numerator) * 3n,
BigInt(tax.value.denominator) * 5n,
);
if (
creditCompare(income, zero()) === 0n &&
creditCompare(nominal, zero()) > 0n
)
throw Error(
'Lucros incluídos zero contradizem IRPC correspondente positivo.',
);
if (
!r.grossUpReference.trim() ||
creditCompare(creditLimit(r.grossUpIncluded), nominal) !== 0n
)
throw Error(
'Acréscimo53(1)a incluído na base deve corresponder ao crédito nominal60%; reconcilie L6 sem alterar a colecta ou o histórico automaticamente.',
);
included = addFractions([income, nominal]);
if (r.nationalMethod === 'proportional') {
if (
!basis?.ordinary ||
!basis.taxableBase ||
creditCompare(basis.taxableBase, zero()) <= 0n ||
!colecta ||
creditCompare(colecta, zero()) < 0n ||
!r.nationalBasisReference?.trim()
)
throw Error(
'Quota económica derivada exige colecta L6 ordinária corrente, matéria colectável positiva e correspondência dos lucros mais acréscimo53 já incluídos.',
);
if (creditCompare(included, basis.taxableBase) > 0n)
throw Error(
'Lucros mais acréscimo53 excedem a matéria colectável; reconcilie a inclusão antes de derivar a quota.',
);
nationalLimit = fraction(
BigInt(colecta.numerator) *
BigInt(included.numerator) *
BigInt(basis.taxableBase.denominator),
BigInt(colecta.denominator) *
BigInt(included.denominator) *
BigInt(basis.taxableBase.numerator),
);
nationalDerivation = {
version: 'IRPC-economic-quota/1',
included,
taxableBase: structuredClone(basis.taxableBase),
colecta: structuredClone(colecta),
reference: r.nationalBasisReference,
};
} else nationalLimit = creditLimit(r.nationalLimit);
if (
(r.nationalMethod !== 'proportional' &&
!r.nationalLimitReference.trim()) ||
!colecta ||
creditCompare(nationalLimit, colecta) > 0n ||
(creditCompare(included, zero()) === 0n &&
creditCompare(nationalLimit, zero()) > 0n)
)
throw Error(
'Quota proporcional da colecta após acréscimo53 exige suporte e correspondência; não pode exceder a colecta nem ser positiva para rendimento zero.',
);
if (!r.previousUse.state || !r.previousUse.reference.trim())
throw Error(
'Utilização anterior efectiva do crédito em falta; simulação não é utilização fiscal.',
);
if (r.previousUse.state === 'absent') {
if (r.previousUse.amount.trim())
throw Error('Ausência de uso anterior contradiz montante');
previousUse = zero();
} else previousUse = creditLimit(r.previousUse.amount);
eligible = minimum(nominal, nationalLimit);
if (creditCompare(previousUse, eligible) > 0n)
throw Error('Uso anterior excede o crédito económico elegível.');
if (!pending.length) {
available = subtractFractions(eligible, previousUse);
status = 'qualified';
}
} catch (e) {
pending.push(String(e));
}
return {
id: r.id,
nominal,
nationalLimit,
...(nationalDerivation ? { nationalDerivation } : {}),
...(liquidation ? { liquidation } : {}),
...(association ? { association } : {}),
eligible,
previousUse,
available,
included,
status,
pending,
input: structuredClone(r),
};
}
export function economicDeduction(
rows: ReturnType<typeof economicPosition>[],
colecta: FiscalFraction | null,
ready: boolean,
) {
const available = addFractions(
rows.flatMap((r) =>
r.available && !r.pending.length ? [r.available] : [],
),
);
const deducted = ready && colecta ? minimum(available, colecta) : null;
return {
version: economicVersion,
rows,
available,
deducted,
afterCredit:
deducted && colecta ? subtractFractions(colecta, deducted) : null,
source:
economicSource +
(rows.some((r) => r.association)
? ' IRPC-economic-association/1:CIRPC64(4), associação em participação com tributação efectiva documentada ou associação à quota, com adaptação, inclusão L6 e imposto correspondente expressamente suportados.'
: '') +
(rows.some((r) => r.liquidation)
? ' IRPC-economic-liquidation/1:CIRPC64(3); rendimento de capitais documentado limitado à diferença positiva entre valor atribuído e entradas efectivas para capital. Excesso separado como mais-valia, sem crédito económico sobre essa parcela; inclusão L6 e imposto subjacente exigem correspondência expressa.'
: '') +
(rows.some((r) => r.nationalDerivation)
? ' IRPC-economic-quota/1:quota ordinária derivada da colecta e matéria colectável L6 com lucros mais acréscimo53 já correspondidos;sem alterar a base ou o acréscimo.'
: ''),
officialBalancesConsumed: false,
carryCreated: false,
};
}