Files
Investidor-V2/resources/js/lib/ispc-quarter.ts
T

359 lines
14 KiB
TypeScript

import { inputText } from './input-text.ts';
import { acquisitionScope } from './iva-acquisitions.ts';
import {
ispcEligibilityBook,
ispcEligibilityCurrent,
type IspcEligibilityResult,
} from './ispc-eligibility.ts';
import {
addFractions,
decimalFraction,
fraction,
fractionPresentation,
subtractFractions,
type FiscalFraction,
} from './fiscal-rational.ts';
export const ispcQuarterSource =
'Lei 9/2025, republicação ISPC, arts 8, 9, 10, 10-A e 11; BR248/2025, PDF11/p2180(11). Taxa do escalão anual aplicada à base trimestral (8(1)a e 9); 20% apenas na parcela em excesso de 4.000.000; actividades cumuladas: taxa mais alta (8(2)). Sem fraccionar os limites anuais por quatro nem copiar o volume do ano anterior. Preparação do apuramento; sem submissão ou pagamento AT.';
export type IspcQuarterInput = {
year: string;
quarter: string;
sales: [string, string, string, string];
salesRefs: [string, string, string, string];
annualBandVolume: string;
annualBandOrigin: '' | 'forecast' | 'closed_year';
annualBandRef: string;
coverage: '' | 'all_taxpayer_activity';
};
export type IspcQuarterResult = {
key: string;
calculatedAt: string;
scope: string;
input: IspcQuarterInput;
eligibility: IspcEligibilityResult | null;
status: 'calculated' | 'indeterminate' | 'not_applicable';
reason: string;
missing: string[];
base: FiscalFraction | null;
prior: FiscalFraction | null;
ordinary: FiscalFraction | null;
excess: FiscalFraction | null;
rate: number | null;
tax: FiscalFraction | null;
payment: FiscalFraction | null;
declaration: boolean;
projected: boolean;
source: string;
};
export type IspcQuarterBook = {
version: 1;
input: IspcQuarterInput;
result?: IspcQuarterResult;
history?: IspcQuarterResult[];
};
export const newIspcQuarterInput = (): IspcQuarterInput => ({
year: '2026',
quarter: '1',
sales: ['', '', '', ''],
salesRefs: ['', '', '', ''],
annualBandVolume: '',
annualBandOrigin: '',
annualBandRef: '',
coverage: '',
});
function snapshot(d: Record<string, unknown>) {
const s = JSON.parse(
inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'),
);
if (
s.version !== 1 ||
!s.books ||
typeof s.books !== 'object' ||
Array.isArray(s.books)
)
throw Error('Livros não recuperáveis; originais preservados.');
return s;
}
function validate(b: IspcQuarterBook) {
if (
!b ||
b.version !== 1 ||
!b.input ||
[
'year',
'quarter',
'annualBandVolume',
'annualBandOrigin',
'annualBandRef',
'coverage',
].some(
(k) => typeof b.input[k as keyof IspcQuarterInput] !== 'string',
) ||
!['', 'forecast', 'closed_year'].includes(b.input.annualBandOrigin) ||
!['', 'all_taxpayer_activity'].includes(b.input.coverage) ||
!Array.isArray(b.input.sales) ||
b.input.sales.length !== 4 ||
b.input.sales.some((v) => typeof v !== 'string') ||
!Array.isArray(b.input.salesRefs) ||
b.input.salesRefs.length !== 4 ||
b.input.salesRefs.some((v) => typeof v !== 'string') ||
(b.history !== undefined && !Array.isArray(b.history)) ||
(b.result !== undefined &&
(!b.result.input || !Array.isArray(b.result.missing)))
)
throw Error(
'Apuramento ISPC não recuperável; sem substituir por zero ou apagar histórico.',
);
}
function books(d: Record<string, unknown>): Record<string, IspcQuarterBook> {
const v = snapshot(d).ispcQuarters;
if (v === undefined) return {};
if (!v || typeof v !== 'object' || Array.isArray(v))
throw Error('Mapa trimestral ISPC inválido; originais preservados.');
return v;
}
export function ispcQuarterBook(d: Record<string, unknown>): IspcQuarterBook {
const b = books(d)[acquisitionScope(d)] ?? {
version: 1,
input: newIspcQuarterInput(),
};
validate(b);
return b;
}
export function saveIspcQuarterBook(
d: Record<string, unknown>,
b: IspcQuarterBook,
): string {
validate(b);
return JSON.stringify({
...snapshot(d),
ispcQuarters: { ...books(d), [acquisitionScope(d)]: b },
});
}
const cmp = (a: FiscalFraction, b: FiscalFraction) =>
BigInt(a.numerator) * BigInt(b.denominator) -
BigInt(b.numerator) * BigInt(a.denominator);
const minimum = (a: FiscalFraction, b: FiscalFraction) =>
cmp(a, b) <= 0n ? a : b;
const positive = (a: FiscalFraction) =>
cmp(a, fraction(0n)) > 0n ? a : fraction(0n);
const atRate = (v: FiscalFraction, r: number) =>
fraction(BigInt(v.numerator) * BigInt(r), BigInt(v.denominator) * 100n);
function eligibility(d: Record<string, unknown>) {
const b = ispcEligibilityBook(d);
return ispcEligibilityCurrent(d, b) ? (b.result ?? null) : null;
}
const key = (d: Record<string, unknown>, b: IspcQuarterBook) =>
JSON.stringify([
'ispc-quarter/1',
acquisitionScope(d),
b.input,
eligibility(d),
]);
export const ispcQuarterCurrent = (
d: Record<string, unknown>,
b: IspcQuarterBook,
) => !!b.result && b.result.key === key(d, b);
export function recordIspcQuarter(
d: Record<string, unknown>,
b: IspcQuarterBook,
now = new Date().toISOString(),
): IspcQuarterBook {
validate(b);
const i = b.input,
e = eligibility(d),
r: IspcQuarterResult = {
key: key(d, b),
calculatedAt: now,
scope: acquisitionScope(d),
input: structuredClone(i),
eligibility: e ? structuredClone(e) : null,
status: 'indeterminate',
reason: '',
missing: [],
base: null,
prior: null,
ordinary: null,
excess: null,
rate: null,
tax: null,
payment: null,
declaration: false,
projected:
e?.input.origin === 'projected' ||
i.annualBandOrigin === 'forecast',
source: ispcQuarterSource,
};
const finish = () => ({
...b,
...(b.result
? { history: [...(b.history ?? []), structuredClone(b.result)] }
: {}),
result: r,
});
if (!e) {
r.missing.push(
'Avaliação de elegibilidade actual correspondente às entradas',
);
r.reason =
'Reveja e guarde o enquadramento; a avaliação anterior continua no histórico.';
return finish();
}
if (e.status === 'incompatible' || e.optionStatus === 'not_opted') {
r.status = 'not_applicable';
r.reason =
'Os factos guardados não permitem aplicar ISPC ou indicam que não optou. Outros regimes mantêm os seus percursos.';
return finish();
}
if (e.status !== 'compatible') {
r.missing.push(...e.missing);
r.reason = 'Enquadramento indeterminado; não produzir imposto zero.';
return finish();
}
// A preparation scenario may estimate tax, but never asserts an effective election.
if (
e.optionStatus === 'indeterminate' ||
(e.input.origin === 'effective' && e.optionStatus !== 'recorded')
) {
r.missing.push(
'Opção documentada para apuramento efectivo; a preparação permite somente simulação',
);
}
if (e.optionStatus === 'preparation') r.projected = true;
if (i.year !== '2026' || !['1', '2', '3', '4'].includes(i.quarter))
r.missing.push('Ano 2026 e trimestre entre 1 e 4');
if (i.coverage !== 'all_taxpayer_activity')
r.missing.push(
'Confirmar volume de todas as actividades do sujeito passivo, não apenas desta oportunidade',
);
const q = Number(i.quarter),
amounts: FiscalFraction[] = [],
allFour =
i.annualBandOrigin === 'closed_year' &&
!e.input.activities.some((a) =>
['liberal15', 'services12'].includes(a),
);
if (
e.input.origin === 'effective' &&
e.input.optionDate &&
['1', '2', '3', '4'].includes(i.quarter) &&
e.input.optionDate >
`2026-${['03-31', '06-30', '09-30', '12-31'][q - 1]}`
)
r.missing.push(
'A opção registada é posterior ao trimestre; esclarecer operações abrangidas sem retroagir automaticamente',
);
if (['1', '2', '3', '4'].includes(i.quarter))
for (let n = 0; n < (allFour ? 4 : q); n++) {
try {
amounts[n] = decimalFraction(i.sales[n]);
} catch {
r.missing.push(
`Volume do trimestre ${n + 1}, incluindo zero expresso`,
);
}
if (!i.salesRefs[n].trim())
r.missing.push(`Suporte ou pressuposto do trimestre ${n + 1}`);
}
const liberal = e.input.activities.includes('liberal15'),
service = e.input.activities.includes('services12');
let band: FiscalFraction | null = null;
if (!liberal && !service) {
try {
band = decimalFraction(i.annualBandVolume);
} catch {
r.missing.push(
'Volume anual de 2026 para identificar o escalão; distinto do volume de acesso 2025',
);
}
if (!i.annualBandOrigin || !i.annualBandRef.trim())
r.missing.push(
'Origem (previsão ou ano encerrado) e suporte do escalão anual',
);
}
if (r.missing.length) {
r.reason =
'Faltam dados específicos para este apuramento; os restantes percursos mantêm-se disponíveis.';
return finish();
}
r.base = amounts[q - 1];
r.prior = addFractions(amounts.slice(0, q - 1));
const cumulative = addFractions(amounts.slice(0, q)),
annualTotal = addFractions(amounts);
if (
band &&
(cmp(band, cumulative) < 0n ||
(i.annualBandOrigin === 'closed_year' &&
cmp(band, annualTotal) !== 0n))
) {
r.reason =
'Volume anual do escalão diverge dos trimestres declarados; reveja a divergência antes de calcular.';
r.missing.push(
'Reconciliar referência anual 2026 com o acumulado trimestral',
);
return finish();
}
r.rate = liberal
? 15
: service
? 12
: band && cmp(band, fraction(1000000n)) <= 0n
? 3
: band && cmp(band, fraction(2500000n)) <= 0n
? 4
: 5;
r.ordinary = minimum(
r.base,
positive(subtractFractions(fraction(4000000n), r.prior)),
);
r.excess = subtractFractions(r.base, r.ordinary);
r.tax = addFractions([atRate(r.ordinary, r.rate), atRate(r.excess, 20)]);
r.payment = cmp(r.tax, fraction(500n)) < 0n ? fraction(0n) : r.tax;
r.declaration = true;
r.status = 'calculated';
r.reason = `${r.projected ? 'Simulação com pressupostos explícitos; sem adesão automática.' : 'Apuramento segundo factos e opção documentados.'} Taxa ${r.rate}% sobre a parcela ordinária trimestral e 20% apenas sobre o excesso anual. ${cmp(r.tax, fraction(500n)) < 0n ? 'Prestação inferior a 500 MZN: sem pagamento; obrigação declarativa preservada.' : 'Prestação igual ou superior a 500 MZN: montante a preparar para pagamento.'} Sem dedução de custos, retenções sofridas presumidas, compensação ou acumulação de prestações dispensadas.`;
return finish();
}
const amount = (v: FiscalFraction | null) =>
v
? `${fractionPresentation(v).decimal} MZN (${v.numerator}/${v.denominator})`
: 'Indeterminado';
export function ispcQuarterReportRows(
d: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const b = ispcQuarterBook(d),
r = b.result;
if (!r) return [];
return [
[
'ISPC',
`Apuramento ${r.input.year}/T${r.input.quarter}`,
`${r.status === 'calculated' ? 'Calculado' : r.status === 'not_applicable' ? 'Não aplicável' : 'Indeterminado'}; ${r.reason}; falta: ${r.missing.join('; ') || 'nenhum dado deste recorte'}`,
`${r.scope}; ${ispcQuarterCurrent(d, b) ? 'Entradas correspondentes' : 'Histórico preservado; rever entradas'}; ${r.calculatedAt}; ${r.source}`,
],
[
'ISPC',
'Base e taxa trimestral',
`Base: ${amount(r.base)}; anterior no ano: ${amount(r.prior)}; ordinária: ${amount(r.ordinary)} a ${r.rate ?? 'indeterminada'}%; excesso: ${amount(r.excess)} a 20%`,
`Volume anual de escalão: ${r.input.annualBandVolume || 'não necessário para taxa de serviços'}; ${r.input.annualBandOrigin}; ${r.input.annualBandRef}; suportes: ${r.input.salesRefs.join('; ')}`,
],
[
'ISPC',
'Imposto, pagamento e declaração',
`Imposto: ${amount(r.tax)}; prestação a pagar: ${amount(r.payment)}; obrigação declarativa: ${r.declaration ? 'mantida' : 'por determinar neste percurso'}; ${r.projected ? 'simulação' : 'factos efectivos'}`,
`Opção: ${r.eligibility?.optionStatus ?? 'indeterminada'}; referência: ${r.eligibility?.input.optionReference ?? ''}; histórico: ${b.history?.length ?? 0} versão(ões) anterior(es). Sem submissão AT.`,
],
];
} catch (error) {
return [
[
'ISPC',
'Apuramento não recuperável',
String(error),
'Originais preservados.',
],
];
}
}