Files
SGF/scripts/sgf-hr-personnel-http.mjs
2026-11-11 15:04:01 +02:00

16 lines
10 KiB
JavaScript

import fs from'node:fs';import assert from'node:assert/strict';import crypto from'node:crypto';import{session}from'./sgf-single-http.mjs';
const root=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json')).root+'/private/receipt-composition-cc50a276c10c',file=root+'/hp-http-progress-private.json',progress=fs.existsSync(file)?JSON.parse(fs.readFileSync(file)):{keys:Object.fromEntries(['contract','day9','day10','entitlement','leave','report'].map(k=>[k,crypto.randomUUID()])),steps:{},checks:[]},save=()=>fs.writeFileSync(file,JSON.stringify(progress)),checks=[],ok=(v,n)=>{assert(v,n);checks.push(n);if(!progress.checks.includes(n)){progress.checks.push(n);save();}};save();
const prep=await session('hr:hr_prepare','human_resources'),review=await session('hr:hr_review','human_resources'),reader=await session('hr:hr_read','human_resources');for(const[role,s,count]of[['prepare',prep,10],['review',review,10],['read',reader,6]]){ok(s.identity.context.modules.map(m=>m.id).join(',')==='human_resources','only HR for '+role);ok(s.identity.context.grants.length===count,'exact approved total grant count '+role);for(const area of['contracts','attendance','leave'])ok(s.identity.context.grants.some(g=>g.permission==='human_resources.'+area+'.view'&&g.unit_id===2),'own scoped '+area+' view '+role);ok(s.identity.context.hr_personnel_enabled===true,'personnel available '+role);}
const get=async(q,s=reader)=>{const r=await s.get('/api/hr-personnel?'+new URLSearchParams(q));assert.equal(r.status,200);return r.json();};let r=await reader.get('/api/remunerations?kind=worker&per_page=100');assert.equal(r.status,200);const workers=await r.json(),worker=workers.records.data.find(r=>r.data.input.code==='QA-COLABORADOR-01'&&r.effective);assert(worker);const wk=worker.record_key;ok(progress.worker_key===undefined||progress.worker_key===wk,'existing worker chosen explicitly exact');progress.worker_key=wk;save();
const input=(kind,key,version,data,action='prepare')=>({kind,record_key:key,version,action,reason:'Conferência individual QA de pessoal, sem efeito financeiro ou documento legal.',data});
async function run(step,s,body){let p=progress.steps[step];if(p?.result){assert.deepEqual(p.body,body);ok(true,'accepted decision reused '+step);return p.result;}p??={key:crypto.randomUUID(),body};assert.deepEqual(p.body,body);progress.steps[step]=p;save();const receipt=await s.get('/api/hr-personnel/receipts/'+p.key);if(receipt.status===200){p.result=(await receipt.json()).result;save();ok(true,'uncertain result recovered by receipt '+step);return p.result;}assert.equal(receipt.status,404);const preview=await s.command('/api/hr-personnel/preview',body,p.key);assert.equal(preview.status,200,JSON.stringify(preview.value));ok(preview.value.impact.journals===0&&preview.value.impact.payments===0&&!preview.value.impact.debt_created,'preview no financial effects '+step);p.preview=preview.value.preview;save();const out=await s.command('/api/hr-personnel/confirm',{preview:p.preview},p.key);assert.equal(out.status,200,JSON.stringify(out.value));p.result=out.value;save();ok(out.value.version===body.version+1&&out.value.state===(body.action==='prepare'?'prepared':'approved'),'persisted individual version '+step);const got=await s.get('/api/hr-personnel/receipts/'+p.key);assert.equal(got.status,200);ok((await got.json()).result.id===out.value.id,'own receipt '+step);return out.value;}
const ranges=[{from:480,to:720},{from:780,to:1020}],evidence='Exercício fictício de RH, sem contrato legal ou efeito salarial.',ck=progress.keys.contract,dk9=progress.keys.day9,dk10=progress.keys.day10,ek=progress.keys.entitlement,lk=progress.keys.leave,rk=progress.keys.report;
const contract={operation:'establish',worker_key:wk,reference:'QA-CONTRATO-01',mode:'fixed',valid_from:'2026-11-01',valid_to:'2026-12-31',gross:'10000.00',evidence},day=(date,actual,conferred)=>({operation:'establish',worker_key:wk,contract_key:ck,date,reference_on:'2026-11-11',planned:ranges,actual,leave_conferred:conferred,evidence}),ent={operation:'establish',worker_key:wk,contract_key:ck,valid_from:'2026-11-01',valid_to:'2026-12-31',minutes:960,evidence},leave={operation:'establish',worker_key:wk,contract_key:ck,entitlement_key:ek,day_key:dk10,date:'2026-11-10',reference_on:'2026-11-11',intervals:ranges,evidence};
for(const[name,kind,key,data,version]of[['contract','contract',ck,contract,0],['day9','day',dk9,day('2026-11-09',ranges,true),0],['day10','day',dk10,day('2026-11-10',null,false),0],['entitlement','entitlement',ek,ent,0],['leave','leave',lk,leave,0],['take','leave',lk,{...leave,operation:'take'},2],['day10-conference','day',dk10,{...day('2026-11-10',[],true),operation:'amend'},2],['report','report',rk,{operation:'establish',worker_key:wk,period:'2026-11',evidence},0]]){await run(name+'-prepare',prep,input(kind,key,version,data));await run(name+'-review',review,input(kind,key,version+1,{decision:'approved'},'review'));}
const list=await get({kind:'report',worker_key:wk,page:1,per_page:25}),row=list.records.data.find(r=>r.record_key===rk);assert(row);const result=row.data.result;ok(result.known_days===2&&result.known_conferred_days===2&&result.other_calendar_days_unknown===28&&!result.complete_month_proven,'actual two conferred days coverage partial month');const balance=result.leave_balances[0];ok(balance.entitlement_minutes===960&&balance.reserved_minutes===0&&balance.taken_minutes===480&&balance.available_minutes===480,'actual960 budget480taken480available no duplication');ok(result.days.every(d=>d.calculation.planned_minutes===480&&d.calculation.unaccounted_minutes===0),'two actual planned480 days no unaccounted after explicit conference');ok(result.days.find(d=>d.calculation.date==='2026-11-09').calculation.declared_minutes===480&&result.days.find(d=>d.calculation.date==='2026-11-10').calculation.declared_minutes===0,'actual work9 and explicit no work10 distinct');ok(result.payroll.length===1&&!result.payroll[0].contract_link_proven&&result.payroll[0].net_cents==='890000','original payroll not retrospectively contract linked or recalculated');
const large=await get({kind:'report',worker_key:wk,page:1,per_page:100});ok(JSON.stringify(large.records.data)===JSON.stringify(list.records.data),'real report25/100 parity');ok((await get({kind:'day',worker_key:wk,period:'2026-11',state:'approved',page:1,per_page:25})).records.total===2,'day person month state filters combined SQL');ok((await get({kind:'day',worker_key:wk,period:'2026-10',page:1,per_page:25})).records.total===0,'other month no fallback');ok((await get({kind:'contract',worker_key:wk,search:'QA-CONTRATO-01',page:1,per_page:25})).records.total===1,'exact contract reference search');ok((await get({kind:'contract',worker_key:wk,page:100000,per_page:25})).records.data.length===0,'far page no fallback');const hist=await get({kind:'leave',worker_key:wk,record_key:lk,page:1,per_page:25});ok(hist.records.data.map(r=>r.version).join(',')==='4,3,2,1','leave reserve and take history four immutable versions');const dh=await get({kind:'day',worker_key:wk,record_key:dk10,page:1,per_page:25});ok(dh.records.data.length===4&&dh.records.data.at(-1).data.input.actual===null&&Array.isArray(dh.records.data[0].data.input.actual),'unknown original day preserved after actual empty conference');
r=await reader.get('/api/hr-personnel/exports/'+row.id);assert.equal(r.status,200);const bytes=Buffer.from(await r.arrayBuffer());ok(/text\/csv/.test(r.headers.get('content-type'))&&r.headers.get('x-content-sha256')===row.data.artifact_sha256&&crypto.createHash('sha256').update(bytes).digest('hex')===row.data.artifact_sha256,'frozen period artifact MIME header and exact SHA');ok(bytes.toString().includes('NAO E DOCUMENTO LEGAL'),'export boundary explicit');fs.writeFileSync(root+'/hp-frozen-period-private.csv',bytes);
r=await reader.get('/api/remunerations?kind=payroll&per_page=25');assert.equal(r.status,200);const original=(await r.json()).records.data[0];r=await reader.get('/api/remunerations/exports/'+original.id);assert.equal(r.status,200);ok(Buffer.from(await r.arrayBuffer()).equals(fs.readFileSync(root+'/hr-frozen-export-private.csv')),'old salary frozen CSV byte-for-byte unchanged');
const denied=await reader.command('/api/hr-personnel/preview',input('contract',ck,2,{...contract,operation:'amend'}));ok(denied.status===403,'reader cannot prepare');const own=await prep.command('/api/hr-personnel/preview',input('contract',ck,2,{decision:'approved'},'review'));ok(own.status===403,'preparer cannot review');for(const q of[{kind:'contract',tenant_id:2},{kind:'day',per_page:26},{kind:'report',record_key:crypto.randomUUID()}]){r=await reader.get('/api/hr-personnel?'+new URLSearchParams(q));ok(r.status===(q.record_key?403:422),'invalid or foreign query refused '+JSON.stringify(q));}r=await reader.get('/api/hr-personnel/exports/2147483647');ok(r.status===403,'foreign export refused');r=await review.get('/api/hr-personnel/receipts/'+progress.steps['contract-prepare'].key);ok(r.status===404,'other actor receipt refused');const finance=await session('sgf:finance_prepare','finance');r=await finance.get('/api/hr-personnel?kind=contract');ok([403,409].includes(r.status),'finance cannot consult personnel');r=await reader.get('/api/cash');ok([403,409].includes(r.status),'RH no financial inheritance');r=await fetch('http://127.0.0.1:8085/api/hr-personnel?kind=contract',{headers:{Accept:'application/json'}});ok(r.status===401,'unauthenticated access refused');r=await reader.get('/api/hr-personnel/confirm',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({preview:'invalid'})});ok(r.status===419,'CSRF enforced');
const manifest=JSON.parse(fs.readFileSync(root+'/app/public/build/manifest.json'));for(const e of Object.values(manifest).filter(v=>v.isEntry||/PersonnelWorkspace|RemunerationsWorkspace|statement-download/.test(v.file))){r=await fetch('http://127.0.0.1:8085/build/'+e.file);const b=Buffer.from(await r.arrayBuffer());ok(r.status===200&&/javascript/.test(r.headers.get('content-type'))&&crypto.createHash('sha256').update(b).digest('hex')===crypto.createHash('sha256').update(fs.readFileSync(root+'/app/public/build/'+e.file)).digest('hex'),'servedJS MIME SHA '+e.file);}const output=root+'/hp-http-20261111.json';fs.writeFileSync(output,JSON.stringify({passed:true,checks,individual_commands:16,new_salary_versions:0,new_payments:0,new_journals:0,browser_visual_review:false},null,2));console.log(JSON.stringify({passed:true,checks:checks.length,proof:output}));