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