453 lines
18 KiB
TypeScript
453 lines
18 KiB
TypeScript
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(' ') ?? '';
|
||
}
|