// Operator-only maintenance primitive. Callers must authenticate and supply an // explicit reviewed identity list; no account is selected by display name alone. export async function retireImportedAccounts(db, company, targets, actor, receipt) { if (!targets.length || new Set(targets.map(x=>x.objectId)).size !== targets.length) throw Error('invalid_target_list'); return db.tx(async t=>{ await t.any('SELECT pg_advisory_xact_lock(hashtext($1))',['direct-sync:'+company]); if(await t.oneOrNone('SELECT "runId" FROM "LegacyDirectRun" WHERE company=$1 AND status=\'running\' AND "leaseUntil">now() LIMIT 1',[company])) throw Error('another_sync_running'); const rows=await t.any('SELECT "objectId","isAdmin",roles,"adminRoleKey","legacyUserId","sourceKey","updatedAt","isDeleted","legacyUserData" FROM "_User" WHERE company=$1 AND "objectId"=ANY($2::text[]) FOR UPDATE',[company,targets.map(x=>x.objectId)]); if(rows.length!==targets.length) throw Error('target_missing_or_wrong_company'); const pending=[]; for(const expected of targets){ const row=rows.find(x=>x.objectId===expected.objectId); if(row.isAdmin || row.adminRoleKey || (row.roles||[]).some(r=>/admin|manager/.test(r))) throw Error('privileged_target'); if(row.isDeleted && row.legacyUserData?.RetirementReceipt===receipt) continue; if(row.isDeleted || Number(row.legacyUserId)!==Number(expected.legacyUserId) || (row.sourceKey||'')!==(expected.sourceKey||'') || new Date(row.updatedAt).getTime()!==new Date(expected.updatedAt).getTime()) throw Error('target_changed'); pending.push(row); } let detached=0; for(const row of pending){ const synthetic=String(row.sourceKey||'').startsWith('reading-unified:user:'); const legacy={...row.legacyUserData,State:0,SyncExcluded:true,OpsDeletedReason:'user-requested-test-or-foreign-account',RetiredLegacyUserID:Number(row.legacyUserId),RetirementReceipt:receipt,OpsDeletedAt:new Date().toISOString()}; if(synthetic){delete legacy.UserID;detached++;} await t.none('UPDATE "_User" SET "isDeleted"=true,"isDisabled"=true,"legacyUserId"=$3,"legacyUserData"=$4::jsonb,"updatedAt"=now() WHERE company=$1 AND "objectId"=$2',[company,row.objectId,synthetic?null:row.legacyUserId,JSON.stringify(legacy)]); } const sessions=await t.any('DELETE FROM "_Session" WHERE "user"=ANY($1::text[]) RETURNING "objectId"',[targets.flatMap(x=>[x.objectId,'_User$'+x.objectId])]); const result={requested:targets.length,retired:pending.length,alreadyRetired:targets.length-pending.length,detachedSyntheticIds:detached,revokedSessions:sessions.length}; await t.none('INSERT INTO "SysLog" ("objectId",company,"sourceKey",type1,type2,type3,detail,"createdAt","updatedAt") SELECT substr(md5($1),1,20),$2,$1,\'migration\',\'retire-imported-accounts\',\'user-request\',$3,now(),now() WHERE NOT EXISTS(SELECT 1 FROM "SysLog" WHERE company=$2 AND "sourceKey"=$1)',[receipt,company,JSON.stringify({actor,...result,objectIds:targets.map(x=>x.objectId)})]); return result; }); }