Files
Investidor-V2/resources/js/lib/irpc-normal-base.ts
T

363 lines
14 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,
decimalFraction,
fraction,
subtractFractions,
type FiscalFraction,
} from './fiscal-rational.ts';
import type { ProfitEvidence } from './irpc-normal-profit.ts';
export const normalBaseVersion = 'IRPC-normal-adjustments/3';
type Choice = '' | 'yes' | 'no';
export type NormalLoss = {
id: string;
year: string;
subjectNuit: string;
assessed: string;
previouslyUsed: string;
requested: string;
reference: string;
kind: '' | 'ordinary' | 'transfer' | 'other';
annualPeriods: Choice;
generalActivity: Choice;
balanceConfirmed: Choice;
};
export type NormalBaseInput = {
version: 1;
subjectNuit: string;
identityReference: string;
method: '' | 'direct' | 'indirect';
methodReference: string;
lossesState: '' | 'known' | 'absent';
lossesReference: string;
unchangedObject: Choice;
objectReference: string;
losses: NormalLoss[];
benefits: ProfitEvidence;
};
export const newNormalBaseInput = (): NormalBaseInput => ({
version: 1,
subjectNuit: '',
identityReference: '',
method: '',
methodReference: '',
lossesState: '',
lossesReference: '',
unchangedObject: '',
objectReference: '',
losses: [],
benefits: { state: '', amount: '', reference: '' },
});
export const newNormalLoss = (id: string): NormalLoss => ({
id,
year: '',
subjectNuit: '',
assessed: '',
previouslyUsed: '',
requested: '',
reference: '',
kind: '',
annualPeriods: '',
generalActivity: '',
balanceConfirmed: '',
});
export function validateNormalBase(i: NormalBaseInput) {
if (
!i ||
i.version !== 1 ||
[
'subjectNuit',
'identityReference',
'methodReference',
'lossesReference',
'objectReference',
].some((k) => typeof i[k as 'subjectNuit'] !== 'string') ||
!['', 'direct', 'indirect'].includes(i.method) ||
!['', 'known', 'absent'].includes(i.lossesState) ||
!['', 'yes', 'no'].includes(i.unchangedObject) ||
!Array.isArray(i.losses) ||
new Set(i.losses.map((l) => l.id)).size !== i.losses.length ||
!i.benefits ||
!['', 'known', 'absent'].includes(i.benefits.state) ||
typeof i.benefits.amount !== 'string' ||
typeof i.benefits.reference !== 'string'
)
throw Error('Preparação da matéria colectável não recuperável.');
for (const l of i.losses)
if (
!l ||
Object.keys(newNormalLoss('')).some(
(k) => typeof l[k as keyof NormalLoss] !== 'string',
) ||
!['', 'ordinary', 'transfer', 'other'].includes(l.kind) ||
['annualPeriods', 'generalActivity', 'balanceConfirmed'].some(
(k) => !['', 'yes', 'no'].includes(l[k as 'annualPeriods']),
)
)
throw Error('Perda fiscal não recuperável;originais conservados.');
}
export type NormalBaseModel = {
input: NormalBaseInput;
taxableProfit: FiscalFraction | null;
requestedLosses: FiscalFraction;
deductedLosses: FiscalFraction | null;
benefitDeduction: FiscalFraction | null;
taxableBase: FiscalFraction | null;
losses: Array<
NormalLoss & {
available: FiscalFraction | null;
requestedAmount: FiscalFraction | null;
deducted: FiscalFraction | null;
remaining: FiscalFraction | null;
lastOrdinaryYear: number | null;
status: 'eligible' | 'excluded' | 'indeterminate';
reason: string;
}
>;
pending: string[];
source: string;
};
const zero = () => fraction(0n);
function amount(s: string) {
if (!/^\d{1,18}(?:\.\d{1,2})?$/.test(s)) throw Error();
return decimalFraction(s);
}
const units = (f: FiscalFraction) =>
(BigInt(f.numerator) * 100n) / BigInt(f.denominator);
export function calculateNormalBase(
input: NormalBaseInput,
profit: FiscalFraction | null,
year: string,
currentLossReference?: string,
short = false,
): NormalBaseModel {
validateNormalBase(input);
const i = structuredClone(input),
pending: string[] = [],
losses: NormalBaseModel['losses'] = [];
if (year !== '2026')
pending.push('Matéria colectável delimitada ao exercício2026.');
if (!/^\d{9}$/.test(i.subjectNuit) || !i.identityReference.trim())
pending.push(
'Identifique NUIT e suporte do sujeito passivo do lucro e das perdas.',
);
if (i.method !== 'direct' || !i.methodReference.trim())
pending.push(
'Método declarativo/directo e suporte por identificar;apuramento indirecto fora deste recorte (15(2),41(2)).',
);
if (!profit)
pending.push(
'Lucro tributável actual indeterminado;nenhuma dedução transforma informação em falta em zero.',
);
else if (BigInt(profit.numerator) < 0n && !currentLossReference?.trim())
pending.push(
'Resultado fiscal negativo conhecido;tratamento da perda do próprio exercício fora deste recorte,sem matéria colectável zero ou reporte automático.',
);
if (!i.lossesState || !i.lossesReference.trim())
pending.push(
'Situação e completude das perdas anteriores em falta;sem ausência presumida.',
);
if (i.lossesState === 'absent' && i.losses.length)
pending.push(
'Ausência declarada de perdas contradiz os registos existentes;reconcilie sem apagar histórico.',
);
if (i.lossesState === 'known' && !i.losses.length)
pending.push(
'Perdas declaradas conhecidas sem discriminação por exercício.',
);
if (
i.losses.some(
(l) =>
!/^20\d{2}$/.test(l.year) || Number(l.year) + 5 >= Number(year),
) &&
(!i.unchangedObject || !i.objectReference.trim())
)
pending.push(
'Documente manutenção ou alteração do objecto/natureza da actividade no termo do exercício (41(7)).',
);
for (const l of i.losses) {
const r: NormalBaseModel['losses'][number] = {
...l,
available: null,
requestedAmount: null,
deducted: null,
remaining: null,
lastOrdinaryYear: null,
status: 'indeterminate',
reason: '',
};
const issues: string[] = [];
if (!l.reference.trim())
issues.push(
'Documento de apuramento/saldo fiscal por identificar.',
);
if (l.subjectNuit !== i.subjectNuit || !/^\d{9}$/.test(l.subjectNuit))
issues.push('Perda não reconciliada com o mesmo sujeito passivo.');
if (!/^20\d{2}$/.test(l.year) || Number(l.year) >= Number(year))
issues.push(
'Exercício da perda deve ser anterior ao exercício de utilização;perda actual não é dedução a si própria.',
);
if (
i.losses.some(
(q) =>
q.id !== l.id &&
q.year === l.year &&
q.subjectNuit === l.subjectNuit,
)
)
issues.push(
'Saldo anual duplicado;reconcilie um saldo por sujeito/exercício.',
);
if (l.kind !== 'ordinary')
issues.push(
'Transmissão/fusão ou outra origem exige correspondência própria;sem aplicação automática de41-A.',
);
if (l.annualPeriods !== 'yes')
issues.push(
'Contagem por exercícios anuais ordinários não demonstrada;períodos especiais/curtos (41(5)) fora deste recorte.',
);
if (l.generalActivity !== 'yes')
issues.push(
'Origem no regime geral,fora de transparência/isenção parcial ou actividade com redução,por demonstrar (41(4/6)).',
);
if (l.balanceConfirmed !== 'yes')
issues.push(
'Saldo fiscal corrigido e utilizações anteriores por reconciliar (41(3));não usar perda contabilística como fiscal.',
);
try {
const assessed = amount(l.assessed),
used = amount(l.previouslyUsed);
if (units(used) > units(assessed)) throw Error();
r.available = subtractFractions(assessed, used);
} catch {
issues.push(
'Apuramento fiscal e utilização anterior devem ser montantes conhecidos não negativos e coerentes.',
);
}
try {
r.requestedAmount = amount(l.requested);
if (r.available && units(r.requestedAmount) > units(r.available))
issues.push(
'Utilização proposta excede saldo fiscal disponível.',
);
} catch {
issues.push(
'Identifique utilização proposta,não negativa,mesmo quando zero.',
);
}
if (l.annualPeriods === 'yes' && /^20\d{2}$/.test(l.year))
r.lastOrdinaryYear = Number(l.year) + 5;
if (!issues.length) {
if (
r.lastOrdinaryYear! < Number(year) ||
i.unchangedObject === 'no'
) {
r.status = 'excluded';
r.reason =
r.lastOrdinaryYear! < Number(year)
? 'Prazo dos cinco exercícios ordinários posteriores expirado (41(1));saldo não utilizável neste exercício.'
: 'Objecto/natureza alterados conforme declaração;dedução41(1)não aplicável (41(7)).';
if (units(r.requestedAmount!) !== 0n)
issues.push(
'Perda excluída com utilização positiva proposta;corrija a proposta sem dedução presumida.',
);
} else {
r.status = 'eligible';
r.reason =
'Saldo fiscal ordinário documentado,mesmo sujeito,cinco exercícios posteriores;utilização escolhida explicitamente,sem ordemFIFO ou percentagem presumida.';
}
}
if (issues.length) {
r.status = 'indeterminate';
r.reason = issues.join(' ');
pending.push(l.year + ': ' + r.reason);
}
losses.push(r);
}
if (
short &&
losses.some(
(l) =>
l.requestedAmount && BigInt(l.requestedAmount.numerator) > 0n,
)
)
pending.push(
'Período curto:sequência dos períodos posteriores por reconciliar;41(5) exclui da contagem o período de transição7(4)d inferior a6meses,sem agregar todos os períodos curtos ou presumir prazo por ano civil.',
);
const requestedLosses = addFractions(
losses
.filter((l) => l.status === 'eligible')
.map((l) => l.requestedAmount!),
);
if (
profit &&
BigInt(requestedLosses.numerator) * BigInt(profit.denominator) >
(BigInt(profit.numerator) > 0n
? BigInt(profit.numerator) * BigInt(requestedLosses.denominator)
: 0n)
)
pending.push(
'Utilizações propostas excedem o lucro tributável disponível;reparta explicitamente sem criar base negativa ou escolher uma ordem de perdas.',
);
let benefitDeduction: FiscalFraction | null = null;
const benefit = i.benefits;
if (!benefit.state || !benefit.reference.trim())
pending.push(
'Benefícios/deduções à matéria colectável por identificar (15(4));créditos à colecta e retenções não são esta dedução.',
);
else if (benefit.state === 'absent') {
if (benefit.amount.trim())
pending.push(
'Ausência de benefícios contradiz montante preenchido.',
);
else benefitDeduction = zero();
} else {
try {
const value = amount(benefit.amount);
if (units(value) !== 0n)
pending.push(
'Benefício de montante conhecido:regra,condições,limite e ordem próprios por implementar;sem subtracção livre do valor.',
);
else benefitDeduction = value;
} catch {
pending.push(
'Montante conhecido das deduções por preencher ou inválido.',
);
}
}
const ready = pending.length === 0 && profit !== null,
deductedLosses = ready ? requestedLosses : null,
taxableBase = ready
? subtractFractions(
subtractFractions(
BigInt(profit!.numerator) < 0n ? zero() : profit!,
requestedLosses,
),
benefitDeduction!,
)
: null;
for (const l of losses) {
if (l.available) {
l.deducted = ready
? l.status === 'eligible'
? l.requestedAmount!
: zero()
: null;
l.remaining = l.deducted
? subtractFractions(l.available, l.deducted)
: null;
}
}
return {
input: i,
taxableProfit: profit,
requestedLosses,
deductedLosses,
benefitDeduction,
taxableBase,
losses,
pending,
source: 'CIRPC15(1)a/15(2–4),41(1–7),Lei34/2007 PDF5/p768(59),PDF12/p768(66). Recorte2026,residente geral,método declarativo,perdas próprias em exercícios anuais ordinários;cinco exercícios posteriores,sem transferência41-A,regimes especiais ou benefícios positivos implementados. Pedido de utilização expresso e limitado ao saldo/lucro;semFIFO obrigatório ou limite percentual inventado. Matéria colectável simulada,não colecta/imposto a pagar,semM20/M22/submissão ou consumo persistente do saldo oficial.',
};
}