import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts'; export const noncashBaseVersion = 'noncash-consideration-base/1'; export const noncashBaseSource = 'CIVA republicado Lei13/2016,15(3)/(4),PDF14/p1426(272): contraprestação não definida,no todo ou em parte,em dinheiro;valor tributável=montante recebido ou a receber acrescido do valor normal dos bens ou serviços dados em troca. Valor normal:preço em concorrência normal,a fornecedor independente,no mesmo estádio de comercialização,tempo e lugar da operação ou mais próximos,aumentado dos elementos15(5)na medida não incluídos. Composição/exclusões15(5)/(6),sem próprioIVA. Lei22/2022PDF3mantém15(3–6)por remissão;Lei10/2025não altera15. Recorte2026/MZN,sem conversão cambial;base apenas,sem avaliação automática,incidência,taxaIVA,IVA,dedução/campo11 ou totaisModeloA.'; export const noncashBaseTreatments = [ { id: 'noncash_only', label: 'Contraprestação inteiramente não monetária' }, { id: 'cash_and_noncash', label: 'Contraprestação em parte monetária e em parte não monetária', }, ] as const; export const noncashBaseConditions = [ [ 'taxableOperation', 'Está identificada a operação sujeita a IVA cuja contraprestação não é definida no todo ou em parte em dinheiro', ], [ 'exchangeIdentified', 'Estão identificados os bens ou serviços dados em troca como contraprestação desta operação; não se somam o valor da própria operação e a respectiva contraprestação', ], [ 'cashComposition', 'A composição escolhida corresponde à operação:inteiramente não monetária,sem parte em dinheiro,ou com montante monetário recebido/a receber identificado', ], [ 'normalValueBasis', 'O valor normal corresponde ao artigo15(4):concorrência normal,fornecedor independente,mesmo estádio de comercialização,tempo/lugar da operação ou mais próximos,com fundamento identificado', ], [ 'netComposition', 'Os montantes excluem o próprio IVA e incluem a composição aplicável15(5)/(6),sem omissões ou dupla contagem dos mesmos elementos', ], ] as const; type Choice = '' | 'yes' | 'no'; type Condition = (typeof noncashBaseConditions)[number][0]; export type NoncashBaseRow = { id: string; treatment: string; description: string; period: string; origin: 'projected' | 'effective'; currency: string; exchangeDescription: string; agreementRef: string; documentRef: string; documentDate: string; cashAmount: string; cashRef: string; normalValue: string; valuationRef: string; valuationScope: string; compositionRef: string; evidence: string; conditions: Partial>; }; type LineResult = { id: string; status: 'determined' | 'not_applicable' | 'indeterminate'; baseAmount: string | null; vat: null; rate: null; reason: string; }; export type NoncashBaseCalculation = { version: string; revision: number; calculatedAt: string; opportunity: string; location: string; sourceKey: string; source: string; inputs: NoncashBaseRow[]; rows: LineResult[]; }; export type NoncashBaseBook = { rows: NoncashBaseRow[]; result?: NoncashBaseCalculation; history?: NoncashBaseCalculation[]; }; type Store = ReturnType & { noncashBases?: Record; }; export function newNoncashBase(id: string, currency: string): NoncashBaseRow { return { id, treatment: '', description: '', period: '', origin: 'projected', currency, exchangeDescription: '', agreementRef: '', documentRef: '', documentDate: '', cashAmount: '', cashRef: '', normalValue: '', valuationRef: '', valuationScope: '', compositionRef: '', evidence: '', conditions: {}, }; } export function switchNoncashComposition( row: NoncashBaseRow, treatment: string, ): NoncashBaseRow { return { ...row, treatment, conditions: { ...row.conditions, cashComposition: '' }, }; } function readStore(draft: Record): Store { const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store; if ( store.noncashBases !== undefined && (!store.noncashBases || typeof store.noncashBases !== 'object' || Array.isArray(store.noncashBases)) ) throw new Error( 'Bases de contraprestação não recuperáveis;originais preservados.', ); const fields = Object.keys(newNoncashBase('', '')).filter( (key) => key !== 'conditions', ); for (const book of Object.values(store.noncashBases ?? {})) 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 NoncashBaseRow] !== '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 noncashBaseBook( draft: Record, ): NoncashBaseBook { return ( readStore(draft).noncashBases?.[acquisitionScope(draft)] ?? { rows: [] } ); } export function saveNoncashBaseBook( draft: Record, book: NoncashBaseBook, ): string { const current = readStore(draft), encoded = JSON.stringify({ ...current, noncashBases: { ...current.noncashBases, [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 evaluateNoncashBase( row: NoncashBaseRow, currency: unknown, ): LineResult { const result: LineResult = { id: row.id, status: 'indeterminate', baseAmount: null, vat: null, rate: null, reason: '', }; const stop = ( reason: string, status: LineResult['status'] = 'indeterminate', ) => ({ ...result, status, reason }); if (!noncashBaseTreatments.some((item) => item.id === row.treatment)) return stop( 'Identifique a composição não monetária total ou parcial;parte monetária desconhecida não equivale a ausência de dinheiro.', ); if (currency !== 'MZN' || row.currency !== currency) return stop( 'Moeda fora do âmbito MZN ou divergente do projecto;sem conversão silenciosa ou taxa de câmbio presumida.', ); 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 de projecção;sem inferir do orçamento.', ); const failed = noncashBaseConditions.filter( ([key]) => row.conditions[key] === 'no', ); if (failed.length) return stop( 'Não se aplica esta determinação da base: ' + failed.map(([, label]) => label).join('; ') + '. Não é isenção nem IVA zero.', 'not_applicable', ); const missing = noncashBaseConditions.filter( ([key]) => row.conditions[key] !== 'yes', ); if (missing.length) return stop( 'Condições por indicar: ' + missing.map(([, label]) => label).join('; ') + '. Desconhecido não significa não aplicável ou zero.', ); if ( !row.description.trim() || !row.exchangeDescription.trim() || !row.agreementRef.trim() || !row.documentRef.trim() || !row.valuationRef.trim() || !row.valuationScope.trim() || !row.compositionRef.trim() || !row.evidence.trim() || !validDate(row.documentDate) ) return stop( 'Identifique operação,contraprestação em bens/serviços,acordo,documento/data,fundamento e âmbito do valor normal e composição,ou pressupostos expressos;reutilize referências existentes,sem novo certificado/avaliador/aprovador.', ); const normal = decimal(row.normalValue); if (normal === null) return stop( 'Indique o valor normal dos bens/serviços dados em troca;desconhecido não é zero nem valor avaliado automaticamente pelo portal.', ); let base = normal, formula = `valor normal ${money(normal)},sem parte monetária expressamente confirmada`; if (row.treatment === 'cash_and_noncash') { const cash = decimal(row.cashAmount); if (cash === null || !row.cashRef.trim()) return stop( 'Identifique montante monetário recebido/a receber e referência ou pressuposto expresso;um montante desconhecido não é zero.', ); base = cash + normal; formula = `montante monetário ${money(cash)} + valor normal ${money(normal)}`; } return { ...result, status: 'determined', baseAmount: money(base), reason: `Base15(3)/(4): ${formula} = ${money(base)}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Valor normal/fundamento/âmbito e composição identificados,sem avaliação automática. Base não é imposto;IVA/taxa,dedução/campo11 e totaisModeloA não são determinados por este cálculo.`, }; } function key(draft: Record, book: NoncashBaseBook): string { return JSON.stringify({ version: noncashBaseVersion, scope: acquisitionScope(draft), location: draft.selection_labels ?? {}, currency: draft.currency, rows: book.rows, }); } export function noncashBaseCurrent( draft: Record, book: NoncashBaseBook, ): boolean { return !!book.result && book.result.sourceKey === key(draft, book); } export function recordNoncashBases( draft: Record, book: NoncashBaseBook, ): NoncashBaseBook { 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: NoncashBaseCalculation = { version: noncashBaseVersion, revision: (book.result?.revision ?? 0) + 1, calculatedAt: new Date().toISOString(), opportunity: acquisitionScope(draft), location: JSON.stringify(draft.selection_labels ?? {}), sourceKey: key(draft, book), source: noncashBaseSource, inputs: structuredClone(book.rows), rows: book.rows.map((row) => evaluateNoncashBase(row, draft.currency)), }; const next = { ...book, result, history: [ ...(book.history ?? []), ...(book.result ? [book.result] : []), ], }; saveNoncashBaseBook(draft, next); return next; } export function noncashBaseReportRows( draft: Record, ): Array<[string, string, string, string]> { try { const book = noncashBaseBook(draft), result = book.result; if (!result) return []; return result.inputs.map((row, index) => [ 'IVA mensal', `Base P100 — ${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};${noncashBaseCurrent(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'};${noncashBaseTreatments.find((item) => item.id === row.treatment)?.label ?? 'Composição por indicar'};bens/serviços dados em troca ${row.exchangeDescription};acordo ${row.agreementRef};${row.treatment === 'cash_and_noncash' ? `montante monetário ${row.cashAmount} ${row.currency},referência ${row.cashRef}` : 'Sem parte monetária,expressamente confirmado'};valor normal ${row.normalValue} ${row.currency};fundamento ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${noncashBaseConditions.map(([key, label]) => label + ': ' + (row.conditions[key] === 'yes' ? 'Sim' : row.conditions[key] === 'no' ? 'Não' : 'Por indicar')).join('; ')};fundamento factual ${row.evidence};não preenche totaisModeloA ou campo11.`, ]); } catch { return [ [ 'IVA mensal', 'Base de contraprestação não monetária', 'Dados não recuperáveis', 'Originais preservados;sem substituir por zero.', ], ]; } }