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

237 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { inputText } from './input-text.ts';
import {
ispcEligibilityBook,
ispcEligibilityCurrent,
} from './ispc-eligibility.ts';
import { acquisitionScope } from './iva-acquisitions.ts';
import {
addFractions,
decimalFraction,
fraction,
fractionPresentation,
type FiscalFraction,
} from './fiscal-rational.ts';
export type IspcWithholdingInput = {
applicable: '' | 'no' | 'yes';
quarter: string;
base: string;
priorAnnualPurchases: string;
naturalOccasional: '' | 'yes' | 'no';
unregistered: '' | 'yes' | 'no';
related: '' | 'yes' | 'no';
activity:
| ''
| 'forestry'
| 'fishery'
| 'livestock'
| 'agriculture'
| 'poultry'
| 'beekeeping'
| 'other';
reference: string;
};
export type IspcWithholdingResult = {
input: IspcWithholdingInput;
key: string;
status: 'calculated' | 'indeterminate' | 'not_applicable';
reason: string;
withheld: FiscalFraction | null;
calculatedAt: string;
source: string;
};
export type IspcWithholdingBook = {
version: 1;
input: IspcWithholdingInput;
result?: IspcWithholdingResult;
history?: IspcWithholdingResult[];
};
export const ispcWithholdingSource =
'Lei9/2025,republicaçãoISPC,arts3(3–4),12(3–4);PDF10–11/p2180(10–11). Retenção liberatória5% sobre aquisição a pessoa singular ocasional não inscrita, ligada à actividade, até limite global anual2.500.000MZN; factura por conta do fornecedor e entrega trimestral em modelo próprio. Não é crédito contra o imposto próprio nem se aplica o mínimo500 desta prestação à retenção.';
const blank = (): IspcWithholdingInput => ({
applicable: '',
quarter: '1',
base: '',
priorAnnualPurchases: '',
naturalOccasional: '',
unregistered: '',
related: '',
activity: '',
reference: '',
});
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: IspcWithholdingBook) {
if (
!b ||
b.version !== 1 ||
!b.input ||
Object.keys(blank()).some(
(k) => typeof b.input[k as keyof IspcWithholdingInput] !== 'string',
) ||
['applicable', 'naturalOccasional', 'unregistered', 'related'].some(
(k) =>
!['', 'yes', 'no'].includes(
b.input[k as keyof IspcWithholdingInput],
),
) ||
![
'',
'forestry',
'fishery',
'livestock',
'agriculture',
'poultry',
'beekeeping',
'other',
].includes(b.input.activity) ||
(b.history !== undefined && !Array.isArray(b.history))
)
throw Error('RetençõesISPC não recuperáveis; originais preservados.');
}
function map(d: Record<string, unknown>): Record<string, IspcWithholdingBook> {
const v = snapshot(d).ispcWithholdings;
if (v === undefined) return {};
if (!v || typeof v !== 'object' || Array.isArray(v))
throw Error('Mapa de retençõesISPC inválido.');
return v;
}
export function ispcWithholdingBook(
d: Record<string, unknown>,
): IspcWithholdingBook {
const b = map(d)[acquisitionScope(d)] ?? { version: 1, input: blank() };
validate(b);
return b;
}
export function saveIspcWithholdingBook(
d: Record<string, unknown>,
b: IspcWithholdingBook,
): string {
validate(b);
return JSON.stringify({
...snapshot(d),
ispcWithholdings: { ...map(d), [acquisitionScope(d)]: b },
});
}
export function recordIspcWithholding(
d: Record<string, unknown>,
b: IspcWithholdingBook,
now = new Date().toISOString(),
): IspcWithholdingBook {
validate(b);
const i = b.input,
e = ispcEligibilityBook(d),
r: IspcWithholdingResult = {
input: structuredClone(i),
key: JSON.stringify([acquisitionScope(d), i]),
status: 'indeterminate',
reason: 'Identifique se existem aquisições abrangidas; não presumir retenção zero.',
withheld: null,
calculatedAt: now,
source: ispcWithholdingSource,
};
if (i.applicable === 'no') {
r.status = 'not_applicable';
r.reason =
'Sem aquisições abrangidas, segundo declaração expressa; entradas anteriores preservadas.';
} else if (
i.applicable === 'yes' &&
(!ispcEligibilityCurrent(d, e) ||
e.result?.status !== 'compatible' ||
e.result.optionStatus === 'not_opted' ||
e.result.optionStatus === 'indeterminate' ||
(e.input.origin === 'effective' &&
e.result.optionStatus !== 'recorded'))
) {
r.reason =
'Guarde o enquadramento e a opção aplicáveis do adquirenteISPC antes de calcular retenção efectiva; sem inferir adesão.';
} else if (i.applicable === 'yes') {
if (
i.naturalOccasional === 'no' ||
i.unregistered === 'no' ||
i.related === 'no' ||
i.activity === 'other'
) {
r.status = 'not_applicable';
r.reason =
'Aquisição fora do recorte12(3–4) declarado; outros regimes de retenção mantêm análise própria,sem presumir isenção.';
} else if (
i.naturalOccasional === 'yes' &&
i.unregistered === 'yes' &&
i.related === 'yes' &&
i.activity &&
i.reference.trim() &&
['1', '2', '3', '4'].includes(i.quarter)
) {
try {
const base = decimalFraction(i.base),
prior = decimalFraction(i.priorAnnualPurchases),
total = addFractions([base, prior]);
if (
BigInt(total.numerator) >
2500000n * BigInt(total.denominator)
)
r.reason =
'Total global anual superior2.500.000MZN: tratamento da aquisição acima do limite por corresponder; não reter automaticamente5% nem truncar a base.';
else {
r.status = 'calculated';
r.withheld = fraction(
BigInt(base.numerator) * 5n,
BigInt(base.denominator) * 100n,
);
r.reason =
'Retenção5% calculada separadamente do imposto próprio; emitir factura por conta do fornecedor e preparar entrega trimestral em modelo próprio. Sem deduzir esta retenção da prestaçãoISPC.';
}
} catch {
r.reason =
'Base e aquisições globais anteriores do ano necessários,incluindo zero expresso; não presumir montantes.';
}
} else
r.reason =
'Identifique pessoa singular ocasional3(3–4),actividade abrangida,não inscrição,ligação à actividade,trimestre e suporte; dados desconhecidos não viram não aplicáveis.';
}
return {
...b,
...(b.result
? { history: [...(b.history ?? []), structuredClone(b.result)] }
: {}),
result: r,
};
}
export function ispcWithholdingReportRows(
d: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const b = ispcWithholdingBook(d),
r = b.result;
if (!r) return [];
return [
[
'ISPC',
'Retenção em aquisições ocasionais',
`${r.status}; ${r.withheld ? fractionPresentation(r.withheld).decimal + ' MZN' : 'Indeterminado/não aplicado'}; ${r.reason}`,
`2026/T${r.input.quarter}; base:${r.input.base}; anteriores globais:${r.input.priorAnnualPurchases}; suporte:${r.input.reference}; ${r.key === JSON.stringify([acquisitionScope(d), b.input]) ? 'Entradas correspondentes' : 'Histórico preservado;rever entradas'}; ${r.calculatedAt}; ${r.source}`,
],
];
} catch (e) {
return [
[
'ISPC',
'Retenção não recuperável',
String(e),
'Originais preservados.',
],
];
}
}