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

221 lines
8.2 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 {
decimalFraction,
fraction,
subtractFractions,
fractionPresentation,
type FiscalFraction,
} from './fiscal-rational.ts';
export type { AdvanceFiscalPeriod } from './irpc-advance-calendar.ts';
import {
advanceCalendar,
type AdvanceFiscalPeriod,
} from './irpc-advance-calendar.ts';
export function ordinaryAdvancePeriodIssues(
period: AdvanceFiscalPeriod | undefined,
year: string,
component: 'PPC' | 'PEC' | 'PPC/PEC' = 'PPC/PEC',
subjectNuit?: string,
): string[] {
return advanceCalendar(period, year, subjectNuit).pending.map(
(message) => component + ': ' + message,
);
}
export const ppcObligationVersion = 'IRPC-PPC-obligation/1';
export const ppcObligationSource =
'RIRPC, Decreto 9/2008, arts.27(1)(a) e28(1–3), BR16/I,16-04-2008,exemplar preservado SHA256 fc373ff5637edf75ec6a9595dec8763a8503d4ee786e3b7d8f1c742eae9d80ee: imposto liquidado no exercício anterior nos termos21(1), líquido das retenções na fonte;80%, dividido em três montantes iguais arredondados por excesso ao milhar de meticais. Maio,Julho,Setembro. Recorte2026,residente geral,actividade comercial/industrial/agrícola,ano civil e exercício anterior completo2025,MZN. Períodos especiais,anualização28(3),dispensas,juros e guias separados.';
export type PpcObligation = {
version: 1;
scope: '' | 'ordinary_resident' | 'other';
subjectNuit: string;
year: string;
previousYear: string;
previousFullCalendarYear: '' | 'yes' | 'no';
previousTax: string;
previousWithholdings: string;
assessmentReference: string;
withholdingReference: string;
};
export const newPpcObligation = (
subjectNuit = '',
year = '',
): PpcObligation => ({
version: 1,
scope: '',
subjectNuit,
year,
previousYear: '',
previousFullCalendarYear: '',
previousTax: '',
previousWithholdings: '',
assessmentReference: '',
withholdingReference: '',
});
export function validatePpcObligation(p: PpcObligation) {
if (
!p ||
p.version !== 1 ||
Object.keys(newPpcObligation())
.filter((k) => k !== 'version')
.some(
(k) =>
typeof (p as unknown as Record<string, unknown>)[k] !==
'string',
) ||
!['', 'ordinary_resident', 'other'].includes(p.scope) ||
!['', 'yes', 'no'].includes(p.previousFullCalendarYear)
)
throw Error('Preparação PPC não recuperável; original preservado.');
}
export function calculatePpcObligation(
p: PpcObligation,
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
) {
validatePpcObligation(p);
const pending: string[] = [];
const calendar = advanceCalendar(period, year, subjectNuit);
const noncivilPreparation =
calendar.candidate && period?.declaration?.mode === 'annual_non_civil';
let base: FiscalFraction | null = null,
annualBeforeRounding: FiscalFraction | null = null,
instalment: FiscalFraction | null = null;
if (
p.scope !== 'ordinary_resident' ||
assessment !== 'normal' ||
p.previousFullCalendarYear !== (noncivilPreparation ? 'no' : 'yes') ||
year !== '2026' ||
p.year !== year ||
p.previousYear !== '2025' ||
!/^\d{9}$/.test(subjectNuit) ||
p.subjectNuit !== subjectNuit
)
pending.push(
noncivilPreparation
? 'PPC: confirmar titular2026 e período anterior anual não civil com termo2025;indique Não em ano civil completo e confronte a correspondência documentada.'
: 'PPC: confirmar titular,2026,residente geral com actividade comercial/industrial/agrícola e exercício civil completo2025. Outros períodos/regimes e anualização permanecem separados.',
);
if (!p.assessmentReference.trim() || !p.withholdingReference.trim())
pending.push(
'PPC: falta suporte do imposto liquidado anterior21(1) e das retenções na fonte, incluindo zero conhecido. Colecta projectada corrente não substitui liquidação anterior.',
);
if (
![p.previousTax, p.previousWithholdings].every((v) =>
/^\d{1,18}(\.\d{1,2})?$/.test(v),
)
)
pending.push(
'PPC: montantes anteriores em MZN desconhecidos ou inválidos; zero deve ser explícito.',
);
pending.push(
...ordinaryAdvancePeriodIssues(period, year, 'PPC', subjectNuit),
);
if (
calendar.mode === 'annual_non_civil' &&
(!period?.ppcBasisKey ||
period.declaration?.ppcBasisKey !== period.ppcBasisKey)
)
pending.push(
'PPC: montantes ou fontes alterados;reconfira a sua correspondência com o período anterior,sem substituir os valores.',
);
if (!pending.length) {
base = subtractFractions(
decimalFraction(p.previousTax),
decimalFraction(p.previousWithholdings),
);
if (BigInt(base.numerator) < 0n)
pending.push(
'PPC: retenções superiores ao imposto anterior; reconciliar o caso antes de determinar a obrigação.',
);
else {
annualBeforeRounding = fraction(
BigInt(base.numerator) * 4n,
BigInt(base.denominator) * 5n,
);
const n = BigInt(annualBeforeRounding.numerator),
d = BigInt(annualBeforeRounding.denominator) * 3n * 1000n;
instalment = fraction(((n + d - 1n) / d) * 1000n);
}
}
return {
version: ppcObligationVersion,
source:
calendar.mode === 'annual_non_civil'
? ppcObligationSource
.replace(
'ano civil e exercício anterior completo2025',
'períodos anuais completos não civis com termo2026/2025',
)
.replace(
'Maio,Julho,Setembro',
'5.º/7.º/9.º mês do período',
)
.replace(
'Períodos especiais,',
'Períodos curtos e outros regimes,',
) +
' ' +
calendar.source
: ppcObligationSource,
nonCivil: calendar.mode === 'annual_non_civil',
pending,
base,
annualBeforeRounding,
instalment,
total: instalment
? fraction(
BigInt(instalment.numerator) * 3n,
BigInt(instalment.denominator),
)
: null,
schedule: instalment
? calendar.ppcMonths.map((month) => ({
month,
amount: instalment,
}))
: [],
paymentExecuted: false,
annualDeductionCreated: false,
};
}
export function ppcObligationReport(
p: PpcObligation,
subjectNuit: string,
year: string,
assessment: string,
period?: AdvanceFiscalPeriod,
): Array<[string, string, string, string]> {
const m = calculatePpcObligation(p, subjectNuit, year, assessment, period),
show = (v: FiscalFraction | null) =>
v ? fractionPresentation(v).decimal + ' MZN' : 'Indeterminado';
return [
[
'IRPC — L8',
'PPC — estimativa fiscal',
m.pending.length
? m.pending.join(' ')
: 'Base líquida anterior: ' +
show(m.base) +
';80% antes de arredondar: ' +
show(m.annualBeforeRounding) +
'; cada prestação: ' +
show(m.instalment) +
'; total arredondado: ' +
show(m.total) +
'. ' +
m.schedule
.map((s) => s.month + ': ' + show(s.amount))
.join('; ') +
'. Estimativa fiscal para viabilidade; não é pagamento nem dedução anual.',
p.assessmentReference +
'; ' +
p.withholdingReference +
'; ' +
m.source,
],
];
}