|
@@ -0,0 +1,87 @@
|
|
|
|
|
+import { test } from 'node:test';
|
|
|
|
|
+import assert from 'node:assert/strict';
|
|
|
|
|
+import { readFileSync } from 'node:fs';
|
|
|
|
|
+import { PGlite } from '@electric-sql/pglite';
|
|
|
|
|
+import vm from 'node:vm';
|
|
|
|
|
+
|
|
|
|
|
+const migration = new URL('../sql/legacy-enrollment-reconciliation.sql', import.meta.url);
|
|
|
|
|
+const sql = readFileSync(migration, 'utf8');
|
|
|
|
|
+const gateway = readFileSync(new URL('../cloud/member-enrollment.js', import.meta.url), 'utf8');
|
|
|
|
|
+async function fixture(run) {
|
|
|
|
|
+ const db = new PGlite();
|
|
|
|
|
+ try {
|
|
|
|
|
+ await db.exec(`
|
|
|
|
|
+ CREATE TABLE "_User" (company text,"legacyUserId" numeric,"identityType" text,"isDisabled" boolean,"isDeleted" boolean,"legacyUserData" jsonb);
|
|
|
|
|
+ CREATE TABLE "Node" (company text,"nodeId" numeric,zstatus numeric,"nodeName" text);
|
|
|
|
|
+ CREATE TABLE "CourseTeachingProfile" (company text,"courseId" numeric,"categoryKey" text,confirmed boolean);
|
|
|
|
|
+ CREATE TABLE "CommonModel" (company text,"modelId" numeric,"itemId" numeric,status numeric,"objectId" text,"sourceKey" text);
|
|
|
|
|
+ CREATE TABLE "CourseBinding" (company text,"objectId" text PRIMARY KEY,id numeric,yhid text,kcid text,"categoryKey" text,"categoryName" text,"enrollmentStatus" text,"sourceKey" text,yxx numeric,cksl numeric,syjd text,"updatedAt" timestamptz);
|
|
|
|
|
+ CREATE UNIQUE INDEX enrollment_active_identity ON "CourseBinding" (company,yhid,"categoryKey",kcid) WHERE "enrollmentStatus"='active';
|
|
|
|
|
+ INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{"Purse":10}');
|
|
|
|
|
+ INSERT INTO "Node" VALUES ('co',100,99,'词表');
|
|
|
|
|
+ INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'word',true);
|
|
|
|
|
+ `);
|
|
|
|
|
+ const binding = async (id, status = 'pending', category = 'word', source = `legacy:model:58:general:${id}`) => db.query(
|
|
|
|
|
+ `INSERT INTO "CourseBinding" VALUES ('co',$1,$2,'20','100',$3,'单词课',$4,$5,123,500,'{"101":"101@第一本@1"}',now())`,
|
|
|
|
|
+ [String(id),id,category,status,source]);
|
|
|
|
|
+ const common = async (id, status = 99) => db.query(`INSERT INTO "CommonModel" VALUES ('co',58,$1,$2,$3,$4)`, [id,status,'c'+id,`legacy:model:58:general:${id}`]);
|
|
|
|
|
+ const get = async id => (await db.query('SELECT * FROM "CourseBinding" WHERE "objectId"=$1',[String(id)])).rows[0];
|
|
|
|
|
+ const install = () => db.exec(sql);
|
|
|
|
|
+ await run({db,binding,common,get,install});
|
|
|
|
|
+ } finally { await db.close(); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+test('历史待确认开课恢复后进入真实排课查询,保留编号、进度和余额,重复迁移无变化', () => fixture(async ({db,binding,common,get,install}) => {
|
|
|
|
|
+ await binding(1); await common(1); const before = await get(1);
|
|
|
|
|
+ await install();
|
|
|
|
|
+ const after = await get(1);
|
|
|
|
|
+ assert.equal(after.enrollmentStatus, 'active');
|
|
|
|
|
+ for (const key of ['objectId','id','sourceKey','yxx','cksl','syjd']) assert.equal(after[key],before[key]);
|
|
|
|
|
+ const c = { Psql: { query: async (s,p) => (await db.query(s,p)).rows }, companyIdOf: () => 'co', text: x => String(x ?? ''), number: Number, TEACHING_CATEGORY_LABELS: new Map([['word','单词课']]) };
|
|
|
|
|
+ vm.createContext(c); vm.runInContext(gateway,c);
|
|
|
|
|
+ const courses = await c.enrollmentBindings({}, {},20);
|
|
|
|
|
+ assert.equal(courses.length,1); assert.equal(courses[0].bindingId,'1');
|
|
|
|
|
+ await install(); assert.deepEqual(await get(1),after);
|
|
|
|
|
+ assert.deepEqual((await db.query('SELECT "legacyUserData" FROM "_User"')).rows[0].legacyUserData,{Purse:10});
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+for (const order of ['binding-first','common-first']) test(`未来同步两张表按${order}顺序写入时自动激活`, () => fixture(async ({binding,common,get,install}) => {
|
|
|
|
|
+ await install();
|
|
|
|
|
+ if(order==='binding-first'){await binding(1,null,null);assert.equal((await get(1)).enrollmentStatus,null);await common(1);}
|
|
|
|
|
+ else {await common(1);await binding(1,null,null);}
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,'active'); assert.equal((await get(1)).categoryKey,'word');
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+test('停用学生保留原状,重新启用后无需删除重开;恢复删除的学生同样生效', () => fixture(async ({db,binding,common,get,install}) => {
|
|
|
|
|
+ await db.exec('UPDATE "_User" SET "isDisabled"=true,"isDeleted"=true');
|
|
|
|
|
+ await binding(1);await common(1);await install();
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,'pending');
|
|
|
|
|
+ await db.exec('UPDATE "_User" SET "isDisabled"=false');assert.equal((await get(1)).enrollmentStatus,'pending');
|
|
|
|
|
+ await db.exec('UPDATE "_User" SET "isDeleted"=false');assert.equal((await get(1)).enrollmentStatus,'active');
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+test('后续补入学生资料、确认映射和恢复内容时补齐开课', () => fixture(async ({db,binding,common,get,install}) => {
|
|
|
|
|
+ await db.exec('DELETE FROM "_User";UPDATE "CourseTeachingProfile" SET confirmed=false;UPDATE "Node" SET zstatus=0');
|
|
|
|
|
+ await binding(1,null,null);await common(1);await install();
|
|
|
|
|
+ await db.exec(`INSERT INTO "_User" VALUES ('co',20,'member',false,false,'{}');UPDATE "CourseTeachingProfile" SET confirmed=true`);
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,null);
|
|
|
|
|
+ await db.exec('UPDATE "Node" SET zstatus=99');assert.equal((await get(1)).enrollmentStatus,'active');
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+test('重复历史开课、已存在有效开课、回收记录及新系统记录保持原状', () => fixture(async ({db,binding,common,get,install}) => {
|
|
|
|
|
+ await binding(1);await common(1);await binding(2);await common(2);await install();
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,'pending');assert.equal((await get(2)).enrollmentStatus,'pending');
|
|
|
|
|
+ await db.exec(`UPDATE "CourseBinding" SET "enrollmentStatus"='active' WHERE "objectId"='2'`);
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,'pending');
|
|
|
|
|
+ await binding(3,'recycled');await common(3,-2);assert.equal((await get(3)).enrollmentStatus,'recycled');
|
|
|
|
|
+ await db.exec('DELETE FROM "CourseBinding";DELETE FROM "CommonModel"');
|
|
|
|
|
+ await binding(4,'pending','word','enroll:4');await common(4);assert.equal((await get(4)).enrollmentStatus,'pending');
|
|
|
|
|
+}));
|
|
|
|
|
+
|
|
|
|
|
+for(const kind of ['unconfirmed','conflicting-profile','explicit-other-category','deleted-common','foreign-tenant']) test(`${kind}不自动授权`, () => fixture(async ({db,binding,common,get,install}) => {
|
|
|
|
|
+ if(kind==='unconfirmed')await db.exec('UPDATE "CourseTeachingProfile" SET confirmed=false');
|
|
|
|
|
+ if(kind==='conflicting-profile')await db.exec(`INSERT INTO "CourseTeachingProfile" VALUES ('co',100,'trial',true)`);
|
|
|
|
|
+ if(kind==='foreign-tenant')await db.exec(`UPDATE "CourseTeachingProfile" SET company='other'`);
|
|
|
|
|
+ await binding(1,'pending',kind==='explicit-other-category'?'trial':null);await common(1,kind==='deleted-common'?-2:99);await install();
|
|
|
|
|
+ assert.equal((await get(1)).enrollmentStatus,'pending');
|
|
|
|
|
+}));
|