Files
Investidor-V2/resources/js/lib/iva-regularizations.ts
T

643 lines
23 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 type {
AcquisitionCalculation,
Acquisition,
AcquisitionResult,
} from './iva-acquisitions.ts';
import {
addFractions,
decimalFraction,
fraction,
fractionLabel,
subtractFractions,
positiveFraction,
type FiscalFraction,
} from './fiscal-rational.ts';
export const regularizationSource =
'L2-E: CIVA22(4–8),Lei13/2016 PDF16/p1426(274),acerto anual no último período do ano;CIVA51(1/4),PDF21/p1426(279),rectificação da dedução pelo adquirente até ao fim do período seguinte à recepção. Decreto7/2008,RCIVA27,BR16 PDF5/p103,registo individual de activos: data,IVA suportado e deduzido. Caixas ModeloA12/13;divergência de remissões textuais11/12 conservada,sem alterar P001/05–06.';
type Choice = '' | 'yes' | 'no';
export type RegularizationEvent = {
id: string;
kind: 'annual' | 'purchase_correction';
period: string;
source: AcquisitionCalculation | null;
rowIds: string[];
numerator: string;
denominator: string;
reference: string;
receivedDate: string;
correctedDeductible: string;
reason: string;
facts: Record<string, Choice>;
};
export type RegularizationResult = {
id: string;
period: string;
status: 'determined' | 'indeterminate';
reason: string;
delta: FiscalFraction | null;
rawRatio?: FiscalFraction;
ratio?: FiscalFraction;
details: string;
source: string;
};
type Calculation = {
version: 1;
calculatedAt: string;
key: string;
events: RegularizationResult[];
};
export type RegularizationBook = {
version: 1;
events: RegularizationEvent[];
completePeriods: string[];
result?: Calculation;
history?: Calculation[];
};
type FrozenLine = { input: Acquisition; result: AcquisitionResult };
const scope = (draft: Record<string, unknown>) =>
inputText(draft.context_opportunity || 'general');
function store(draft: Record<string, unknown>): Record<string, unknown> {
const s = JSON.parse(
inputText(draft.modelo_a_snapshot || '{"version":1,"books":{}}'),
);
if (s.version !== 1 || !s.books)
throw Error('Origem fiscal não recuperável;conserve os originais.');
return s;
}
export function validateRegularizationStore(s: Record<string, unknown>): void {
if (s.regularizations === undefined) return;
if (
!s.regularizations ||
typeof s.regularizations !== 'object' ||
Array.isArray(s.regularizations)
)
throw Error(
'Mapa de regularizações não recuperável;originais preservados.',
);
for (const value of Object.values(s.regularizations))
assertRegularizationBook(value as RegularizationBook);
}
function assertRegularizationBook(value: RegularizationBook): void {
const badSource = (s: AcquisitionCalculation | null) =>
s !== null &&
(!s ||
typeof s !== 'object' ||
!Array.isArray(s.inputs) ||
!Array.isArray(s.rows) ||
!Array.isArray(s.periods) ||
!Number.isInteger(s.revision) ||
!['currency', 'opportunity', 'calculatedAt', 'sourceKey'].every(
(k) => typeof s[k as keyof AcquisitionCalculation] === 'string',
));
if (
!value ||
value.version !== 1 ||
!Array.isArray(value.events) ||
!Array.isArray(value.completePeriods) ||
value.completePeriods.some((p) => typeof p !== 'string') ||
(value.history !== undefined && !Array.isArray(value.history)) ||
value.events.some(
(e) =>
!e ||
!['annual', 'purchase_correction'].includes(e.kind) ||
![
'id',
'period',
'numerator',
'denominator',
'reference',
'receivedDate',
'correctedDeductible',
'reason',
].every(
(k) =>
typeof e[k as keyof RegularizationEvent] === 'string',
) ||
!Array.isArray(e.rowIds) ||
e.rowIds.some((id) => typeof id !== 'string') ||
!e.facts ||
typeof e.facts !== 'object' ||
Array.isArray(e.facts) ||
Object.values(e.facts).some(
(v) => !['', 'yes', 'no'].includes(v),
) ||
badSource(e.source),
)
)
throw Error(
'Regularizações não recuperáveis;originais preservados,sem substituir por zero.',
);
if (
value.result &&
(!Array.isArray(value.result.events) ||
value.result.version !== 1 ||
typeof value.result.key !== 'string' ||
typeof value.result.calculatedAt !== 'string' ||
value.result.events.some(
(e) =>
!e ||
typeof e.id !== 'string' ||
typeof e.period !== 'string' ||
!['determined', 'indeterminate'].includes(e.status) ||
(e.delta !== null &&
(!e.delta ||
!/^-?\d{1,80}$/.test(e.delta.numerator) ||
!/^\d{1,80}$/.test(e.delta.denominator) ||
BigInt(e.delta.denominator) === 0n)),
))
)
throw Error(
'Resultado de regularizações não recuperável;originais preservados.',
);
}
export function regularizationBook(
draft: Record<string, unknown>,
): RegularizationBook {
const value = (
store(draft).regularizations as
| Record<string, RegularizationBook>
| undefined
)?.[scope(draft)];
if (!value) return { version: 1, events: [], completePeriods: [] };
assertRegularizationBook(value);
return value;
}
export function saveRegularizationBook(
draft: Record<string, unknown>,
book: RegularizationBook,
): string {
const s = store(draft);
return JSON.stringify({
...s,
regularizations: {
...(s.regularizations as object),
[scope(draft)]: book,
},
});
}
export function regularizationSources(
draft: Record<string, unknown>,
): AcquisitionCalculation[] {
const b = (
store(draft).books as Record<
string,
{
result?: AcquisitionCalculation;
history?: AcquisitionCalculation[];
}
>
)[scope(draft)];
return b
? [...(b.history ?? []), ...(b.result ? [b.result] : [])].filter(
(v, i, a) =>
a.findIndex(
(x) => JSON.stringify(x) === JSON.stringify(v),
) === i,
)
: [];
}
export function newRegularization(
id: string,
kind: RegularizationEvent['kind'],
source: AcquisitionCalculation | null,
): RegularizationEvent {
return {
id,
kind,
period: source ? source.inputs[0]?.period.slice(0, 4) + '-12' : '',
source: source ? structuredClone(source) : null,
rowIds:
source?.inputs
.filter((r) =>
kind === 'annual' ? r.d?.method === 'pro_rata' : true,
)
.map((r) => r.id) ?? [],
numerator: '',
denominator: '',
reference: '',
receivedDate: '',
correctedDeductible: '',
reason: '',
facts: {},
};
}
function frozenLines(e: RegularizationEvent): FrozenLine[] {
return e.rowIds.map((id) => ({
input: e.source!.inputs.find((r) => r.id === id)!,
result: e.source!.rows.find((r) => r.id === id)!,
}));
}
function sourceAvailable(
draft: Record<string, unknown>,
e: RegularizationEvent,
): boolean {
return (
!!e.source &&
regularizationSources(draft).some(
(s) => JSON.stringify(s) === JSON.stringify(e.source),
)
);
}
function multiply(a: FiscalFraction, b: FiscalFraction) {
return fraction(
BigInt(a.numerator) * BigInt(b.numerator),
BigInt(a.denominator) * BigInt(b.denominator),
);
}
const validDate = (s: string) =>
/^2026-\d{2}-\d{2}$/.test(s) &&
!Number.isNaN(Date.parse(s)) &&
new Date(s).toISOString().slice(0, 10) === s;
function identity(e: RegularizationEvent, id: string): string {
const r = e.source?.inputs.find((r) => r.id === id);
return JSON.stringify([
e.kind,
r?.supplier.trim().toLowerCase(),
r?.documentType,
r?.documentRef.trim().toLowerCase(),
r?.documentDate,
r?.documentLine.trim(),
e.kind === 'annual' ? e.period.slice(0, 4) : '',
]);
}
export function evaluateRegularization(
draft: Record<string, unknown>,
e: RegularizationEvent,
): RegularizationResult {
const stop = (reason: string): RegularizationResult => ({
id: e.id,
period: e.period,
status: 'indeterminate',
reason,
delta: null,
details: 'Histórico preservado;sem lançamento automático.',
source: regularizationSource,
});
if (
!/^2026-(0[1-9]|1[0-2])$/.test(e.period) ||
draft.currency !== 'MZN' ||
e.source?.currency !== 'MZN' ||
e.source.opportunity !== scope(draft) ||
!sourceAvailable(draft, e)
)
return stop(
'Âmbito2026/MZN e resultado original congelado na mesma oportunidade por identificar;uma edição actual não substitui o resultado original.',
);
if (!e.rowIds.length || new Set(e.rowIds).size !== e.rowIds.length)
return stop('Seleccione parcelas originais distintas.');
const lines = frozenLines(e);
if (
lines.some(
(l) =>
!l.input ||
!l.result ||
l.result.deductibleExact === undefined ||
l.result.eligibility?.status !== 'eligible' ||
!l.input.documentRef.trim(),
)
)
return stop(
'Parcela original sem dedução exacta/elegível/documento;não presumir dedução inicial.',
);
if (
!e.reference.trim() ||
!e.reason.trim() ||
e.facts.scope !== 'yes' ||
e.facts.recorded !== 'yes'
)
return stop(
'Identifique documento/fonte,motivo,âmbito e montante originalmente deduzido (efectivo ou pressuposto da projecção).',
);
let delta: FiscalFraction,
details: string,
rawRatio: FiscalFraction | undefined,
ratio: FiscalFraction | undefined;
if (e.kind === 'annual') {
if (
!e.period.endsWith('-12') ||
lines.some(
(l) =>
l.input.period.slice(0, 4) !== e.period.slice(0, 4) ||
l.input.d?.method !== 'pro_rata' ||
!l.result.allocationCalculation,
)
)
return stop(
'Acerto22(6):apenas pro rata provisório do mesmo ano,no último período;afectação real e18(6) têm regras próprias.',
);
if (e.facts.annual !== 'yes' || e.facts.exclusions !== 'yes')
return stop(
'Confirme bases efectivas anuais22(4),semIVA,incluindo fora de campo,e exclusões22(5);não usar apenas vendas do mês.',
);
if (
!/^\d{1,15}(\.\d{1,6})?$/.test(e.numerator) ||
!/^\d{1,15}(\.\d{1,6})?$/.test(e.denominator)
)
return stop('Bases anuais finais em falta ou inválidas.');
const n = decimalFraction(e.numerator),
d = decimalFraction(e.denominator);
if (BigInt(d.numerator) === 0n)
return stop('Denominador anual zero;sem percentagem por defeito.');
rawRatio = fraction(
BigInt(n.numerator) * BigInt(d.denominator),
BigInt(n.denominator) * BigInt(d.numerator),
);
if (BigInt(rawRatio.numerator) > BigInt(rawRatio.denominator))
return stop('Numerador anual excede denominador.');
ratio = fraction(
(BigInt(rawRatio.numerator) * 100n +
BigInt(rawRatio.denominator) -
1n) /
BigInt(rawRatio.denominator),
100n,
);
delta = addFractions(
lines.map((l) =>
subtractFractions(
multiply(l.result.allocationCalculation!.before, ratio!),
l.result.deductibleExact!,
),
),
);
details =
lines
.map(
(l) =>
`${l.input.documentRef}/linha${l.input.documentLine || 'única'}:IVA elegível antesD${fractionLabel(l.result.allocationCalculation!.before)},deduzido${fractionLabel(l.result.deductibleExact!)},final${fractionLabel(multiply(l.result.allocationCalculation!.before, ratio!))}`,
)
.join('; ') +
`;quociente final${fractionLabel(rawRatio)};aplicado${fractionLabel(ratio)}`;
} else {
if (lines.length !== 1 || !validDate(e.receivedDate))
return stop(
'Rectificação51(4):seleccione uma linha e a data de recepção do documento rectificativo.',
);
const received = e.receivedDate.slice(0, 7),
next = new Date(e.receivedDate + 'T00:00:00Z');
next.setUTCMonth(next.getUTCMonth() + 1, 1);
const last = next.toISOString().slice(0, 7);
if (
e.receivedDate < lines[0].input.documentDate ||
e.period < received ||
e.period > last ||
e.period < lines[0].input.period
)
return stop(
'Período fora da recepção/seguinte51(4),ou anterior à dedução original;sem alteração retroactiva.',
);
if (
e.facts.supplierCorrection !== 'yes' ||
e.facts.rightUnchanged !== 'yes'
)
return stop(
'Confirme rectificação pelo fornecedor e manutenção do âmbito/condições da dedução. Mudança de direito exige enquadramento próprio.',
);
let corrected: FiscalFraction;
try {
corrected = decimalFraction(e.correctedDeductible);
} catch {
return stop(
'Indique a dedução corrigida documentada,sem taxa ou dedução presumida.',
);
}
if (
BigInt(
subtractFractions(corrected, lines[0].result.deductibleExact!)
.numerator,
) > 0n
)
return stop(
'Este percurso51(4) trata anulação/redução da dedução pelo adquirente;acréscimos ou erros têm fundamento/prazo próprios.',
);
delta = subtractFractions(corrected, lines[0].result.deductibleExact!);
details = `${lines[0].input.documentRef};deduzido${fractionLabel(lines[0].result.deductibleExact!)};corrigido${fractionLabel(corrected)};recebido${e.receivedDate};limite${last}`;
}
const sign = BigInt(delta.numerator);
return {
id: e.id,
period: e.period,
status: 'determined',
delta,
...(rawRatio && ratio ? { rawRatio, ratio } : {}),
reason:
sign > 0n
? 'A favor do sujeito passivo — campo12'
: sign < 0n
? 'A favor do Estado — campo13'
: 'Sem diferença;evento conservado,sem nova dedução',
details: `${details};revisão original${e.source.revision}/${e.source.calculatedAt};${lines.some((l) => l.input.origin === 'projected') ? 'Pressuposto de simulação;sem declaração fiscal submetida' : 'Dados efectivos declarados;submissão fora do portal'};${e.reference};${e.reason}`,
source: regularizationSource,
};
}
// Change detection only. Keep frozen source values, without embedding them again in every calculation key.
function fingerprint(value: unknown): string {
const raw = JSON.stringify(value);
let hash = 14695981039346656037n;
for (let i = 0; i < raw.length; i++)
hash = BigInt.asUintN(
64,
(hash ^ BigInt(raw.charCodeAt(i))) * 1099511628211n,
);
return `${raw.length}:${hash.toString(16)}`;
}
function key(book: RegularizationBook) {
return fingerprint([book.version, book.events, book.completePeriods]);
}
export function recordRegularizations(
draft: Record<string, unknown>,
book: RegularizationBook,
now = new Date().toISOString(),
): RegularizationBook {
if (new Set(book.events.map((e) => e.id)).size !== book.events.length)
throw Error('Evento repetido;sem dupla regularização.');
const identities = book.events.flatMap((e) =>
e.rowIds.map((id) => identity(e, id)),
);
if (new Set(identities).size !== identities.length)
throw Error(
'Mesma aquisição/ano ou documento já incluído noutra regularização deste motivo;consolide o evento original.',
);
// Mixing an annual correction and a credit note on the same deduction needs sequencing, not two independent deltas.
const seen = new Map<string, string>();
for (const e of book.events)
for (const id of e.rowIds) {
const k = identity({ ...e, kind: 'purchase_correction' }, id),
prior = seen.get(k);
if (prior && prior !== e.kind)
throw Error(
'Acerto anual e documento rectificativo sobre a mesma parcela exigem reconciliação sequencial;sem dupla correcção.',
);
seen.set(k, e.kind);
}
const result: Calculation = {
version: 1,
calculatedAt: now,
key: key(book),
events: book.events.map((e) => evaluateRegularization(draft, e)),
};
return {
...book,
...(book.result
? {
history: [
...(book.history ?? []),
structuredClone(book.result),
],
}
: {}),
result,
};
}
export function regularizationsCurrent(
draft: Record<string, unknown>,
b: RegularizationBook,
): boolean {
return (
!!b.result &&
b.result.version === 1 &&
b.result.key === key(b) &&
b.result.events.length === b.events.length &&
b.events.every(
(e) =>
sourceAvailable(draft, e) &&
b.result!.events.filter(
(r) => r.id === e.id && r.period === e.period,
).length === 1,
)
);
}
export function regularizationFields(
draft: Record<string, unknown>,
period: string,
currentRows?: Acquisition[],
): { fields: Record<string, FiscalFraction | null>; reasons: string[] } | null {
const b = regularizationBook(draft);
if (
!b.events.some((e) => e.period === period) &&
!b.completePeriods.includes(period)
)
return null;
const events = b.result?.events.filter((e) => e.period === period) ?? [];
if (
currentRows &&
b.events.some(
(e) =>
e.period === period &&
e.source &&
frozenLines(e).some(
(l) =>
l.input &&
currentRows.some(
(r) =>
r.id === l.input.id &&
JSON.stringify(r) !== JSON.stringify(l.input),
),
),
)
)
return {
fields: { '12': null, '13': null },
reasons: [
'A aquisição actual difere da parcela congelada usada emE;reconcilie a diferença antes de somar a regularização,sem recalcular o histórico.',
],
};
if (
!b.completePeriods.includes(period) ||
!regularizationsCurrent(draft, b) ||
events.some((e) => !e.delta)
)
return {
fields: { '12': null, '13': null },
reasons: [
'RegularizaçõesE:complete/reconcilie o período e guarde o cálculo correspondente;sem zero presumido nem uso de resultado histórico.',
],
};
return {
fields: {
'12': addFractions(events.map((e) => positiveFraction(e.delta!))),
'13': addFractions(
events.map((e) =>
positiveFraction(
fraction(
-BigInt(e.delta!.numerator),
BigInt(e.delta!.denominator),
),
),
),
),
},
reasons: [
'E:apenas eventos documentados declarados completos;12/13 separados,sem recalcular aquisições históricas.',
],
};
}
export function regularizationSourceKey(
draft: Record<string, unknown>,
): string {
const b = regularizationBook(draft);
const reduced =
(
store(draft).regulatedReducedBases as
| Record<string, { rows: unknown[] }>
| undefined
)?.[scope(draft)]?.rows ?? [];
return b.events.length || b.completePeriods.length
? fingerprint([b, regularizationsCurrent(draft, b), reduced])
: '';
}
export function regularizationReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
const b = regularizationBook(draft);
if (!b.events.length) return [];
return [
[
'IVA mensal',
'RegularizaçõesE',
regularizationsCurrent(draft, b)
? 'Resultado correspondente às entradas'
: 'Histórico preservado;rever entradas/origem',
regularizationSource,
],
...(b.result?.events ?? []).map(
(e) =>
[
'IVA mensal',
`Regularização ${e.id} — ${e.period}`,
`${e.status === 'determined' ? e.reason : 'Indeterminado: ' + e.reason};delta${e.delta ? fractionLabel(e.delta) : 'indeterminado'}`,
e.details + ';' + e.source,
] as [string, string, string, string],
),
];
}
export function frozenAssetRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
const sources = regularizationSources(draft),
seen = new Set<string>(),
out: Array<[string, string, string, string]> = [];
for (const s of [...sources].reverse())
for (const r of s.inputs.filter(
(r) => r.field === '08' || r.investmentAsset === true,
)) {
const k = JSON.stringify([
r.supplier,
r.documentRef,
r.documentDate,
r.documentLine,
]);
if (seen.has(k)) continue;
seen.add(k);
const v = s.rows.find((v) => v.id === r.id);
out.push([
'IVA mensal',
`Activo — ${r.description || r.id}`,
`${r.documentRef};linha${r.documentLine || 'única'};aquisição${r.documentDate};IVA suportado${v?.vat ?? 'indeterminado'};IVA deduzido${v?.deductibleExact ? fractionLabel(v.deductibleExact) : (v?.deductible ?? 'indeterminado')}`,
`RCIVA27,Decreto7/2008 PDF5/p103;${s.currency};revisão${s.revision}/${s.calculatedAt};${r.origin === 'projected' ? 'Pressuposto de simulação' : 'Dados efectivos declarados'};sem amortização fiscal ou regularização plurianual presumidas.`,
]);
}
return out;
}