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