teacher-earned-months.test.mjs 11 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import {test,before,after} from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {webcrypto} from 'node:crypto';import {PGlite} from '@electric-sql/pglite';import {teacherEarningsSupport} from '../teacher-earnings-transform.mjs';
  2. const read=p=>fs.readFileSync(new URL(p,import.meta.url),'utf8'),gateway=read('../deploy-admin-functions.mjs').match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1],support=teacherEarningsSupport(gateway,read('../cloud/payroll-integrity.js'));
  3. const currentCode=read('../cloud/teacher-earnings.js'),oldCode=read('./fixtures/teacher-earnings-monthly-baseline.js');
  4. const db=new PGlite();before(()=>db.exec(read('../sql/teacher-earned-months.sql')));after(()=>db.close());
  5. const clone=v=>JSON.parse(JSON.stringify(v));
  6. const lesson=(id,date,pay=40,extra={})=>({objectId:id,lessonObjectId:id,standardLessonKey:'lesson:'+id,jsmz:'30',xymz:'20',kclx:'2',lessonAt:date,courseCategoryKey:'word',teacherPaySnapshot:pay,commonStatus:99,...extra});
  7. function harness({company='co',rows=[],rules=[],rates=[],mappings=[],old=false,crypto=true,index=true,code=support,store=true}={}){
  8. const state={rows:clone(rows),rules:clone(rules),rates:clone(rates),mappings:clone(mappings),review:2.5},calls=[];
  9. const c=vm.createContext({Date,Promise,TextEncoder,Uint8Array,crypto:crypto?webcrypto:undefined,calcCalls:0,DEFAULT_COMPANY_ID:company});
  10. const queryFor=snapshot=>async(sql,args)=>{calls.push({sql,args});if(sql.includes('xs_payroll_sources'))return clone(snapshot.rows.filter(r=>String(r.coachId??r.jsmz)==args[1]&&(!args[2]||args[2].includes(String(r.appointmentGeneralId??r.yyds))))).map(s=>({s}));if(sql.includes('"CoachPayRate"'))return clone(snapshot.rates);if(sql.includes('"TeachingCompensationRule"'))return clone(snapshot.rules);if(sql.startsWith('WITH RECURSIVE'))return clone(snapshot.mappings);if(sql.includes('LessonCompletionReport'))return[];throw Error('Unexpected SQL '+sql);};
  11. const one=async(sql,args,inSnapshot=false)=>{calls.push({sql,args,inSnapshot});if(sql.includes('to_regprocedure'))return{available:sql.includes('xs_teacher_earned_month_read')?store:index};if(!store)throw Error('monthly helpers unavailable');return(await db.query(sql,args)).rows[0];};
  12. c.Psql={query:(s,a)=>queryFor(state)(s,a),one,tx:async run=>{const fresh=clone(state);let ready=false;return run({none:async sql=>{assert.equal(sql,'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');ready=true;calls.push({sql});},one:async(s,a)=>{assert.equal(ready,true);return one(s,a,true);},query:async(s,a)=>{assert.equal(ready,true);return queryFor(fresh)(s,a);}});}};
  13. vm.runInContext(code.replace('calculate:payrollCalculatedLine','calculate:(...args)=>{calcCalls++;return payrollCalculatedLine(...args)}')+'\n'+(old?oldCode:currentCode),c);
  14. c.reviewIncomeTotal=async()=>state.review;
  15. return{c,state,calls,monthly:()=>c.teacherMonthlyLessonEarnings(company,30),summary:()=>c.teacherIncomeSummary(company,30)};
  16. }
  17. function baseline(h){const b=harness({old:true,rows:h.state.rows,rates:h.state.rates,rules:h.state.rules,mappings:h.state.mappings});b.state.review=h.state.review;return b.summary();}
  18. test('unchanged historical months reuse exactly; a changed month alone recalculates and reviews stay live',async()=>{
  19. const h=harness({company:'reuse',rows:[lesson('old','2026-09-10T00:00:00Z',40.01),lesson('current','2026-11-10T00:00:00Z',0),lesson('unknown',null,40)]});
  20. assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));assert.equal(h.c.calcCalls,3);assert.equal((await h.monthly()).monthlyReuse.matched,3);assert.equal(h.c.calcCalls,3);
  21. h.state.rows[1].teacherPaySnapshot=45.17;const changed=await h.monthly();assert.deepEqual(clone(changed.monthlyReuse),{available:true,matched:2,computed:1});assert.equal(h.c.calcCalls,4);assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));
  22. h.state.review=-.5;const correction=await h.summary();assert.equal(correction.reviewIncome,-.5);assert.deepEqual(clone(correction),clone(await baseline(h)));assert.equal(h.c.calcCalls,4);
  23. assert.ok(h.calls.filter(call=>call.sql.startsWith('SELECT xs_teacher_earned_month_read')).every(call=>call.inSnapshot===true));
  24. assert.ok(h.calls.filter(call=>call.sql.startsWith('SELECT xs_teacher_earned_month_write')).every(call=>call.inSnapshot===false));
  25. });
  26. test('cross-month duplicates preserve global first representative; move/delete/empty months cannot retain phantom income',async()=>{
  27. const h=harness({company:'moves',rows:[lesson('first','2026-09-30T15:59:59.999Z',40,{standardLessonKey:'duplicate'}),lesson('second','2026-11-01T00:00:00Z',40,{standardLessonKey:'duplicate'}),lesson('free','2026-10-31T16:00:00Z',0),lesson('cancelled','2026-11-02T00:00:00Z',99,{commonStatus:-2})]});
  28. assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));const initial=await h.monthly();assert.equal(initial.pendingPayCount,1);assert.equal(initial.monthlyReuse.matched,2);
  29. h.state.rows[1].teacherPaySnapshot=60;assert.equal((await h.monthly()).monthlyReuse.computed,1,'duplicate outside representative month still invalidates that group');
  30. h.state.rows.splice(0,1);assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));h.state.rows[0].lessonAt='2026-12-01T00:00:00Z';assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));
  31. h.state.rows=[];assert.deepEqual(clone(await h.summary()),{lessonIncome:0,reviewIncome:2.5,totalIncome:2.5,pendingPayCount:0});assert.equal((await h.monthly()).monthlyReuse.matched,0);
  32. });
  33. test('rules, mapping input and complete calculator closure invalidate every affected cached calculation',async()=>{
  34. const rows=[lesson('early','2026-08-01T00:00:00Z',null),lesson('later','2026-11-01T00:00:00Z',20)],h=harness({company:'policy',rows});await h.summary();assert.equal((await h.monthly()).monthlyReuse.matched,2);
  35. h.state.rates=[{classType:2,effectiveFrom:'1970-01-01',amount:50,durationHours:1,active:true}];assert.equal((await h.monthly()).monthlyReuse.computed,2);assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));
  36. h.state.mappings=[{courseId:99,confirmed:false,unknownPolicyField:'new'}];assert.equal((await h.monthly()).monthlyReuse.computed,2);
  37. // Alter a private default helper, while calculate's own function text stays unchanged.
  38. const changedSupport=support.replace("amount:40,durationHours:1,effectiveFrom:'1970-01-01'","amount:41,durationHours:1,effectiveFrom:'1970-01-01'");assert.notEqual(changedSupport,support);
  39. const other=harness({company:'policy',rows:h.state.rows,rates:h.state.rates,mappings:h.state.mappings,code:changedSupport});assert.equal((await other.monthly()).monthlyReuse.computed,2);
  40. });
  41. test('details bypass monthly summaries and preserve appointment filtering; missing index and crypto have explicit exact fallbacks',async()=>{
  42. const h=harness({company:'details',index:false,rows:[lesson('a','2026-11-01T00:00:00Z',40,{yyds:1}),lesson('b','2026-10-01T00:00:00Z',50,{yyds:2})]});
  43. const actual=await h.c.teacherLessonEarnings('details',30,['2']),before=harness({old:true,rows:h.state.rows});assert.deepEqual(clone(actual),clone(await before.c.teacherLessonEarnings('co',30,['2'])));assert.equal(h.calls.some(call=>call.sql.includes('xs_teacher_earned_month')),false);assert.ok(h.calls.some(call=>call.sql.startsWith('SELECT s FROM xs_payroll_sources(')));
  44. const noCrypto=harness({company:'nocrypto',rows:h.state.rows,crypto:false});assert.equal((await noCrypto.monthly()).monthlyReuse.available,false);assert.deepEqual(clone(await noCrypto.summary()),clone(await baseline(noCrypto)));assert.equal(noCrypto.calls.some(call=>call.sql.includes('xs_teacher_earned_month')),false);
  45. const noStore=harness({company:'nostore',rows:h.state.rows,store:false});assert.equal((await noStore.monthly()).monthlyReuse.available,false);assert.deepEqual(clone(await noStore.summary()),clone(await baseline(noStore)));
  46. });
  47. test('SQL derived summaries reject direct writes, enforce numeric fields, isolate tenants and reject stale CAS overwrites',async()=>{
  48. const sig='a'.repeat(64),next='b'.repeat(64),entry={month:'2026-11',signature:sig,amountCents:100,pendingPayCount:0,lineCount:1,expectedSignature:null};
  49. const write=async(co,entries)=>(await db.query('SELECT xs_teacher_earned_month_write($1,30,$2::jsonb) n',[co,JSON.stringify(entries)])).rows[0].n;
  50. assert.equal(await write('cas',[entry]),1);assert.equal(await write('cas',[{...entry,signature:next,expectedSignature:sig,amountCents:200}]),1);assert.equal(await write('cas',[{...entry,amountCents:999,expectedSignature:sig}]),0);
  51. const stored=(await db.query('SELECT xs_teacher_earned_month_read($1,30,$2::jsonb) rows',['cas','["2026-11"]'])).rows[0].rows;assert.equal(stored[0].amountCents,200);assert.deepEqual((await db.query('SELECT xs_teacher_earned_month_read($1,30,$2::jsonb) rows',['other','["2026-11"]'])).rows[0].rows,[]);
  52. await assert.rejects(db.exec('UPDATE "TeacherEarnedMonth" SET "amountCents"=999 WHERE company=\'cas\''),/controlled writer/);await assert.rejects(write('cas',[{...entry,amountCents:1.5}]),/invalid teacher monthly/);await assert.rejects(write('cas',[{...entry,month:'2026-13'}]));
  53. });
  54. test('new raw fields invalidate the relevant month; hash or derived write failure leaves amounts exact',async()=>{
  55. const h=harness({company:'failure',rows:[lesson('a','2026-09-01T00:00:00Z',40),lesson('b','2026-10-01T00:00:00Z',50)]});await h.summary();
  56. h.state.rows[0].futureSourceField={value:'changed'};assert.deepEqual(clone((await h.monthly()).monthlyReuse),{available:true,matched:1,computed:1});
  57. const fingerprint=h.c.teacherEarningsFingerprint;let calls=0;h.c.teacherEarningsFingerprint=async value=>{if(++calls>1)throw Error('digest unavailable');return fingerprint(value);};
  58. assert.equal((await h.monthly()).monthlyReuse.available,false);assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));
  59. h.c.teacherEarningsFingerprint=fingerprint;const one=h.c.Psql.one;h.c.Psql.one=async(sql,args)=>{if(sql.includes('xs_teacher_earned_month_write'))throw Error('derived write unavailable');return one(sql,args);};
  60. h.state.rows[0].teacherPaySnapshot=41.99;assert.deepEqual(clone(await h.summary()),clone(await baseline(h)));assert.equal((await h.monthly()).monthlyReuse.computed,1);
  61. });
  62. test('scoped source conservative supersets retain exact SQL ownership, appointment text semantics and original order',async()=>{
  63. await db.exec(`CREATE TABLE scoped_test_sources(ord bigint,raw jsonb);CREATE FUNCTION xs_payroll_source_entries_scoped(text,text,text,text[],boolean) RETURNS TABLE(row_data jsonb,ordinal bigint) LANGUAGE sql STABLE AS $$ SELECT raw,ord FROM scoped_test_sources ORDER BY ord DESC $$;`);
  64. const rows=[{coachId:'30',appointmentGeneralId:'7',tag:'first'},{coachId:'030',appointmentGeneralId:'7',tag:'padded-other'},{coachId:'',jsmz:'30',appointmentGeneralId:'7',tag:'empty-primary'},{jsmz:'30',yyds:'7',tag:'second'},{coachId:'30',appointmentGeneralId:'',yyds:'7',tag:'empty-appointment'},{coachId:'30',appointmentGeneralId:'8',tag:'other-appointment'}];
  65. for(let i=0;i<rows.length;i++)await db.query('INSERT INTO scoped_test_sources VALUES($1,$2)',[i,JSON.stringify(rows[i])]);
  66. const h=harness(),sqlDb={one:async(s,a)=>(await db.query(s,a)).rows[0],query:async(s,a)=>(await db.query(s,a)).rows};
  67. const actual=await h.c.teacherEarningsSourceRows(sqlDb,'scoped',30,['7']);assert.deepEqual(actual.map(row=>row.s.tag),['first','second']);
  68. });