79 lines
10 KiB
TypeScript
79 lines
10 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;
|
||
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};
|
||
}
|