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

382 lines
13 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 { 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,
};
}