Files
Investidor-V2/resources/js/lib/irpc-economic-credit.ts
T

79 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 {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;
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.');
const keys = Object.keys(newEconomicPosition('')).filter(k => k !== 'previousUse');
for (const r of c.rows) if (!r || 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 economicPosition(r: EconomicPosition, subjectNuit: string, year: string, assessmentKey: string, colecta: FiscalFraction | null, proofIssues: string[]) {
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 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 (r.kind !== 'ordinary_profit') {status = r.kind ? 'outside_scope' : 'indeterminate'; throw Error('Lucros ordinários64(1–2) por identificar; liquidação64(3) e associações64(4) exigem correspondência específica, sem 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 tax = economicTaxProof(r), income = creditMoney(r.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]);
nationalLimit = creditLimit(r.nationalLimit);
if (!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, 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, officialBalancesConsumed: false, carryCreated: false};
}