|
|
@@ -1,4 +1,4 @@
|
|
|
-import{test,before,after}from'node:test';import assert from'node:assert/strict';import{readFileSync}from'node:fs';import vm from'node:vm';import{PGlite}from'@electric-sql/pglite';import{legacyAppointmentDefaults}from'../legacy-appointment-transform.mjs';
|
|
|
+import{test,before,after}from'node:test';import assert from'node:assert/strict';import{readFileSync}from'node:fs';import vm from'node:vm';import{PGlite}from'@electric-sql/pglite';import{legacyAppointmentDefaults,legacyWritingStageSupport}from'../legacy-appointment-transform.mjs';
|
|
|
const db=new PGlite(),source=readFileSync(new URL('../deploy-admin-functions.mjs',import.meta.url),'utf8'),ops=source.match(/const operationsGatewayCode = String.raw`([\s\S]*?)\n`;/)[1],ctx=vm.createContext({});vm.runInContext(legacyAppointmentDefaults(ops),ctx);const seeds=JSON.stringify(ctx.legacyAppointmentSeedRules());
|
|
|
const q=async(s,p=[])=>(await db.query(s,p)).rows,one=async(s,p=[])=>(await q(s,p))[0];
|
|
|
const prepare=async(id,actor='teacher')=>(await one('SELECT xs_prepare_legacy_appointment(\'co\',$1,$2,$3::jsonb) r',[id,actor,seeds])).r;
|
|
|
@@ -78,6 +78,38 @@ test('旧体验课可复用已开通的普通词库,不重写词库分类且
|
|
|
const b=await one(`SELECT "categoryKey","enrollmentStatus" FROM "CourseBinding" WHERE "objectId"='binding'`);assert.equal(b.categoryKey,'word');assert.equal(b.enrollmentStatus,'active');
|
|
|
}finally{await db.exec('ROLLBACK');}
|
|
|
});
|
|
|
+test('同词库同时开通体验课和普通课时,旧体验预约优先使用体验课开课',async()=>{
|
|
|
+ await db.exec('BEGIN');try{
|
|
|
+ await db.exec(`UPDATE "_User" SET "legacyUserData"='{"UserExp":3}' WHERE "objectId"='student';
|
|
|
+ UPDATE "CourseBinding" SET "categoryKey"='word',"categoryName"='单词课',"enrollmentStatus"='active' WHERE "objectId"='binding';
|
|
|
+ INSERT INTO "CourseBinding" VALUES('trial-binding','co',2,'20','100','trial','体验课','active','schedule-textbook:trial-binding',now());
|
|
|
+ INSERT INTO "CommonModel"("objectId",company,"modelId","itemId",status) VALUES('trial-binding-common','co',58,2,99);`);
|
|
|
+ const id=await fixture({dslx:'3'});
|
|
|
+ assert.equal((await prepare(id)).bindingId,'trial-binding');
|
|
|
+ assert.equal((await prepare(id)).changed,false);
|
|
|
+ const a=await one('SELECT "bindingId","courseCategoryKey","creditCostsSnapshot" FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)]);
|
|
|
+ assert.equal(a.bindingId,'trial-binding');
|
|
|
+ assert.equal(a.courseCategoryKey,'trial');
|
|
|
+ assert.deepEqual(a.creditCostsSnapshot.map(c=>[c.account,Number(c.amount)]),[['UserExp',1]]);
|
|
|
+ assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,1);
|
|
|
+ const b=await one('SELECT "categoryKey","enrollmentStatus" FROM "CourseBinding" WHERE "objectId"=\'binding\'');
|
|
|
+ assert.deepEqual(b,{categoryKey:'word',enrollmentStatus:'active'});
|
|
|
+ const explicit=await fixture({dslx:'3',bindingId:'binding'});
|
|
|
+ assert.equal((await prepare(explicit)).bindingId,'binding');
|
|
|
+
|
|
|
+ await db.exec(`INSERT INTO "CourseBinding" VALUES('other-trial','co',3,'20','100','trial','体验课','pending','legacy:other-trial',now());
|
|
|
+ INSERT INTO "CommonModel"("objectId",company,"modelId","itemId",status) VALUES('other-trial-common','co',58,3,99);`);
|
|
|
+ const uniqueActive=await fixture({dslx:'3'});
|
|
|
+ assert.equal((await prepare(uniqueActive)).bindingId,'trial-binding');
|
|
|
+
|
|
|
+ await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='active' WHERE "objectId"='other-trial'`);
|
|
|
+ const duplicate=await fixture({dslx:'3'});
|
|
|
+ await db.exec('SAVEPOINT ambiguous_trial');
|
|
|
+ await assert.rejects(prepare(duplicate),/唯一/);
|
|
|
+ await db.exec('ROLLBACK TO SAVEPOINT ambiguous_trial');
|
|
|
+ assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[duplicate])).n,0);
|
|
|
+ }finally{await db.exec('ROLLBACK');}
|
|
|
+});
|
|
|
test('体验课时不足时拒绝预留,且不激活原有开课记录',async()=>{
|
|
|
await db.exec('BEGIN');try{
|
|
|
await db.exec(`UPDATE "_User" SET "legacyUserData"='{"UserExp":0,"UserPoint":30,"Purse":30,"SilverCoin":30}' WHERE "objectId"='student';UPDATE "CourseBinding" SET "categoryKey"=NULL,"enrollmentStatus"=NULL WHERE "objectId"='binding'`);
|
|
|
@@ -95,3 +127,99 @@ test('先准备体验课再准备同教材普通课,各自使用正确课时
|
|
|
assert.equal(a.courseCategoryKey,'word');assert.deepEqual(a.creditCostsSnapshot.map(c=>c.account).sort(),['SilverCoin','UserPoint']);
|
|
|
}finally{await db.exec('ROLLBACK');}
|
|
|
});
|
|
|
+
|
|
|
+test('旧阅读写作开课以一致的初三档案和本学年记录补齐;旧系统已开始的预约不再重复准备',async()=>{
|
|
|
+ await db.exec('BEGIN');
|
|
|
+ try {
|
|
|
+ await db.exec(`ALTER TABLE "CommonModel" ADD title text,ADD subtitle text;ALTER TABLE "_User" ADD "realName" text,ADD nickname text,ADD username text,ADD "legacyGroupId" numeric;
|
|
|
+ ALTER TABLE "CommonModel" ADD "teachingManaged" boolean,ADD "updatedAt" timestamptz;
|
|
|
+ UPDATE "_User" SET "legacyGroupId"=3 WHERE "objectId"='teacher';
|
|
|
+ ALTER TABLE "CourseAppointment" ADD IF NOT EXISTS kssj text,ADD IF NOT EXISTS jssj text;
|
|
|
+ CREATE TABLE IF NOT EXISTS "LessonRecord"(company text,yyds text,pf numeric,pjnr text,pldp text,"createdAt" timestamptz);
|
|
|
+ CREATE TABLE IF NOT EXISTS "LessonReview"(company text,"appointmentGeneralId" text,score numeric,comment text,"submittedAt" timestamptz);
|
|
|
+ CREATE TABLE "LessonCompletionReport"(company text,"appointmentGeneralId" text);
|
|
|
+ INSERT INTO "_User" ("objectId",company,"legacyUserId","legacyUserData",nickname) VALUES('writing-student','co',25,'{"SilverCoin":10,"UserPoint":10}','测试学员初三');`);
|
|
|
+ const install=async (file,names)=>{
|
|
|
+ const sql=readFileSync(new URL('../sql/'+file,import.meta.url),'utf8');
|
|
|
+ for(const name of names)await db.exec(sql.match(new RegExp('CREATE OR REPLACE FUNCTION '+name+'\\([\\s\\S]*?\\$\\$;'))[0]);
|
|
|
+ };
|
|
|
+ await install('member-enrollment.sql',['xs_validate_appointment','xs_start_appointment']);
|
|
|
+ await install('lesson-workflow.sql',['xs_start_appointment_for_actor']);
|
|
|
+ await install('payroll-integrity.sql',['xs_payroll_writing_mapping']);
|
|
|
+ const gateway=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1].replace("const DEFAULT_COMPANY_ID = '7pIbDBJmKx'","const DEFAULT_COMPANY_ID = 'co'");
|
|
|
+ const tx=async fn=>{
|
|
|
+ await db.exec('SAVEPOINT writing_prepare');
|
|
|
+ try { const result=await fn({one,oneOrNone:one,query:q});await db.exec('RELEASE SAVEPOINT writing_prepare');return result; }
|
|
|
+ catch(error){await db.exec('ROLLBACK TO SAVEPOINT writing_prepare');await db.exec('RELEASE SAVEPOINT writing_prepare');throw error;}
|
|
|
+ };
|
|
|
+ let legacyStatus='0', legacyStudent='25', reads=0, oldRows=1;
|
|
|
+ const fetch=async url=>{
|
|
|
+ const params=new URL(url).searchParams;
|
|
|
+ assert.equal(params.get('action'),'content_list');
|
|
|
+ assert.equal(params.get('modelId'),'54');
|
|
|
+ assert.equal(params.get('ids'),order.GeneralID);
|
|
|
+ reads++;
|
|
|
+ const row={GeneralID:order.GeneralID,NodeID:29,bxrq:order.bxrq,szyh:legacyStudent,pl:order.pl,kcid:order.kcid,dslx:order.dslx,jffs:order.jffs,yysj:order.yysj,dszt:legacyStatus};
|
|
|
+ return {ok:true,json:async()=>({retcode:1,result:JSON.stringify(oldRows?[row]:[]),page:{itemCount:oldRows,pageCount:oldRows}})};
|
|
|
+ };
|
|
|
+ const c=vm.createContext({Psql:{one:(sql,args)=>tx(db=>db.one(sql,args)),oneOrNone:one,query:q,tx},fetch,URLSearchParams,AbortSignal});
|
|
|
+ const integrity=readFileSync(new URL('../cloud/payroll-integrity.js',import.meta.url),'utf8');
|
|
|
+ vm.runInContext(gateway+legacyAppointmentDefaults(ops)+legacyWritingStageSupport(integrity)+readFileSync(new URL('../cloud/legacy-appointment.js',import.meta.url),'utf8')+readFileSync(new URL('../cloud/lesson-workflow.js',import.meta.url),'utf8'),c);
|
|
|
+ vm.runInContext(readFileSync(new URL('../cloud/member-enrollment-app.js',import.meta.url),'utf8'),c);
|
|
|
+ c.appLegacyConfig=async()=>({url:'https://legacy.test/api',apiId:'fixture-id',apiKey:'fixture-key'});
|
|
|
+ const teacher={id:'teacher',get:k=>({company:{id:'co'},legacyUserId:30,legacyGroupId:3}[k])};
|
|
|
+ const confirmed=await fixture({szyh:'25',kcid:'398',yykcid:'398',dszt:'30',courseCategoryKey:'middle_writing',categoryEvidence:'同一学年审核初中'});
|
|
|
+ const appointment=async()=>{
|
|
|
+ const objectId=await fixture({szyh:'25',kcid:'398',yykcid:'398'});
|
|
|
+ await q('UPDATE "CommonModel" SET title=$2,subtitle=$3 WHERE "objectId"=$1',[objectId,'测试学员初三','阅读写作']);
|
|
|
+ const a=await one('SELECT * FROM "CourseAppointment" WHERE "objectId"=$1',[objectId.slice(0,-1)]);
|
|
|
+ return {...a,objectId,GeneralID:String(serial)};
|
|
|
+ };
|
|
|
+ let order=await appointment();
|
|
|
+ const detail=await c.lessonOrderCapabilities(order,teacher);
|
|
|
+ assert.equal(detail.courseCategoryKey,'middle_writing',detail.blockedReason);
|
|
|
+ assert.equal(detail.creditHoldStatus,'ready');
|
|
|
+ assert.deepEqual(Array.from(detail.allowedActions),['start']);
|
|
|
+ assert.match((await one('SELECT "categoryEvidence" FROM "CourseAppointment" WHERE id=$1',[order.id])).categoryEvidence,/初中/);
|
|
|
+ await c.lessonOrderCapabilities(order,teacher);
|
|
|
+ assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[order.objectId])).n,1);
|
|
|
+ assert.equal((await one('SELECT dszt FROM "CourseAppointment" WHERE id=$1',[order.id])).dszt,'0');
|
|
|
+
|
|
|
+ // Recheck the live source on the actual start command after the page had offered Start.
|
|
|
+ legacyStatus='11';
|
|
|
+ await assert.rejects(c.enrollmentOrderTransition({},teacher,order,order.GeneralID,10),/旧系统.*完课/);
|
|
|
+ assert.equal((await one('SELECT dszt FROM "CourseAppointment" WHERE id=$1',[order.id])).dszt,'0');
|
|
|
+
|
|
|
+ order=await appointment();
|
|
|
+ for(const status of ['10','11','30']){
|
|
|
+ legacyStatus=status;
|
|
|
+ const blocked=await c.lessonOrderCapabilities(order,teacher);
|
|
|
+ assert.match(blocked.blockedReason,/旧系统.*(上课|完课)/);
|
|
|
+ assert.equal(blocked.allowedActions.length,0);
|
|
|
+ }
|
|
|
+ legacyStatus='0';legacyStudent='21';
|
|
|
+ const wrongStudent=await c.lessonOrderCapabilities(order,teacher);
|
|
|
+ assert.match(wrongStudent.blockedReason,/旧系统.*核对/);
|
|
|
+ legacyStudent='25';oldRows=0;
|
|
|
+ assert.match((await c.lessonOrderCapabilities(order,teacher)).blockedReason,/旧系统.*核对|核对旧系统/);
|
|
|
+ oldRows=1;
|
|
|
+ await db.exec(`UPDATE "_User" SET nickname='测试学员高一' WHERE "objectId"='writing-student'`);
|
|
|
+ const conflict=await c.lessonOrderCapabilities(order,teacher);
|
|
|
+ assert.match(conflict.blockedReason,/学段证据冲突/);
|
|
|
+ assert.equal((await one('SELECT "courseCategoryKey" FROM "CourseAppointment" WHERE id=$1',[order.id])).courseCategoryKey,null);
|
|
|
+ assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[order.objectId])).n,0);
|
|
|
+ await db.exec(`UPDATE "_User" SET nickname='测试学员初三',"legacyUserData"='{}' WHERE "objectId"='writing-student'`);
|
|
|
+ assert.match((await c.lessonOrderCapabilities(order,teacher)).blockedReason,/可用课时不足/);
|
|
|
+ assert.equal((await one('SELECT "courseCategoryKey" FROM "CourseAppointment" WHERE id=$1',[order.id])).courseCategoryKey,null);
|
|
|
+ assert.equal((await one('SELECT count(*)::int n FROM audits WHERE target=$1',[order.objectId])).n,0);
|
|
|
+ await db.exec(`UPDATE "_User" SET "legacyUserData"='{"SilverCoin":10,"UserPoint":10}' WHERE "objectId"='writing-student'`);
|
|
|
+ const changeEvidence=async evidence=>q('UPDATE "CourseAppointment" SET "categoryEvidence"=$1 WHERE "objectId"=$2',[evidence,confirmed.slice(0,-1)]);
|
|
|
+ await changeEvidence(null);
|
|
|
+ assert.match((await c.lessonOrderCapabilities(order,teacher)).blockedReason,/缺少一致的本学年已确认/);
|
|
|
+ await changeEvidence('同一学年审核初中');
|
|
|
+ const started=await c.enrollmentOrderTransition({},teacher,order,order.GeneralID,10);
|
|
|
+ assert.equal(started.status,10);
|
|
|
+ assert.equal((await one('SELECT dszt FROM "CourseAppointment" WHERE id=$1',[order.id])).dszt,'10');
|
|
|
+ assert.ok(reads>=5);
|
|
|
+ }finally{await db.exec('ROLLBACK');}
|
|
|
+});
|