| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- 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;
- before(async()=>{
- await db.exec(`CREATE TABLE "_User"("objectId" text primary key,company text,"legacyUserId" numeric,"legacyUserData" jsonb,"isDisabled" boolean,"isDeleted" boolean,"adminRoleKey" text);
- CREATE TABLE "CommonModel"("objectId" text primary key,company text,"modelId" numeric,"itemId" numeric,"generalId" numeric,status numeric,"sourceKey" text);
- CREATE TABLE "CourseAppointment"("objectId" text primary key,company text,id numeric,szyh text,pl text,kcid text,yykcid text,dszt text,dslx text,jffs text,bxrq text,yysj text,"sourceKey" text,"teachingManaged" boolean,"bindingId" text,"courseCategoryKey" text,"courseCategoryName" text,"categoryEvidence" text,"durationMinutes" numeric,"deliveryMode" text,"teachingRuleId" text,"teacherPaySnapshot" numeric,"creditCostsSnapshot" jsonb,"creditHoldStatus" text,"creditHoldReason" text,"updatedAt" timestamptz);
- CREATE TABLE "CourseBinding"("objectId" text primary key,company text,id numeric,yhid text,kcid text,"categoryKey" text,"categoryName" text,"enrollmentStatus" text,"sourceKey" text,"updatedAt" timestamptz);
- CREATE TABLE "CourseTeachingProfile"(company text,"courseId" numeric,"categoryKey" text,confirmed boolean);
- CREATE TABLE "TeachingCourseCategory"(company text,key text,active boolean);
- CREATE TABLE "TeachingCompensationRule"(company text,"objectId" text,"categoryKey" text,"durationMinutes" numeric,"deliveryMode" text,"effectiveFrom" text,active boolean,"teacherPay" numeric,"creditCosts" jsonb,revision numeric);
- CREATE TABLE "CourseCreditReservation"("objectId" text primary key,company text,"sourceKey" text,"createdAt" timestamptz,"updatedAt" timestamptz,"appointmentId" text unique,"appointmentGeneralId" numeric,"studentId" numeric,"ruleId" text,"creditCosts" jsonb,state text,"reservedAt" timestamptz);
- CREATE TABLE "Node"(company text,"nodeId" numeric,"parentId" numeric,zstatus numeric);
- CREATE TABLE audits(target text,before_data jsonb,after_data jsonb);
- CREATE FUNCTION xs_payroll_audit(co text,actor text,action text,target text,reason text,before_data jsonb,after_data jsonb) RETURNS void LANGUAGE sql AS $$ INSERT INTO audits VALUES(target,before_data,after_data) $$;
- INSERT INTO "_User" VALUES('teacher','co',30,'{}',false,false,null),('student','co',20,'{"Purse":100,"SilverCoin":100,"UserPoint":100}',false,false,null),('other','other',30,'{}',false,false,null),('stranger','co',31,'{}',false,false,null);
- INSERT INTO "Node" VALUES('co',100,null,99),('co',101,null,99);
- INSERT INTO "CommonModel" VALUES('word','co',52,null,1,99,null),('binding-common','co',58,1,10,99,'legacy:model:58:general:10');UPDATE "CommonModel" SET "itemId"=NULL WHERE "objectId"='word';
- ALTER TABLE "CommonModel" ADD "nodeId" numeric;UPDATE "CommonModel" SET "nodeId"=100 WHERE "objectId"='word';
- INSERT INTO "CourseBinding" VALUES('binding','co',1,'20','100','word','单词课','pending','legacy:model:58:general:10',now());
- INSERT INTO "CourseTeachingProfile" VALUES('co',100,'word',true);`);
- const enrollment=readFileSync(new URL('../sql/member-enrollment.sql',import.meta.url),'utf8');for(const name of ['xs_object_id','xs_insert','xs_appointment_day','xs_appointment_valid_at','xs_reserved','xs_course_size']){const def=enrollment.match(new RegExp('CREATE OR REPLACE FUNCTION '+name+'\\([\\s\\S]*?\\$\\$;'))[0];await db.exec(def);}
- await db.exec(readFileSync(new URL('../sql/legacy-appointment-preparation.sql',import.meta.url),'utf8'));
- });after(()=>db.close());
- let serial=0;async function fixture(values={}){const id='a'+(++serial),day=(await one("SELECT to_char(now() AT TIME ZONE 'Asia/Shanghai','YYYY-MM-DD') AS day")).day;const r={objectId:id,company:'co',id:serial,szyh:'20',pl:'30',kcid:'100',yykcid:'100',dszt:'0',dslx:'2',jffs:'线上',bxrq:day,yysj:day+' 00:00',sourceKey:'legacy:model:54:general:'+serial,...values};await q('INSERT INTO "CourseAppointment" SELECT * FROM jsonb_populate_record(NULL::"CourseAppointment",$1::jsonb)',[JSON.stringify(r)]);await q('INSERT INTO "CommonModel"("objectId",company,"modelId","itemId","generalId",status,"sourceKey") VALUES($1,\'co\',54,$2,$2,99,$3)',[id+'c',serial,r.sourceKey]);return id+'c';}
- test('当天已过钟点的旧预约恢复唯一开课、工资快照和预留,重试不改余额或状态',async()=>{const id=await fixture(),before=await one('SELECT "legacyUserData" FROM "_User" WHERE "objectId"=\'student\'');assert.equal((await prepare(id)).changed,true);assert.equal((await prepare(id)).changed,false);const a=await one('SELECT * FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)]);assert.equal(a.bindingId,'binding');assert.equal(a.kcid,'100');assert.equal(a.dszt,'0');assert.equal(Number(a.teacherPaySnapshot),30);assert.equal(a.creditHoldStatus,'ready');assert.deepEqual(await one('SELECT "legacyUserData" FROM "_User" WHERE "objectId"=\'student\''),before);assert.equal((await one('SELECT count(*)::int n FROM audits WHERE target=$1',[id])).n,1);assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,1);});
- test('越权、跨帐套、过期、取消、新系统预约及已开课不能自动修复',async()=>{const id=await fixture();for(const actor of ['stranger','other'])await assert.rejects(prepare(id,actor),/无权/);await assert.rejects(prepare(await fixture({pl:null}),'stranger'),/无权/);for(const changes of [{bxrq:'2000-01-01'},{dszt:'10'},{sourceKey:'schedule:new'},{kcid:'101'}])await assert.rejects(prepare(await fixture(changes)),/过期|未开始|旧预约|不一致/);const cancelled=await fixture();await q('UPDATE "CommonModel" SET status=-2 WHERE "objectId"=$1',[cancelled]);await assert.rejects(prepare(cancelled),/未取消/);});
- test('课时不足时整个事务回滚,不激活开课、不保存快照或审计',async()=>{await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='pending';UPDATE "_User" SET "legacyUserData"='{}' WHERE "objectId"='student'`);const id=await fixture();await assert.rejects(prepare(id),/不足/);assert.equal((await one('SELECT "enrollmentStatus" FROM "CourseBinding" WHERE "objectId"=\'binding\'')).enrollmentStatus,'pending');assert.equal((await one('SELECT "bindingId" FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)])).bindingId,null);await db.exec(`UPDATE "_User" SET "legacyUserData"='{"Purse":100,"SilverCoin":100,"UserPoint":100}' WHERE "objectId"='student'`);});
- test('重复开课、回收开课、未确认课程分类不能猜测恢复',async()=>{const id=await fixture();await db.exec(`INSERT INTO "CourseBinding" SELECT 'duplicate',company,2,yhid,kcid,"categoryKey","categoryName","enrollmentStatus","sourceKey",now() FROM "CourseBinding" WHERE "objectId"='binding';INSERT INTO "CommonModel"("objectId",company,"modelId","itemId",status) VALUES('duplicate-common','co',58,2,99)`);await assert.rejects(prepare(id),/唯一/);await db.exec(`DELETE FROM "CourseBinding" WHERE "objectId"='duplicate';DELETE FROM "CommonModel" WHERE "objectId"='duplicate-common';UPDATE "CourseBinding" SET "enrollmentStatus"='recycled'`);await assert.rejects(prepare(id),/唯一/);await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='pending';UPDATE "CourseTeachingProfile" SET confirmed=false`);await assert.rejects(prepare(id),/分类/);await db.exec(`UPDATE "CourseTeachingProfile" SET confirmed=true`);});
- test('新系统已有工资快照保留,正式规则覆盖默认版本,禁用版本不能回退同版本默认价',async()=>{await db.exec(`INSERT INTO "TeachingCompensationRule" VALUES('co','custom','word',60,'online','2026-09-09',true,35,'[{"account":"SilverCoin","amount":1},{"account":"UserPoint","amount":1}]',1)`);const id=await fixture({teacherPaySnapshot:37});await prepare(id);assert.equal(Number((await one('SELECT "teacherPaySnapshot" FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)])).teacherPaySnapshot),37);const next=await fixture();await prepare(next);assert.equal(Number((await one('SELECT "teacherPaySnapshot" FROM "CourseAppointment" WHERE "objectId"=$1',[next.slice(0,-1)])).teacherPaySnapshot),35);});
- test('教师详情自动核对后返回开始权限与实际绑定;学习单元采用同一开课记录',async()=>{
- await db.exec(`ALTER TABLE "CourseAppointment" ADD kssj text,ADD jssj text;CREATE TABLE "LessonRecord"(company text,yyds text,pf numeric,pjnr text,pldp text,"createdAt" timestamptz);CREATE TABLE "LessonReview"(company text,"appointmentGeneralId" text,score numeric,comment text,"submittedAt" timestamptz);`);
- const app=source.match(/const appGatewayCode = String.raw`([\s\S]*?)\n`;/)[1].replace("const DEFAULT_COMPANY_ID = '7pIbDBJmKx'","const DEFAULT_COMPANY_ID = 'co'");
- const c=vm.createContext({Psql:{one,oneOrNone:one,query:q}});vm.runInContext(app+legacyAppointmentDefaults(ops)+readFileSync(new URL('../cloud/legacy-appointment.js',import.meta.url),'utf8')+readFileSync(new URL('../cloud/lesson-workflow.js',import.meta.url),'utf8'),c);
- const id=await fixture(),teacher={id:'teacher',get:k=>({company:{id:'co'},legacyUserId:30}[k])};
- const detail=await c.lessonOrderCapabilities({GeneralID:String(serial),objectId:id,pl:'30',szyh:'20',dszt:'0',kcid:'100'},teacher);
- assert.equal(detail.bindingId,'binding');assert.equal(detail.kcid,'100');assert.deepEqual(JSON.parse(JSON.stringify(detail.allowedActions)),['start']);assert.equal(detail.blockedReason,'');
- const binding=await one('SELECT "enrollmentStatus" FROM "CourseBinding" WHERE "objectId"=$1',[detail.bindingId]);assert.equal(binding.enrollmentStatus,'active');
- });
- test('审计失败回滚已激活的开课、课时预留和快照,修复可重试',async()=>{
- await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='pending';CREATE FUNCTION reject_audit() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN RAISE EXCEPTION 'audit unavailable';END$$;CREATE TRIGGER reject_audit BEFORE INSERT ON audits FOR EACH ROW EXECUTE FUNCTION reject_audit()`);
- const id=await fixture();await assert.rejects(prepare(id),/audit unavailable/);
- assert.equal((await one('SELECT "enrollmentStatus" FROM "CourseBinding" WHERE "objectId"=\'binding\'')).enrollmentStatus,'pending');
- assert.equal((await one('SELECT "bindingId" FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)])).bindingId,null);
- assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,0);
- await db.exec('DROP TRIGGER reject_audit ON audits');assert.equal((await prepare(id)).changed,true);
- });
- test('新导入的空分类开课只补明确的非写作分类;全局写作映射不能替代逐人确认',async()=>{
- await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"=null,"categoryKey"=null,"categoryName"=null`);const id=await fixture();await prepare(id);assert.equal((await one('SELECT "categoryKey" FROM "CourseBinding" WHERE "objectId"=\'binding\'')).categoryKey,'word');
- await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"=null,"categoryKey"=null,"categoryName"=null;UPDATE "CourseTeachingProfile" SET "categoryKey"='high_writing'`);await assert.rejects(prepare(await fixture()),/按学员/);await db.exec(`UPDATE "CourseTeachingProfile" SET "categoryKey"='word'`);
- });
- test('旧阅读写作预约经逐人确认高中后免词库核验,保留原课程并预留课时',async()=>{
- const id=await fixture({kcid:'398',yykcid:'398',courseCategoryKey:'high_writing',categoryEvidence:'用户确认高中'});
- const r=await prepare(id);assert.equal(r.changed,true);
- const a=await one('SELECT * FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)]);
- assert.equal(a.kcid,'398');assert.equal(a.bindingId,null);assert.equal(a.creditHoldStatus,'ready');assert.equal(Number(a.teacherPaySnapshot),65);
- assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,1);
- assert.equal((await prepare(id)).changed,false);
- });
- test('旧阅读写作未确认学段不得自动预留',async()=>{const id=await fixture({kcid:'398',yykcid:'398'});await assert.rejects(prepare(id),/单词|分类|课程/);});
- test('旧系统体验课按体验课时预留,不按词库的普通课分类扣费',async()=>{
- await db.exec('BEGIN');try{
- await db.exec(`UPDATE "_User" SET "legacyUserData"='{"UserExp":1,"UserPoint":0,"Purse":0,"SilverCoin":0}' WHERE "objectId"='student';UPDATE "CourseBinding" SET "categoryKey"=NULL,"categoryName"=NULL,"enrollmentStatus"=NULL WHERE "objectId"='binding';`);
- const id=await fixture({dslx:'3'}),before=await one('SELECT "legacyUserData" FROM "_User" WHERE "objectId"=\'student\'');
- const first=await prepare(id),a=await one('SELECT * FROM "CourseAppointment" WHERE "objectId"=$1',[id.slice(0,-1)]);
- assert.equal(first.changed,true);assert.equal(a.courseCategoryKey,'trial');assert.equal(a.creditHoldStatus,'ready');assert.equal(Number(a.teacherPaySnapshot),30);assert.deepEqual(a.creditCostsSnapshot.map(c=>[c.account,Number(c.amount)]),[['UserExp',1]]);
- assert.equal((await one('SELECT "categoryKey" FROM "CourseBinding" WHERE "objectId"=\'binding\'')).categoryKey,'word');assert.deepEqual(await one('SELECT "legacyUserData" FROM "_User" WHERE "objectId"=\'student\''),before);
- assert.equal((await prepare(id)).changed,false);assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,1);
- }finally{await db.exec('ROLLBACK');}
- });
- test('旧体验课可复用已开通的普通词库,不重写词库分类且准备幂等',async()=>{
- await db.exec('BEGIN');try{
- await db.exec(`UPDATE "_User" SET "legacyUserData"='{"UserExp":1,"UserPoint":0,"Purse":0,"SilverCoin":0}' WHERE "objectId"='student';UPDATE "CourseBinding" SET "categoryKey"='word',"categoryName"='单词课',"enrollmentStatus"='active' WHERE "objectId"='binding'`);
- const id=await fixture({dslx:'3'});assert.equal((await prepare(id)).changed,true);assert.equal((await prepare(id)).changed,false);
- 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'`);
- const id=await fixture({dslx:'3'});await db.exec('SAVEPOINT insufficient');await assert.rejects(prepare(id),/UserExp/);await db.exec('ROLLBACK TO SAVEPOINT insufficient');
- assert.equal((await one(`SELECT "enrollmentStatus" FROM "CourseBinding" WHERE "objectId"='binding'`)).enrollmentStatus,null);assert.equal((await one('SELECT count(*)::int n FROM "CourseCreditReservation" WHERE "appointmentId"=$1',[id])).n,0);
- }finally{await db.exec('ROLLBACK');}
- });
- test('先准备体验课再准备同教材普通课,各自使用正确课时且不改写共享词库分类',async()=>{
- await db.exec('BEGIN');try{
- await db.exec(`UPDATE "_User" SET "legacyUserData"='{"UserExp":1,"UserPoint":100,"Purse":0,"SilverCoin":100}' WHERE "objectId"='student';UPDATE "CourseBinding" SET "categoryKey"=NULL,"categoryName"=NULL,"enrollmentStatus"=NULL WHERE "objectId"='binding'`);
- const trial=await fixture({dslx:'3'});await prepare(trial);
- const normal=await fixture({dslx:'2'});await prepare(normal);
- const a=await one('SELECT "courseCategoryKey","creditCostsSnapshot" FROM "CourseAppointment" WHERE "objectId"=$1',[normal.slice(0,-1)]);
- 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');}
- });
|