Files
Investidor-V2/resources/js/lib/iva-operations.ts
T

175 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { acquisitionScope, readAcquisitionStore } from './iva-acquisitions.ts';
import { monthlySalesFromDraft, readMonthlyIvaSettings } from './monthly-iva.ts';
import { addFractions, decimalFraction, fraction, fractionLabel, fractionPresentation, type FiscalFraction } from './fiscal-rational.ts';
export const operationVersion = 'iva-operations-l1/2';
// The accepted v2 full-catalogue key predates scoped keys. Freeze its members
// so later, independent catalogue additions cannot invalidate those snapshots.
const legacyV2TreatmentIds = ['general','private_health','private_education','private_training','personal_lessons','export_goods','housing_rent','public_health','public_education','public_training','foreign_operation','other'];
const source2022 = 'Lei22/2022, BR250, 2.º suplemento, PDF1–4 (2252(25)–(28)); arts.1/2/6; efeitos01/01/2023';
const source2016 = 'CIVA republicado Lei13/2016, BR156, 10.º suplemento, PDF9–15 (1426(267)–(273))';
const source2025 = 'Lei10/2025, BR248, suplemento1, PDF14–15 (2180(14)–(15)); arts.1/2/3/5; efeitos01/01/2026';
const healthGoodsSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(1)(b–f); PDF15, p.1426(273), art.19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(1)(b–f). Âmbito técnico2026/MZN';
const internalOperation = 'Operação interna localizada em Moçambique; não é importação nem operação localizada no estrangeiro';
export const modeloA06Correspondence = 'Exemplar recebido:06 é base de operações isentas, com remissão18(1)(b); nessa lei18(1)(b) trata IVA de importações, enquanto19(1)(b) enumera operações com direito. O mesmo exemplar já tem05 para vendas isentas com direito. O formulário antigo da AT usa03 para a remissão e06 para existências. Não há correspondência unívoca nem instrução aplicável que separe05/06 no acervo conferido: não atribuir06 automaticamente a operações no estrangeiro ou importar instruções antigas por número.';
export type OperationTreatment = {id:string; label:string; field:string|null; rate:number|null; article:string; source:string; conditions:string[]};
const nonprofitConditions = [
'Não distribui nem disponibiliza lucros; os corpos gerentes não têm interesse directo ou indirecto nos resultados, por si ou interposta pessoa',
'Escrituração abrange todas as actividades e está à disposição dos serviços fiscais para comprovação',
'Preços homologados pela autoridade competente ou, quando não susceptíveis de homologação, inferiores aos de operações análogas das empresas comerciais sujeitas a imposto',
'Não entra em concorrência directa com sujeitos passivos do imposto',
];
function socialTreatment(id:string,label:string,article:string,conditions:string[],nonprofit:boolean):OperationTreatment {
return {id,label,field:'07',rate:0,article:`CIVA arts.9(2)(${article}),10(${nonprofit?'2':'1'}),19(1)`,source:'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(2)(a/c/d); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN',conditions:[internalOperation,...(nonprofit?nonprofitConditions:['Entidade pública: órgão ou instituição da Administração Pública que desempenha funções administrativas do Estado, nos termos da legislação aplicável']),...conditions]};
}
const culturalSource = 'CIVA republicado Lei13/2016, PDF9, p.1426(267), art.9(2)(e–g); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF1, p.2252(25), mantém9(2)(e–g), e PDF2, p.2252(26),10(1–2). Âmbito técnico2026/MZN';
const culturalTreatments:OperationTreatment[] = ([false,true] as const).flatMap(nonprofit=>{
const suffix=nonprofit?'nonprofit':'public',entity=nonprofit?'organismo sem finalidade lucrativa':'entidade pública';
const make=(id:string,label:string,article:string,conditions:string[]):OperationTreatment=>({...socialTreatment(id,label,article,conditions,nonprofit),source:culturalSource});
return [
make(`cultural_visits_${suffix}`,`Visitas culturais/naturais — ${entity}`,'e',['Visita, guiada ou não, a museu, galeria de arte, monumento, parque, perímetro florestal, jardim botânico/zoológico ou similar pertencente ao Estado, a entidade pública ou a entidade sem finalidade lucrativa','Serviço efectuado pela própria entidade, por intermédio dos seus próprios agentes; organismo sem finalidade lucrativa devidamente autorizado quando aplicável','Se houver bens, são transmissões estreitamente conexas com essa visita; não venda autónoma nem serviço de intermediário']),
make(`religious_personnel_${suffix}`,`Cedência de pessoal religiosa/filosófica — ${entity}`,'f',['Cedência de pessoal efectuada por instituição religiosa ou filosófica identificada','Destino identificado: actividade isenta nos termos do CIVA, com fundamento específico, ou assistência espiritual; não qualquer fornecimento de mão-de-obra']),
];
});
culturalTreatments.push({id:'statutory_membership',label:'Serviços colectivos aos associados por quota estatutária',field:'07',rate:0,article:'CIVA arts.9(2)(g),10(2),19(1)',source:culturalSource,conditions:[internalOperation,...nonprofitConditions,'Serviços efectuados no interesse colectivo dos próprios associados por organismo sem finalidade lucrativa','Objectivos de natureza política, sindical, religiosa, patriótica, filantrópica, recreativa, desportiva, cultural, cívica ou de representação de interesses económicos, identificados','A única contraprestação é uma quota fixada nos respectivos estatutos; não pagamentos adicionais por serviços individuais']});
export const operationTreatments:OperationTreatment[] = [
{id:'author_rights',label:'Direitos de autor pelo autor, herdeiro ou legatário',field:'07',rate:0,article:'CIVA arts.9(9)(a),19(1)',source:'CIVA republicado Lei13/2016, PDF10, p.1426(268),9(9)(a); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(9). Âmbito técnico2026/MZN',conditions:[internalOperation,'Operação identificada como transmissão de direitos de autor ou autorização para utilização de obra intelectual','Efectuada pelo próprio autor, seu herdeiro ou legatário, com qualidade identificada; não presumir isenção de revenda/intermediação por terceiro']},
{id:'cultural_publications',label:'Jornais, revistas e livros de natureza elegível',field:'07',rate:0,article:'CIVA arts.9(9)(b),19(1)',source:'CIVA republicado Lei13/2016, PDF10, p.1426(268),9(9)(b); PDF15, p.1426(273),19(1); Lei22/2022 art.1, PDF2, p.2252(26), mantém9(9). Âmbito técnico2026/MZN',conditions:[internalOperation,'Transmissão de jornal, revista ou livro identificado; não qualquer material escolar, serviço editorial ou fornecimento autónomo','Natureza cultural, educativa, técnica ou recreativa da publicação identificada e sustentada; não inferida apenas do nome do fornecedor ou sector']},
...culturalTreatments,
{id:'general',label:'Operação interna geral',field:'01',rate:16,article:'CIVA arts.1,6,15(1),17(1–2); art.34 (Lei10/2025)',source:`${source2016}; ${source2022}; ${source2025}`,conditions:['Operação localizada e tributável em Moçambique, no regime normal','Contraprestação completa; sem base especial do art.15(2), isenção ou taxa reduzida']},
{id:'private_health',label:'Serviços médicos e sanitários privados',field:'03',rate:5,article:'CIVA arts.15(10),17-A(a)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Prestador hospitalar privado, clínica, dispensário ou similar']},
{id:'private_education',label:'Ensino privado reconhecido',field:'03',rate:5,article:'CIVA arts.15(10),17-A(b)',source:source2022,conditions:['Ensino ou bens/serviços conexos prestados pelo estabelecimento','Estabelecimento privado integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
{id:'private_training',label:'Formação profissional privada',field:'03',rate:5,article:'CIVA arts.15(10),17-A(c)',source:source2022,conditions:['Formação profissional ou fornecimentos conexos pelo prestador, não venda autónoma de materiais','Prestador privado']},
{id:'personal_lessons',label:'Lições ministradas a título pessoal',field:'03',rate:5,article:'CIVA arts.15(10),17-A(d)',source:source2022,conditions:['Lições ministradas a título pessoal','Matérias de ensino escolar ou superior']},
{id:'export_goods',label:'Exportação de bens pelo vendedor',field:'05',rate:0,article:'CIVA arts.13(1)(a),19(1)(b)(i)',source:source2016,conditions:['Bens expedidos/transportados para o estrangeiro pelo vendedor ou terceiro por sua conta','Saída documentada, ou pressuposto expresso da projecção; não mera venda a cliente estrangeiro']},
{id:'housing_rent',label:'Locação de imóvel para habitação',field:'07',rate:0,article:'CIVA art.9(5), redacção Lei22/2022; art.19(1)',source:source2022,conditions:['Locação de imóvel para fins de habitação','Não é hospedagem nem locação comercial/industrial ou serviço autónomo']},
{id:'public_health',label:'Serviços médicos e sanitários públicos',field:'07',rate:0,article:'CIVA arts.9(1)(a),10(1),19(1)',source:source2022,conditions:['Serviço médico/sanitário ou operação estreitamente conexa','Estabelecimento hospitalar público, dispensário ou similar; natureza pública confirmada']},
{id:'public_education',label:'Ensino público reconhecido',field:'07',rate:0,article:'CIVA arts.9(3)(a),10(1),19(1)',source:source2022,conditions:['Ensino ou fornecimentos conexos pelo estabelecimento','Estabelecimento público integrado no Sistema Nacional de Ensino e reconhecido pelo Ministério competente']},
{id:'public_training',label:'Formação profissional pública',field:'07',rate:0,article:'CIVA arts.9(3)(b),10(1),19(1)',source:source2022,conditions:['Formação profissional ou fornecimentos conexos pelo prestador','Prestador público']},
{id:'financial_stamp',label:'Operação bancária/financeira sujeita a Selo',field:'07',rate:0,article:'CIVA art.9(4), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['A operação é bancária ou financeira, não um serviço acessório autónomo','Sujeição a Imposto do Selo identificada para esta operação; não depende de calcular o Selo neste bloco']},
{id:'insurance_stamp',label:'Seguro/resseguro ou mediação conexa sujeitos a Selo',field:'07',rate:0,article:'CIVA art.9(6), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Seguro/resseguro ou serviço conexo efectuado por corrector ou outro mediador de seguros','Sujeição a Imposto do Selo identificada para esta operação']},
{id:'public_waste',label:'Remoção de lixo pública ou por entidade contratada',field:'07',rate:0,article:'CIVA art.9(12)(b), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Prestação de serviço de remoção de lixo, não qualquer serviço de limpeza','Efectuada por entidade pública ou por entidade por ela contratada, com relação identificada']},
{id:'public_funeral',label:'Serviços funerários/cremação públicos e bens acessórios',field:'07',rate:0,article:'CIVA art.9(12)(c), redacção Lei22/2022; art.19(1)',source:`${source2022}; ${source2016}`,conditions:['Serviço funerário/cremação ou transmissão de bem acessório a esse serviço','Efectuado por entidade pública; não aplicar automaticamente a funerária privada']},
{id:'health_assistive',label:'Bens de compensação da deficiência, próteses e braille',field:'07',rate:0,article:'CIVA arts.9(1)(b),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Bem identificado na enumeração: cadeira de rodas/veículo semelhante, aparelho/máquina/impressora braille, prótese ou material de compensação','Destino documentado conforme a categoria: deficiência, substituição de membro/órgão, fractura, pessoa invisual ou correcção da audição; não qualquer equipamento médico']},
{id:'human_materials',label:'Órgãos, sangue e leite humanos',field:'07',rate:0,article:'CIVA arts.9(1)(c),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Transmissão de órgãos, sangue ou leite de origem humana, identificada; não produto de origem animal nem serviço autónomo']},
{id:'patient_transport',label:'Transporte de doentes/feridos por organismo autorizado',field:'07',rate:0,article:'CIVA arts.9(1)(d),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Transporte de doente ou ferido em ambulância ou outro veículo apropriado','Organismo que efectua o transporte devidamente autorizado, com autorização identificada; não basta o destino hospitalar']},
{id:'mosquito_nets',label:'Transmissão de redes mosquiteiras',field:'07',rate:0,article:'CIVA arts.9(1)(e),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Bem identificado como rede mosquiteira; não extensão automática a outros produtos de prevenção']},
{id:'medicines',label:'Medicamentos e artigos terapêuticos/profilácticos elegíveis',field:'07',rate:0,article:'CIVA arts.9(1)(f),19(1)',source:healthGoodsSource,conditions:[internalOperation,'Medicamento/especialidade farmacêutica ou produto farmacêutico exclusivamente terapêutico/profiláctico; ou pasta, gaze, algodão hidrófilo, tira/penso adesivo ou análogo','Finalidade elegível identificada: terapêutica/profiláctica nos produtos farmacêuticos; higiénica, medicinal ou cirúrgica nos artigos enumerados, mesmo impregnados/revestidos; não qualquer produto vendido por farmácia']},
...([false,true] as const).flatMap(nonprofit=>{
const suffix=nonprofit?'nonprofit':'public',entity=nonprofit?'organismo sem finalidade lucrativa':'entidade pública';
return [
socialTreatment(`social_assistance_${suffix}`,`Assistência social — ${entity}`,'a',['Prestação de assistência social ou transmissão de bens estreitamente conexa, identificada','Fins e objecto da entidade reconhecidos pelas autoridades competentes, com fundamento identificado'],nonprofit),
socialTreatment(`social_care_${suffix}`,`Equipamentos sociais/creches/lares — ${entity}`,'c',['Prestação ou bem estreitamente conexo no exercício da actividade habitual de creche, jardim de infância, centro de tempos livres, lar ou outro equipamento social enumerado no art.9(2)(c)','Equipamento pertencente à entidade e fins/objecto reconhecidos pelas autoridades competentes; não qualquer alojamento ou empresa de cuidados'],nonprofit),
socialTreatment(`activity_facilities_${suffix}`,`Prática artística/desportiva/recreativa — ${entity}`,'d',['Serviço efectuado pela própria entidade que explora o estabelecimento ou instalação','Instalação destinada à prática artística, desportiva, recreativa ou de educação física e serviço prestado a quem a pratica; não isenção automática de bilhetes para espectadores'],nonprofit),
];
}),
{id:'foreign_operation',label:'Operação no estrangeiro — analisar territorialidade',field:null,rate:null,article:'CIVA art.6: localização por natureza da operação; art.19(1)(b)(ii) não determina sozinho a caixa06 do formulário',source:source2016,conditions:[]},
{id:'other',label:'Outra operação / enquadramento ainda desconhecido',field:null,rate:null,article:'Fundamento específico ainda necessário; não presumir taxa ou isenção',source:'Sem fonte operacional determinada para esta linha',conditions:[]},
];
export type SalesOperation = {id:string;period:string;description:string;establishment:string;amount:string;basis:''|'net'|'included';treatment:string;confirmed:number[];evidence:string;sourceAmount:string|null;sourcePeriod:string;sourceCurrency:string;source:string; splitSource?:{id:string;amount:string|null;period:string;currency:string;basis:string}};
export type SalesBook = {currency:string;rows:SalesOperation[];periods:Array<{period:string;complete:boolean}>; result?:SalesResult; history?:SalesResult[]};
export type SalesLineResult = {id:string;field:string|null;net:string|null;vat:string|null;gross:string|null;rate:number|null;status:'calculated'|'exempt'|'indeterminate';reason:string;article:string;source:string;exact?:{net:FiscalFraction;vat:FiscalFraction;gross:FiscalFraction}};
export type SalesResult = {version:string;revision:number;calculatedAt:string;opportunity:string;location:string;currency:string;key:string;inputs:SalesOperation[];rows:SalesLineResult[];periods:Array<{period:string;fields:Record<string,string|null>;exactFields?:Record<string,FiscalFraction|null>;reasons:string[]}>;rounding:string};
type Store = ReturnType<typeof readAcquisitionStore> & {salesOperations?:Record<string,SalesBook>};
export function newSalesOperation(id:string,period:string,currency:string):SalesOperation {return {id,period,description:'',establishment:'',amount:'',basis:'',treatment:'other',confirmed:[],evidence:'',sourceAmount:null,sourcePeriod:period,sourceCurrency:currency,source:'Introdução directa; projecção, não declaração submetida'};}
export function salesBook(draft:Record<string,unknown>):SalesBook {
const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;
if(store.salesOperations!==undefined&&(!store.salesOperations||typeof store.salesOperations!=='object'||Array.isArray(store.salesOperations)))throw new Error('Formato das operações IVA não recuperável; dados preservados, sem substituição.');
const book=store.salesOperations?.[acquisitionScope(draft)];
if(!book)return {currency:String(draft.currency??''),rows:[],periods:[]};
if(typeof book.currency!=='string'||!Array.isArray(book.rows)||!Array.isArray(book.periods)||book.rows.some(r=>!r||Object.keys(newSalesOperation('','','')).some(k=>k==='confirmed'?!Array.isArray(r.confirmed)||r.confirmed.some(c=>!Number.isInteger(c)):k==='sourceAmount'?!(r.sourceAmount===null||typeof r.sourceAmount==='string'):typeof r[k as keyof SalesOperation]!=='string'))||book.periods.some(p=>!p||!/^\d{4}-\d{2}$/.test(p.period)||typeof p.complete!=='boolean'))throw new Error('Operações IVA não recuperáveis; os dados originais foram preservados.');
return book;
}
export function saveSalesBook(draft:Record<string,unknown>,book:SalesBook):string {const store=readAcquisitionStore(draft.modelo_a_snapshot) as Store;return JSON.stringify({...store,salesOperations:{...store.salesOperations,[acquisitionScope(draft)]:book}});}
export function reuseSales(draft:Record<string,unknown>,book:SalesBook):SalesBook {
const source=monthlySalesFromDraft(draft),settings=readMonthlyIvaSettings(String(draft.monthly_iva_settings??''));
if(!/^2026-(0[1-9]|1[0-2])$/.test(settings.start)||Number(settings.start.slice(5))+source.amounts.length>13)throw new Error('Defina o período da projecção dentro de2026 antes de reutilizar vendas.');
if(book.currency!==draft.currency)throw new Error('Moeda da origem divergente; valores preservados, sem conversão.');
const rows=[...book.rows],periods=[...book.periods];
source.amounts.forEach((value,index)=>{const period=`2026-${String(Number(settings.start.slice(5))+index).padStart(2,'0')}`;
if(!periods.some(p=>p.period===period))periods.push({period,complete:false});
if(!rows.some(r=>r.sourcePeriod===period&&r.source!=='Introdução directa; projecção, não declaração submetida'))rows.push({...newSalesOperation(`sales-${period}`,period,book.currency),description:`Vendas do plano — ${period}`,amount:value??'',basis:settings.basis==='unknown'?'':settings.basis,sourceAmount:value,source:source.source,evidence:source.source});
});return {...book,rows,periods};
}
export function splitSales(book:SalesBook,id:string,newId:string):SalesBook {
const row=book.rows.find(r=>r.id===id);if(!row)throw new Error('Operação de origem não encontrada.');
if(row.sourceAmount===null&&!row.splitSource)throw new Error('Reparta uma venda reutilizada com valor de origem determinado; para entradas directas acrescente as operações em falta.');
const source=row.splitSource??{id:row.id,amount:row.sourceAmount??row.amount,period:row.period,currency:row.sourceCurrency,basis:row.basis};
if(!source.basis||source.amount===null||amount(source.amount)===null)throw new Error('Determine o valor e a representação do preço antes de repartir a origem.');
const first={...row,amount:'',sourceAmount:null,splitSource:source,source:`Repartição explícita de ${source.id}: ${source.amount} ${source.currency}; ${row.source}`};
return {...book,rows:[...book.rows.map(r=>r.id===id?first:r),{...first,id:newId,description:'Parcela adicional da mesma venda',treatment:'other',confirmed:[],evidence:''}],periods:book.periods.map(p=>p.period===row.period?{...p,complete:false}:p)};
}
export function recordSales(draft:Record<string,unknown>,book:SalesBook):SalesBook {return {...book,history:book.result?[...(book.history??[]),book.result]:book.history,result:calculateSales(draft,book)};}
function amount(raw:unknown):bigint|null {if(typeof raw!=='string'||!/^\d{1,15}(\.\d{1,6})?$/.test(raw.trim()))return null;const [n,d='']=raw.trim().split('.');return BigInt(n)*1000000n+BigInt(d.padEnd(6,'0'));}
function money(n:bigint):string {return `${n/1000000n}.${(n%1000000n).toString().padStart(6,'0').replace(/0+$/,'').padEnd(2,'0')}`;}
function sum(values:Array<string|null>):string|null {const parsed=values.map(amount);return parsed.some(v=>v===null)?null:money((parsed as bigint[]).reduce((a,b)=>a+b,0n));}
export function evaluateSalesOperation(row:SalesOperation,currency:string):SalesLineResult {
const rule=operationTreatments.find(r=>r.id===row.treatment),n=amount(row.amount);
const result:SalesLineResult={id:row.id,field:rule?.field??null,net:null,vat:null,gross:null,rate:rule?.rate??null,status:'indeterminate',reason:'',article:rule?.article??'Tratamento desconhecido',source:rule?.source??'Fonte em falta'};
const stop=(reason:string)=>({...result,reason});
if(currency!=='MZN'||row.sourceCurrency!==currency)return stop('Moeda fora de MZN ou divergente da origem; sem conversão silenciosa.');
if(!/^2026-(0[1-9]|1[0-2])$/.test(row.period))return stop('Período fora da cobertura de2026; sem cálculo histórico presumido.');
if(!rule||rule.rate===null||!rule.field)return stop('Operação não calculada: delimite incidência, territorialidade e fundamento desta linha. A remissão do campo06 não constitui uma regra de classificação de todas as operações no estrangeiro.');
if(n===null||!['net','included'].includes(row.basis))return stop('Montante ou indicação de IVA incluído/sem IVA em falta; vazio não significa zero.');
if(!row.description.trim()||!row.establishment.trim())return stop('Indique a operação e o estabelecimento a que pertence; não inferidos da localização do investimento.');
if(rule.conditions.some((_,i)=>!row.confirmed.includes(i))||!row.evidence.trim())return stop('Condições da operação ou fundamento/pressuposto em falta; o sector não prova o tratamento fiscal.');
if(row.sourceAmount!==null&&amount(row.sourceAmount)!==n)return stop('Divergência com o valor reutilizado: conserve a origem e prepare a repartição explicitamente; não substituir silenciosamente.');
if(row.basis==='included'&&rule.rate===5){
const exact={net:fraction(n*20n,21000000n),vat:fraction(n,21000000n),gross:fraction(n,1000000n)};
return {...result,net:fractionPresentation(exact.net).decimal,vat:fractionPresentation(exact.vat).decimal,gross:money(n),exact,status:'calculated',reason:'Base e IVA conservados como fracções exactas (preço×20/21 e preço/21). Decimal a6casas é apresentação técnica, metade para cima; não é regra de arredondamento da declaração nem imposto final a pagar.'};
}
let net=n,vat=0n;
if(rule.rate===16&&row.basis==='included') {net=((n*100n+58000000n)/116000000n)*1000000n;vat=n-net;if(vat<0n)return stop('Arredondamento à unidade produz IVA negativo para este montante; reveja a linha e a documentação.');}
else vat=(n*BigInt(rule.rate)+50n)/100n;
return {...result,net:money(net),vat:money(vat),gross:money(row.basis==='included'?n:net+vat),status:rule.rate===0?'exempt':'calculated',reason:rule.rate===0?(rule.field==='05'?'Isenção com direito à dedução nas condições legais; não é taxa zero nem dedução automática de compras.':'Isenção sem direito à dedução para esta operação; não é isenção geral da entidade.'):'IVA da operação projectada; não é imposto final a pagar nem declaração submetida.'};
}
function key(draft:Record<string,unknown>,book:SalesBook,catalog=operationTreatments.filter(t=>book.rows.some(r=>r.treatment===t.id))):string {return JSON.stringify([operationVersion,catalog,acquisitionScope(draft),draft.currency,draft.province_id,draft.district_id,draft.business_plan_snapshot,draft.cashflow_snapshot,draft.monthly_iva_settings,book.currency,book.rows,book.periods]);}
export function salesCurrent(draft:Record<string,unknown>,book:SalesBook):boolean {return !!book.result&&book.result.version===operationVersion&&book.result.opportunity===acquisitionScope(draft)&&(book.result.key===key(draft,book)||book.result.key===key(draft,book,operationTreatments.filter(t=>legacyV2TreatmentIds.includes(t.id))));}
export function calculateSales(draft:Record<string,unknown>,book:SalesBook,now=new Date().toISOString()):SalesResult {
if(book.currency!=='MZN'||draft.currency!==book.currency)throw new Error('Confirme a moeda original MZN; sem conversão ou reinterpretação.');
if(new Set(book.rows.map(r=>r.id)).size!==book.rows.length||new Set(book.periods.map(p=>p.period)).size!==book.periods.length)throw new Error('Identificadores/períodos duplicados; corrija sem somar em duplicado.');
if(!book.periods.length||book.rows.some(r=>!book.periods.some(p=>p.period===r.period))||book.periods.some(p=>!/^2026-(0[1-9]|1[0-2])$/.test(p.period)))throw new Error('Cada operação deve pertencer a um período válido de2026 incluído na cobertura.');
let live:ReturnType<typeof monthlySalesFromDraft>|null=null;try {live=monthlySalesFromDraft(draft);}catch{ /* Explicitly block stale imported values below. */ }
const start=readMonthlyIvaSettings(String(draft.monthly_iva_settings??'')).start;
const rows=book.rows.map(r=>{
const result=evaluateSalesOperation(r,book.currency);
if(r.splitSource) {
const source=r.splitSource,group=book.rows.filter(v=>v.splitSource?.id===source.id),index=Number(source.period.slice(5))-Number(start.slice(5));
if(group.some(v=>v.period!==source.period||v.basis!==source.basis||v.sourceCurrency!==source.currency||JSON.stringify(v.splitSource)!==JSON.stringify(source))||sum(group.map(v=>v.amount))!==(amount(source.amount)===null?null:money(amount(source.amount)!))||!live||live.amounts[index]!==source.amount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'Repartição não reconciliada com o total, período, moeda e representação do preço da origem; nenhuma parcela foi presumida.'};
}
if(r.sourceAmount!==null||r.source.startsWith('Vendas por produto')||r.source.startsWith('Receita mensal')) {
const index=Number(r.sourcePeriod.slice(5))-Number(start.slice(5));
if(!live||!/^2026-/.test(start)||r.sourcePeriod!==r.period||live.amounts[index]!==r.sourceAmount)return {...result,exact:undefined,net:null,vat:null,gross:null,status:'indeterminate' as const,reason:'A origem/período das vendas mudou. Reveja e confirme a nova origem; o resultado anterior foi conservado.'};
}
return result;
});
const periods=book.periods.map(p=>{
const matching=book.rows.map((r,i)=>({input:r,result:rows[i]})).filter(r=>r.input.period===p.period),fields:Record<string,string|null>={},exactFields:Record<string,FiscalFraction|null>={},reasons=matching.filter(r=>r.result.status==='indeterminate').map(r=>`${r.input.description||r.input.id}: ${r.result.reason}`);
const known=p.complete&&!matching.some(r=>r.result.status==='indeterminate');
for(const f of ['01','03','05','07'])fields[f]=known?sum(matching.filter(r=>r.result.field===f).map(r=>r.result.net)):null;
for(const [f,base] of [['02','01'],['04','03']])fields[f]=known?sum(matching.filter(r=>r.result.field===base).map(r=>r.result.vat)):null;
fields['06']=known?'0.00':null;
if(matching.some(r=>r.result.exact)) {
for(const [f,base,part] of [['01','01','net'],['02','01','vat'],['03','03','net'],['04','03','vat'],['05','05','net'],['06','06','net'],['07','07','net']] as const) {
exactFields[f]=known?addFractions(matching.filter(r=>r.result.field===base).map(r=>r.result.exact?.[part]??decimalFraction(r.result[part]!))):null;
fields[f]=exactFields[f]?fractionPresentation(exactFields[f]!).decimal:null;
}
}
if(!p.complete)reasons.push('Confirme que todas as operações do período foram incluídas; uma lista vazia só é zero após essa confirmação.');
return {period:p.period,fields,...(Object.keys(exactFields).length?{exactFields}:{}),reasons};
});
const labels=draft.selection_labels as Record<string,unknown>|undefined;
return {version:operationVersion,revision:(book.result?.revision??0)+1,calculatedAt:now,opportunity:acquisitionScope(draft),location:['province_id','district_id'].filter(k=>draft[k]).map(k=>String(labels?.[k]??draft[k])).join(' / '),currency:book.currency,key:key(draft,book),inputs:structuredClone(book.rows),rows,periods,rounding:'Base líquida: precisão técnica6decimais, metade para cima, sem arredondamento final da declaração. Preço geral com IVA: base=preço×100/116, arredondada à unidade mais próxima (metade para cima), IVA=preço−base, CIVA art.34 redacção Lei10/2025. Preço incluído5%: fracções exactas preço×20/21 e preço/21 conservadas; decimal a6casas/metade para cima apenas na apresentação. A regra declarativa não é presumida.'};
}
export function salesReportRows(draft:Record<string,unknown>):Array<[string,string,string,string]> {
try {const book=salesBook(draft),r=book.result;if(!r)return [];
const context=`${r.currency}; oportunidade ${r.opportunity}; ${r.location||'localização não indicada'}; ${r.version}; revisão ${r.revision}; ${r.calculatedAt}`;
const out:Array<[string,string,string,string]>=[['IVA mensal','Operações IVA L1',salesCurrent(draft,book)?'Resultado correspondente às entradas':'Histórico desactualizado; não aplicar à projecção actual',context],['IVA mensal','Método e limites L1',r.rounding,'Sem apuramento integral, dedução automática ou submissão AT']];
for(const row of r.inputs){const value=r.rows.find(v=>v.id===row.id)!;out.push(['IVA mensal',`${row.period} — ${row.description} — ${row.establishment}`,`Declarado ${row.amount||'em falta'} ${row.sourceCurrency}; ${row.basis==='included'?'IVA incluído':row.basis==='net'?'sem IVA':'base desconhecida'}; líquido ${value.net??'Indeterminado'}; IVA ${value.vat??'Indeterminado'}; bruto ${value.gross??'Indeterminado'}; campo ${value.field??'não determinado'}; ${value.reason}`,`${context}; ${value.article}; ${value.source}; origem: ${row.source}; pressuposto/documento: ${row.evidence||'em falta'}; condições confirmadas: ${row.confirmed.join(',')||'nenhuma'}`]);}
for(const value of r.rows.filter(v=>v.exact))out.push(['IVA mensal',`Precisão exacta — ${value.id}`,`Líquido ${fractionLabel(value.exact!.net)} MZN; IVA ${fractionLabel(value.exact!.vat)} MZN; bruto ${fractionLabel(value.exact!.gross)} MZN`,`${context}; apresentação decimal a6casas, aproximação quando a fracção não termina; não aplicar à declaração sem a sua regra própria.`]);
for(const p of r.periods){out.push(['IVA mensal',`Operações L1 — ${p.period}`,Object.entries(p.fields).map(([f,v])=>`${f}: ${v??'Indeterminado'}`).join('; '),`${context}; ${p.reasons.join(' ')}`]);
if(p.exactFields)out.push(['IVA mensal',`Precisão exacta do período — ${p.period}`,Object.entries(p.exactFields).map(([f,v])=>`${f}: ${v?fractionLabel(v):'Indeterminado'}`).join('; '),`${context}; totais em fracções MZN, calculados antes da apresentação decimal, sem regra declarativa presumida.`]);
}return out;
}catch{return [['IVA mensal','Operações IVA L1','Não foi possível recuperar as operações guardadas','Originais preservados; sem substituição por zero']];}
}