Files
Investidor-V2/resources/js/lib/irpc-relief-preparation.ts
T

267 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 type { IrpcRow } from './irpc-withholdings.ts';
export const irpcReliefVersion = 'IRPC-L5-reliefs/1';
export type IrpcReliefPreparation = {
rule?: '' | 'cirpc68d' | 'cirpc68f' | 'ptMz12';
facts?: Partial<
Record<
| 'subjectToIrpc'
| 'notTransparent'
| 'treatyResident'
| 'beneficialOwner'
| 'payerResidentMz'
| 'armLength'
| 'royaltyDefinition',
'' | 'yes' | 'no'
>
>;
kind: 'dispensation' | 'exemption' | 'treaty';
instrument: string;
article: string;
country: string;
residenceDocument: string;
validFrom: string;
validTo: string;
conditions: string;
evidence: string;
};
export const irpcReliefRules = [
['', 'Correspondência por identificar'],
['cirpc68d', 'Dispensa — artigo 68(d), serviços / uso de equipamento'],
['cirpc68f', 'Dispensa — artigo 68(f), gestão de imóveis próprios'],
['ptMz12', 'Portugal–Moçambique — royalties, artigo 12'],
] as const;
const documentLabels: Record<string, string> = {
instrument: 'diploma/convenção',
article: 'artigo',
validFrom: 'início do período',
validTo: 'fim do período',
conditions: 'condições',
evidence: 'documentos',
country: 'Estado contratante',
residenceDocument: 'prova de residência',
};
const factLabels: Record<string, string> = {
subjectToIrpc: 'Titular sujeito a IRPC neste rendimento',
notTransparent: 'Fora da transparência fiscal',
treatyResident: 'Residência convencional em Portugal',
beneficialOwner: 'Beneficiário efectivo',
payerResidentMz: 'Devedor residente em Moçambique',
armLength: 'Sem excesso por relações especiais',
royaltyDefinition: 'Definição convencional de royalties',
};
export function evaluateIrpcRelief(
row: IrpcRow,
eventDate: string,
):
| { applied: false; reason: string }
| {
applied: true;
treatment: 'dispensed' | 'treaty';
rate: number;
reason: string;
rule: string;
} {
const e = row.reliefPreparation;
if (!e)
return {
applied: false,
reason: 'Identifique a regra da dispensa/convenção e os documentos aplicáveis.',
};
const preparation = prepareIrpcRelief(row, e);
if (
preparation.missing.length ||
!preparation.validPeriod ||
eventDate < e.validFrom ||
eventDate > e.validTo
)
return {
applied: false,
reason:
'Informação insuficiente ou período incompatível na dispensa/convenção: ' +
preparation.missing.map((k) => documentLabels[k]).join(', ') +
'. Confira o período e a prova aplicáveis ao primeiro evento.',
};
const f = e.facts ?? {};
const article = e.article.toLowerCase().replace(/[^a-z0-9]/g, '');
if (
(e.rule === 'cirpc68d' && article !== '68d') ||
(e.rule === 'cirpc68f' && article !== '68f') ||
(e.rule === 'ptMz12' && article !== '12')
)
return {
applied: false,
reason: 'O artigo invocado diverge da regra seleccionada; reconcilie a referência sem apagar a prova.',
};
const onAccount =
row.residence === 'resident' ||
(row.residence === 'nonresident' &&
row.permanentEstablishment === 'yes' &&
row.attributable === 'yes');
if (e.rule === 'cirpc68d' || e.rule === 'cirpc68f') {
if (
e.kind !== 'dispensation' ||
f.subjectToIrpc !== 'yes' ||
row.recipientIrpc !== 'yes' ||
!row.ordinary ||
row.ordinary.nonDigital !== 'yes' ||
!row.ordinary.reference.trim()
)
return {
applied: false,
reason: 'Dispensa: identifique o titular sujeito a IRPC relativamente a este rendimento e a classificação não digital, com suporte.',
};
if (
e.rule === 'cirpc68d' &&
(!onAccount || !['equipment', 'services'].includes(row.kind))
)
return {
applied: false,
reason: 'Artigo 68(d) exige rendimento 67(1)b/g com natureza de imposto por conta; não dispensa retenções definitivas.',
};
if (
e.rule === 'cirpc68f' &&
(row.kind !== 'rent' ||
row.ordinary.rentOwnPropertyManagement !== 'yes' ||
f.notTransparent !== 'yes')
)
return {
applied: false,
reason: 'Artigo 68(f): faltam rendas de sociedade de gestão de imóveis próprios e exclusão documentada da transparência fiscal (6(1)c).',
};
return {
applied: true,
treatment: 'dispensed',
rate: 0,
rule: e.rule,
reason: `Dispensa de retenção ${e.rule === 'cirpc68d' ? '68(d), rendimentos 67(1)b/g' : '68(f), rendimentos prediais 67(1)c e 6(1)c'}; CIRPC Lei 34/2007, PDF19/p768(73). Sem retenção nesta operação; não é isenção do rendimento nem crédito anual.`,
};
}
if (e.rule === 'ptMz12') {
if (
e.kind !== 'treaty' ||
!['PT', 'PORTUGAL'].includes(e.country.trim().toUpperCase()) ||
row.residence !== 'nonresident' ||
row.permanentEstablishment !== 'no' ||
!['royalty', 'equipment'].includes(row.kind) ||
row.ordinary?.nonDigital !== 'yes' ||
!row.ordinary.reference.trim()
)
return {
applied: false,
reason: 'Convenção PT–MZ, artigo 12: este recorte exige titular residente em Portugal, sem EP em Moçambique e royalties/uso de equipamento qualificados não digitais. Outros casos conservam correspondência própria.',
};
if (
[
'treatyResident',
'beneficialOwner',
'payerResidentMz',
'armLength',
'royaltyDefinition',
].some((k) => f[k as keyof typeof f] !== 'yes')
)
return {
applied: false,
reason: 'Informação insuficiente: documente residência convencional (art.4), beneficiário efectivo, devedor residente em Moçambique, definição de royalties e ausência de excesso por relações especiais (art.12(2–6)).',
};
return {
applied: true,
treatment: 'treaty',
rate: 10,
rule: e.rule,
reason: 'Limite convencional de 10% sobre royalties brutas: CDT PT–MZ artigo12(2–6), RAR36/1992; definição12(3) e IRPC actualizados pelo Protocolo/RAR36/2009 arts1/7. Cálculo da retenção; formalidades de accionamento e MRFTA não equivalem a submissão ou aceitação AT.',
};
}
return {
applied: false,
reason: 'Dispensa, isenção ou convenção sem correspondência executável neste recorte; conserve os documentos e identifique a regra específica.',
};
}
export const newIrpcReliefPreparation = (): IrpcReliefPreparation => ({
kind: 'dispensation',
instrument: '',
article: '',
country: '',
residenceDocument: '',
validFrom: '',
validTo: '',
conditions: '',
evidence: '',
});
// A preparation records an invocation; it does not activate a tax rule.
export function prepareIrpcRelief(
row: IrpcRow,
evidence: IrpcReliefPreparation,
) {
const missing = (
[
'instrument',
'article',
'validFrom',
'validTo',
'conditions',
'evidence',
] as const
)
.filter((key) => !evidence[key].trim())
.map(String);
if (evidence.kind === 'treaty') {
if (!evidence.country.trim()) missing.push('country');
if (!evidence.residenceDocument.trim())
missing.push('residenceDocument');
}
const validDate = (s: string) =>
/^\d{4}-\d{2}-\d{2}$/.test(s) &&
Number.isFinite(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s;
const validPeriod =
validDate(evidence.validFrom) &&
validDate(evidence.validTo) &&
evidence.validFrom <= evidence.validTo;
const outsidePeriod =
validPeriod &&
row.events.some(
(event) =>
validDate(event.date) &&
(event.date < evidence.validFrom ||
event.date > evidence.validTo),
);
return {
counterparty: row.counterparty,
residence: row.residence,
permanentEstablishment: row.permanentEstablishment,
attributable: row.attributable,
documentRef: row.documentRef,
qualificationRef: row.qualificationRef,
previousReference: row.reliefRef,
evidence: structuredClone(evidence),
missing,
validPeriod,
outsidePeriod,
status: 'preparation' as const,
notice: 'Condições e documentos invocados; o cálculo verifica apenas a regra seleccionada e o período aplicável.',
};
}
export function irpcReliefEvidenceLabel(row: IrpcRow): string {
const e = row.reliefPreparation;
if (!e) return row.reliefRef || 'Sem benefício invocado';
const p = prepareIrpcRelief(row, e);
return `${irpcReliefRules.find(([rule]) => rule === e.rule)?.[1] ?? 'Regra por corresponder'};${e.instrument};artigo:${e.article};período:${e.validFrom}–${e.validTo};Estado:${e.country};residência fiscal:${e.residenceDocument};condições:${e.conditions};provas:${e.evidence};${Object.entries(
e.facts ?? {},
)
.map(
([k, v]) =>
`${factLabels[k] ?? k}:${v === 'yes' ? 'Sim' : v === 'no' ? 'Não' : 'Por esclarecer'}`,
)
.join(
';',
)};${p.missing.length ? 'Falta: ' + p.missing.map((k) => documentLabels[k]).join(', ') : 'Elementos documentais registados'};${p.validPeriod ? 'Período válido' : 'Período por resolver'};referência anterior:${row.reliefRef}`;
}