import { modeloAPurchaseReferences } from './iva-modelo-a.ts'; import { monthlyIvaInput, monthlyIvaKey, readMonthlyIvaRecord, readMonthlyIvaSettings } from './monthly-iva.ts'; import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts'; import { salesBook, salesCurrent as salesOperationsCurrent } from './iva-operations.ts'; import { addFractions, decimalFraction, fractionLabel, fractionPresentation, positiveFraction, subtractFractions, type FiscalFraction } from './fiscal-rational.ts'; export const acquisitionVersion = 'modelo-a-internal-acquisitions/1'; export const acquisitionBasis = 'CIVA arts.18(1)(a),18(2) (Lei10/2025),18(3–5),19(1)(a),19(2),20 (Lei22/2022),21(1–3); âmbito interno, afectação exclusiva, mesmo período, sem exclusões ou dedução parcial.'; export const acquisitionSources = 'Modelo A DOCX 62c198e9 / v1 63d45fa8; VAT Return.pdf SHA256 9c0b99a810c7db53eb75d63c22ee22fb86a50ba57bd5509041ed80ee10f75741, p.1; CIVA republicado Lei13/2016, PDF14–16; Lei22/2022, art.20; Lei10/2025, arts.18/19, PDF13.'; type Choice = '' | 'yes' | 'no'; export type Acquisition = { id: string; field: '08'|'09'|'10'; period: string; origin: 'projected'|'effective'; sourceMonth: number|null; sourcePeriod?:string; sourceField?:'inventory'|'asset'|'operating'; sourceAmount: string|null; sourceCurrency: string; sourceBasis: ''|'net'|'gross'; net: string; vat: string; gross: string; documentType: ''|'invoice'|'equivalent'; documentRef: string; documentDate: string; documentLine:string; supplier: string; supplierRegistered: Choice; legalDocument: Choice; held: Choice; domestic: Choice; allocation: ''|'taxable_only'|'mixed'|'no_right'; exclusion: ''|'none'|'excluded'|'special'; integrity: Choice; description: string; }; export type AcquisitionResult = {id:string; net:string|null; vat:string|null; gross:string|null; deductible:string|null; reason:string}; type PeriodInput = {period:string; complete:boolean; absent:string[]}; export type AcquisitionBook = {currency:string; taxpayerRegistered?:Choice; rows:Acquisition[]; periods:PeriodInput[]; result?:AcquisitionCalculation}; export type AcquisitionCalculation = {version:string; revision:number; calculatedAt:string; opportunity:string; location:string; currency:string; sourceKey:string; basis:string; sources:string; declarations?:{taxpayerRegistered:Choice;periods:PeriodInput[]}; rows:AcquisitionResult[]; inputs:Acquisition[]; periods:Array<{period:string; fields:Record; exactFields?:Record; reasons:string[]}>}; type Store = {version:1; books:Record}; const excluded = ['03','04','05','06','07','11','12','13']; export const acquisitionExcludedFields = excluded; export function acquisitionScope(draft:Record):string { if(!draft.context_opportunity) { try {const context=JSON.parse(String(draft.context_applicability_snapshot??''));if(context.opportunity_choices?.length>1)throw new Error('Escolha a oportunidade em tratamento antes de preparar as compras.');} catch(cause) {if(cause instanceof Error && cause.message.startsWith('Escolha'))throw cause;} } return String(draft.context_opportunity || 'general'); } export function readAcquisitionStore(raw:unknown):Store { if (!raw) return {version:1,books:{}}; const value=JSON.parse(String(raw)); if (value?.version!==1 || !value.books || typeof value.books!=='object' || Array.isArray(value.books)) throw new Error('Compras fiscais guardadas não recuperáveis; dados preservados.'); for(const book of Object.values(value.books) as AcquisitionBook[]) { if(!book || !Array.isArray(book.rows) || !Array.isArray(book.periods) || typeof book.currency!=='string') throw new Error('Formato de compras não recuperável; sem substituição.'); const stringKeys=Object.keys(newAcquisition('','','')).filter(key=>!['sourceMonth','sourceAmount'].includes(key)); if(book.rows.some(row=>!row || stringKeys.some(key=>typeof row[key as keyof Acquisition]!=='string') || !(row.sourceMonth===null || Number.isInteger(row.sourceMonth)) || !(row.sourceAmount===null || typeof row.sourceAmount==='string')) || book.periods.some(p=>!p || typeof p.period!=='string' || typeof p.complete!=='boolean' || !Array.isArray(p.absent) || p.absent.some(f=>typeof f!=='string'))) throw new Error('Linhas ou períodos de compras não recuperáveis; os dados guardados foram preservados.'); } return value; } export function acquisitionBook(draft:Record):AcquisitionBook { return readAcquisitionStore(draft.modelo_a_snapshot).books[acquisitionScope(draft)] ?? {currency:String(draft.currency ?? ''),rows:[],periods:[]}; } export function saveAcquisitionBook(draft:Record,book:AcquisitionBook):string { const store=readAcquisitionStore(draft.modelo_a_snapshot); return JSON.stringify({...store,books:{...store.books,[acquisitionScope(draft)]:book}}); } export function newAcquisition(id:string,period:string,currency:string):Acquisition { return {id,field:'09',period,origin:'projected',sourceMonth:null,sourceAmount:null,sourceCurrency:currency,sourceBasis:'',net:'',vat:'',gross:'',documentType:'',documentRef:'',documentDate:'',documentLine:'',supplier:'',supplierRegistered:'',legalDocument:'',held:'',domestic:'',allocation:'',exclusion:'',integrity:'',description:''}; } function periods(draft:Record):string[] { try {return monthlyIvaInput(draft,readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''))).monthly_sales.map(r=>r.date.slice(0,7));} catch {return [];} } export function acquisitionPlanReferences(draft:Record):Array<{key:string;kind:'inventory'|'asset'|'operating';month:number;period:string;amount:string|null;currency:string;label:string}> { const dates=periods(draft),plan=restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot??'')); if(!plan || !dates.length)return []; if(plan.currency!==draft.currency)throw new Error('Moeda do plano divergente; não reutilizar como moeda do projecto.'); const inventory=modeloAPurchaseReferences(draft).map(r=>({key:`inventory:${r.month}`,kind:'inventory' as const,month:r.month,period:dates[r.month-1],amount:r.declared,currency:plan.currency,label:`Compras/encomendas — mês ${r.month}`})); const asset={key:'asset:1',kind:'asset' as const,month:1,period:dates[0],amount:plan.assetInvestment?.trim()||null,currency:plan.currency,label:'Orçamento de investimento em activos — período a confirmar'}; const operating=dates.map((period,index)=>({key:`operating:${index+1}`,kind:'operating' as const,month:index+1,period,amount:(plan.monthlyOperatingExpensesByPeriod?plan.monthlyOperatingExpensesByPeriod[index]:plan.monthlyOperatingExpenses)?.trim()||null,currency:plan.currency,label:`Orçamento de despesas operacionais — mês ${index+1}`})); return [...inventory,asset,...operating]; } export function reuseAcquisitions(draft:Record,book:AcquisitionBook):AcquisitionBook { const dates=periods(draft); if(!dates.length) throw new Error('Defina o período mensal e resolva a moeda da projecção antes de reutilizar compras.'); const rows=[...book.rows]; for(const reference of modeloAPurchaseReferences(draft)) { if(rows.some(r=>r.sourceMonth===reference.month)) continue; rows.push({...newAcquisition(`plan-${reference.month}`,dates[reference.month-1],String(draft.currency ?? '')),sourceMonth:reference.month,sourcePeriod:dates[reference.month-1],sourceAmount:reference.declared,description:`Compras/encomendas do plano — mês ${reference.month}`}); } return {...book,rows,periods:dates.map(period=>book.periods.find(p=>p.period===period) ?? {period,complete:false,absent:[]})}; } // Decimal arithmetic for aggregation only. No binary rounding and no invented purchase tax rate. function amount(raw:unknown):bigint|null { if(typeof raw!=='string' || !/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim())) return null; const [a,b='']=raw.trim().split('.');return BigInt(a)*1000000n+BigInt(b.padEnd(6,'0')); } function money(value:bigint):string {const a=value/1000000n,b=(value%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0');return `${a}.${b}`;} function sum(values:Array):string|null {const parsed=values.map(amount);return parsed.some(v=>v===null)?null:money((parsed as bigint[]).reduce((a,b)=>a+b,0n));} function dateValid(raw:string):boolean {const date=new Date(`${raw}T00:00:00Z`);return /^\d{4}-\d{2}-\d{2}$/.test(raw) && Number.isFinite(date.getTime()) && date.toISOString().slice(0,10)===raw;} export function evaluateAcquisition(row:Acquisition,currency:string,taxpayerRegistered:Choice=''):AcquisitionResult { let n=amount(row.net),v=amount(row.vat),g=amount(row.gross); if(n!==null && v!==null && g===null && !row.gross.trim()) g=n+v; if(g!==null && v!==null && n===null && !row.net.trim() && g>=v) n=g-v; if(n!==null && g!==null && v===null && !row.vat.trim() && g>=n) v=g-n; const result:AcquisitionResult={id:row.id,net:n===null?null:money(n),vat:v===null?null:money(v),gross:g===null?null:money(g),deductible:null,reason:''}; const stop=(reason:string,deductible:string|null=null)=>({...result,reason,deductible}); if(currency!=='MZN' || row.sourceCurrency!==currency) return stop('Divergência ou moeda fora de MZN; sem conversão.'); if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period)) return stop('Período fora do âmbito 2026 ou em falta.'); if(n===null || v===null || g===null || n+v!==g) return stop('Indique dois valores válidos; líquido + IVA deve corresponder ao bruto.'); if(row.sourceMonth!==null && (!row.sourceBasis || amount(row.sourceAmount)!==(row.sourceBasis==='net'?n:g))) return stop('Classifique ou resolva a divergência do valor de origem; não foi reinterpretado.'); if(v===0n) return stop('IVA explicitamente indicado como zero; não existe imposto a deduzir nesta aquisição.','0.00'); if(taxpayerRegistered!=='yes') return stop('Confirme o enquadramento do adquirente como sujeito passivo de IVA no regime normal; não é inferido da organização.'); if(row.allocation==='no_right' || row.exclusion==='excluded' || row.supplierRegistered==='no' || row.legalDocument==='no' || row.integrity==='no') return stop('Sem dedução no âmbito declarado: afectação, exclusão, fornecedor ou documento não elegível.','0.00'); if(row.domestic!=='yes') return stop('Aquisição interna por confirmar; importações fora deste lote.'); if(row.allocation!=='taxable_only' || row.exclusion!=='none') return stop('Confirme afectação exclusiva a operações tributadas e ausência de exclusões; casos mistos/especiais não calculados.'); if(!['invoice','equivalent'].includes(row.documentType) || !row.documentRef.trim() || !row.supplier.trim() || row.supplierRegistered!=='yes' || row.legalDocument!=='yes' || row.held!=='yes' || row.integrity!=='yes') return stop('Documento, fornecedor inscrito, posse e condições de dedução ainda incompletos.'); if(!dateValid(row.documentDate) || row.documentDate.slice(0,7)!==row.period) return stop('Data do documento e período de dedução devem coincidir neste lote; dedução posterior não presumida.'); return stop(row.origin==='projected'?'Dedução projectada sob pressupostos explícitos; não certifica factura ou declaração.':'Dedução segundo dados e condições declarados; sem submissão ou certificação pela AT.',money(v)); } function sourceKey(draft:Record,book:AcquisitionBook):string { const raw=JSON.stringify([acquisitionScope(draft),draft.currency??'',draft.province_id??'',draft.district_id??'',draft.business_plan_snapshot,draft.monthly_iva_settings,draft.monthly_iva_snapshot,book.currency,book.taxpayerRegistered,book.rows,book.periods]); // Change detection only, never an authorization or authenticity token. let hash=14695981039346656037n;for(let i=0;i}).salesOperations?.[acquisitionScope(draft)]; return `${raw.length}:${hash.toString(16)}${operations?`:${JSON.stringify(operations)}`:''}`; } function location(draft:Record):string { const labels=draft.selection_labels as Record|undefined; return ['province_id','district_id'].filter(key=>draft[key]).map(key=>String(draft[`${key}_label`]??labels?.[key]??draft[key])).join(' / '); } export function acquisitionCurrent(draft:Record,book:AcquisitionBook):boolean {return !!book.result && book.result.version===acquisitionVersion && book.result.opportunity===acquisitionScope(draft) && book.result.sourceKey===sourceKey(draft,book);} export function calculateAcquisitions(draft:Record,book:AcquisitionBook,now=new Date().toISOString()):AcquisitionCalculation { const scope=acquisitionScope(draft),dates=periods(draft); if(book.currency!=='MZN' || draft.currency!==book.currency) throw new Error('Resolva a moeda de origem das compras; valores preservados sem conversão.'); if(!dates.length) throw new Error('Período de vendas por determinar.'); if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length) throw new Error('Existem compras com identidade repetida.'); const documents=book.rows.filter(r=>r.documentRef.trim() && r.supplier.trim()).map(r=>JSON.stringify([r.origin,r.supplier.trim().toLowerCase(),r.documentRef.trim().toLowerCase(),r.documentDate,r.documentLine?.trim()??''])); if(new Set(documents).size!==documents.length) throw new Error('Documento/linha repetido: discrimine linhas distintas ou remova a duplicação antes de deduzir.'); if(book.rows.some(r=>!['08','09','10'].includes(r.field) || !dates.includes(r.period))) throw new Error('Classifique cada aquisição num campo08–10 e num período da projecção.'); const references=acquisitionPlanReferences(draft); for(const row of book.rows.filter(r=>r.sourceMonth!==null)) { const current=references.find(p=>p.month===row.sourceMonth && p.kind===(row.sourceField??'inventory')); if(!current || current.amount!==row.sourceAmount || (current.kind!=='asset' && row.sourcePeriod && current.period!==row.sourcePeriod)) throw new Error('As compras ou orçamentos do plano mudaram. Resolva a divergência de origem antes de calcular; histórico preservado.'); } const rows=book.rows.map(row=>evaluateAcquisition(row,book.currency,book.taxpayerRegistered)); const record=readMonthlyIvaRecord(String(draft.monthly_iva_snapshot ?? '')); let salesCurrent=false; try {salesCurrent=!!record && record.execution.status==='completed' && monthlyIvaKey(record.input)===monthlyIvaKey(monthlyIvaInput(draft,readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''))));} catch { /* Preserve history, never fabricate 01/02. */ } const projection=record?.execution.result_snapshot?.monthly_projection as {rows?:Array<{date:string;net_sales:string|null;vat:string|null}>}|undefined; const calculated=dates.map(period=>{ const input=book.periods.find(p=>p.period===period), reasons:string[]=[], fields:Record={}; const declaredAmount=(key:string)=>{const value=amount(String(draft[key]??''));return value!==null && value>0n;}; if(input?.absent.includes('11') && (declaredAmount('imports') || book.rows.some(r=>r.period===period && r.domestic==='no'))) throw new Error('Existem importações declaradas. Não confirme ausência no campo11; enquadramento fora deste lote.'); if(input?.absent.includes('07') && declaredAmount('sales_exempt')) throw new Error('Existem vendas isentas declaradas. Resolva o período e a classificação antes de confirmar ausência no campo07.'); if(input?.absent.includes('05') && (declaredAmount('exports')||declaredAmount('sales_zero_rate'))) throw new Error('Existem exportações ou vendas de outro enquadramento. Não confirme ausência no campo05 sem resolver a classificação.'); const sales=salesCurrent?projection?.rows?.find(r=>r.date.slice(0,7)===period):null; fields['01']=sales?.net_sales??null;fields['02']=sales?.vat??null; if(!sales || sales.net_sales===null || sales.vat===null) reasons.push('01/02: vendas ou IVA mensal em falta, execução incompleta ou desactualizada.'); for(const field of excluded) fields[field]=input?.absent.includes(field)?'0.00':null; const operations=salesBook(draft); if(operations.rows.length || operations.result) { const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null; for(const field of ['01','02','03','04','05','06','07'])fields[field]=classified?.fields[field]??null; if(!classified)reasons.push('Operações L1 alteradas ou sem resultado correspondente: recalcule a classificação; não usar01/02 antigos nem ausência presumida.'); else reasons.push(...classified.reasons); } for(const field of ['08','09','10']) { const matching=book.rows.filter(r=>r.period===period && r.field===field); fields[field]=input?.complete ? sum(matching.map(r=>rows.find(x=>x.id===r.id)!.deductible)) : null; if((operations.rows.length || operations.result) && operations.rows.some(r=>r.period===period && !['general','export_goods'].includes(r.treatment)) && matching.some(r=>rows.find(x=>x.id===r.id)!.deductible!=='0.00')) { fields[field]=null; reasons.push(`Campo${field}: operações reduzidas/isentas/outras exigem afectar cada aquisição; não reutilizar dedução exclusiva geral como se fosse universal (L2).`); } } if(!input?.complete) reasons.push('Aquisições08–10: confirme que todas as compras do período foram incluídas, mesmo quando não existam.'); for(const row of book.rows.filter(r=>r.period===period)) {const r=rows.find(x=>x.id===row.id)!;if(r.deductible===null) reasons.push(`${row.description||row.id}: ${r.reason}`);} for(const field of excluded) if(fields[field]===null) reasons.push(`Campo${field}: fora deste lote, sem declaração expressa de inexistência de operações.`); fields['14']=sum(['01','03','05','06','07'].map(f=>fields[f])); fields['15']=sum(['08','09','10','11','12'].map(f=>fields[f])); fields['16']=sum(['02','04','13'].map(f=>fields[f])); const state=amount(fields['16']),taxpayer=amount(fields['15']); fields['17']=state===null||taxpayer===null?null:money(state>taxpayer?state-taxpayer:0n); fields['18']=state===null||taxpayer===null?null:money(taxpayer>state?taxpayer-state:0n); const classified=salesOperationsCurrent(draft,operations)?operations.result?.periods.find(p=>p.period===period):null; if(classified?.exactFields) { const exactFields:Record={}; for(const f of ['01','02','03','04','05','06','07','08','09','10','11','12','13'])exactFields[f]=classified.exactFields[f]??(fields[f]===null?null:decimalFraction(fields[f]!)); for(const [f,parts] of [['14',['01','03','05','06','07']],['15',['08','09','10','11','12']],['16',['02','04','13']]] as const)exactFields[f]=parts.some(k=>exactFields[k]===null)?null:addFractions(parts.map(k=>exactFields[k]!)); exactFields['17']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['16'],exactFields['15'])):null; exactFields['18']=exactFields['16']&&exactFields['15']?positiveFraction(subtractFractions(exactFields['15'],exactFields['16'])):null; for(const f of Object.keys(exactFields))fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null; reasons.push('Totais derivados das fracções exactas, sem somar aproximações das linhas; decimais a6casas são apresentação técnica, não arredondamento declarativo.'); return {period,fields,exactFields,reasons}; } return {period,fields,reasons}; }); return {version:acquisitionVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:scope,location:location(draft),currency:book.currency,sourceKey:sourceKey(draft,book),basis:acquisitionBasis,sources:acquisitionSources,declarations:{taxpayerRegistered:book.taxpayerRegistered??'',periods:structuredClone(book.periods)},rows,inputs:structuredClone(book.rows),periods:calculated}; } export function acquisitionReportRows(draft:Record):Array<[string,string,string,string]> { if(!draft.modelo_a_snapshot) return []; try { const book=acquisitionBook(draft),result=book.result; if(!result) return [['IVA mensal','Compras — Modelo A','Sem cálculo guardado para a oportunidade escolhida','Sem dedução presumida']]; const context=`${result.currency}; oportunidade ${result.opportunity}; localização ${result.location || 'não indicada'}; ${result.version}; revisão ${result.revision}; ${result.calculatedAt}`; const out:Array<[string,string,string,string]>=[['IVA mensal','Apuramento parcial Modelo A',acquisitionCurrent(draft,book)?'Versão correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Fundamento das aquisições',result.basis,result.sources]]; for(const row of result.inputs) { const r=result.rows.find(x=>x.id===row.id)!; const source=row.sourceMonth===null?'Introduzido pelo utilizador':`Plano (${row.sourceField==='asset'?'orçamento de activos':row.sourceField==='operating'?'orçamento operacional':'compras/encomendas'}), mês ${row.sourceMonth}, ${row.sourcePeriod??'período de origem não indicado'}, ${row.sourceAmount??'valor em falta'} ${row.sourceCurrency}; ${row.sourceBasis==='net'?'sem IVA':row.sourceBasis==='gross'?'com IVA':'base por classificar'}`; const allocation={taxable_only:'exclusiva a operações tributadas',mixed:'mista',no_right:'sem direito à dedução','':'por indicar'}[row.allocation]; out.push(['IVA mensal',`Aquisição ${row.id} — campo${row.field} — ${row.period}`,`${row.description}; líquido ${r.net??'Indeterminado'}; IVA ${r.vat??'Indeterminado'}; bruto ${r.gross??'Indeterminado'}; dedutível ${r.deductible??'Indeterminado'}`,`${context}; ${row.origin==='projected'?'Projecção':'Dados efectivos declarados'}; ${source}; ${row.documentType==='invoice'?'Factura':row.documentType==='equivalent'?'Documento equivalente':'Documento por indicar'} ${row.documentRef}, ${row.documentDate}, linha ${row.documentLine||'única'}; fornecedor ${row.supplier}; afectação ${allocation}; ${r.reason}`]); } for(const period of result.periods) { const entries=Object.entries(period.fields).sort(([a],[b])=>Number(a)-Number(b)); const declaration=result.declarations?.periods.find(p=>p.period===period.period); const declarationOrigin=declaration?`Aquisições08–10 ${declaration.complete?'declaradas completas':'por completar'}; ausência de operações expressamente declarada nos campos ${declaration.absent.join(', ')||'nenhum'}.`:'Declarações detalhadas não recuperáveis nesta versão histórica.'; out.push(['IVA mensal',`Modelo A — ${period.period} — parcelas01–13`,entries.filter(([field])=>Number(field)<=13).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),`${context}; ${declarationOrigin}`]); out.push(['IVA mensal',`Modelo A — ${period.period} — totais14–18`,entries.filter(([field])=>Number(field)>=14).map(([field,value])=>`${field}: ${value??'Indeterminado'}`).join('; '),context]); if(period.exactFields)out.push(['IVA mensal',`Modelo A — ${period.period} — precisão exacta`,Object.entries(period.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),'Fracções em MZN; apresentação decimal técnica, sem regra declarativa presumida.']); if(period.reasons.length)out.push(['IVA mensal',`Modelo A — ${period.period} — pendências`,period.reasons.join(' '),result.sources]); } return out; } catch {return [['IVA mensal','Compras — Modelo A','Não foi possível recuperar os dados guardados','Preservados, sem substituição por zero']];} }