510 lines
20 KiB
TypeScript
510 lines
20 KiB
TypeScript
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,
|
||
};
|
||
}
|