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; const fieldLabels: Record = { 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 = { 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, ): 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 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, ): 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), ], ]; } }