221 lines
8.2 KiB
TypeScript
221 lines
8.2 KiB
TypeScript
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,
|
||
],
|
||
];
|
||
}
|