169 lines
6.0 KiB
TypeScript
169 lines
6.0 KiB
TypeScript
import {
|
||
decimalFraction,
|
||
fraction,
|
||
subtractFractions,
|
||
fractionPresentation,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
|
||
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,
|
||
) {
|
||
validatePpcObligation(p);
|
||
const pending: string[] = [];
|
||
let base: FiscalFraction | null = null,
|
||
annualBeforeRounding: FiscalFraction | null = null,
|
||
instalment: FiscalFraction | null = null;
|
||
if (
|
||
p.scope !== 'ordinary_resident' ||
|
||
assessment !== 'normal' ||
|
||
p.previousFullCalendarYear !== 'yes' ||
|
||
year !== '2026' ||
|
||
p.year !== year ||
|
||
p.previousYear !== '2025' ||
|
||
!/^\d{9}$/.test(subjectNuit) ||
|
||
p.subjectNuit !== subjectNuit
|
||
)
|
||
pending.push(
|
||
'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.',
|
||
);
|
||
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: ppcObligationSource,
|
||
pending,
|
||
base,
|
||
annualBeforeRounding,
|
||
instalment,
|
||
total: instalment
|
||
? fraction(
|
||
BigInt(instalment.numerator) * 3n,
|
||
BigInt(instalment.denominator),
|
||
)
|
||
: null,
|
||
schedule: instalment
|
||
? ['2026-05', '2026-07', '2026-09'].map((month) => ({
|
||
month,
|
||
amount: instalment,
|
||
}))
|
||
: [],
|
||
paymentExecuted: false,
|
||
annualDeductionCreated: false,
|
||
};
|
||
}
|
||
export function ppcObligationReport(
|
||
p: PpcObligation,
|
||
subjectNuit: string,
|
||
year: string,
|
||
assessment: string,
|
||
): Array<[string, string, string, string]> {
|
||
const m = calculatePpcObligation(p, subjectNuit, year, assessment),
|
||
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('; ') +
|
||
'. Previsão de obrigação; não é pagamento nem dedução anual.',
|
||
p.assessmentReference +
|
||
'; ' +
|
||
p.withholdingReference +
|
||
'; ' +
|
||
m.source,
|
||
],
|
||
];
|
||
}
|