355 lines
13 KiB
TypeScript
355 lines
13 KiB
TypeScript
import type { Acquisition, AcquisitionResult } from './iva-acquisitions.ts';
|
||
import { acquisitionScope } from './iva-acquisitions.ts';
|
||
import { evaluateAcquisitionBC } from './iva-acquisition-bc.ts';
|
||
import {
|
||
reducedBaseBook,
|
||
reducedBaseCurrent,
|
||
reducedBaseTreatments,
|
||
evaluateReducedBase,
|
||
} from './iva-reduced-bases.ts';
|
||
import {
|
||
decimalFraction,
|
||
fraction,
|
||
fractionLabel,
|
||
fractionPresentation,
|
||
subtractFractions,
|
||
type FiscalFraction,
|
||
} from './fiscal-rational.ts';
|
||
export const acquisitionAllocationVersion = 'acquisition-allocation/1';
|
||
export const acquisitionAllocationSource =
|
||
'L2-D: CIVA22(1–8),Lei13/2016 PDF16/p1426(274);Lei10/2025,18(6),PDF13/p2180(13).22(4)numerador anual semIVA com direito18/19;denominador anual semIVA de todas operações,incluindo fora de campo;22(5)exclui vendas de activo usado e operações imobiliárias/financeiras acessórias.22(8)quociente à centésima imediatamente superior.22(2)afectação real com comunicação prévia;22(6–7)provisório,regularização anual emE. Não substitui condições18–20 ou calcula restituiçãoL3.';
|
||
type Choice = '' | 'yes' | 'no';
|
||
export type AcquisitionAllocation = {
|
||
version: 1;
|
||
method: '' | 'actual' | 'pro_rata' | 'reduced';
|
||
numerator: string;
|
||
denominator: string;
|
||
unit: string;
|
||
referenceYear: string;
|
||
basis: '' | 'prior_year' | 'initial_changed';
|
||
scopeRef: string;
|
||
source: string;
|
||
communicationRef: string;
|
||
declarationRef: string;
|
||
actualRequired: Choice;
|
||
noExcludedPayments: Choice;
|
||
facts: Record<string, Choice>;
|
||
reducedId: string;
|
||
reducedFingerprint: string;
|
||
};
|
||
export function newAcquisitionAllocation(
|
||
period: string,
|
||
): AcquisitionAllocation {
|
||
return {
|
||
version: 1,
|
||
method: '',
|
||
numerator: '',
|
||
denominator: '',
|
||
unit: '',
|
||
referenceYear: String(Number(period.slice(0, 4)) - 1),
|
||
basis: '',
|
||
scopeRef: '',
|
||
source: '',
|
||
communicationRef: '',
|
||
declarationRef: '',
|
||
actualRequired: '',
|
||
noExcludedPayments: '',
|
||
facts: {},
|
||
reducedId: '',
|
||
reducedFingerprint: '',
|
||
};
|
||
}
|
||
export function validAcquisitionAllocation(
|
||
v: unknown,
|
||
): v is AcquisitionAllocation {
|
||
if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
|
||
const d = v as AcquisitionAllocation;
|
||
return (
|
||
d.version === 1 &&
|
||
['', 'actual', 'pro_rata', 'reduced'].includes(d.method) &&
|
||
['', 'prior_year', 'initial_changed'].includes(d.basis) &&
|
||
[
|
||
'numerator',
|
||
'denominator',
|
||
'unit',
|
||
'referenceYear',
|
||
'scopeRef',
|
||
'source',
|
||
'communicationRef',
|
||
'declarationRef',
|
||
'reducedId',
|
||
'reducedFingerprint',
|
||
].every(
|
||
(k) => typeof d[k as keyof AcquisitionAllocation] === 'string',
|
||
) &&
|
||
[d.actualRequired, d.noExcludedPayments].every((v) =>
|
||
['', 'yes', 'no'].includes(v),
|
||
) &&
|
||
!!d.facts &&
|
||
typeof d.facts === 'object' &&
|
||
!Array.isArray(d.facts) &&
|
||
Object.values(d.facts).every((v) => ['', 'yes', 'no'].includes(v))
|
||
);
|
||
}
|
||
export function allocationReducedChoices(draft: Record<string, unknown>) {
|
||
return reducedBaseBook(draft).rows.map((row) => ({
|
||
id: row.id,
|
||
label: `${row.description || row.id} — ${row.period} — ${reducedBaseTreatments.find((t) => t.id === row.treatment)?.label ?? row.treatment}`,
|
||
row,
|
||
}));
|
||
}
|
||
export function adoptAllocationReduced(
|
||
draft: Record<string, unknown>,
|
||
d: AcquisitionAllocation,
|
||
id: string,
|
||
): AcquisitionAllocation {
|
||
const source = allocationReducedChoices(draft).find((r) => r.id === id);
|
||
if (!source)
|
||
throw Error(
|
||
'Base reduzida de origem indisponível;originais preservados.',
|
||
);
|
||
return {
|
||
...d,
|
||
reducedId: id,
|
||
reducedFingerprint: JSON.stringify(source.row),
|
||
facts: { ...d.facts, scopeConfirmed: '' },
|
||
};
|
||
}
|
||
export function allocationSourceKey(
|
||
draft: Record<string, unknown>,
|
||
rows: Acquisition[],
|
||
): string {
|
||
return rows.some((r) => r.d)
|
||
? JSON.stringify({
|
||
version: acquisitionAllocationVersion,
|
||
reducedOperations: reducedBaseBook(draft).rows,
|
||
sources: rows
|
||
.filter((r) => r.d?.method === 'reduced')
|
||
.map((r) => ({
|
||
row:
|
||
allocationReducedChoices(draft).find(
|
||
(s) => s.id === r.d?.reducedId,
|
||
)?.row ?? null,
|
||
current: reducedBaseCurrent(
|
||
draft,
|
||
reducedBaseBook(draft),
|
||
),
|
||
})),
|
||
})
|
||
: '';
|
||
}
|
||
export function allocationHasUnconsolidatedOutputs(
|
||
draft: Record<string, unknown>,
|
||
period: string,
|
||
): boolean {
|
||
return reducedBaseBook(draft).rows.some((r) => r.period === period);
|
||
}
|
||
type Decision = {
|
||
ratio: FiscalFraction | null;
|
||
rawRatio?: FiscalFraction;
|
||
reason: string;
|
||
source: string;
|
||
};
|
||
export function acquisitionAllocationDecision(
|
||
draft: Record<string, unknown> | undefined,
|
||
row: Acquisition,
|
||
): Decision {
|
||
const d = row.d;
|
||
const stop = (reason: string): Decision => ({
|
||
ratio: null,
|
||
reason,
|
||
source: acquisitionAllocationSource,
|
||
});
|
||
if (!d || !validAcquisitionAllocation(d))
|
||
return stop(
|
||
'Método/parâmetrosD não recuperáveis;sem percentagem por defeito.',
|
||
);
|
||
if (
|
||
!draft ||
|
||
draft.currency !== 'MZN' ||
|
||
row.sourceCurrency !== 'MZN' ||
|
||
!/^2026-(0[1-9]|1[0-2])$/.test(row.period)
|
||
)
|
||
return stop('ÂmbitoD2026/MZN ou contexto em falta.');
|
||
if (
|
||
!d.scopeRef.trim() ||
|
||
!d.source.trim() ||
|
||
d.facts.scopeConfirmed !== 'yes'
|
||
)
|
||
return stop(
|
||
'Âmbito da aquisição/actividade,fonte e correspondência por confirmar;sem inferir da oportunidade.',
|
||
);
|
||
if (d.method === 'reduced') {
|
||
if (row.allocation !== 'taxable_only')
|
||
return stop(
|
||
'18(6) neste percurso exige afectação exclusiva à operação de base reduzida;combinação com afectação mista ainda não demonstrada.',
|
||
);
|
||
const b = reducedBaseBook(draft),
|
||
source = b.rows.find((r) => r.id === d.reducedId);
|
||
if (
|
||
!source ||
|
||
!reducedBaseCurrent(draft, b) ||
|
||
JSON.stringify(source) !== d.reducedFingerprint
|
||
)
|
||
return stop(
|
||
'Origem de base reduzida alterada/incompleta:rever e adoptar expressamente;histórico conservado.',
|
||
);
|
||
const result = evaluateReducedBase(source, draft.currency),
|
||
rule = reducedBaseTreatments.find((r) => r.id === source.treatment);
|
||
if (!rule || result.status !== 'determined')
|
||
return stop(
|
||
'Condições da base reduzida de origem ainda indeterminadas;não basta escolher percentagem.',
|
||
);
|
||
return {
|
||
ratio: fraction(BigInt(rule.percent), 100n),
|
||
reason: `18(6):proporção específica${rule.percent}% reutilizada de${rule.branch}/${source.id};aplicada uma vez ao IVA elegível,sem converter percentagem de base em taxaIVA.`,
|
||
source: acquisitionAllocationSource + ';' + b.result!.source,
|
||
};
|
||
}
|
||
if (row.allocation !== 'mixed')
|
||
return stop(
|
||
'Método22 exige afectação mista expressamente identificada;conserve a afectação exclusiva quando aplicável.',
|
||
);
|
||
if (d.facts.rightBasis !== 'yes')
|
||
return stop(
|
||
'Identifique a parte/operações com direito18/19;isenção ou sector não estabelecem o direito.',
|
||
);
|
||
if (
|
||
!/^\d{1,15}(\.\d{1,6})?$/.test(d.numerator) ||
|
||
!/^\d{1,15}(\.\d{1,6})?$/.test(d.denominator)
|
||
)
|
||
return stop(
|
||
'Numerador/denominador em falta ou inválidos;sem percentagem presumida.',
|
||
);
|
||
const n = decimalFraction(d.numerator),
|
||
den = decimalFraction(d.denominator);
|
||
if (BigInt(den.numerator) === 0n)
|
||
return stop('Denominador zero:não existe quociente aplicável.');
|
||
const rawRatio = fraction(
|
||
BigInt(n.numerator) * BigInt(den.denominator),
|
||
BigInt(n.denominator) * BigInt(den.numerator),
|
||
);
|
||
if (BigInt(rawRatio.numerator) > BigInt(rawRatio.denominator))
|
||
return stop(
|
||
'Numerador excede denominador;resolver bases sem limitar silenciosamente a100%.',
|
||
);
|
||
if (d.method === 'actual') {
|
||
if (
|
||
!d.unit.trim() ||
|
||
d.facts.actualScope !== 'yes' ||
|
||
d.facts.communication !== 'yes' ||
|
||
!d.communicationRef.trim() ||
|
||
d.facts.conditionsMet !== 'yes'
|
||
)
|
||
return stop(
|
||
'Afectação real22(2):critério/unidade,repartição efectiva,comunicação prévia e condições concretamente aplicáveis por completar;sem criar aprovação adicional.',
|
||
);
|
||
return {
|
||
ratio: rawRatio,
|
||
rawRatio,
|
||
reason: `Afectação real22(2):${d.numerator}/${d.denominator} ${d.unit};critério e comunicação${d.communicationRef}. Fracção real exacta,sem arredondamento de pro rata.`,
|
||
source: acquisitionAllocationSource,
|
||
};
|
||
}
|
||
if (d.method !== 'pro_rata')
|
||
return stop(
|
||
'Escolha afectação real,pro rata provisório ou proporção específica18(6).',
|
||
);
|
||
if (d.actualRequired === 'yes')
|
||
return stop(
|
||
'Está identificada imposição de afectação real22(3);não aplicar pro rata em substituição.',
|
||
);
|
||
if (d.facts.annualScope !== 'yes' || d.facts.netExclusions !== 'yes')
|
||
return stop(
|
||
'Confirmar totais anuais semIVA e âmbito22(4–5):inclui fora de campo;exclui vendas de activo usado e imobiliárias/financeiras acessórias. Não usar só vendas de um mês como total anual.',
|
||
);
|
||
const year = Number(row.period.slice(0, 4));
|
||
if (d.basis === 'prior_year') {
|
||
if (d.referenceYear !== String(year - 1))
|
||
return stop(
|
||
'Pro rata provisório22(6) usa operações do ano anterior ao exercício;sem substituir pelo mês corrente.',
|
||
);
|
||
} else if (d.basis === 'initial_changed') {
|
||
if (
|
||
d.referenceYear !== String(year) ||
|
||
d.facts.initialChanged !== 'yes' ||
|
||
!d.declarationRef.trim()
|
||
)
|
||
return stop(
|
||
'Estimativa22(7):início/alteração substancial,ano e referência da declaração de início/alterações por identificar.',
|
||
);
|
||
} else
|
||
return stop(
|
||
'Indique base provisória22(6) ou estimativa expressa22(7);regularização final22(6) segueE.',
|
||
);
|
||
const numerator = BigInt(rawRatio.numerator),
|
||
denominator = BigInt(rawRatio.denominator);
|
||
const hundredths = (numerator * 100n + denominator - 1n) / denominator;
|
||
return {
|
||
ratio: fraction(hundredths, 100n),
|
||
rawRatio,
|
||
reason: `Pro rata provisório22(4/6/7/8):quociente${fractionLabel(rawRatio)}→${hundredths}% (quociente à centésima superior). Base anual${d.referenceYear},${d.basis === 'prior_year' ? 'ano anterior' : 'estimativa declarada'}. Acerto anual emE,sem recalcular históricos.`,
|
||
source: acquisitionAllocationSource,
|
||
};
|
||
}
|
||
export function evaluateAcquisitionAllocation(
|
||
row: Acquisition,
|
||
currency: string,
|
||
registered: Choice,
|
||
draft: Record<string, unknown> | undefined,
|
||
raw: AcquisitionResult,
|
||
): AcquisitionResult {
|
||
const decision = acquisitionAllocationDecision(draft, row),
|
||
d = row.d!;
|
||
const unknown = (reason: string): AcquisitionResult => ({
|
||
...raw,
|
||
deductible: null,
|
||
reason,
|
||
eligibility: {
|
||
status: 'indeterminate',
|
||
reason,
|
||
source: decision.source,
|
||
},
|
||
});
|
||
if (!decision.ratio) return unknown(decision.reason);
|
||
if (!row.bc)
|
||
return unknown(
|
||
'Complete a natureza/condiçõesB/C da aquisição antes da proporção;sem isenção de condições documentais.',
|
||
);
|
||
const base = evaluateAcquisitionBC(row, currency, registered, draft, raw, {
|
||
right: {
|
||
status: 'eligible',
|
||
reason: decision.reason,
|
||
source: decision.source,
|
||
scopeId: draft ? acquisitionScope(draft) : undefined,
|
||
},
|
||
noExcludedPayments: d.noExcludedPayments,
|
||
});
|
||
if (base.deductible === null || base.eligibility?.status !== 'eligible')
|
||
return base;
|
||
const before = base.deductibleExact ?? decimalFraction(base.deductible);
|
||
const exact = fraction(
|
||
BigInt(before.numerator) * BigInt(decision.ratio.numerator),
|
||
BigInt(before.denominator) * BigInt(decision.ratio.denominator),
|
||
);
|
||
const reason = base.reason + ' ' + decision.reason;
|
||
return {
|
||
...base,
|
||
deductible: fractionPresentation(exact).decimal,
|
||
deductibleExact: exact,
|
||
nonDeductibleExact: subtractFractions(
|
||
decimalFraction(base.vat!),
|
||
exact,
|
||
),
|
||
reason,
|
||
eligibility: { ...base.eligibility, status: 'eligible', reason },
|
||
allocationCalculation: {
|
||
version: acquisitionAllocationVersion,
|
||
ratio: decision.ratio,
|
||
rawRatio: decision.rawRatio ?? decision.ratio,
|
||
before,
|
||
source: decision.source,
|
||
},
|
||
};
|
||
}
|