359 lines
14 KiB
TypeScript
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.',
|
|
],
|
|
];
|
|
}
|
|
}
|