Files
SGF/public/sgf-fiscal-rules.mjs
2026-11-11 09:00:17 +02:00

26 lines
16 KiB
JavaScript
Raw Permalink 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 {confirmation} from './sgf-stock-final.mjs';
const $=id=>document.getElementById(id),csrf=document.querySelector('meta[name="csrf-token"]').content,context=document.querySelector('meta[name="sgf-context"]').content;
const states={prepared:'Preparado',reviewed:'Revisto',refused:'Recusado',withdrawn:'Retirado',determined_qa:'Calculado QA',pending:'Pendente',pending_representation:'Representação pendente'};let state,page=1,selected=null,preview=null,operationKey=null,preparing=false,reading=false,readGeneration=0,dirty=false,documentRowCounter=0;
const el=(tag,text,parent)=>{const n=document.createElement(tag);if(text!==null)n.textContent=text;parent.append(n);return n;};const error=e=>$('message').textContent=e.message;
async function api(path,body,key){const r=await fetch(path,{credentials:'same-origin',headers:{Accept:'application/json','Content-Type':'application/json','X-CSRF-TOKEN':csrf,'X-SGF-Context':context,...(key?{'Idempotency-Key':key}:{})},...(body?{method:'POST',body:JSON.stringify(body)}:{})});return{status:r.status,value:await r.json()};}
const op=confirmation(async body=>{if(operationKey){const old=await api('/api/fiscal-rules/receipts/'+operationKey);if(old.status===200)return{status:200,value:{...old.value.result,replayed:true}};if(old.status!==404)return old;}return api('/api/fiscal-rules/confirm',body);});
function locked(){const busy=Boolean(preparing||reading||preview||op.pending);$('fields').disabled=busy||!state||(!state.capabilities.prepare&&!state.capabilities.review);$('filters').disabled=busy;$('confirm').hidden=!preview||op.uncertain;$('retry').hidden=!op.uncertain;$('cancel').hidden=!preview||op.pending;}
function input(id,label,type='text',value='',required=true){const l=el('label',label,$('inputs')),i=el(type==='textarea'?'textarea':'input',null,l);i.id=id;if(type!=='textarea')i.type=type;i.value=value;i.required=required;return i;}
function select(id,label,choices){const l=el('label',label,$('inputs')),s=el('select',null,l);s.id=id;s.required=true;for(const [v,t]of choices)s.append(new Option(t,v));return s;}
function documentLine(){if(preview||op.pending||preparing)return;const table=$('documentLines');if(table.children.length>=25){error(Error('Máximo25 linhas.'));return;}const id=++documentRowCounter,tr=el('tr',null,table);tr.dataset.rowId=String(id);for(const [name,label]of [['reference','Referência'],['quantity','Quantidade'],['price','Preço unitário']]){const td=el('td',null,tr),i=el('input',null,td);i.id='doc-'+name+'-'+id;i.required=true;i.type='text';i.maxLength=name==='reference'?80:25;i.setAttribute('aria-label',label+' linha '+id);}const td=el('td',null,tr),b=el('button','Retirar linha',td);b.type='button';b.onclick=()=>{if(preview||op.pending||preparing)return;if(table.children.length<=1){error(Error('Mantenha pelo menos uma linha.'));return;}tr.remove();dirty=true;};}
function documentFields(){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: resultado pendente, sem taxa zero por omissão.',$('inputs'));select('documentMode','Modalidade explícita',[['','Escolher…'],['net_added','Preço sem imposto'],['gross_included','Preço com imposto incluído']]);input('documentOn','Data para conferir a vigência','date','');select('documentCurrency','Moeda',[['','Escolher…'],['MZN','MZN']]);el('p','Política: imposto truncado por linha; no incluído, base documental = bruto − imposto documental. Exactos e ambas as diferenças preservados. Totais somam as linhas.',$('inputs'));const wrap=el('div',null,$('inputs')),table=el('table',null,wrap),head=el('thead',null,table),tr=el('tr',null,head);for(const title of ['Referência','Quantidade','Preço unitário','Linha'])el('th',title,tr);const body=el('tbody',null,table);body.id='documentLines';documentRowCounter=0;documentLine();const add=el('button','Adicionar linha', $('inputs'));add.id='addDocumentLine';add.type='button';add.onclick=()=>{documentLine();dirty=true;};}
function fields(){if(preview||op.pending)return;$('inputs').replaceChildren();const action=$('action').value;if(!action)return;
if(action==='prepare'){input('ruleKey','Chave da regra (nova chave ou sucessora)','text',selected?.rule_key??'').maxLength=80;const template=select('template','Definição a preparar',[['','Escolher…'],...state.fixtures.cases.map(c=>[c.fixture_id,c.definition.label]),['pending','Referência ainda não conferida']]);const container=el('div',null,$('inputs'));container.id='templateFields';template.onchange=()=>{container.replaceChildren();if(template.value==='pending'){const l=el('label','Nome da referência',container),name=el('input',null,l);name.id='metadataLabel';name.required=true;const lf=el('label','Família',container),family=el('select',null,lf);family.id='metadataFamily';family.required=true;for(const [v,t]of [['','Escolher…'],['iva','IVA'],['irps','IRPS'],['irpc','IRPC'],['ispc','ISPC'],['ice','ICE'],['inss','INSS'],['other','Outros']])family.append(new Option(t,v));for(const [id,title]of [['sourceReference','Referência da fonte'],['conditions','Condições conhecidas ou lacunas']]){const label=el('label',title,container),i=el('textarea',null,label);i.id=id;i.required=true;}}else{const d=state.fixtures.cases.find(c=>c.fixture_id===template.value)?.definition;if(d)el('p','Família QA artificial · MZN · taxa de ensaio '+d.parameters.rate+' × base · vigência '+d.valid_from+' a '+(d.valid_until??'sem termo neste caso')+' · representação em duas casas, truncagem. Fonte '+d.source_reference+'.',container);}};}
if(action==='calculate_document')documentFields();
if(action==='review'||action==='withdraw'){el('p',selected?'Versão escolhida: '+selected.rule_key+' · v'+selected.rule_version+' · revisão actual '+selected.current_revision:'Escolha explicitamente uma versão no catálogo antes da decisão.',$('inputs'));if(action==='review')select('decision','Decisão',[['','Escolher…'],['reviewed','Rever favoravelmente'],['refused','Recusar']]);}
if(action==='calculate'){el('p',selected?'Regra escolhida: '+selected.rule_key+' · versão '+selected.rule_version:'Sem regra escolhida: o resultado será uma pendência, nunca taxa zero.',$('inputs'));input('base','Base decimal explícita','text','');input('on','Data para verificar vigência','date','');select('currency','Moeda',[['','Escolher…'],['MZN','MZN'],['USD','USD (incompatível neste ensaio)']]);}}
function actions(){const before=$('action').value;$('action').replaceChildren(new Option('Escolher…',''));for(const [a,label,right]of [['prepare','Preparar regra/fonte','prepare'],['review','Rever versão escolhida','review'],['withdraw','Retirar versão revista','review'],['calculate','Calcular com versão escolhida','prepare'],...(state.document_enabled?[['calculate_document','Calcular por linhas / imposto incluído','prepare']]:[])])if(state.capabilities[right])$('action').append(new Option(label,a));if([...$('action').children].some(o=>o.value===before))$('action').value=before;fields();}
async function choose(id,type){if(preview||op.pending||preparing||reading)return;try{const r=await api('/api/fiscal-rules/records/'+id+'?type='+type);if(r.status!==200)throw Error(r.value.message??'Versão indisponível.');$('detail').textContent=JSON.stringify(r.value,null,2);$('documentDownload').replaceChildren();if(type==='calculation'&&r.value.result.price_mode){const a=el('a','Descarregar mapa interno por linhas',$('documentDownload'));a.href='/api/fiscal-rules/documents/'+id+'/report';}if(type==='rule'){selected=r.value;$('selection').textContent='Escolhida '+selected.rule_key+' · v'+selected.rule_version+' · '+states[selected.state]+(selected.id!==selected.current_version_event_id?' · versão já alterada':'');if(['review','withdraw','calculate','calculate_document'].includes($('action').value))fields();}else{selected=null;if(['calculate','calculate_document'].includes($('action').value))fields();$('selection').textContent='Cálculo histórico escolhido; a regra de outro registo não é seleccionada automaticamente.';}}catch(e){error(e);}}
async function load(){if(op.pending||preview)throw Error('Conclua ou cancele a conferência em curso.');const generation=++readGeneration,oldCapabilities=JSON.stringify(state?.capabilities);reading=true;locked();try{const view=$('view').value,query={view,page,per_page:$('size').value};if(view!=='calculations'){if($('filterKey').value)query.rule_key=$('filterKey').value;if($('family').value)query.family=$('family').value;if($('filterState').value)query.state=$('filterState').value;if($('search').value)query.search=$('search').value;}if(view==='history'&&!query.rule_key)throw Error('Indique a chave para consultar o seu histórico.');const r=await api('/api/fiscal-rules?'+new URLSearchParams(query));if(generation!==readGeneration)return;if(r.status!==200)throw Error(r.value.message??'Catálogo indisponível.');state=r.value;$('events').replaceChildren();for(const row of state.records.data){const tr=el('tr',null,$('events')),isCalc=view==='calculations';el('td',isCalc?'Cálculo #'+row.id:row.rule_key+' · '+row.definition.label,tr);el('td',isCalc?states[row.result.state]:'v'+row.rule_version+' / r'+row.revision+' · '+states[row.state],tr);el('td',isCalc?'Imposto: '+(row.result.tax_document??'indeterminado')+' · total: '+(row.result.total??'indeterminado'):row.definition.source_reference+' · '+(row.definition.valid_from??'vigência desconhecida')+' a '+(row.definition.valid_until??'sem termo')+' · '+(row.definition.source_status==='qa_checked'?'fonte artificial conferida':'fonte pendente'),tr);const td=el('td',null,tr);el('button',isCalc?'Consultar cálculo':'Escolher esta versão',td).onclick=()=>choose(row.id,isCalc?'calculation':'rule');if(row.command_reference)el('small','Referência: '+row.command_reference,td);}const p=state.records;$('page').textContent='Página '+p.current_page+' de '+p.last_page+' · '+p.total+' registos';$('previous').disabled=page<=1;$('next').disabled=page>=p.last_page;if(oldCapabilities!==JSON.stringify(state.capabilities))actions();}catch(e){if(generation===readGeneration){state=null;selected=null;$('events').replaceChildren();$('detail').textContent='';$('selection').textContent='Consulta não conferida; reabra o contexto autorizado.';}throw e;}finally{if(generation===readGeneration){reading=false;locked();}}}
$('operation').onsubmit=async e=>{e.preventDefault();if(preparing)return;preparing=true;locked();try{const action=$('action').value;if(!action)throw Error('Escolha a acção.');const data={};let body={action,revision:0,data,reason:$('reason').value};if(action==='prepare'){const key=$('ruleKey').value;if(!/^[A-Z0-9_-]{3,80}$/.test(key))throw Error('Indique uma chave válida.');const stream=await api('/api/fiscal-rules?'+new URLSearchParams({view:'history',rule_key:key,per_page:25}));if(stream.status!==200)throw Error(stream.value.message??'Versão da regra não conferida.');body.rule_key=key;body.revision=stream.value.stream_revision;const template=$('template').value,c=state.fixtures.cases.find(c=>c.fixture_id===template);if(c)body.data={...c.definition,source_sha256:state.fixtures.sha256};else if(template==='pending')body.data={label:$('metadataLabel').value,family:$('metadataFamily').value,operation:'metadata_only',conditions:$('conditions').value,currency:null,unit:'none',precision:2,rounding:'DOWN',priority:0,source_reference:$('sourceReference').value,source_sha256:null,source_status:'pending',valid_from:null,valid_until:null,parameters:{},expression:{}};else throw Error('Escolha a definição explicitamente.');}
else if(action==='calculate_document')body.data={rule_event_id:selected?.id??null,currency:$('documentCurrency').value,date:$('documentOn').value,price_mode:$('documentMode').value,lines:[...$('documentLines').children].map(row=>({reference:$('doc-reference-'+row.dataset.rowId).value,quantity:$('doc-quantity-'+row.dataset.rowId).value,unit_price:$('doc-price-'+row.dataset.rowId).value}))};else if(action==='calculate')body.data={rule_event_id:selected?.id??null,base:$('base').value,date:$('on').value,currency:$('currency').value};else{if(!selected)throw Error('Escolha uma versão no catálogo.');body.rule_key=selected.rule_key;body.revision=selected.current_revision;body.target_id=selected.id;body.data=action==='review'?{decision:$('decision').value}:{};}
operationKey=crypto.randomUUID();const r=await api('/api/fiscal-rules/preview',body,operationKey);if(r.status!==200)throw Error(r.value.message??'Prévia recusada.');preview=r.value;$('referenceKey').value=operationKey;$('impact').replaceChildren();el('h3','Impacto antes da confirmação',$('impact'));const i=preview.impact;if(i.result){if(i.result.price_mode){for(const [k,v]of [['Estado',states[i.result.state]],['Base documental',i.result.base_document??'indeterminada'],['Imposto documental',i.result.tax_document??'indeterminado'],['Total',i.result.total??'indeterminado'],['Diferença da base (fracção)',i.result.base_difference_fraction??'indeterminada'],['Diferença do imposto (fracção)',i.result.tax_difference_fraction??'indeterminada']])el('p',k+': '+v,$('impact'));const table=el('table',null,$('impact')),head=el('thead',null,table),h=el('tr',null,head);for(const title of ['Linha','Base exacta','Imposto exacto','Base documental','Imposto documental','Total','Diferenças base / imposto'])el('th',title,h);const body=el('tbody',null,table);for(const line of i.result.lines){const row=el('tr',null,body);for(const v of [line.reference,line.base_exact_fraction,line.tax_exact_fraction,line.base_document??'indeterminada',line.tax_document,line.total_document??'indeterminado',(line.base_difference_fraction??'indeterminada')+' / '+line.tax_difference_fraction])el('td',v,row);}}else for(const [k,v]of [['Resultado',states[i.result.state]],['Base',i.result.base??body.data.base],['Imposto exacto',i.result.tax_exact??'indeterminado'],['Imposto documental',i.result.tax_document??'indeterminado'],['Diferença preservada',i.result.difference??'indeterminada'],['Total',i.result.total??'indeterminado']])el('p',k+': '+v,$('impact'));for(const p of i.result.pending)el('p',p,$('impact'));if(i.result.steps)el('pre',JSON.stringify(i.result.steps,null,2),$('impact'));}else{el('p','Estado anterior: '+(states[i.before.state]??'sem preparação')+'; posterior: '+states[i.after.state]+' · versão '+i.after.rule_version+' · revisão '+i.after.revision,$('impact'));el('p','Fonte: '+i.definition.source_reference+'; conferida apenas para ensaio artificial: '+(i.source_checked?'sim':'não'),$('impact'));el('p','Vigência: '+(i.definition.valid_from??'desconhecida')+' a '+(i.definition.valid_until??'sem termo'),$('impact'));}el('p','Novos lançamentos: nenhum. Activação legal: não. Documentos existentes: inalterados.',$('impact'));}catch(e){error(e);}finally{preparing=false;locked();}};
async function finish(retry=false){try{const r=await(retry?op.retry():op.run({preview:preview.preview}));preview=null;dirty=false;selected=null;$('selection').textContent='Escolha uma versão para a próxima operação.';$('impact').replaceChildren();$('receipt').textContent=(r.type==='rule'?'Regra revisão '+r.revision:'Cálculo #'+r.id)+' · '+states[r.state]+(r.replayed?' · recuperado sem duplicação':' · registado');fields();await load();}catch(e){error(e);if(!op.pending){preview=null;$('impact').replaceChildren();}}finally{locked();}}
$('recovery').onsubmit=async e=>{e.preventDefault();try{const key=$('referenceKey').value.trim().toLowerCase(),r=await api('/api/fiscal-rules/receipts/'+encodeURIComponent(key));if(r.status!==200)throw Error(r.value.message??'Sem recibo desta função para a referência.');$('receipt').textContent='Recibo histórico '+r.value.result.type+' #'+r.value.result.id+'. Nenhuma operação repetida.';if(op.pending&&key===operationKey)await finish(true);}catch(e){error(e);}};
$('action').onchange=fields;$('operation').oninput=()=>{dirty=true;};$('confirm').onclick=()=>finish();$('retry').onclick=()=>finish(true);$('cancel').onclick=()=>{if(op.pending)return;preview=null;$('impact').replaceChildren();locked();};$('apply').onclick=()=>{page=1;load().catch(error);};$('refresh').onclick=()=>load().catch(error);$('previous').onclick=()=>{page--;load().catch(error);};$('next').onclick=()=>{page++;load().catch(error);};window.addEventListener('beforeunload',e=>{if(dirty||preview||op.pending){e.preventDefault();e.returnValue='';}});load().catch(error);