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

405 lines
16 KiB
TypeScript

import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
export const valueReviewVersion = 'value-review-base/1';
export const valueReviewSource =
'CIVA republicado Lei13/2016,15(7),PDF14/p1426(272):para efeitos15(1),quando a contraprestação é inferior à resultante dos preços correntes ou normais de venda à porta da fábrica,por grosso ou a retalho,ou aos preços correntes/normais do serviço,tendo em conta a natureza das transmissões,a Administração Tributária pode proceder à correcção. Lei22/2022PDF3mantém15(7);Lei10/2025não altera15. Recorte2026/MZN;comparação não exerce o poder da AT,nem substitui automaticamente a contraprestação pelo valor de referência. Sem incidência,taxa,IVA,dedução/campo11,regularizações ou totaisModeloA.';
export const valueReviewTreatments = [
{ id: 'price_comparison', label: 'Comparar sem correcção automática' },
{
id: 'referenced_correction',
label: 'Registar correcção referenciada / cenário condicionado',
},
] as const;
export const priceContexts = [
{ id: 'factory', label: 'Venda à porta da fábrica' },
{ id: 'wholesale', label: 'Venda por grosso' },
{ id: 'retail', label: 'Venda a retalho' },
{ id: 'service', label: 'Preço corrente ou normal do serviço' },
] as const;
export const valueReviewConditions = [
[
'taxableOperation',
'A operação e a contraprestação para efeitos15(1)estão identificadas',
],
[
'referenceCorresponds',
'O preço corrente ou normal e o contexto escolhido correspondem à natureza desta transmissão ou serviço,no período e âmbito identificados',
],
[
'netComposition',
'Os montantes comparados têm composição equivalente,sem próprioIVA,com inclusões/exclusões aplicáveis15(5)/(6)consideradas',
],
] as const;
type Choice = '' | 'yes' | 'no';
type Condition = (typeof valueReviewConditions)[number][0];
export type ValueReviewRow = {
id: string;
treatment: string;
priceContext: string;
description: string;
period: string;
origin: 'projected' | 'effective';
currency: string;
operationRef: string;
documentRef: string;
documentDate: string;
declaredAmount: string;
referenceAmount: string;
referenceRef: string;
referenceScope: string;
compositionRef: string;
evidence: string;
correctedAmount: string;
correctionRef: string;
correctionScope: string;
correctionDate: string;
correctionApplies: Choice;
conditions: Partial<Record<Condition, Choice>>;
};
type LineResult = {
id: string;
status: 'determined' | 'not_applicable' | 'indeterminate';
baseAmount: string | null;
vat: null;
rate: null;
comparison: 'below' | 'not_below' | null;
reason: string;
};
export type ValueReviewCalculation = {
version: string;
revision: number;
calculatedAt: string;
opportunity: string;
location: string;
sourceKey: string;
source: string;
inputs: ValueReviewRow[];
rows: LineResult[];
};
export type ValueReviewBook = {
rows: ValueReviewRow[];
result?: ValueReviewCalculation;
history?: ValueReviewCalculation[];
};
type Store = ReturnType<typeof readAcquisitionStore> & {
valueReviews?: Record<string, ValueReviewBook>;
};
export function newValueReview(id: string, currency: string): ValueReviewRow {
return {
id,
treatment: '',
priceContext: '',
description: '',
period: '',
origin: 'projected',
currency,
operationRef: '',
documentRef: '',
documentDate: '',
declaredAmount: '',
referenceAmount: '',
referenceRef: '',
referenceScope: '',
compositionRef: '',
evidence: '',
correctedAmount: '',
correctionRef: '',
correctionScope: '',
correctionDate: '',
correctionApplies: '',
conditions: {},
};
}
export function switchValueReview(
row: ValueReviewRow,
treatment: string,
): ValueReviewRow {
return { ...row, treatment, correctionApplies: '' };
}
export function switchPriceContext(
row: ValueReviewRow,
priceContext: string,
): ValueReviewRow {
return {
...row,
priceContext,
conditions: { ...row.conditions, referenceCorresponds: '' },
correctionApplies: '',
};
}
export function switchValueOrigin(
row: ValueReviewRow,
origin: ValueReviewRow['origin'],
): ValueReviewRow {
return { ...row, origin, correctionApplies: '' };
}
function readStore(draft: Record<string, unknown>): Store {
const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if (
store.valueReviews !== undefined &&
(!store.valueReviews ||
typeof store.valueReviews !== 'object' ||
Array.isArray(store.valueReviews))
)
throw new Error(
'Bases de contraprestação não recuperáveis;originais preservados.',
);
const fields = Object.keys(newValueReview('', '')).filter(
(key) => key !== 'conditions',
);
for (const book of Object.values(store.valueReviews ?? {}))
if (
!book ||
!Array.isArray(book.rows) ||
(book.history !== undefined && !Array.isArray(book.history)) ||
book.rows.some(
(row) =>
!row ||
fields.some(
(key) =>
typeof row[key as keyof ValueReviewRow] !==
'string',
) ||
!row.conditions ||
typeof row.conditions !== 'object' ||
Array.isArray(row.conditions),
)
)
throw new Error(
'Linhas de contraprestação não recuperáveis;originais preservados,sem substituição por lista vazia.',
);
return store;
}
export function valueReviewBook(
draft: Record<string, unknown>,
): ValueReviewBook {
return (
readStore(draft).valueReviews?.[acquisitionScope(draft)] ?? { rows: [] }
);
}
export function saveValueReviewBook(
draft: Record<string, unknown>,
book: ValueReviewBook,
): string {
const current = readStore(draft),
encoded = JSON.stringify({
...current,
valueReviews: {
...current.valueReviews,
[acquisitionScope(draft)]: book,
},
});
readStore({ ...draft, modelo_a_snapshot: encoded });
return encoded;
}
function decimal(raw: unknown): bigint | null {
if (typeof raw !== 'string' || !/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))
return null;
const [whole, fraction = ''] = raw.trim().split('.');
return BigInt(whole) * 1000000n + BigInt(fraction.padEnd(6, '0'));
}
function money(n: bigint): string {
return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0').replace(/0+$/, '').padEnd(2, '0')}`;
}
function validDate(value: string): boolean {
const d = new Date(value + 'T00:00:00Z');
return (
/^\d{4}-\d{2}-\d{2}$/.test(value) &&
Number.isFinite(d.getTime()) &&
d.toISOString().slice(0, 10) === value
);
}
export function evaluateValueReview(
row: ValueReviewRow,
currency: unknown,
): LineResult {
const result: LineResult = {
id: row.id,
status: 'indeterminate',
baseAmount: null,
vat: null,
rate: null,
comparison: null,
reason: '',
};
const stop = (
reason: string,
status: LineResult['status'] = 'indeterminate',
) => ({ ...result, status, reason });
if (
!valueReviewTreatments.some((item) => item.id === row.treatment) ||
!priceContexts.some((item) => item.id === row.priceContext)
)
return stop(
'Identifique o percurso e o contexto do preço de referência;desconhecido não permite presumir correcção.',
);
if (currency !== 'MZN' || row.currency !== currency)
return stop(
'Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa.',
);
if (
!/^2026-(0[1-9]|1[0-2])$/.test(row.period) ||
!['projected', 'effective'].includes(row.origin)
)
return stop(
'Indique mês em2026 e dados efectivos declarados ou pressupostos expressos de projecção.',
);
const failed = valueReviewConditions.filter(
([key]) => row.conditions[key] === 'no',
);
if (failed.length)
return stop(
'Não se aplica esta comparação: ' +
failed.map(([, label]) => label).join('; ') +
'. Não é isenção nem IVA zero.',
'not_applicable',
);
if (valueReviewConditions.some(([key]) => row.conditions[key] !== 'yes'))
return stop(
'Confirme as condições próprias da comparação;desconhecido não é zero,não aplicável ou correcção.',
);
if (
!row.description.trim() ||
!row.operationRef.trim() ||
!row.documentRef.trim() ||
!validDate(row.documentDate) ||
!row.referenceRef.trim() ||
!row.referenceScope.trim() ||
!row.compositionRef.trim() ||
!row.evidence.trim()
)
return stop(
'Identifique operação,documento/data,preço corrente/normal,referência/âmbito e composição,ou pressupostos expressos;sem avaliação automática/certificado/aprovador novo.',
);
const declared = decimal(row.declaredAmount),
reference = decimal(row.referenceAmount);
if (declared === null || reference === null)
return stop(
'Indique contraprestação e preço de referência comparáveis;valor desconhecido ou inválido não é zero.',
);
result.comparison = declared < reference ? 'below' : 'not_below';
const comparison =
declared < reference
? `Contraprestação ${money(declared)} inferior à referência ${money(reference)} por ${money(reference - declared)}MZN;AT pode proceder à correcção,sem automatismo.`
: `Contraprestação ${money(declared)} não inferior à referência ${money(reference)}MZN;não se demonstra este pressuposto15(7).`;
if (row.treatment === 'price_comparison')
return {
...result,
status: 'determined',
baseAmount: money(declared),
reason:
comparison +
' Contraprestação declarada conservada como base neste recorte,sem correcção pelo portal;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não determinados.',
};
if (declared >= reference)
return stop(
comparison +
' A correcção deste percurso15(7)não é aplicada. Registo e referências conservados.',
'not_applicable',
);
if (row.correctionApplies === 'no')
return stop(
comparison +
' A correcção referenciada não abrange esta operação/período/condições;sem aplicação automática.',
'not_applicable',
);
if (
row.correctionApplies !== 'yes' ||
!row.correctionRef.trim() ||
!row.correctionScope.trim() ||
!validDate(row.correctionDate)
)
return stop(
comparison +
' Identifique referência,data e âmbito da correcção AT aplicável ou pressuposto expresso do cenário condicionado;comparar não substitui uma correcção.',
);
const corrected = decimal(row.correctedAmount);
if (corrected === null)
return stop(
comparison +
' Indique o montante da correcção referenciada ou pressuposto condicionado;sem usar automaticamente o maior valor ou a referência.',
);
return {
...result,
status: 'determined',
baseAmount: money(corrected),
reason:
comparison +
` ${row.origin === 'projected' ? 'Cenário condicionado à correcção expressamente assumida,não decisão AT concedida' : 'Correcção AT nos dados efectivos declarados,com referência/data/âmbito identificados'}:base ${money(corrected)}MZN. Montante da referência não substituído automaticamente. Sem IVA/taxa,dedução/campo11,regularizações ou totaisModeloA.`,
};
}
function key(draft: Record<string, unknown>, book: ValueReviewBook): string {
return JSON.stringify({
version: valueReviewVersion,
scope: acquisitionScope(draft),
location: draft.selection_labels ?? {},
currency: draft.currency,
rows: book.rows,
});
}
export function valueReviewCurrent(
draft: Record<string, unknown>,
book: ValueReviewBook,
): boolean {
return !!book.result && book.result.sourceKey === key(draft, book);
}
export function recordValueReviews(
draft: Record<string, unknown>,
book: ValueReviewBook,
): ValueReviewBook {
if (
!book.rows.length ||
new Set(book.rows.map((row) => row.id)).size !== book.rows.length
)
throw new Error(
'Indique operações com identificadores distintos;originais conservados.',
);
const result: ValueReviewCalculation = {
version: valueReviewVersion,
revision: (book.result?.revision ?? 0) + 1,
calculatedAt: new Date().toISOString(),
opportunity: acquisitionScope(draft),
location: JSON.stringify(draft.selection_labels ?? {}),
sourceKey: key(draft, book),
source: valueReviewSource,
inputs: structuredClone(book.rows),
rows: book.rows.map((row) => evaluateValueReview(row, draft.currency)),
};
const next = {
...book,
result,
history: [
...(book.history ?? []),
...(book.result ? [book.result] : []),
],
};
saveValueReviewBook(draft, next);
return next;
}
export function valueReviewReportRows(
draft: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
const book = valueReviewBook(draft),
result = book.result;
if (!result) return [];
return result.inputs.map((row, index) => [
'IVA mensal',
`Base P103 — ${row.period} — ${row.id}`,
`${row.description};base tributável: ${result.rows[index].baseAmount ?? 'Indeterminada'} ${row.currency};IVA: Indeterminado;${result.rows[index].reason}`,
`${result.opportunity};${result.location};${valueReviewCurrent(draft, book) ? 'Resultado actual' : 'Entradas alteradas;resultado histórico conservado'};versão${result.revision};${result.calculatedAt};${result.source};${row.origin === 'projected' ? 'Projecção' : 'Dados efectivos declarados'};${valueReviewTreatments.find((t) => t.id === row.treatment)?.label ?? 'Percurso por indicar'};contexto ${priceContexts.find((t) => t.id === row.priceContext)?.label ?? 'Por indicar'};operação ${row.operationRef};contraprestação ${row.declaredAmount};preço referência ${row.referenceAmount},${row.referenceRef};âmbito ${row.referenceScope};${row.treatment === 'referenced_correction' ? `correcção ${row.correctedAmount},referência ${row.correctionRef},data ${row.correctionDate},âmbito ${row.correctionScope},aplica:${row.correctionApplies === 'yes' ? 'Sim' : row.correctionApplies === 'no' ? 'Não' : 'Por indicar'}` : 'Sem correcção automática'};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${valueReviewConditions.map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento ${row.evidence};não preenche totaisModeloA ou campo11.`,
]);
} catch {
return [
[
'IVA mensal',
'Comparação/correcção da contraprestação',
'Dados não recuperáveis',
'Originais preservados;sem substituir por zero.',
],
];
}
}