193 lines
7.0 KiB
TypeScript
193 lines
7.0 KiB
TypeScript
import {
|
|
advanceCalendar,
|
|
type AdvanceFiscalPeriod,
|
|
} from './irpc-advance-calendar.ts';
|
|
import {
|
|
decimalFraction,
|
|
addFractions,
|
|
subtractFractions,
|
|
fractionPresentation,
|
|
type FiscalFraction,
|
|
} from './fiscal-rational.ts';
|
|
export const pecPlanVersion = 'IRPC-PEC-plan/1';
|
|
export const pecPlanSource =
|
|
'RIRPC29(1):mesesJunho/Agosto/Outubro e opção de pagamento único. Montantes repartidos e mês único são hipótese explícita do utilizador,sem regra de igualdade,arredondamento,guia ou prazo legal presumidos. Conferência de prazo da opção única permanece separada.';
|
|
export type PecPlan = {
|
|
version: 1;
|
|
periodFrom?: string;
|
|
periodTo?: string;
|
|
mode: '' | 'three' | 'single';
|
|
reference: string;
|
|
singleMonth: string;
|
|
june: string;
|
|
august: string;
|
|
october: string;
|
|
};
|
|
export const newPecPlan = (period?: AdvanceFiscalPeriod): PecPlan => ({
|
|
...(period?.from && period?.to
|
|
? { periodFrom: period.from, periodTo: period.to }
|
|
: {}),
|
|
version: 1,
|
|
mode: '',
|
|
reference: '',
|
|
singleMonth: '',
|
|
june: '',
|
|
august: '',
|
|
october: '',
|
|
});
|
|
export function validatePecPlan(p: PecPlan) {
|
|
if (
|
|
(p?.periodFrom !== undefined || p?.periodTo !== undefined) &&
|
|
(typeof p.periodFrom !== 'string' || typeof p.periodTo !== 'string')
|
|
)
|
|
throw Error(
|
|
'Contexto da hipótese PEC não recuperável;original preservado.',
|
|
);
|
|
if (
|
|
!p ||
|
|
p.version !== 1 ||
|
|
Object.keys(newPecPlan())
|
|
.filter((k) => k !== 'version')
|
|
.some(
|
|
(k) =>
|
|
typeof (p as unknown as Record<string, unknown>)[k] !==
|
|
'string',
|
|
) ||
|
|
!['', 'three', 'single'].includes(p.mode)
|
|
)
|
|
throw Error('Projecção PEC não recuperável;original preservado.');
|
|
}
|
|
export function calculatePecPlan(
|
|
p: PecPlan,
|
|
total: FiscalFraction | null,
|
|
notApplicable: boolean,
|
|
year: string,
|
|
period?: AdvanceFiscalPeriod,
|
|
) {
|
|
validatePecPlan(p);
|
|
const pending: string[] = [];
|
|
const calendar = advanceCalendar(period, year);
|
|
pending.push(...calendar.pending.map((p) => 'Projecção PEC: ' + p));
|
|
const needsPeriodConfirmation =
|
|
(calendar.mode === 'annual_non_civil' || p.periodFrom !== undefined) &&
|
|
(p.periodFrom !== calendar.from || p.periodTo !== calendar.to);
|
|
if (needsPeriodConfirmation)
|
|
pending.push(
|
|
'Projecção PEC: reconfira a distribuição no período actual;montantes e mês anterior conservados sem deslocação automática.',
|
|
);
|
|
let rows: Array<{ month: string; amount: FiscalFraction }> = [];
|
|
if (!total)
|
|
pending.push(
|
|
'Projecção PEC:total estimado indeterminada;reconciliar a base antes de preparar os montantes.',
|
|
);
|
|
else if (notApplicable || BigInt(total.numerator) === 0n)
|
|
pending.push(
|
|
'Projecção PEC:sem estimativa positiva,não se distribuem saídas de caixa.',
|
|
);
|
|
if (year !== '2026')
|
|
pending.push('Projecção PEC:exercício fora do recorte2026.');
|
|
if (!p.reference.trim())
|
|
pending.push(
|
|
'Projecção PEC:identifique o fundamento da opção e dos montantes preparados.',
|
|
);
|
|
if (!['three', 'single'].includes(p.mode))
|
|
pending.push(
|
|
'Projecção PEC:escolha três prestações ou pagamento único.',
|
|
);
|
|
if (!pending.length && total) {
|
|
if (p.mode === 'three') {
|
|
const values = [p.june, p.august, p.october];
|
|
if (!values.every((v) => /^\d{1,18}(\.\d{1,2})?$/.test(v)))
|
|
pending.push(
|
|
'Projecção PEC:preencha os três montantes em MZN,com até dois decimais;sem preenchimento automático.',
|
|
);
|
|
else {
|
|
const amounts = values.map(decimalFraction);
|
|
if (amounts.some((v) => BigInt(v.numerator) <= 0n))
|
|
pending.push(
|
|
'Projecção PEC:três prestações exigem três montantes positivos.',
|
|
);
|
|
else if (
|
|
BigInt(
|
|
subtractFractions(addFractions(amounts), total)
|
|
.numerator,
|
|
) !== 0n
|
|
)
|
|
pending.push(
|
|
'Projecção PEC:a soma das três prestações difere do total preparado;sem arredondamento ou compensação silenciosos.',
|
|
);
|
|
else
|
|
rows = amounts.map((amount, i) => ({
|
|
month: calendar.pecMonths[i],
|
|
amount,
|
|
}));
|
|
}
|
|
} else if (
|
|
!/^20\d{2}-(0[1-9]|1[0-2])$/.test(p.singleMonth) ||
|
|
p.singleMonth < calendar.from.slice(0, 7) ||
|
|
p.singleMonth > calendar.to.slice(0, 7)
|
|
)
|
|
pending.push(
|
|
'Projecção PEC:indique o mês previsto do pagamento único;isto não valida o prazo legal.',
|
|
);
|
|
else rows = [{ month: p.singleMonth, amount: total }];
|
|
}
|
|
return {
|
|
version: pecPlanVersion,
|
|
source:
|
|
calendar.mode === 'annual_non_civil'
|
|
? pecPlanSource.replace(
|
|
'mesesJunho/Agosto/Outubro',
|
|
'6.º/8.º/10.º mês do período anual não civil',
|
|
) +
|
|
' ' +
|
|
calendar.source
|
|
: pecPlanSource,
|
|
months: calendar.pecMonths,
|
|
needsPeriodConfirmation,
|
|
contextFrom: calendar.from,
|
|
contextTo: calendar.to,
|
|
calendarPending: calendar.mode === 'unavailable',
|
|
nonCivil: calendar.mode === 'annual_non_civil',
|
|
mode: p.mode,
|
|
pending,
|
|
rows,
|
|
total,
|
|
allocationReconciled: pending.length === 0,
|
|
legalDeadlineValidated: false,
|
|
paymentExecuted: false,
|
|
annualDeductionCreated: false,
|
|
};
|
|
}
|
|
export function pecPlanReport(
|
|
p: PecPlan,
|
|
total: FiscalFraction | null,
|
|
notApplicable: boolean,
|
|
year: string,
|
|
period?: AdvanceFiscalPeriod,
|
|
): Array<[string, string, string, string]> {
|
|
const m = calculatePecPlan(p, total, notApplicable, year, period);
|
|
return [
|
|
[
|
|
'IRPC — L8',
|
|
'PEC — projecção de tesouraria',
|
|
m.pending.length
|
|
? m.pending.join(' ')
|
|
: (m.mode === 'single'
|
|
? 'Saída de caixa concentrada (hipótese): '
|
|
: 'Distribuição de caixa (hipótese): ') +
|
|
m.rows
|
|
.map(
|
|
(r) =>
|
|
r.month +
|
|
': ' +
|
|
fractionPresentation(r.amount).decimal +
|
|
' MZN',
|
|
)
|
|
.join('; ') +
|
|
'. Soma reconciliada;prazo legal não validado. Não é pagamento ou dedução.',
|
|
p.reference + '; ' + m.source,
|
|
],
|
|
];
|
|
}
|