97 lines
8.2 KiB
TypeScript
97 lines
8.2 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}`;
|
||
}
|