Files
Investidor-V2/resources/js/lib/irpc-pec-obligation.ts
T

453 lines
18 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 { advanceCalendar } from './irpc-advance-calendar.ts';
import {
ordinaryAdvancePeriodIssues,
type AdvanceFiscalPeriod,
} from './irpc-ppc-obligation.ts';
import {
validatePecCorrection,
correctedPecTurnover,
pecCorrectionSource,
type PecCorrection,
} from './irpc-pec-correction.ts';
import {
validatePecPlan,
pecPlanReport,
type PecPlan,
} from './irpc-pec-plan.ts';
import {
decimalFraction,
fraction,
subtractFractions,
fractionPresentation,
type FiscalFraction,
} from './fiscal-rational.ts';
export const pecObligationVersion = 'IRPC-PEC-obligation/1';
export const pecPpcExcessVersion = 'IRPC-PEC-PPC-excess/1';
export const pecObligationSource =
'RIRPC,Decreto9/2008,art29(1–5),BR16/I16-04-2008,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee.0,5% do volume de negócios anterior,limites30.000/100.000MZN,menos PPC efectivamente pagos no ano anterior.Junho/Agosto/Outubro;opção de pagamento único prevista,sem data ou repartição inventada. Ano de início excluído29(4);sem volume anterior exige último montante29(5). Recorte2026,residente geral comercial/industrial/agrícola,ano civil,MZN. Regimes/períodos especiais,rectificação29(3),base de anos anteriores29(5),guias/juros separados.';
export type PecLastAmount = {
version: 1;
subjectNuit: string;
year: string;
amount: string;
reference: string;
confirmed: '' | 'yes';
};
export const pecLastAmountSource =
'RIRPC,Decreto9/2008,art29(5),BR16/I16-04-2008,p122/PDF24,original SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee. Hipótese documentada: último montante PEC já determinado,para titular/ano civil/MZN correspondentes;sem recalcular 0,5%,limites ou deduzir PPC pela segunda vez. Não demonstra por si a qualificação fiscal do caso.';
export type PecObligation = {
lastAmount?: PecLastAmount;
version: 1;
plan?: PecPlan;
correction?: PecCorrection;
scope: '' | 'ordinary_resident' | 'other';
subjectNuit: string;
year: string;
activityStartYear: string;
activityReference: string;
previousYear: string;
previousTurnover: string;
turnoverReference: string;
previousPpcPaid: string;
ppcReference: string;
};
export const newPecObligation = (
subjectNuit = '',
year = '',
): PecObligation => ({
version: 1,
scope: '',
subjectNuit,
year,
activityStartYear: '',
activityReference: '',
previousYear: '',
previousTurnover: '',
turnoverReference: '',
previousPpcPaid: '',
ppcReference: '',
});
export function validatePecObligation(p: PecObligation) {
if (p?.lastAmount !== undefined) {
const v = p.lastAmount;
if (
!v ||
v.version !== 1 ||
!['', 'yes'].includes(v.confirmed) ||
['subjectNuit', 'year', 'amount', 'reference'].some(
(k) =>
typeof (v as unknown as Record<string, unknown>)[k] !==
'string',
)
)
throw Error(
'Referência PEC anterior não recuperável;original preservado.',
);
}
if (p?.correction !== undefined) validatePecCorrection(p.correction);
if (p?.plan !== undefined) validatePecPlan(p.plan);
if (
!p ||
p.version !== 1 ||
Object.keys(newPecObligation())
.filter((k) => k !== 'version')
.some(
(k) =>
typeof (p as unknown as Record<string, unknown>)[k] !==
'string',
) ||
!['', 'ordinary_resident', 'other'].includes(p.scope)
)
throw Error(
'Preparação da estimativa PEC não recuperável;original preservado.',
);
}
export function calculatePecObligation(
p: PecObligation,
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePecObligation(p);
const pending: string[] = [];
const calendar = advanceCalendar(period, year, subjectNuit);
const turnover = correctedPecTurnover(
p.correction,
p.previousTurnover,
p.subjectNuit,
p.previousYear,
);
let percentage: FiscalFraction | null = null,
limited: FiscalFraction | null = null,
total: FiscalFraction | null = null,
ppcExcess: FiscalFraction | null = null,
notApplicable = false,
lastAmountApplied = false;
if (
p.scope !== 'ordinary_resident' ||
assessment !== 'normal' ||
year !== '2026' ||
p.year !== year ||
p.subjectNuit !== subjectNuit ||
!/^\d{9}$/.test(subjectNuit)
)
pending.push(
'PEC: confirmar titular2026,residente geral com actividade comercial/industrial/agrícola,ano civil e MZN;outros regimes/períodos separados.',
);
if (
!/^\d{4}$/.test(p.activityStartYear) ||
Number(p.activityStartYear) < 1 ||
Number(p.activityStartYear) > 2026 ||
!p.activityReference.trim()
)
pending.push(
'PEC: falta ano de início efectivo da actividade e suporte;constituição da empresa não substitui início.',
);
if (!pending.length && p.activityStartYear === '2026') {
const validStartDate = (value?: string | null) =>
Boolean(
value &&
/^2026-\d{2}-\d{2}$/.test(value) &&
Number.isFinite(Date.parse(value)) &&
new Date(value).toISOString().slice(0, 10) === value,
);
if (
period &&
!(
validStartDate(period.from) &&
validStartDate(period.to) &&
period.from! <= period.to!
)
)
pending.push(
...ordinaryAdvancePeriodIssues(
period,
year,
'PEC',
subjectNuit,
),
);
if (calendar.mode === 'annual_non_civil')
pending.push(
'PEC: início de actividade em período não civil exige correspondência própria;fora deste recorte de continuidade.',
);
if (!pending.length) {
notApplicable = true;
total = fraction(0n);
}
} else if (!pending.length) {
pending.push(
...ordinaryAdvancePeriodIssues(period, year, 'PEC', subjectNuit),
);
if (
calendar.mode === 'annual_non_civil' &&
(!period?.pecBasisKey ||
period.declaration?.pecBasisKey !== period.pecBasisKey)
)
pending.push(
'PEC: montantes ou fontes alterados;reconfira a sua correspondência com o período anterior,sem substituir os valores.',
);
pending.push(...turnover.pending);
if (
calendar.mode === 'annual_non_civil' &&
Number(p.activityStartYear) >=
Number(calendar.previousFrom.slice(0, 4))
)
pending.push(
'PEC: este recorte exige actividade já iniciada antes do período anterior documentado;início ou transição têm correspondência própria.',
);
if (
p.previousYear !== '2025' ||
!p.turnoverReference.trim() ||
!p.ppcReference.trim()
)
pending.push(
'PEC: falta exercício anterior2025 e fontes do volume de negócios e PPC efectivamente pagos,mesmo quando zero.',
);
if (
![turnover.value, p.previousPpcPaid].every((v) =>
/^\d{1,18}(\.\d{1,2})?$/.test(v),
)
)
pending.push(
'PEC: valores anteriores desconhecidos/inválidos;zero deve ser explícito.',
);
if (!pending.length) {
const v = decimalFraction(turnover.value),
ppc = decimalFraction(p.previousPpcPaid);
if (
BigInt(v.numerator) === 0n &&
calendar.mode === 'annual_non_civil'
) {
pending.push(
'PEC29(5): último montante num período não civil exige correspondência da sua origem;fora deste recorte de continuidade.',
);
} else if (BigInt(v.numerator) === 0n) {
const last = p.lastAmount;
if (
!last ||
last.confirmed !== 'yes' ||
!last.reference.trim() ||
last.subjectNuit !== subjectNuit ||
!/^\d{4}$/.test(last.year) ||
Number(last.year) < Number(p.activityStartYear) ||
Number(last.year) >= Number(year) ||
!/^\d{1,18}(\.\d{1,2})?$/.test(last.amount)
) {
pending.push(
'PEC29(5):sem volume de negócios em2025,recuperar último montante PEC já determinado dos anos anteriores,com titular,exercício,fonte e confirmação de que é o último aplicável;zero não determina automaticamente o mínimo.',
);
} else {
total = decimalFraction(last.amount);
lastAmountApplied = true;
}
} else {
percentage = fraction(
BigInt(v.numerator),
BigInt(v.denominator) * 200n,
);
const n = BigInt(percentage.numerator),
d = BigInt(percentage.denominator);
limited =
n < 30000n * d
? fraction(30000n)
: n > 100000n * d
? fraction(100000n)
: percentage;
const residual = subtractFractions(limited, ppc);
if (BigInt(residual.numerator) < 0n) {
total = fraction(0n);
ppcExcess = subtractFractions(ppc, limited);
} else total = residual;
}
}
}
let source = p.correction
? pecObligationSource.replace('rectificação29(3),', '') +
' ' +
pecCorrectionSource
: pecObligationSource;
if (p.lastAmount)
source =
source.replace('base de anos anteriores29(5),', '') +
' ' +
pecLastAmountSource;
if (calendar.mode === 'annual_non_civil')
source =
source
.replace(
'ano civil,MZN',
'período anual não civil completo,MZN',
)
.replace('Junho/Agosto/Outubro', '6.º/8.º/10.º mês do período')
.replace(
'Regimes/períodos especiais',
'Períodos curtos e outros regimes',
) +
' ' +
calendar.source;
if (ppcExcess)
source +=
' Reconciliação da estimativa29(2):PPC documentados cobrem a base limitada;diferença conservada sem gerar direito a reembolso ou crédito.';
return {
version: pecObligationVersion,
source,
nonCivil: calendar.mode === 'annual_non_civil',
lastAmountSource: p.lastAmount ? pecLastAmountSource : null,
pending,
percentage,
limited,
total,
notApplicable,
lastAmountApplied,
ppcExcess,
requiresLastAmount:
!notApplicable &&
turnover.pending.length === 0 &&
/^0+(\.0+)?$/.test(turnover.value),
turnoverCorrection: p.correction
? {
original: p.correction.originalTurnover,
revised: p.correction.revisedTurnover,
applied: !notApplicable && turnover.applied,
pending: turnover.pending,
reference: p.correction.reference,
source: pecCorrectionSource,
}
: null,
months: total && !notApplicable ? calendar.pecMonths : [],
instalmentAmountsDetermined: false,
paymentExecuted: false,
annualDeductionCreated: false,
};
}
export function pecObligationReport(
p: PecObligation,
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePecObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
const originalEstimate = p.correction
? calculatePecObligation(
{ ...p, correction: undefined },
subjectNuit,
year,
assessment,
period,
).total
: null;
const correctionEffect =
originalEstimate && m.total
? fractionPresentation(subtractFractions(m.total, originalEstimate))
.decimal + ' MZN'
: 'por determinar';
return [
...(m.ppcExcess
? [
[
'IRPC — L8',
'PEC — PPC acima da base limitada',
'PPC documentados: ' +
p.previousPpcPaid +
' MZN;base limitada: ' +
show(m.limited) +
';diferença: ' +
show(m.ppcExcess) +
';estimativa PEC: 0 MZN. A diferença é apenas reconciliação desta hipótese; não cria reembolso,crédito,transporte ou dedução anual.',
p.ppcReference +
'; ' +
p.turnoverReference +
'; RIRPC29(2); ' +
pecPpcExcessVersion,
] as [string, string, string, string],
]
: []),
...(p.lastAmount
? [
[
'IRPC — L8',
'PEC — último montante documentado',
'Exercício ' +
(p.lastAmount.year || 'por confirmar') +
';montante PEC já determinado: ' +
(p.lastAmount.amount || 'por confirmar') +
' MZN;' +
(m.lastAmountApplied
? 'usado como hipótese documentada29(5),sem novo cálculo percentual ou segunda dedução de PPC.'
: 'conservado sem aplicação à estimativa.') +
' Não regista pagamento nem dedução anual.',
(p.lastAmount.reference || 'Fonte por indicar') +
'; ' +
pecLastAmountSource,
] as [string, string, string, string],
]
: []),
...(p.correction
? [
[
'IRPC — L8',
'PEC — rectificação do volume',
'Original: ' +
p.correction.originalTurnover +
' MZN;corrigido: ' +
p.correction.revisedTurnover +
' MZN;' +
(m.turnoverCorrection?.applied
? 'aplicada à estimativa.'
: 'conservada sem aplicação à estimativa.') +
' Estimativa original: ' +
show(originalEstimate) +
';efeito na estimativa: ' +
correctionEffect +
'. ' +
turnoverCorrectionNotice(m.turnoverCorrection),
p.correction.reference + '; ' + pecCorrectionSource,
] as [string, string, string, string],
]
: []),
...(p.plan
? pecPlanReport(p.plan, m.total, m.notApplicable, year, period)
: []),
[
'IRPC — L8',
'PEC — estimativa fiscal',
m.pending.length
? m.pending.join(' ')
: m.notApplicable
? 'Não aplicável no ano de início da actividade29(4);total0 MZN.'
: m.lastAmountApplied
? 'Hipótese documentada29(5): ' +
show(m.total) +
';último montante PEC já determinado,sem segunda dedução de PPC. Confirmar enquadramento com o suporte indicado.'
: '0,5%: ' +
show(m.percentage) +
';limitado antes de PPC: ' +
show(m.limited) +
';total estimado: ' +
show(m.total) +
';meses: ' +
m.months.join(', ') +
(p.plan
? '. Plano explícito apresentado separadamente;prazo legal não validado.'
: '. Montantes por prestação e opção de pagamento único não determinados neste recorte.') +
' Preparação não é pagamento nem dedução anual.',
p.activityReference +
'; ' +
p.turnoverReference +
'; ' +
p.ppcReference +
'; ' +
m.source,
],
];
}
function turnoverCorrectionNotice(c: { pending: string[] } | null) {
return c?.pending.join(' ') ?? '';
}