import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts'; export const compositionBaseVersion = 'consideration-composition-base/1'; export const compositionBaseSource = 'CIVA republicado Lei13/2016,15(5)(a)/(b),PDF14/p1426(272):o valor tributável inclui impostos,direitos,taxas e outras imposições,excepto o próprioIVA;e despesas acessórias debitadas relativas a comissões,embalagem,transporte e seguros por conta do cliente. Respeitar regras especiais15(2) e exclusões15(6);não substitui a exclusãoTC emP095. Lei22/2022PDF3mantém15(5)por remissão;Lei10/2025não altera15. Recorte2026/MZN,composição da base apenas,sem incidência,taxas,IVA,dedução/campo11 ou totaisModeloA.'; export const compositionTreatments = [ { id: 'tax_component', label: 'Impostos,direitos,taxas ou outras imposições,excepto próprio IVA', }, { id: 'accessory_component', label: 'Despesas acessórias debitadas por conta do cliente', }, ] as const; export const compositionConditions = [ [ 'taxableOperation', 'A operação sujeita a IVA e a contraprestação correspondente estão identificadas', ], [ 'initialNet', 'O montante inicial está identificado,sem próprio IVA e com as exclusões aplicáveis15(6)já consideradas; a referência explica a composição', ], [ 'completeComposition', 'As parcelas aplicáveis15(5)estão reconciliadas com o montante inicial,sem omissões ou dupla contagem', ], [ 'specialRules', 'Esta composição respeita a regra aplicável da operação,sem substituir bases especiais15(2),exclusões15(6)ou a exclusão da TC de combustíveis', ], ] as const; type Choice = '' | 'yes' | 'no'; type Condition = (typeof compositionConditions)[number][0]; export type CompositionPart = { id: string; treatment: string; description: string; amount: string; included: Choice; reference: string; natureConfirmed: Choice; ownVatExcluded: Choice; }; export type CompositionRow = { id: string; description: string; period: string; origin: 'projected' | 'effective'; currency: string; initialAmount: string; initialRef: string; operationRef: string; documentRef: string; documentDate: string; compositionRef: string; evidence: string; conditions: Partial>; parts: CompositionPart[]; }; type LineResult = { id: string; status: 'determined' | 'not_applicable' | 'indeterminate'; baseAmount: string | null; vat: null; rate: null; reason: string; }; export type CompositionCalculation = { version: string; revision: number; calculatedAt: string; opportunity: string; location: string; sourceKey: string; source: string; inputs: CompositionRow[]; rows: LineResult[]; }; export type CompositionBook = { rows: CompositionRow[]; result?: CompositionCalculation; history?: CompositionCalculation[]; }; type Store = ReturnType & { considerationCompositions?: Record; }; export function newCompositionPart(id: string): CompositionPart { return { id, treatment: '', description: '', amount: '', included: '', reference: '', natureConfirmed: '', ownVatExcluded: '', }; } export function newCompositionBase( id: string, currency: string, ): CompositionRow { return { id, description: '', period: '', origin: 'projected', currency, initialAmount: '', initialRef: '', operationRef: '', documentRef: '', documentDate: '', compositionRef: '', evidence: '', conditions: {}, parts: [], }; } export function switchCompositionPart( part: CompositionPart, treatment: string, ): CompositionPart { return { ...part, treatment, natureConfirmed: '' }; } function readStore(draft: Record): Store { const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store; if ( store.considerationCompositions !== undefined && (!store.considerationCompositions || typeof store.considerationCompositions !== 'object' || Array.isArray(store.considerationCompositions)) ) throw new Error('Composições não recuperáveis;originais preservados.'); const fields = Object.keys(newCompositionBase('', '')).filter( (key) => !['conditions', 'parts'].includes(key), ); const partFields = Object.keys(newCompositionPart('')); for (const book of Object.values(store.considerationCompositions ?? {})) 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 CompositionRow] !== 'string', ) || !row.conditions || typeof row.conditions !== 'object' || Array.isArray(row.conditions) || !Array.isArray(row.parts) || row.parts.some( (part) => !part || partFields.some( (key) => typeof part[ key as keyof CompositionPart ] !== 'string', ), ), ) ) throw new Error( 'Linhas de composição não recuperáveis;originais preservados,sem substituição por lista vazia.', ); return store; } export function compositionBook( draft: Record, ): CompositionBook { return ( readStore(draft).considerationCompositions?.[ acquisitionScope(draft) ] ?? { rows: [] } ); } export function saveCompositionBook( draft: Record, book: CompositionBook, ): string { const current = readStore(draft), encoded = JSON.stringify({ ...current, considerationCompositions: { ...current.considerationCompositions, [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 evaluateComposition( row: CompositionRow, 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 (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 de projecção.', ); const failed = compositionConditions.filter( ([key]) => row.conditions[key] === 'no', ); if (failed.length) return stop( 'Não se aplica esta composição: ' + failed.map(([, label]) => label).join('; ') + '. Não é isenção nem IVA zero.', 'not_applicable', ); const missing = compositionConditions.filter( ([key]) => row.conditions[key] !== 'yes', ); if (missing.length) return stop( 'Condições por indicar: ' + missing.map(([, label]) => label).join('; ') + '. Desconhecido não é zero.', ); if ( !row.description.trim() || !row.initialRef.trim() || !row.operationRef.trim() || !row.documentRef.trim() || !row.compositionRef.trim() || !row.evidence.trim() || !validDate(row.documentDate) ) return stop( 'Identifique operação,montante inicial,documento/data,composição e fundamento ou pressupostos expressos;reutilize referências existentes,sem certificado/aprovador novo.', ); const initial = decimal(row.initialAmount); if (initial === null) return stop( 'Indique o montante inicial sem próprio IVA e com exclusões aplicáveis consideradas;desconhecido não é zero.', ); if ( !row.parts.length || new Set(row.parts.map((p) => p.id)).size !== row.parts.length || row.parts.some((p) => !p.id.trim()) ) return stop( 'Identifique as parcelas desta operação com identificadores distintos;sem soma de linhas vazias ou duplicadas.', ); let additions = 0n; const formulas: string[] = []; for (const part of row.parts) { const rule = compositionTreatments.find((t) => t.id === part.treatment); if (!rule) return stop( 'Identifique a natureza de cada parcela:imposição excepto próprioIVA ou despesa acessória debitada por conta do cliente.', ); if (part.natureConfirmed === 'no' || part.ownVatExcluded === 'no') return stop( 'A parcela ' + part.id + ' não corresponde ao tratamento ou inclui próprioIVA;reveja a composição,não se transforma em exclusão/isenção automática.', 'not_applicable', ); if ( part.natureConfirmed !== 'yes' || part.ownVatExcluded !== 'yes' || !['yes', 'no'].includes(part.included) ) return stop( 'Confirme a natureza,exclusão do próprioIVA e se a parcela ' + part.id + ' já está no montante inicial;desconhecido não permite presumir soma ou ausência.', ); if (!part.description.trim() || !part.reference.trim()) return stop( 'Identifique descrição e referência da parcela ' + part.id + ',ou pressuposto expresso;sem novo documento obrigatório.', ); const amount = decimal(part.amount); if (amount === null) return stop( 'Indique o montante exacto da parcela ' + part.id + ';desconhecido não é zero.', ); if (part.included === 'no') additions += amount; formulas.push( `${part.id}:${rule.label},${money(amount)}MZN,${part.included === 'yes' ? 'já incluída,sem segunda soma' : 'acrescida uma vez'}`, ); } const base = initial + additions; return { ...result, status: 'determined', baseAmount: money(base), reason: `Base15(5):montante inicial ${money(initial)} + parcelas ainda não incluídas ${money(additions)} = ${money(base)}MZN;${formulas.join('; ')},${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. Base única por operação,não uma base por parcela. Base não é imposto;IVA/taxa,dedução/campo11 e totaisModeloA não são determinados por esta composição.`, }; } function key(draft: Record, book: CompositionBook): string { return JSON.stringify({ version: compositionBaseVersion, scope: acquisitionScope(draft), location: draft.selection_labels ?? {}, currency: draft.currency, rows: book.rows, }); } export function compositionCurrent( draft: Record, book: CompositionBook, ): boolean { return !!book.result && book.result.sourceKey === key(draft, book); } export function recordCompositions( draft: Record, book: CompositionBook, ): CompositionBook { 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: CompositionCalculation = { version: compositionBaseVersion, revision: (book.result?.revision ?? 0) + 1, calculatedAt: new Date().toISOString(), opportunity: acquisitionScope(draft), location: JSON.stringify(draft.selection_labels ?? {}), sourceKey: key(draft, book), source: compositionBaseSource, inputs: structuredClone(book.rows), rows: book.rows.map((row) => evaluateComposition(row, draft.currency)), }; const next = { ...book, result, history: [ ...(book.history ?? []), ...(book.result ? [book.result] : []), ], }; saveCompositionBook(draft, next); return next; } export function compositionReportRows( draft: Record, ): Array<[string, string, string, string]> { try { const book = compositionBook(draft), result = book.result; if (!result) return []; return result.inputs.map((row, index) => [ 'IVA mensal', `Base P101 — ${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};${compositionCurrent(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'};operação ${row.operationRef};montante inicial ${row.initialAmount} ${row.currency},referência ${row.initialRef};parcelas ${row.parts.map((p) => `${p.id}/${p.treatment},${p.description},${p.amount} ${row.currency},já incluída:${p.included === 'yes' ? 'Sim' : p.included === 'no' ? 'Não' : 'Por indicar'},natureza:${p.natureConfirmed},sem próprioIVA:${p.ownVatExcluded},referência ${p.reference}`).join('; ')};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${compositionConditions.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', 'Composição da contraprestação', 'Dados não recuperáveis', 'Originais preservados;sem substituir por zero.', ], ]; } }