payroll-preview-cache.test.mjs 4.4 KB

123456789101112131415161718192021
  1. import {test} from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';
  2. const source=fs.readFileSync(new URL('../cloud/payroll-integrity.js',import.meta.url),'utf8');
  3. function setup(){const calls=[],c=vm.createContext({Date,Psql:{one:async(sql,args)=>{calls.push({sql,args});return sql.includes('_read')?{result:{stamp:'current',payload:null}}:{saved:true};}}});vm.runInContext(source,c);c.text=v=>String(v||'');c.number=v=>Number(v)||0;c.payrollTenant=()=> 'co';let count=0;c.payrollPreviewUncached=async()=>{count++;return{payableAmount:123,sourceStatus:{sync:{state:'batch_verified',settlementReady:false}}};};return{c,calls,count:()=>count};}
  4. test('commands and require-live callers cannot receive a cached approval',async()=>{const {c,calls,count}=setup();for(const [used,live,targetId]of[[false,false,''],[true,true,''],[true,false,'batch']])await c.payrollPreviewData({}, {targetId},'2026-11',used,live);assert.equal(count(),3);assert.equal(calls.length,0);});
  5. test('cache miss computes authoritative output and publishes with original dependency stamp',async()=>{const {c,calls,count}=setup();const r=await c.payrollPreviewData({storeId:12},{storeId:99,search:'a'},'2026-11',true);assert.equal(r.payableAmount,123);assert.equal(count(),1);assert.equal(calls.length,2);assert.equal(calls[1].args[2],'current');assert.match(calls[0].args[1],/12,99,"a",false,false/);});
  6. test('cached hit preserves amounts and denied settlement; roles are checked before reading cache',async()=>{const {c,count}=setup();c.Psql.one=async()=>({result:{stamp:'current',payload:{payableAmount:123,sourceStatus:{sync:{state:'batch_verified',settlementReady:false}}}}});const r=await c.payrollPreviewData({}, {},'2026-11',true);assert.equal(r.payableAmount,123);assert.equal(r.sourceStatus.sync.settlementReady,false);assert.equal(r.sourceStatus.cacheHit,true);assert.equal(count(),0);c.payrollTenant=()=>{throw Error('denied');};await assert.rejects(c.payrollPreviewData({}, {},'2026-11',true),/denied/);});
  7. test('cache error falls back; failed verification is never cached',async()=>{const {c,calls,count}=setup();c.Psql.one=async()=>{throw Error('database missing helper');};assert.equal((await c.payrollPreviewData({}, {},'2026-11',true)).payableAmount,123);assert.equal(count(),1);const other=setup();other.c.payrollPreviewUncached=async()=>({sourceStatus:{sync:{state:'error'}}});await other.c.payrollPreviewData({}, {},'2026-11',true);assert.equal(other.calls.length,1);});
  8. test('summary transport preserves every verified total; coach details preserve scope and exceptions',async()=>{
  9. const {c}=setup(),lines=[{coachId:1,storeId:10,amount:20},{coachId:2,storeId:20,amount:30,exception:'needs review'}];
  10. c.businessDate=()=> '2026-11-10';c.payrollPreviewData=async()=>({lines,sourceStatus:{sync:{state:'confirmed',settlementReady:true}}});c.payrollPage=async()=>({items:[]});c.storeNameOf=()=> 'store';c.Psql.query=async()=>[];
  11. c.payrollResult=(month,rows,status,input)=>{const visible=rows.filter(l=>!input.storeId||l.storeId===input.storeId);return{month,lines:visible,exceptions:visible.filter(l=>l.exception),coachItems:visible.map(l=>({coachId:l.coachId,amount:l.amount})),payableAmount:visible.reduce((a,l)=>a+l.amount,0),exceptionCount:visible.filter(l=>l.exception).length,sourceStatus:status};};
  12. const full=await c.payrollOverviewData({},{}),summary=await c.payrollOverviewData({},{summaryOnly:true}),detail=await c.payrollOverviewData({},{lineCoachId:2});
  13. const withoutRows=r=>JSON.parse(JSON.stringify({...r,lines:[],exceptions:[]}));assert.deepEqual(withoutRows(full),withoutRows(summary));assert.deepEqual(withoutRows(full),withoutRows(detail));assert.equal(summary.lines.length,0);assert.equal(detail.lines.length,1);assert.equal(detail.exceptions.length,1);assert.equal(detail.lines[0].coachId,2);
  14. const scoped=await c.payrollOverviewData({storeId:10,store:{toJSON:()=>({})}},{storeId:20,lineCoachId:2});assert.equal(scoped.lines.length,0);assert.equal(scoped.payableAmount,20);
  15. });
  16. test('export rejects presentation-only projections even when caller forwards overview flags',async()=>{
  17. const {c}=setup();let seen;c.payrollOverviewData=async(_context,input)=>{seen=input;return{month:'2026-11',lines:[],sourceStatus:{}};};c.payrollVisibleLines=rows=>rows;c.payrollResult=()=>({coachItems:[]});
  18. await c.payrollExportData({},{summaryOnly:true,lineCoachId:9,coachId:2});assert.equal(seen.summaryOnly,false);assert.equal(seen.lineCoachId,0);assert.equal(seen.coachId,2);
  19. });