175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
import { fraction, type FiscalFraction } from './fiscal-rational.ts';
|
||
|
||
export const normalAssessmentVersion = 'IRPC-normal-adjustments/4';
|
||
export type NormalAssessmentInput = {
|
||
version: 1;
|
||
treatment: '' | 'general_61_1' | 'other';
|
||
reference: string;
|
||
};
|
||
export const newNormalAssessmentInput = (): NormalAssessmentInput => ({
|
||
version: 1,
|
||
treatment: '',
|
||
reference: '',
|
||
});
|
||
export function validateNormalAssessment(i: NormalAssessmentInput) {
|
||
if (
|
||
!i ||
|
||
i.version !== 1 ||
|
||
!['', 'general_61_1', 'other'].includes(i.treatment) ||
|
||
typeof i.reference !== 'string'
|
||
)
|
||
throw Error(
|
||
'Enquadramento da colecta não recuperável;original conservado.',
|
||
);
|
||
}
|
||
export type NormalAssessmentModel = {
|
||
input: NormalAssessmentInput;
|
||
period: { year: string; from: string; to: string };
|
||
classification: string;
|
||
rate: FiscalFraction | null;
|
||
taxableBase: FiscalFraction | null;
|
||
colecta: FiscalFraction | null;
|
||
centExact: boolean | null;
|
||
pending: string[];
|
||
rule: string;
|
||
source: string;
|
||
finalBalance: {
|
||
scope: 'L8';
|
||
status: 'not_calculated';
|
||
payable: null;
|
||
recoverable: null;
|
||
reason: string;
|
||
};
|
||
};
|
||
export function calculateNormalAssessment(
|
||
input: NormalAssessmentInput,
|
||
base: FiscalFraction | null,
|
||
context: {
|
||
year: string;
|
||
scope: string;
|
||
currency: string;
|
||
extended?: boolean;
|
||
short?: boolean;
|
||
period?: { from: string; to: string };
|
||
},
|
||
): NormalAssessmentModel {
|
||
validateNormalAssessment(input);
|
||
const pending: string[] = [],
|
||
period = {
|
||
year: context.year,
|
||
from: /^20\d{2}$/.test(context.year) ? context.year + '-01-01' : '',
|
||
to: /^20\d{2}$/.test(context.year) ? context.year + '-12-31' : '',
|
||
};
|
||
if (context.year !== '2026')
|
||
pending.push(
|
||
'Taxa geral correspondida neste incremento apenas ao exercício2026;sem taxa transportada para outro período.',
|
||
);
|
||
if (
|
||
!(
|
||
context.scope === 'resident_general' ||
|
||
(context.extended && context.scope === 'ep_general')
|
||
) ||
|
||
context.currency !== 'MZN'
|
||
)
|
||
pending.push(
|
||
'Colecta delimitada ao sujeito residente no regime geral emMZN;outros âmbitos por corresponder.',
|
||
);
|
||
if (input.treatment !== 'general_61_1')
|
||
pending.push(
|
||
input.treatment === 'other'
|
||
? 'Benefício,taxa reduzida,tributação autónoma ou outro caso especial:tratamento próprio;sem taxa geral aplicada por aproximação.'
|
||
: 'Enquadramento da taxa geral por identificar;sem escolha presumida.',
|
||
);
|
||
if (!input.reference.trim())
|
||
pending.push(
|
||
'Identifique suporte ou pressuposto da aplicação da taxa geral ao caso;uma actividade semelhante não prova o regime.',
|
||
);
|
||
if (base === null)
|
||
pending.push(
|
||
'Matéria colectável actual indeterminada;lucro tributável não é substituto da base.',
|
||
);
|
||
else if (
|
||
!/^-?\d{1,80}$/.test(base.numerator) ||
|
||
!/^\d{1,80}$/.test(base.denominator) ||
|
||
BigInt(base.denominator) === 0n ||
|
||
BigInt(base.numerator) < 0n
|
||
)
|
||
pending.push(
|
||
'Matéria colectável deve ser um valor conhecido não negativo e reconciliado.',
|
||
);
|
||
if (context.period) {
|
||
period.from = context.period.from;
|
||
period.to = context.period.to;
|
||
const start = new Date(period.from + 'T00:00:00Z'),
|
||
end = new Date(period.to + 'T00:00:00Z');
|
||
if (
|
||
!Number.isFinite(+start) ||
|
||
!Number.isFinite(+end) ||
|
||
start.toISOString().slice(0, 10) !== period.from ||
|
||
end.toISOString().slice(0, 10) !== period.to ||
|
||
!period.from.endsWith('-01') ||
|
||
!period.to.startsWith('2026-')
|
||
)
|
||
pending.push('Período fiscal anual não reconciliado.');
|
||
else if (context.short) {
|
||
if (
|
||
period.from > period.to ||
|
||
!period.from.startsWith('2026-') ||
|
||
(period.from === context.year + '-01-01' &&
|
||
period.to === context.year + '-12-31')
|
||
)
|
||
pending.push('Período curto por reconciliar.');
|
||
} else {
|
||
start.setUTCFullYear(start.getUTCFullYear() + 1);
|
||
start.setUTCDate(start.getUTCDate() - 1);
|
||
if (start.toISOString().slice(0, 10) !== period.to)
|
||
pending.push(
|
||
'Colecta anual: período de12 meses por reconciliar.',
|
||
);
|
||
}
|
||
}
|
||
const rate =
|
||
input.treatment === 'general_61_1' && context.year === '2026'
|
||
? fraction(32n)
|
||
: null;
|
||
const colecta =
|
||
pending.length === 0 && base !== null && rate
|
||
? fraction(
|
||
BigInt(base.numerator) * 32n,
|
||
BigInt(base.denominator) * 100n,
|
||
)
|
||
: null;
|
||
return {
|
||
input: structuredClone(input),
|
||
period,
|
||
classification:
|
||
input.treatment === 'general_61_1'
|
||
? 'Regime normal — taxa geral do artigo61(1)'
|
||
: input.treatment === 'other'
|
||
? 'Caso especial fora deste recorte'
|
||
: 'Enquadramento por identificar',
|
||
rate,
|
||
taxableBase: base,
|
||
colecta,
|
||
centExact: colecta
|
||
? (BigInt(colecta.numerator) * 100n) %
|
||
BigInt(colecta.denominator) ===
|
||
0n
|
||
: null,
|
||
pending,
|
||
rule: 'CIRPC61(1)-general-2026/1',
|
||
source: context.short
|
||
? 'CIRPC7(4)/61(1):período curto documentado em2026,matéria colectável efectiva ×32%;sem anualizar lucro/base ou proporcionalizar a taxa;saldo finalL8 indeterminado.'
|
||
: context.extended
|
||
? 'CIRPC61(1), taxa geral32% mantida pelos diplomas inventariados até Lei12/2025; período anual documentado com termo em2026. CIRPC15(1)a/c,7(1–3): sujeito residente ou resultado imputável aEP no regime geral; ano civil, autorização ou comunicação expressamente identificadas. Sem arredondamento fiscal ou saldoL8 presumidos.'
|
||
: 'CIRPC Lei34/2007 art61(1),PDF17/p768(71):taxa geral32%. Lei4/2012 altera61(2);Lei20/2022 PDF1/p2731 mantém61(1),altera61(2) com termo31/12/2025;Lei12/2025 PDF18/20 não altera61(1),adita61-A para mais-valias,fora deste recorte. Exercício anual2026/MZN/residente geral,matéria colectável actual×32/100 em aritmética racional exacta. Sem arredondamento fiscal ou transcriçãoM20/M22 presumidos.',
|
||
finalBalance: {
|
||
scope: 'L8',
|
||
status: 'not_calculated',
|
||
payable: null,
|
||
recoverable: null,
|
||
reason: 'Saldo final a pagar ou recuperar por determinar emL8:retenções,pagamentos e créditos do fecho anual não apurados nem abatidos à colecta neste incremento. Colecta não equivale ao saldo final.',
|
||
},
|
||
};
|
||
}
|