import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts'; export const serviceNormalVersion = 'assimilated-service-normal/1'; export const serviceNormalSource = 'CIVA republicado Lei13/2016,15(2)(c),PDF13/p1426(271):operações4(2),base=valor normal do serviço definido15(4),PDF14/p1426(272).4(2)(a),PDF7/p1426(265):serviços gratuitos pela própria empresa para necessidades particulares do titular/pessoal ou fins alheios;4(2)(b):utilização de bens da empresa para uso próprio/titular/pessoal/fins alheios ou sectores isentos,quando houve dedução total/parcial nesses bens ou elementos constitutivos. A condição de dedução anterior é de4(2)(b),não de4(2)(a). Composição15(4)/(5)/(6),sem próprioIVA. Lei22/2022mantém15c/4por remissão,não altera4;Lei10/2025não altera4/15. Recorte2026/MZN;base apenas,sem calcularIVA/dedução/campo11/regularizações/totaisModeloA,nem confundir uso com transmissão permanenteP088.'; export const serviceNormalTreatments = [ { id: 'free_company_service', label: 'Serviço gratuito pela própria empresa para necessidades particulares ou fins alheios', }, { id: 'private_company_asset_use', label: 'Utilização de bens da empresa para uso próprio,particular ou fins alheios', }, { id: 'exempt_sector_asset_use', label: 'Utilização de bens da empresa em sector de actividade isento', }, ] as const; export const serviceNormalConditions = [ [ 'domesticOperation', 'A operação e o âmbito territorial aplicável estão identificados para esta base', ], [ 'normalValueBasis', 'O valor normal do serviço corresponde15(4):concorrência normal,fornecedor independente,estádio de comercialização,tempo/lugar da operação ou mais próximos,com fundamento e âmbito identificados', ], [ 'netComposition', 'O montante exclui o próprio IVA e reflecte15(4)/(5)/(6),sem omissões ou dupla contagem', ], ] as const; export const serviceOwnConditions = { free_company_service: [ [ 'freeByCompany', 'A prestação é gratuita e efectuada pela própria empresa', ], [ 'nonBusinessPurpose', 'Destina-se a necessidades particulares do titular/pessoal ou,em geral,a fins alheios à empresa', ], ], private_company_asset_use: [ [ 'companyAssetUse', 'É utilização de bens da empresa,não afectação/transmissão permanente de bens', ], [ 'privatePurpose', 'Destina-se a uso próprio do titular/pessoal ou,em geral,a fins alheios à empresa', ], [ 'priorDeduction', 'Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem', ], ], exempt_sector_asset_use: [ [ 'companyAssetUse', 'É utilização de bens da empresa,não afectação/transmissão permanente de bens', ], [ 'exemptSector', 'A utilização corresponde a sector de actividade isento identificado', ], [ 'priorDeduction', 'Houve dedução total ou parcial relativamente aos bens ou aos elementos que os constituem', ], ], } as const; export function ownServiceConditions( treatment: string, ): ReadonlyArray { return ( serviceOwnConditions[treatment as keyof typeof serviceOwnConditions] ?? [] ); } type Choice = '' | 'yes' | 'no'; export type ServiceNormalRow = { id: string; treatment: string; description: string; period: string; origin: 'projected' | 'effective'; currency: string; operationRef: string; purposeRef: string; assetRef: string; deductionRef: string; documentRef: string; documentDate: 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 ServiceNormalCalculation = { version: string; revision: number; calculatedAt: string; opportunity: string; location: string; sourceKey: string; source: string; inputs: ServiceNormalRow[]; rows: LineResult[]; }; export type ServiceNormalBook = { rows: ServiceNormalRow[]; result?: ServiceNormalCalculation; history?: ServiceNormalCalculation[]; }; type Store = ReturnType & { serviceNormals?: Record; }; export function newServiceNormal( id: string, currency: string, ): ServiceNormalRow { return { id, treatment: '', description: '', period: '', origin: 'projected', currency, operationRef: '', purposeRef: '', assetRef: '', deductionRef: '', documentRef: '', documentDate: '', normalValue: '', valuationRef: '', valuationScope: '', compositionRef: '', evidence: '', conditions: {}, }; } export function switchServiceTreatment( row: ServiceNormalRow, treatment: string, ): ServiceNormalRow { return { ...row, treatment, conditions: Object.fromEntries( serviceNormalConditions.map(([key]) => [ key, row.conditions[key] ?? '', ]), ), }; } function readStore(draft: Record): Store { const store = readAcquisitionStore(draft.modelo_a_snapshot) as Store; if ( store.serviceNormals !== undefined && (!store.serviceNormals || typeof store.serviceNormals !== 'object' || Array.isArray(store.serviceNormals)) ) throw new Error( 'Bases de contraprestação não recuperáveis;originais preservados.', ); const fields = Object.keys(newServiceNormal('', '')).filter( (key) => key !== 'conditions', ); for (const book of Object.values(store.serviceNormals ?? {})) 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 ServiceNormalRow] !== '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 serviceNormalBook( draft: Record, ): ServiceNormalBook { return ( readStore(draft).serviceNormals?.[acquisitionScope(draft)] ?? { rows: [], } ); } export function saveServiceNormalBook( draft: Record, book: ServiceNormalBook, ): string { const current = readStore(draft), encoded = JSON.stringify({ ...current, serviceNormals: { ...current.serviceNormals, [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 evaluateServiceNormal( row: ServiceNormalRow, 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 }); const rule = serviceNormalTreatments.find((t) => t.id === row.treatment); if (!rule) return stop( 'Identifique serviço gratuito,utilização particular/alheia ou em sector isento;desconhecido não permite presumir aplicaçã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 required = [ ...serviceNormalConditions, ...ownServiceConditions(row.treatment), ]; const failed = required.filter(([key]) => row.conditions[key] === 'no'); if (failed.length) return stop( 'Não se aplica esta base4(2)/15(2)(c): ' + failed.map(([, label]) => label).join('; ') + '. Não é isenção nem IVA zero.', 'not_applicable', ); if (required.some(([key]) => row.conditions[key] !== 'yes')) return stop( 'Confirme as condições próprias deste percurso;desconhecido não é zero,não aplicável ou dedução presumida.', ); if ( !row.description.trim() || !row.operationRef.trim() || !row.purposeRef.trim() || !row.documentRef.trim() || !validDate(row.documentDate) || !row.valuationRef.trim() || !row.valuationScope.trim() || !row.compositionRef.trim() || !row.evidence.trim() ) return stop( 'Identifique serviço/operação,finalidade,documento/data,fundamento e âmbito do valor normal e composição,ou pressupostos expressos;reutilize dados existentes,sem certificado/avaliador/aprovador novo.', ); if ( row.treatment !== 'free_company_service' && (!row.assetRef.trim() || !row.deductionRef.trim()) ) return stop( 'Identifique os bens/elementos e a referência da dedução anterior total/parcial,ou pressupostos expressos;esta referência não calcula nem concede dedução/campo11.', ); const normal = decimal(row.normalValue); if (normal === null) return stop( 'Indique o valor normal do serviço;desconhecido não é zero nem custo/preço de aquisição dos bens presumido.', ); return { ...result, status: 'determined', baseAmount: money(normal), reason: `Base15(2)(c),${rule.label}:valor normal do serviço ${money(normal)}MZN,${row.origin === 'projected' ? 'nas condições expressamente assumidas na projecção' : 'nos dados efectivos declarados'}. ${row.treatment === 'free_company_service' ? '4(2)(a),sem impor a condição de dedução anterior da alínea b).' : '4(2)(b),bens/elementos e dedução anterior total/parcial referenciados;uso não é transmissão permanente.'} Valor normal com fundamento/âmbito,sem avaliação automática. Base não é imposto;IVA/taxa,dedução/campo11,regularizações e totaisModeloA não determinados por este cálculo.`, }; } function key(draft: Record, book: ServiceNormalBook): string { return JSON.stringify({ version: serviceNormalVersion, scope: acquisitionScope(draft), location: draft.selection_labels ?? {}, currency: draft.currency, rows: book.rows, }); } export function serviceNormalCurrent( draft: Record, book: ServiceNormalBook, ): boolean { return !!book.result && book.result.sourceKey === key(draft, book); } export function recordServiceNormals( draft: Record, book: ServiceNormalBook, ): ServiceNormalBook { 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: ServiceNormalCalculation = { version: serviceNormalVersion, revision: (book.result?.revision ?? 0) + 1, calculatedAt: new Date().toISOString(), opportunity: acquisitionScope(draft), location: JSON.stringify(draft.selection_labels ?? {}), sourceKey: key(draft, book), source: serviceNormalSource, inputs: structuredClone(book.rows), rows: book.rows.map((row) => evaluateServiceNormal(row, draft.currency), ), }; const next = { ...book, result, history: [ ...(book.history ?? []), ...(book.result ? [book.result] : []), ], }; saveServiceNormalBook(draft, next); return next; } export function serviceNormalReportRows( draft: Record, ): Array<[string, string, string, string]> { try { const book = serviceNormalBook(draft), result = book.result; if (!result) return []; return result.inputs.map((row, index) => [ 'IVA mensal', `Base P089 — ${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};${serviceNormalCurrent(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'};${serviceNormalTreatments.find((t) => t.id === row.treatment)?.label ?? 'Percurso por indicar'};operação ${row.operationRef};finalidade ${row.purposeRef};${row.treatment === 'free_company_service' ? 'Dedução anterior não é condição de4(2)(a)' : `bens/elementos ${row.assetRef},dedução anterior ${row.deductionRef}`};valor normal ${row.normalValue} ${row.currency};fundamento ${row.valuationRef};âmbito ${row.valuationScope};composição ${row.compositionRef};documento ${row.documentRef},${row.documentDate};condições ${[...serviceNormalConditions, ...ownServiceConditions(row.treatment)].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', 'Valor normal de serviços assimilados', 'Dados não recuperáveis', 'Originais preservados;sem substituir por zero.', ], ]; } }