95 lines
9.8 KiB
TypeScript
95 lines
9.8 KiB
TypeScript
import {countryIsoCodes} from './country-iso-codes.ts';
|
||
import {creditCompare, creditMoney, creditLimit} from './irpc-international-credit.ts';
|
||
import {addFractions, fraction, subtractFractions, type FiscalFraction} from './fiscal-rational.ts';
|
||
|
||
export const carryVersion = 'IRPC-credit-carry/1';
|
||
export const carrySource = 'CIRPC, Lei 34/2007, art.65(3): crédito internacional elegível não deduzido por insuficiência de colecta, até ao fim dos cinco exercícios seguintes; art.66(1–2): PEC até três exercícios seguintes, depois dos créditos precedentes, sem reembolso. Exemplar BR52, PDF18/p.768-(72). Recorte: fecho2026, mesmo sujeito, exercícios anuais ordinários, montantes comprovados em MZN. A origem e os usos efectivos são documentados; simulações não criam nem consomem saldos oficiais.';
|
||
type State = '' | 'known' | 'absent';
|
||
export type CarryUse = {id: string; year: string; amount: string; document: string; reference: string};
|
||
export type CarryLot = {
|
||
id: string; kind: '' | 'international' | 'pec'; country: string; subjectNuit: string; year: string;
|
||
currency: string; ordinary: '' | 'yes' | 'no'; originDocument: string; originReference: string;
|
||
ruleReference: string; eligible: string; originDeducted: string; insufficient: '' | 'yes' | 'no';
|
||
usesState: State; usesReference: string; uses: CarryUse[]; selected: string;
|
||
};
|
||
export type CreditCarry = {
|
||
version: 1; state: State; reference: string; lots: CarryLot[];
|
||
currentInternational: string; currentPec: string; allocationReference: string; assessmentKey: string;
|
||
};
|
||
export const newCreditCarry = (): CreditCarry => ({version: 1, state: '', reference: '', lots: [], currentInternational: '', currentPec: '', allocationReference: '', assessmentKey: ''});
|
||
export const newCarryLot = (id: string): CarryLot => ({id, kind: '', country: '', subjectNuit: '', year: '', currency: 'MZN', ordinary: '', originDocument: '', originReference: '', ruleReference: '', eligible: '', originDeducted: '', insufficient: '', usesState: '', usesReference: '', uses: [], selected: ''});
|
||
export const newCarryUse = (id: string): CarryUse => ({id, year: '', amount: '', document: '', reference: ''});
|
||
const states = ['', 'known', 'absent'];
|
||
const zero = () => fraction(0n);
|
||
const canonical = (s: string) => s.normalize('NFKC').trim().toLocaleLowerCase('pt').replace(/\s+/g, ' ');
|
||
// One consolidated balance per nature/country/origin exercise; changing the document does not create a second quota.
|
||
export const carryIdentity = (r: CarryLot) => JSON.stringify([r.subjectNuit, r.kind, r.country, r.year]);
|
||
export function validateCreditCarry(c: CreditCarry) {
|
||
if (!c || c.version !== 1 || !states.includes(c.state) || ['reference', 'currentInternational', 'currentPec', 'allocationReference', 'assessmentKey'].some(k => typeof (c as unknown as Record<string, unknown>)[k] !== 'string') || !Array.isArray(c.lots)) throw Error('Saldos anteriores não recuperáveis; originais conservados.');
|
||
const keys = Object.keys(newCarryLot('')).filter(k => k !== 'uses');
|
||
const ids: string[] = [];
|
||
for (const r of c.lots) {
|
||
if (!r || keys.some(k => typeof (r as unknown as Record<string, unknown>)[k] !== 'string') || !['', 'international', 'pec'].includes(r.kind) || !['', 'yes', 'no'].includes(r.ordinary) || !['', 'yes', 'no'].includes(r.insufficient) || !states.includes(r.usesState) || !Array.isArray(r.uses)) throw Error('Origem do saldo anterior não recuperável.');
|
||
ids.push(r.id);
|
||
for (const u of r.uses) {
|
||
if (!u || Object.keys(newCarryUse('')).some(k => typeof (u as unknown as Record<string, unknown>)[k] !== 'string')) throw Error('Utilização anterior não recuperável.');
|
||
ids.push(u.id);
|
||
}
|
||
}
|
||
if (new Set(ids).size !== ids.length || ids.some(id => !id.trim())) throw Error('Identificador de saldo/utilização ausente ou repetido.');
|
||
}
|
||
export function calculateCarryLot(r: CarryLot, subjectNuit: string, year: string) {
|
||
let opening: FiscalFraction | null = null, used: FiscalFraction | null = null, available: FiscalFraction | null = null, selected: FiscalFraction | null = null, remaining: FiscalFraction | null = null;
|
||
let lastYear: number | null = null, expired = false;
|
||
const pending: string[] = [];
|
||
try {
|
||
if (!r.kind || r.subjectNuit !== subjectNuit || !/^\d{9}$/.test(r.subjectNuit) || year !== '2026' || !/^20\d{2}$/.test(r.year) || Number(r.year) >= Number(year) || r.ordinary !== 'yes' || r.currency !== 'MZN') throw Error('Origem exige mesmo titular, exercício anterior anual ordinário e montante documentado em MZN.');
|
||
if (r.kind === 'international' ? !countryIsoCodes.includes(r.country) || r.country === 'MZ' : r.country !== '') throw Error('País estrangeiro obrigatório para crédito internacional; PEC não tem país estrangeiro.');
|
||
if (!r.originDocument.trim() || !r.originReference.trim() || !r.ruleReference.trim()) throw Error('Faltam apuramento original e regra aplicável ao exercício de origem, com suporte.');
|
||
if (r.insufficient !== 'yes') throw Error('Falta comprovar que o remanescente resulta de insuficiência de colecta na origem; imposto acima do limite não é saldo transportável.');
|
||
lastYear = Number(r.year) + (r.kind === 'international' ? 5 : 3);
|
||
expired = Number(year) > lastYear;
|
||
const amount = r.kind === 'international' ? creditLimit : creditMoney;
|
||
const eligible = amount(r.eligible), originDeducted = amount(r.originDeducted);
|
||
if (creditCompare(originDeducted, eligible) > 0n) throw Error('Dedução na origem ultrapassa o montante elegível comprovado.');
|
||
opening = subtractFractions(eligible, originDeducted);
|
||
if (!r.usesState || !r.usesReference.trim() || r.usesState === 'absent' && r.uses.length || r.usesState === 'known' && !r.uses.length) throw Error('Utilizações efectivas posteriores à origem exigem inventário conhecido ou ausência expressa com suporte.');
|
||
const documents = new Set<string>();
|
||
const values = r.uses.map(u => {
|
||
if (!/^20\d{2}$/.test(u.year) || Number(u.year) <= Number(r.year) || Number(u.year) > Number(year) || Number(u.year) > lastYear! || !u.document.trim() || !u.reference.trim()) throw Error('Uso exige exercício elegível, posterior à origem, documento e suporte; pode incluir uso efectivo já ocorrido no exercício corrente.');
|
||
const identity = canonical(u.document);
|
||
if (documents.has(identity)) throw Error('Documento de utilização repetido; consolide a dedução efectiva.');
|
||
documents.add(identity);
|
||
return amount(u.amount);
|
||
});
|
||
used = addFractions(values);
|
||
if (creditCompare(used, opening) > 0n) throw Error('Utilizações efectivas ultrapassam o remanescente da origem.');
|
||
const balance = subtractFractions(opening, used);
|
||
selected = amount(r.selected);
|
||
available = expired ? zero() : balance;
|
||
if (creditCompare(selected, available) > 0n) throw Error(expired ? 'Prazo ordinário terminado; saldo conservado no histórico, sem dedução neste exercício.' : 'Parcela seleccionada ultrapassa o saldo documentado disponível.');
|
||
remaining = subtractFractions(balance, selected);
|
||
} catch (e) { pending.push(String(e)); selected = null; }
|
||
return {id: r.id, kind: r.kind, country: r.country, year: r.year, opening, used, available, selected, remaining, lastYear, expired, pending, input: structuredClone(r)};
|
||
}
|
||
export function carryPlan(c: CreditCarry, subjectNuit: string, year: string, assessmentKey: string, currentInternational: FiscalFraction, currentPec: FiscalFraction, colecta: FiscalFraction | null, duplicates: string[] = []) {
|
||
validateCreditCarry(c);
|
||
const pending = [...duplicates];
|
||
if (!c.state || !c.reference.trim() || c.state === 'absent' && c.lots.length || c.state === 'known' && !c.lots.length) pending.push('Inventário de saldos anteriores em falta ou contraditório; desconhecido não significa ausente.');
|
||
const ids = c.lots.map(carryIdentity);
|
||
if (new Set(ids).size !== ids.length) pending.push('Saldo de mesma natureza/país/exercício repetido; consolide a origem e os usos.');
|
||
const lots = c.lots.map(r => calculateCarryLot(r, subjectNuit, year));
|
||
pending.push(...lots.flatMap(r => r.pending.map(p => r.year + '/' + r.kind + ': ' + p)));
|
||
let international: FiscalFraction | null = null, pec: FiscalFraction | null = null;
|
||
try {
|
||
if (c.state !== 'known') return {version: carryVersion, lots, international, pec, priorInternational: zero(), priorPec: zero(), pending, source: carrySource, officialBalancesConsumed: false, carryCreated: false};
|
||
if (!c.allocationReference.trim() || !c.assessmentKey || c.assessmentKey !== assessmentKey) throw Error('Confirme a escolha das parcelas correntes e anteriores com suporte na colecta seleccionada; não se presume prioridade entre exercícios.');
|
||
international = creditLimit(c.currentInternational); pec = creditMoney(c.currentPec);
|
||
if (creditCompare(international, currentInternational) > 0n || creditCompare(pec, currentPec) > 0n) throw Error('Parcela corrente escolhida ultrapassa o respectivo crédito/PEC comprovado disponível.');
|
||
} catch (e) {pending.push(String(e));}
|
||
const priorInternational = addFractions(lots.filter(r => r.kind === 'international' && !r.pending.length && r.selected).map(r => r.selected!));
|
||
const priorPec = addFractions(lots.filter(r => r.kind === 'pec' && !r.pending.length && r.selected).map(r => r.selected!));
|
||
if (international && pec && colecta && creditCompare(addFractions([international, pec, priorInternational, priorPec]), colecta) > 0n) pending.push('As parcelas escolhidas ultrapassam a colecta; ajuste a afectação entre exercícios antes de confirmar, sem reduzir valores automaticamente.');
|
||
return {version: carryVersion, lots, international, pec, priorInternational, priorPec, pending, source: carrySource, officialBalancesConsumed: false, carryCreated: false};
|
||
}
|