|
@@ -0,0 +1,40 @@
|
|
|
|
|
+import {test} from 'node:test';import assert from 'node:assert/strict';import fs from 'node:fs';import vm from 'node:vm';import {execFileSync} from 'node:child_process';import {PGlite} from '@electric-sql/pglite';
|
|
|
|
|
+const deploy=process.env.STATS_SOURCE_REV?execFileSync('git',['show',process.env.STATS_SOURCE_REV+':scripts/deploy-admin-functions.mjs'],{encoding:'utf8',maxBuffer:10*1024*1024}):fs.readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8');const start=deploy.indexOf('async function orderStatistics('),fn=deploy.slice(start,deploy.indexOf('\nfunction appOverviewDateKey',start));
|
|
|
|
|
+test('homepage counts completed lesson records even while appointments retain pending states; mixed and trial durations use actual minutes without writes',async()=>{
|
|
|
|
|
+ const db=new PGlite();try{await db.exec(fs.readFileSync(new URL('./fixtures/lesson-records-schema.sql',import.meta.url),'utf8')+';ALTER TABLE "CourseAppointment" ADD dszt text;');await db.exec(`CREATE FUNCTION xiaoshu_safe_shanghai_timestamp(text) RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$SELECT NULL::timestamptz$$;CREATE TABLE "PracticeRecord"(company text,yhid text);INSERT INTO "PracticeRecord" VALUES('co','20');ALTER TABLE "CourseAppointment" ADD dslx text;
|
|
|
|
|
+ INSERT INTO "LessonRecord"("objectId",company,id,xymz,kclx,yyds) VALUES('a','co',1,'20','2','101'),('b','co',2,'20','1','102'),('trial','co',3,'20','3','103'),('duplicate','co',4,'20','2','101'),('void','co',5,'20','2','104'),('foreign','other',6,'20','2','105');
|
|
|
|
|
+ INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId",status) VALUES('ca','co',54,11,101,99),('cb','co',54,12,102,99),('ct','co',54,13,103,99),('cv','co',59,5,104,-2);
|
|
|
|
|
+ INSERT INTO "CourseAppointment"("objectId",company,id,szyh,"durationMinutes",dszt,dslx) VALUES('pa','co',11,'20',60,'0','2'),('pb','co',12,'20',30,'10','1'),('pt','co',13,'20',30,'30','3');`);
|
|
|
|
|
+ const calls=[],query=async(s,p)=>{assert.match(s,/^\s*(SELECT|WITH)\b/);calls.push(s);return(await db.query(s,p)).rows};const ctx=vm.createContext({DEFAULT_COMPANY_ID:'co',number:(v,d=0)=>Number.isFinite(Number(v))?Number(v):d,authorizeRequestedUser:async()=>20,ownLegacyId:()=>20,Psql:{query,one:async(s,p)=>(await query(s,p))[0]}});
|
|
|
|
|
+ vm.runInContext(fs.readFileSync(new URL('../cloud/student-lesson-statistics.js',import.meta.url),'utf8')+'\n'+fn,ctx);
|
|
|
|
|
+ const before=(await db.query('SELECT * FROM "CourseAppointment" ORDER BY id')).rows;
|
|
|
|
|
+ const r=await ctx.orderStatistics({uid:20},{get:k=>k==='legacyGroupId'?1:undefined});
|
|
|
|
|
+ assert.equal(r.t_shichang,'2');assert.equal(r.t30,'1');assert.equal(r.t60,'1');assert.equal(r.t_tiyan,'1');assert.equal(r.t_total,'1');assert.deepEqual((await db.query('SELECT * FROM "CourseAppointment" ORDER BY id')).rows,before);
|
|
|
|
|
+ }finally{await db.close();}
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('18 one-hour completed lessons plus a one-hour trial produce 19 hours independently of stale appointment states',async()=>{
|
|
|
|
|
+ const db=new PGlite();try{await db.exec(fs.readFileSync(new URL('./fixtures/lesson-records-schema.sql',import.meta.url),'utf8')+';ALTER TABLE "CourseAppointment" ADD dszt text;');await db.exec(`CREATE FUNCTION xiaoshu_safe_shanghai_timestamp(text) RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$SELECT NULL::timestamptz$$;INSERT INTO "LessonRecord"("objectId",company,id,xymz,kclx,yyds) SELECT 'lesson'||i,'co',i,'1850',CASE WHEN i=19 THEN '3' ELSE '2' END,(100+i)::text FROM generate_series(1,19)i;`);const ctx=vm.createContext({Psql:{query:async(s,p)=>(await db.query(s,p)).rows}});vm.runInContext(fs.readFileSync(new URL('../cloud/student-lesson-statistics.js',import.meta.url),'utf8'),ctx);const r=await ctx.studentLessonStatistics('co',1850);assert.equal(r.total,19);assert.equal(r.t60,18);assert.equal(r.tiyan,1);assert.equal(Number(r.totalMinutes),1140);}finally{await db.close();}
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('unresolved durations and ended appointments without lesson facts are unknown; empty new accounts are zero',async()=>{
|
|
|
|
|
+ const db=new PGlite();try{await db.exec(fs.readFileSync(new URL('./fixtures/lesson-records-schema.sql',import.meta.url),'utf8')+';ALTER TABLE "CourseAppointment" ADD dszt text;');await db.exec(`CREATE FUNCTION xiaoshu_safe_shanghai_timestamp(text) RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$SELECT NULL::timestamptz$$;
|
|
|
|
|
+ INSERT INTO "LessonRecord"("objectId",company,id,xymz,kclx,yyds) VALUES('unknown','co',1,'20','4','100');INSERT INTO "CourseAppointment"("objectId",company,szyh,dszt) VALUES('no-facts','co','21','30');`);const ctx=vm.createContext({Psql:{query:async(s,p)=>(await db.query(s,p)).rows}});vm.runInContext(fs.readFileSync(new URL('../cloud/student-lesson-statistics.js',import.meta.url),'utf8'),ctx);
|
|
|
|
|
+ assert.equal((await ctx.studentLessonStatistics('co',20)).totalMinutes,null);assert.equal((await ctx.studentLessonStatistics('co',21)).totalMinutes,null);assert.equal((await ctx.studentLessonStatistics('co',22)).totalMinutes,0);
|
|
|
|
|
+ }finally{await db.close();}
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('same-student completion report outranks reservation duration; foreign reports and invalid numbers cannot inflate hours',async()=>{
|
|
|
|
|
+ const db=new PGlite();try{await db.exec(fs.readFileSync(new URL('./fixtures/lesson-records-schema.sql',import.meta.url),'utf8')+';ALTER TABLE "CourseAppointment" ADD dszt text;');await db.exec(`CREATE FUNCTION xiaoshu_safe_shanghai_timestamp(text) RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$SELECT NULL::timestamptz$$;
|
|
|
|
|
+ INSERT INTO "LessonRecord"("objectId",company,id,xymz,kclx,yyds) VALUES('a','co',1,'20','2','101'),('b','co',2,'20','1','102'),('c','co',3,'20','4','103');
|
|
|
|
|
+ INSERT INTO "CourseAppointment"("objectId",company,id,szyh,"durationMinutes") VALUES('pa','co',11,'20',60),('pb','co',12,'99',60);
|
|
|
|
|
+ INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId") VALUES('ac','co',54,11,101),('bc','co',54,12,102);
|
|
|
|
|
+ INSERT INTO "LessonCompletionReport"("objectId",company,"lessonId","studentId","appointmentGeneralId","durationMinutes") VALUES('valid','co','a',20,101,30),('foreign','co','b',99,102,60),('invalid','co','c',20,103,'NaN');`);const ctx=vm.createContext({Psql:{query:async(s,p)=>(await db.query(s,p)).rows}});vm.runInContext(fs.readFileSync(new URL('../cloud/student-lesson-statistics.js',import.meta.url),'utf8'),ctx);const r=await ctx.studentLessonStatistics('co',20);assert.equal(r.t30,2);assert.equal(r.unknownDurationCount,1);assert.equal(r.totalMinutes,null);
|
|
|
|
|
+ }finally{await db.close();}
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+test('partially missing lesson facts remain unknown while cancelled appointments do not count as missing',async()=>{
|
|
|
|
|
+ const db=new PGlite();try{await db.exec(fs.readFileSync(new URL('./fixtures/lesson-records-schema.sql',import.meta.url),'utf8')+';ALTER TABLE "CourseAppointment" ADD dszt text;');await db.exec(`CREATE FUNCTION xiaoshu_safe_shanghai_timestamp(text) RETURNS timestamptz LANGUAGE sql IMMUTABLE AS $$SELECT NULL::timestamptz$$;
|
|
|
|
|
+ INSERT INTO "LessonRecord"("objectId",company,id,xymz,kclx,yyds) VALUES('a','co',1,'20','2','101');INSERT INTO "CourseAppointment"("objectId",company,id,szyh,dszt) VALUES('a','co',11,'20','30'),('b','co',12,'20','30'),('c','co',13,'20','30');INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId",status) VALUES('a','co',54,11,101,99),('b','co',54,12,102,99),('c','co',54,13,103,-2);`);const ctx=vm.createContext({Psql:{query:async(s,p)=>(await db.query(s,p)).rows}});vm.runInContext(fs.readFileSync(new URL('../cloud/student-lesson-statistics.js',import.meta.url),'utf8'),ctx);const r=await ctx.studentLessonStatistics('co',20);assert.equal(r.total,1);assert.equal(r.unknownDurationCount,1);assert.equal(r.totalMinutes,null);
|
|
|
|
|
+ }finally{await db.close();}
|
|
|
|
|
+});
|