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

326 lines
14 KiB
TypeScript

import { inputText } from './input-text.ts';
import { salesBook, salesCurrent } from './iva-operations.ts';
import { acquisitionBook, acquisitionCurrent } from './iva-acquisitions.ts';
import { travelBook, travelCurrent } from './iva-travel.ts';
import { travelEventBook, travelEventCurrent } from './iva-travel-events.ts';
import { secondhandBook, secondhandCurrent } from './iva-secondhand.ts';
import {
currencyEventBook,
currencyEventCurrent,
} from './iva-currency-events.ts';
export type FiscalCalculationGap = {
id: string;
book: string;
period: string;
calculation: string;
reason: string;
required: string[];
resolution: string;
kind: 'inputs_or_rule' | 'implementation' | 'stale' | 'not_calculated';
source: string;
};
type Row = Record<string, unknown>;
const fieldLabels: Record<string, string> = {
net: 'Base líquida',
vat: 'IVA',
gross: 'Total com IVA',
deductible: 'IVA dedutível',
baseAmount: 'Base tributável',
deduction: 'Dedução',
requested: 'Pedido',
approved: 'Reconhecido',
received: 'Recebido',
awaitingReceipt: 'Por receber',
delta: 'Regularização',
grossMargin: 'Margem bruta',
scenarioAmount: 'Montante do cenário',
outstanding: 'Por liquidar',
};
const financial = new Set([
'net',
'vat',
'gross',
'deductible',
'baseAmount',
'deduction',
'requested',
'approved',
'received',
'awaitingReceipt',
'delta',
'grossMargin',
'scenarioAmount',
'outstanding',
]);
const details: Record<string, { required: string[]; resolution: string }> = {
ivaBeverageRestitutions: {
required: [
'origem e parcela13(2) actuais,sem sobreposição',
'referência/data do pedido e da decisão externa,quando exista',
'montante reconhecido e comprovativos dos recebimentos efectivamente ocorridos',
],
resolution:
'Reconciliar a parcela ou completar apenas o acto externo em falta. Sem decisão/recebimento documentado,conservar esses estados pendentes;sem concessão ou caixa presumidas.',
},
ivaRefunds: {
required: [
'afectação25/26/27 actual',
'fundamento e origem do crédito',
'pedido,decisão e comprovativos de recebimento próprios',
],
resolution:
'Resolver a condição concreta do pedido ou registar o acto externo documentado;pedido não equivale a decisão ou recebimento.',
},
ivaSubstitutions: {
required: [
'declaração anterior congelada e fonte actual',
'referência,motivo e data da substituição',
'juros e pagamentos adicionais próprios',
],
resolution:
'Reconciliar anterior e actual,sem repetir créditos/pagamentos;guardar novo diferencial. A correspondência ModeloB/edição recebida permanece separada.',
},
salesOperations: {
required: [
'natureza e localização da operação',
'preço e indicação de IVA incluído/sem IVA',
'tratamento, condições e suporte próprios do fornecimento',
],
resolution:
'Resolver o motivo da linha e calcular a operação de venda. O IVA documental da aquisição não é origem do IVA do fornecimento.',
},
books: {
required: [
'IVA documental e documento legal',
'direito e afectação da aquisição',
'cobertura das parcelas do período e vendas/regularizações aplicáveis',
],
resolution:
'Resolver as linhas/campos indicados e guardar novo apuramento; preservar versões e valores indeterminados até completar as parcelas.',
},
regulatedSecondhands: {
required: [
'categoria, origem e regime do bem',
'venda, compra ou adjudicação e comissão documentadas',
'base da margem e regra de liquidação aplicável',
],
resolution:
'A base/margem já demonstrada conserva-se. Implementar separadamente a liquidação e a integração nos totais com a regra e o âmbito próprios; base calculada não significa IVA calculado.',
},
travelOperations: {
required: [
'operador, período e âmbito do registo global',
'contraprestação da parte especial e custos directos elegíveis de terceiros',
'excesso anterior, completude e enquadramento tributável',
],
resolution:
'Completar o registo global e a base; implementar IVA devido, dedução43(d) e integração do período com correspondência própria. A taxa do divisor da base não preenche o IVA devido.',
},
travelSpecialEvents: {
required: [
'origem da operação e natureza do evento',
'data e referência do pagamento integral/início ou cessação do regime',
'base, taxa e direito à dedução no cálculo próprio da operação',
],
resolution:
'Resolver o momento do evento e ligá-lo ao apuramento próprio. Momento determinado ou diferimento não calcula base, IVA ou dedução.',
},
currencyEvents: {
required: [
'natureza e âmbito do evento',
'datas de factura, prazo legal e recebimentos aplicáveis',
'moeda e câmbio documentado quando aplicável',
'base e regra fiscal da operação',
],
resolution:
'Resolver os factos/câmbio indicados e implementar ou reutilizar o cálculo próprio da operação; o evento isolado não liquida imposto.',
},
};
function record(value: unknown): value is Row {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
/** Reads the applicable scope only; never recalculates or promotes a frozen result. */
export function fiscalCalculationGaps(
d: Record<string, unknown>,
): FiscalCalculationGap[] {
const store = JSON.parse(
inputText(d.modelo_a_snapshot || '{"version":1,"books":{}}'),
);
if (store.version !== 1 || !record(store.books))
throw Error(
'Livros não recuperáveis; conservar o original antes de diagnosticar.',
);
const scope = inputText(d.context_opportunity || 'general'),
out: FiscalCalculationGap[] = [];
const current: Record<string, () => boolean> = {
salesOperations: () => salesCurrent(d, salesBook(d)),
books: () => acquisitionCurrent(d, acquisitionBook(d)),
travelOperations: () => travelCurrent(d, travelBook(d)),
travelSpecialEvents: () => travelEventCurrent(d, travelEventBook(d)),
regulatedSecondhands: () => secondhandCurrent(d, secondhandBook(d)),
currencyEvents: () => currencyEventCurrent(d, currencyEventBook(d)),
};
for (const [name, map] of Object.entries(store)) {
if (!record(map) || !(scope in map)) continue;
if (!record(map[scope]))
throw Error(`Livro ${name} não recuperável;original preservado.`);
const book = map[scope] as Row;
if (book.result !== undefined && !record(book.result))
throw Error(
`Resultado ${name} não recuperável;original preservado.`,
);
const result = record(book.result) ? book.result : null,
inputs = (
Array.isArray(book.rows)
? book.rows
: Array.isArray(book.events)
? book.events
: []
) as Row[],
spec = details[name];
if (!result) {
for (const row of inputs)
out.push({
id: `${name}:${inputText(row.id ?? out.length)}:uncalculated`,
book: name,
period: inputText(row.period ?? ''),
calculation: inputText(row.description ?? row.id ?? name),
reason: 'Preparação sem resultado guardado; nenhum valor fiscal foi presumido.',
required: spec?.required ?? [
'dados e condições específicos da preparação existente',
],
resolution:
spec?.resolution ??
'Completar a preparação no componente existente e guardar o cálculo correspondente.',
kind: 'not_calculated',
source: '',
});
continue;
}
let fresh = true;
try {
fresh = current[name]
? current[name]()
: !(
Array.isArray(result.inputs) &&
JSON.stringify(result.inputs) !== JSON.stringify(inputs)
);
} catch {
fresh = false;
}
if (!fresh)
out.push({
id: `${name}:stale`,
book: name,
period: '',
calculation: 'Actualidade do resultado',
reason: 'Entradas ou origem alteradas; resultado histórico conservado.',
required: ['reconciliação das entradas/origens alteradas'],
resolution:
'Rever apenas o delta e guardar nova versão antes de usar os valores como actuais.',
kind: 'stale',
source: inputText(result.source ?? result.sources ?? ''),
});
const visit = (value: unknown, path: string) => {
if (Array.isArray(value)) {
value.forEach((v, i) => visit(v, `${path}[${i}]`));
return;
}
if (!record(value) || value.status === 'not_applicable') return;
const missing = Object.entries(value)
.filter(([k, v]) => v === null && financial.has(k))
.map(([k]) => k);
if (record(value.fields))
for (const [k, v] of Object.entries(value.fields))
if (v === null) missing.push(`campo${k}`);
if (value.status === 'indeterminate' || missing.length) {
const frozen = Array.isArray(result.inputs)
? (result.inputs as Row[])
: inputs,
input = frozen.find((i) => i.id === value.id),
origin = record(input?.source) ? input.source : null,
prior = record(input?.prior) ? input.prior : null,
supply =
name === 'salesOperations' &&
inputText(input?.treatment ?? '').startsWith(
'onboard_',
);
const implementation =
missing.length > 0 &&
value.status !== 'indeterminate' &&
[
'regulatedSecondhands',
'travelOperations',
'travelSpecialEvents',
'currencyEvents',
].includes(name);
out.push({
id: `${name}:${path}`,
book: name,
period: inputText(
value.period ??
input?.period ??
origin?.period ??
prior?.period ??
'',
),
calculation: `${inputText(input?.description ?? origin?.documentRef ?? value.id ?? (value.period ? 'Apuramento do período' : 'Apuramento'))}${missing.length ? ` — ${missing.map((f) => fieldLabels[f] ?? f.replace('campo', 'Campo ')).join(', ')}` : ''}`,
reason: inputText(
value.reason ??
(Array.isArray(value.reasons)
? value.reasons.join('; ')
: 'Parcelas incompletas; consultar o apuramento existente.'),
),
required: supply
? [
'natureza do abastecimento e meio elegível',
'tratamento e montante liquidado do fornecimento, separados da aquisição',
'procedimento próprio de13(2), quando aplicável',
]
: (spec?.required ?? [
'dados, documentos e parâmetros indicados no motivo desta linha',
]),
resolution: supply
? 'Identificar e implementar o tratamento do fornecimento e respectivo montante liquidado; conservar o IVA da aquisição separado. O acompanhamento E não resolve este cálculo.'
: (spec?.resolution ??
'Resolver o motivo concreto no percurso de origem e guardar nova versão; campos em falta continuam indeterminados.'),
kind:
implementation || supply
? 'implementation'
: 'inputs_or_rule',
source: inputText(
value.source ?? result.source ?? result.sources ?? '',
),
});
}
for (const [k, v] of Object.entries(value))
if (['rows', 'periods', 'events'].includes(k))
visit(v, `${path}.${k}`);
};
visit(result, 'result');
}
return out;
}
export function fiscalCalculationGapReportRows(
d: Record<string, unknown>,
): Array<[string, string, string, string]> {
try {
return fiscalCalculationGaps(d).map((g) => [
'IVA mensal',
`Cálculo pendente — ${g.calculation}`,
`${g.period};${g.kind};${g.reason};Dados necessários:${g.required.join('; ')};Condição:${g.resolution}`,
`${g.book};${g.id};${g.source};sem inferir IVA do fornecimento a partir da aquisição.`,
]);
} catch (error) {
return [
[
'IVA mensal',
'Diagnóstico dos cálculos',
'Livros não recuperáveis; originais preservados',
String(error),
],
];
}
}