# Batch 14 Controllers Analysis (Controllers 131-140)

## Controllers in this batch:
1. RankController
2. RelationshipController
3. ReligionController
4. ReportController (partial - file too large, analyzed first 100 lines)
5. ScriptProfileOrgController
6. SocketController
7. SubDistrictController
8. UserController
9. ViewWorkFlowController
10. WorkflowController

---

## Critical Issues Found

### 1. **UserController** - Multiple Unhandled forEach Async Operations

**File & Location:** [UserController.ts](src/controllers/UserController.ts) - Methods: `createUserImport()`, `addroleStaffToUser()`, `addroleStaffToUserEmp()`, `changeUserPasswordAll()`

**Problem Type:** 1. Unhandled Exception / 2. Missing Error Handle

**Root Cause:**
Multiple critical methods use `for await` loops and `forEach()` with async Keycloak API operations without proper error handling. When Keycloak operations fail, the errors can crash the Node.js process.

**Affected Code Locations:**
- Line 977-1032: `for await` loop in `createUserImport()` - No error handling for individual user creation failures
- Line 1133-1148: `for await` loop in `changeUserPasswordAll()` - Errors are silently ignored but not properly handled
- Line 1169-1227: `for await` loop in `addroleStaffToUser()` - Keycloak operations without error handling
- Line 1249-1307: `for await` loop in `addroleStaffToUserEmp()` - Keycloak operations without error handling
- Line 1066-1118: `Promise.all()` in `createUserImportEmp()` - Limited error handling

**Code Examples:**

```typescript
// Line 977-1032 - DANGEROUS: for await without error handling
for await (const _item of profiles) {
  let password = _item.citizenId;
  if (_item.birthDate != null) {
    const _date = new Date(_item.birthDate.toDateString())
      .getDate()
      .toString()
      .padStart(2, "0");
    const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1)
      .toString()
      .padStart(2, "0");
    const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543;
    password = `${_date}${_month}${_year}`;
  }
  const checkUser = await getUserByUsername(_item.citizenId);
  let userId: any = "";
  if (checkUser.length == 0) {
    userId = await createUser(_item.citizenId, password, {
      firstName: _item.firstName,
      lastName: _item.lastName,
    });
    if (typeof userId !== "string") {
      throw new Error(userId.errorMessage); // This can crash the entire process
    }
  } else {
    userId = checkUser[0].id;
  }

  const list = await getRoles();
  if (!Array.isArray(list)) throw new Error("Failed. Cannot get role(s) data from the server.");
  const result = await addUserRoles(
    userId,
    list.filter((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212"),
  );

  if (!result) {
    throw new Error("Failed. Cannot set user's role."); // This can crash the entire process
  }
  // ... more operations without error handling
}
```

```typescript
// Line 1066-1118 - Promise.all with some error handling but not comprehensive
await Promise.all(
  batch.map(async (_item) => {
    // ... operations
    try {
      const checkUser = await getUserByUsername(_item.citizenId);
      // ... more operations
    } catch (error) {
      console.error(`Error processing ${_item.citizenId}:`, error);
    }
  }),
);
```

**Recommended Fix:**

```typescript
// For createUserImport() - Add comprehensive error handling
@Post("user/create")
@Security("bearerAuth", ["system", "admin"])
async createUserImport(
  @Request() request: { user: { sub: string; preferred_username: string } },
) {
  const profiles = await this.profileRepo.find({
    where: {
      keycloak: IsNull(),
    },
    relations: ["roleKeycloaks"],
  });

  const results = {
    total: profiles.length,
    success: 0,
    failed: 0,
    errors: [] as Array<{ citizenId: string; error: string }>,
  };

  // Cache roles list to avoid repeated API calls
  let rolesList: any[] = [];
  try {
    rolesList = await getRoles();
    if (!Array.isArray(rolesList)) {
      throw new Error("Failed. Cannot get role(s) data from the server.");
    }
  } catch (error) {
    console.error('Failed to fetch roles:', error);
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to fetch roles from Keycloak'
    );
  }

  const defaultRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212");
  if (!defaultRole) {
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Default role not found in Keycloak'
    );
  }

  for await (const _item of profiles) {
    try {
      let password = _item.citizenId;
      if (_item.birthDate != null) {
        const _date = new Date(_item.birthDate.toDateString())
          .getDate()
          .toString()
          .padStart(2, "0");
        const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1)
          .toString()
          .padStart(2, "0");
        const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543;
        password = `${_date}${_month}${_year}`;
      }

      const checkUser = await getUserByUsername(_item.citizenId);
      let userId: string = "";

      if (checkUser.length == 0) {
        const createdUser = await createUser(_item.citizenId, password, {
          firstName: _item.firstName,
          lastName: _item.lastName,
        });

        if (typeof createdUser !== "string") {
          throw new Error(createdUser.errorMessage || 'Failed to create user');
        }
        userId = createdUser;
      } else {
        userId = checkUser[0].id;
      }

      const result = await addUserRoles(userId, [defaultRole]);

      if (!result) {
        throw new Error("Failed. Cannot set user's role.");
      }

      if (typeof userId === "string") {
        _item.keycloak = userId;
      }

      const roleKeycloak = await this.roleKeycloakRepo.find({
        where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" },
      });

      if (_item) {
        _item.roleKeycloaks = Array.from(new Set([..._item.roleKeycloaks, ...roleKeycloak]));
        await this.profileRepo.save(_item);
      }

      results.success++;
    } catch (error: any) {
      results.failed++;
      results.errors.push({
        citizenId: _item.citizenId,
        error: error.message || 'Unknown error',
      });
      console.error(`Error processing user ${_item.citizenId}:`, error);
    }
  }

  return new HttpSuccess({
    message: 'User import completed',
    ...results,
  });
}

// For addroleStaffToUser() - Add error handling with detailed logging
@Post("add-role-staff/user/{child1Id}")
@Security("bearerAuth", ["system", "admin"])
async addroleStaffToUser(
  @Path() child1Id: string,
  @Request() request: { user: { sub: string; preferred_username: string } },
) {
  const profiles = await this.profileRepo.find({
    where: {
      keycloak: Not(IsNull()),
      current_holders: {
        orgChild1Id: child1Id,
      },
    },
    relations: ["roleKeycloaks"],
  });

  const results = {
    total: profiles.length,
    success: 0,
    failed: 0,
    errors: [] as Array<{ citizenId: string; error: string }>,
  };

  // Cache roles
  let rolesList: any[] = [];
  try {
    rolesList = await getRoles();
    if (!Array.isArray(rolesList)) {
      throw new Error("Failed. Cannot get role(s) data from the server.");
    }
  } catch (error) {
    console.error('Failed to fetch roles:', error);
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to fetch roles from Keycloak'
    );
  }

  const userRole = rolesList.find((v) => v.id == "8a1a0dc9-304c-4e5b-a90a-65f841048212");
  const staffRole = rolesList.find((v) => v.id == "f1fff8db-0795-47c1-9952-f3c18d5b6172");

  if (!userRole || !staffRole) {
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Required roles not found in Keycloak'
    );
  }

  for await (const _item of profiles) {
    try {
      let password = _item.citizenId;
      if (_item.birthDate != null) {
        const _date = new Date(_item.birthDate.toDateString())
          .getDate()
          .toString()
          .padStart(2, "0");
        const _month = (new Date(_item.birthDate.toDateString()).getMonth() + 1)
          .toString()
          .padStart(2, "0");
        const _year = new Date(_item.birthDate.toDateString()).getFullYear() + 543;
        password = `${_date}${_month}${_year}`;
      }

      const checkUser = await getUserByUsername(_item.citizenId);
      let userId: string = "";

      if (checkUser.length == 0) {
        const createdUser = await createUser(_item.citizenId, password, {
          firstName: _item.firstName,
          lastName: _item.lastName,
        });

        if (typeof createdUser !== "string") {
          throw new Error(createdUser.errorMessage || 'Failed to create user');
        }
        userId = createdUser;
      } else {
        userId = checkUser[0].id;
      }

      // Add both roles
      await Promise.all([
        addUserRoles(userId, [userRole]),
        addUserRoles(userId, [staffRole]),
      ]);

      if (typeof userId === "string") {
        _item.keycloak = userId;
      }

      const roleKeycloakUser = await this.roleKeycloakRepo.find({
        where: { id: "8a1a0dc9-304c-4e5b-a90a-65f841048212" },
      });
      const roleKeycloakStaff = await this.roleKeycloakRepo.find({
        where: { id: "f1fff8db-0795-47c1-9952-f3c18d5b6172" },
      });

      if (_item) {
        _item.roleKeycloaks = Array.from(new Set([...roleKeycloakUser, ...roleKeycloakStaff]));
        await this.profileRepo.save(_item);
      }

      results.success++;
    } catch (error: any) {
      results.failed++;
      results.errors.push({
        citizenId: _item.citizenId,
        error: error.message || 'Unknown error',
      });
      console.error(`Error processing user ${_item.citizenId}:`, error);
    }
  }

  return new HttpSuccess({
    message: 'Role assignment completed',
    ...results,
  });
}

// For changeUserPasswordAll() - Add proper error handling
@Post("user/change-password-all")
async changeUserPasswordAll(
  @Request() request: { user: { sub: string; preferred_username: string } },
) {
  const profiles = await this.profileRepo.find({
    where: {
      keycloak: Not(IsNull()),
    },
  });

  const results = {
    total: profiles.length,
    success: 0,
    failed: 0,
    errors: [] as Array<{ citizenId: string; error: string }>,
  };

  for await (const _item of profiles) {
    try {
      let password = _item.citizenId;
      if (_item.birthDate != null) {
        const gregorianYear = _item.birthDate.getFullYear() + 543;

        const formattedDate =
          _item.birthDate.toISOString().slice(8, 10) +
          _item.birthDate.toISOString().slice(5, 7) +
          gregorianYear;
        password = formattedDate;
      }

      const result = await changeUserPassword(_item.keycloak, password);
      if (!result) {
        throw new Error('Failed to change password');
      }

      results.success++;
    } catch (error: any) {
      results.failed++;
      results.errors.push({
        citizenId: _item.citizenId,
        error: error.message || 'Unknown error',
      });
      console.error(`Error changing password for ${_item.citizenId}:`, error);
    }
  }

  return new HttpSuccess({
    message: 'Password change completed',
    ...results,
  });
}
```

---

### 2. **WorkflowController** - Multiple Database Operations Without Transactions

**File & Location:** [WorkflowController.ts](src/controllers/WorkflowController.ts) - Method: `checkWorkflow()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
Complex multi-step workflow creation process without transactional integrity. If intermediate operations fail, the database can be left in an inconsistent state with partial data.

**Affected Code Locations:**
- Line 46-273: `checkWorkflow()` - Multiple sequential database operations without transaction
- Line 143-180: `forEach()` with state operator creation - No error handling for individual operations
- Line 214-230: `forEach()` for officer state operator users - No error handling
- Line 258-270: Fire-and-forget API call with only console error logging

**Code Examples:**

```typescript
// Line 111-133 - Multiple database operations without transaction
const [savedWorkflow, metaStates] = await Promise.all([
  this.workflowRepo.save(workflow),
  this.metaStateRepo.find({
    where: { metaWorkflowId: metaWorkflow.id },
    order: { order: "ASC" },
  }),
]);

// ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว
const statesToCreate = metaStates.map((item) => {
  const state = new State();
  Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta });
  return state;
});

const savedStates = await this.stateRepo.save(statesToCreate);

// ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก
const firstState = savedStates.find((state) => state.order === 1);
if (firstState) {
  savedWorkflow.stateId = firstState.id;
  await this.workflowRepo.save(savedWorkflow);
}
```

```typescript
// Line 214-230 - forEach without error handling
profileOfficers.forEach((item) => {
  if (item.current_holderId) {
    orderNum += 1;
    const isPersonnelOfficer = item.orgChild1?.isOfficer === true;

    const officerStateOperatorUser = new StateOperatorUser();
    Object.assign(officerStateOperatorUser, {
      profileId: item.current_holderId,
      operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer",
      profileType: "OFFICER",
      order: orderNum,
      workflowId: savedWorkflow.id,
      ...meta,
    });
    stateOperatorUsersToCreate.push(officerStateOperatorUser);
  }
});
```

```typescript
// Line 258-270 - Fire-and-forget API call
new CallAPI()
  .PostData(req, "/placement/noti/profiles", {
    subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`,
    body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`,
    receiverUserIds: notificationReceivers,
    payload: "",
    isSendMail: true,
    isSendInbox: true,
    isSendNotification: true,
  })
  .catch((error) => {
    console.error("Error calling API:", error);
  });
```

**Recommended Fix:**

```typescript
@Post("add-workflow")
public async checkWorkflow(
  @Request() req: RequestWithUser,
  @Body()
  body: {
    refId: string;
    sysName: string;
    posLevelName: string;
    posTypeName: string;
    fullName?: string | null;
    isDeputy?: boolean | null;
    orgRootId?: string | null;
  },
) {
  const queryRunner = AppDataSource.createQueryRunner();
  await queryRunner.connect();
  await queryRunner.startTransaction();

  try {
    // ขั้นที่ 1: ทำการค้นหา profile และ metaWorkflow แบบ parallel
    const [userProfileOfficer, userProfileEmployee, metaWorkflow] = await Promise.all([
      queryRunner.manager.findOne(Profile, {
        where: { keycloak: req.user.sub },
        select: ["id", "keycloak"],
      }),
      queryRunner.manager.findOne(ProfileEmployee, {
        where: { keycloak: req.user.sub },
        select: ["id", "keycloak"],
      }),
      queryRunner.manager.findOne(MetaWorkflow, {
        where: {
          sysName: body.sysName,
          posLevelName: body.posLevelName,
          posTypeName: body.posTypeName,
        },
      }),
    ]);

    // กำหนด profile type และ profile
    let profileType = "OFFICER";
    let profile: any = userProfileOfficer;

    if (!profile) {
      profileType = "EMPLOYEE";
      profile = userProfileEmployee;
      if (!profile) {
        await queryRunner.rollbackTransaction();
        throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลผู้ใช้งาน");
      }
    }

    if (!metaWorkflow) {
      await queryRunner.rollbackTransaction();
      throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบกระบวนการนี้ได้");
    }

    const meta = {
      createdUserId: req.user.sub,
      createdFullName: req.user.name,
      lastUpdateUserId: req.user.sub,
      lastUpdateFullName: req.user.name,
      createdAt: new Date(),
      lastUpdatedAt: new Date(),
    };

    // ขั้นที่ 2: สร้าง workflow และดึง metaState แบบ parallel
    const workflow = new Workflow();
    Object.assign(workflow, {
      ...metaWorkflow,
      id: undefined,
      ...meta,
      ...body,
      profileType: profileType,
      system: body.sysName,
    });

    const savedWorkflow = await queryRunner.manager.save(workflow);

    const metaStates = await queryRunner.manager.find(MetaState, {
      where: { metaWorkflowId: metaWorkflow.id },
      order: { order: "ASC" },
    });

    // ขั้นที่ 3: สร้าง states ทั้งหมดในครั้งเดียว
    const statesToCreate = metaStates.map((item) => {
      const state = new State();
      Object.assign(state, { ...item, id: undefined, workflowId: savedWorkflow.id, ...meta });
      return state;
    });

    const savedStates = await queryRunner.manager.save(statesToCreate);

    // ขั้นที่ 4: อัปเดต workflow.stateId กับ state แรก
    const firstState = savedStates.find((state) => state.order === 1);
    if (firstState) {
      savedWorkflow.stateId = firstState.id;
      await queryRunner.manager.save(savedWorkflow);
    }

    // ขั้นที่ 5: ดึง metaStateOperators ทั้งหมดและสร้าง stateOperators
    const metaStateIds = metaStates.map((item) => item.id);
    const allMetaStateOperators = await queryRunner.manager.find(MetaStateOperator, {
      where: { metaStateId: In(metaStateIds) },
    });

    // สร้าง stateOperators ทั้งหมดในครั้งเดียว
    const stateOperatorsToCreate: StateOperator[] = [];
    allMetaStateOperators.forEach((metaStateOp) => {
      const correspondingState = savedStates.find(
        (state) =>
          metaStates.find((metaState) => metaState.id === metaStateOp.metaStateId)?.order ===
          state.order,
      );
      if (body.isDeputy) {
        // Task #2207 กรณีคนขอโอนอยู่ในสำนักปลัดกรุงเทพมหานคร
        if (body.sysName == "SYS_TRANSFER_REQ") {
          if (metaStateOp.operator == "PersonnelOfficer" && correspondingState?.order == 1) {
            return;
          } else if (
            metaStateOp.operator == "Officer" &&
            [1, 2].includes(correspondingState?.order as number)
          ) {
            metaStateOp.operator = "PersonnelOfficer";
          }
        }
        // Task #2208 กรณีขอแก้ไขข้อมูลทะเบียนประวัติ และ IDP และคนขออยู่ในสำนักปลัดกรุงเทพมหานคร
        if (
          metaStateOp.operator == "Officer" &&
          ["REGISTRY_PROFILE", "REGISTRY_PROFILE_EMP", "REGISTRY_IDP"].includes(body.sysName)
        ) {
          metaStateOp.operator = "PersonnelOfficer";
        }
      }
      if (correspondingState) {
        const stateOperator = new StateOperator();
        Object.assign(stateOperator, {
          ...metaStateOp,
          id: undefined,
          stateId: correspondingState.id,
          ...meta,
        });
        stateOperatorsToCreate.push(stateOperator);
      }
    });

    await queryRunner.manager.save(stateOperatorsToCreate);

    // ขั้นที่ 6: สร้าง StateOperatorUsers แบบ bulk
    const stateOperatorUsersToCreate: StateOperatorUser[] = [];
    let orderNum = 1;

    // เพิ่ม Owner ก่อน
    if (profile) {
      const ownerStateOperatorUser = new StateOperatorUser();
      Object.assign(ownerStateOperatorUser, {
        profileId: profileType === "OFFICER" ? profile.id : null,
        profileEmployeeId: profileType !== "OFFICER" ? profile.id : null,
        profileType: profileType,
        operator: "Owner",
        order: orderNum,
        workflowId: savedWorkflow.id,
        ...meta,
      });
      stateOperatorUsersToCreate.push(ownerStateOperatorUser);
    }

    // ดึงข้อมูล profileOfficers และสร้าง StateOperatorUsers
    const profileOfficers = await queryRunner.manager.find(PosMaster, {
      where: {
        posMasterAssigns: { assignId: body.sysName },
        orgRevision: { orgRevisionIsDraft: false, orgRevisionIsCurrent: true },
        current_holderId: Not(IsNull()),
        ...(body.orgRootId && { orgRootId: body.orgRootId }),
      },
      relations: ["orgChild1"],
    });

    // สร้าง StateOperatorUsers สำหรับ officers - with error handling
    for (const item of profileOfficers) {
      try {
        if (item.current_holderId) {
          orderNum += 1;
          const isPersonnelOfficer = item.orgChild1?.isOfficer === true;

          const officerStateOperatorUser = new StateOperatorUser();
          Object.assign(officerStateOperatorUser, {
            profileId: item.current_holderId,
            operator: isPersonnelOfficer ? "PersonnelOfficer" : "Officer",
            profileType: "OFFICER",
            order: orderNum,
            workflowId: savedWorkflow.id,
            ...meta,
          });
          stateOperatorUsersToCreate.push(officerStateOperatorUser);
        }
      } catch (error) {
        console.error(`Error processing officer ${item.current_holderId}:`, error);
        // Continue with next officer
      }
    }

    // บันทึก StateOperatorUsers ทั้งหมดในครั้งเดียว
    await queryRunner.manager.save(stateOperatorUsersToCreate);

    // Commit transaction
    await queryRunner.commitTransaction();

    // ขั้นที่ 7: ส่ง notification (fire-and-forget after transaction commits)
    const firstStateOperators = stateOperatorsToCreate.filter((so) =>
      savedStates.find((state) => state.id === so.stateId && state.order === 1),
    );

    let notiLink = "";
    if (body.sysName === "REGISTRY_PROFILE") {
      notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit/personal/${body.refId}`;
    } else if (body.sysName === "REGISTRY_PROFILE_EMP") {
      notiLink = `${process.env.VITE_URL_MGT}/registry-employee/request-edit/personal/${body.refId}`;
    } else if (body.sysName === "REGISTRY_IDP") {
      notiLink = `${process.env.VITE_URL_MGT}/registry-officer/request-edit-page/${body.refId}`;
    }

    const notificationReceivers = stateOperatorUsersToCreate
      .filter((user) => firstStateOperators.some((op) => op.operator === user.operator))
      .map((user) => ({
        receiverUserId: user.profileType === "OFFICER" ? user.profileId : user.profileEmployeeId,
        notiLink: notiLink,
      }));

    // Send notification asynchronously with proper error handling
    new CallAPI()
      .PostData(req, "/placement/noti/profiles", {
        subject: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`,
        body: `แจ้ง${savedWorkflow.name}ของ ${body.fullName}`,
        receiverUserIds: notificationReceivers,
        payload: "",
        isSendMail: true,
        isSendInbox: true,
        isSendNotification: true,
      })
      .catch((error) => {
        console.error("Error calling notification API:", {
          workflowId: savedWorkflow.id,
          error: error instanceof Error ? error.message : error,
        });
      });

    return new HttpSuccess();
  } catch (error) {
    await queryRunner.rollbackTransaction();
    console.error('Error in checkWorkflow:', {
      refId: body.refId,
      sysName: body.sysName,
      error: error instanceof Error ? error.message : error,
      stack: error instanceof Error ? error.stack : undefined,
    });

    if (error instanceof HttpError) {
      throw error;
    }

    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to create workflow'
    );
  } finally {
    await queryRunner.release();
  }
}
```

---

### 3. **ScriptProfileOrgController** - Missing Error Handling for External API Calls

**File & Location:** [ScriptProfileOrgController.ts](src/controllers/ScriptProfileOrgController.ts) - Method: `cronjobUpdateOrg()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
While this controller has good error handling structure, the external API call to the leave service and Keycloak sync operations need better error handling for individual batch failures.

**Affected Code Locations:**
- Line 184-190: External API call without detailed error handling
- Line 228-250: Batch processing with limited error context
- Line 71-159: Complex database queries without error handling

**Code Examples:**

```typescript
// Line 184-190 - External API call needs better error handling
await axios.put(`${process.env.API_URL}/leave-beginning/schedule/update-dna`, payloads, {
  headers: {
    "Content-Type": "application/json",
    api_key: process.env.API_KEY,
  },
  timeout: 30000,
});
```

```typescript
// Line 228-250 - Batch processing could use more error context
try {
  const batchResult: any = await keycloakSyncController.syncByProfileIds({
    profileIds: batch,
    profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE",
  });

  const resultData = (batchResult as any)?.data || batchResult;
  typeResult.success += resultData.success || 0;
  typeResult.failed += resultData.failed || 0;

  console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, {
    success: resultData.success || 0,
    failed: resultData.failed || 0,
  });
} catch (error: any) {
  console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, {
    error: error.message,
    batchSize: batch.length,
  });
  typeResult.failed += batch.length;
}
```

**Recommended Fix:**

```typescript
// Improve external API call error handling
try {
  const response = await axios.put(
    `${process.env.API_URL}/leave-beginning/schedule/update-dna`,
    payloads,
    {
      headers: {
        "Content-Type": "application/json",
        api_key: process.env.API_KEY,
      },
      timeout: 30000,
    }
  );

  console.log("cronjobUpdateOrg: Leave service API call successful", {
    status: response.status,
    payloadCount: payloads.length,
  });
} catch (error: any) {
  console.error("cronjobUpdateOrg: Leave service API call failed", {
    error: error.message,
    response: error.response?.data,
    status: error.response?.status,
    payloadCount: payloads.length,
  });

  // Don't fail completely - log and continue
  // Optionally: implement retry logic or circuit breaker
}

// Improve batch processing error handling
for (let i = 0; i < batches.length; i++) {
  const batch = batches[i];
  console.log(
    `cronjobUpdateOrg: Processing batch ${i + 1}/${batches.length} for ${profileType}`,
    {
      batchSize: batch.length,
      batchRange: `${i * this.BATCH_SIZE + 1}-${Math.min(
        (i + 1) * this.BATCH_SIZE,
        profileIds.length,
      )}`,
    },
  );

  try {
    const batchResult: any = await keycloakSyncController.syncByProfileIds({
      profileIds: batch,
      profileType: profileType as "PROFILE" | "PROFILE_EMPLOYEE",
    });

    const resultData = (batchResult as any)?.data || batchResult;
    typeResult.success += resultData.success || 0;
    typeResult.failed += resultData.failed || 0;

    console.log(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} completed`, {
      success: resultData.success || 0,
      failed: resultData.failed || 0,
    });
  } catch (error: any) {
    console.error(`cronjobUpdateOrg: Batch ${i + 1}/${batches.length} failed`, {
      error: error.message,
      stack: error.stack,
      batchSize: batch.length,
      batchIndex: i,
      profileType: profileType,
    });

    // Count all profiles in failed batch as failed
    typeResult.failed += batch.length;

    // Optionally: Store failed batch for retry
    // failedBatches.push({ index: i, batch, error: error.message });
  }
}
```

---

### 4. **SubDistrictController** - Generic Error Handling Without Logging

**File & Location:** [SubDistrictController.ts](src/controllers/SubDistrictController.ts) - Method: `Delete()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
The delete operation uses a generic catch block without logging or differentiating between error types. This makes debugging difficult and may mask underlying issues.

**Affected Code Locations:**
- Line 183-190: Generic catch block without error logging

**Code Example:**

```typescript
// Line 183-190 - Generic error handling
let result: any;
try {
  result = await this.subDistrictRepository.delete({ id: id });
} catch {
  throw new HttpError(
    HttpStatusCode.NOT_FOUND,
    "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่",
  );
}
```

**Recommended Fix:**

```typescript
let result: any;
try {
  result = await this.subDistrictRepository.delete({ id: id });
} catch (error: any) {
  console.error('Error deleting sub-district:', {
    id,
    error: error.message,
    stack: error.stack,
    code: error.code,
  });

  // Check for foreign key constraint error
  if (error.code === 'ER_ROW_IS_REFERENCED_2' ||
      error.message?.includes('foreign key constraint') ||
      error.message?.includes('Cannot delete or update a parent row')) {
    throw new HttpError(
      HttpStatusCode.CONFLICT,
      "ไม่สามารถลบได้เนื่องจากมีการใช้งานข้อมูลแขวง/ตำบลนี้อยู่",
    );
  }

  throw new HttpError(
    HttpStatusCode.INTERNAL_SERVER_ERROR,
    "เกิดข้อผิดพลาดในการลบข้อมูลแขวง/ตำบล",
  );
}
```

---

### 5. **UserController** - Promise.all Without Comprehensive Error Handling

**File & Location:** [UserController.ts](src/controllers/UserController.ts) - Method: `listUserKeycloak()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
Complex database query with multiple conditions and joins without proper error handling. If the query fails or the database is unavailable, the error will propagate unhandled.

**Affected Code Locations:**
- Line 566-608: Complex query builder operations for OFFICER type
- Line 610-653: Complex query builder operations for EMPLOYEE type

**Code Example:**

```typescript
// Line 566-608 - Complex query without error handling
[profiles, total] = await this.profileRepo
  .createQueryBuilder("profile")
  .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks")
  .leftJoinAndSelect("profile.current_holders", "current_holders")
  .leftJoinAndSelect("current_holders.orgRoot", "orgRoot")
  .leftJoinAndSelect("current_holders.orgChild1", "orgChild1")
  .leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
  .leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
  .leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
  .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''")
  .andWhere("profile.isDelete = :isDelete", { isDelete: false })
  .andWhere(checkChildFromRole)
  .andWhere(conditions)
  .andWhere(
    new Brackets((qb) => {
      qb.orWhere(
        body.keyword != null && body.keyword != ""
          ? `profile.citizenId like '%${body.keyword}%'`
          : "1=1",
      )
        .orWhere(
          body.keyword != null && body.keyword != ""
            ? `profile.email like '%${body.keyword}%'`
            : "1=1",
        )
        .orWhere(
          body.keyword != null && body.keyword != ""
            ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName)  like '%${body.keyword}%'`
            : "1=1",
        );
    }),
  )
  .orderBy("profile.citizenId", "ASC")
  .orderBy("orgRoot.orgRootOrder", "ASC")
  .addOrderBy("orgChild1.orgChild1Order", "ASC")
  .addOrderBy("orgChild2.orgChild2Order", "ASC")
  .addOrderBy("orgChild3.orgChild3Order", "ASC")
  .addOrderBy("orgChild4.orgChild4Order", "ASC")
  .addOrderBy("current_holders.posMasterOrder", "ASC")
  .addOrderBy("current_holders.posMasterCreatedAt", "ASC")
  .skip((body.page - 1) * body.pageSize)
  .take(body.pageSize)
  .getManyAndCount();
```

**Recommended Fix:**

```typescript
let profiles: any = [];
let total: any;

try {
  if (body.type.trim().toUpperCase() == "OFFICER") {
    try {
      [profiles, total] = await this.profileRepo
        .createQueryBuilder("profile")
        .leftJoinAndSelect("profile.roleKeycloaks", "roleKeycloaks")
        .leftJoinAndSelect("profile.current_holders", "current_holders")
        .leftJoinAndSelect("current_holders.orgRoot", "orgRoot")
        .leftJoinAndSelect("current_holders.orgChild1", "orgChild1")
        .leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
        .leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
        .leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
        .where("profile.keycloak IS NOT NULL AND profile.keycloak != ''")
        .andWhere("profile.isDelete = :isDelete", { isDelete: false })
        .andWhere(checkChildFromRole)
        .andWhere(conditions)
        .andWhere(
          new Brackets((qb) => {
            qb.orWhere(
              body.keyword != null && body.keyword != ""
                ? `profile.citizenId like :citizenKeyword`
                : "1=1",
              { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' },
            )
              .orWhere(
                body.keyword != null && body.keyword != ""
                  ? `profile.email like :emailKeyword`
                  : "1=1",
                { emailKeyword: body.keyword ? `%${body.keyword}%` : '' },
              )
              .orWhere(
                body.keyword != null && body.keyword != ""
                  ? `CONCAT(profile.prefix, profile.firstName," ",profile.lastName) like :nameKeyword`
                  : "1=1",
                { nameKeyword: body.keyword ? `%${body.keyword}%` : '' },
              );
          }),
        )
        .orderBy("profile.citizenId", "ASC")
        .addOrderBy("orgRoot.orgRootOrder", "ASC")
        .addOrderBy("orgChild1.orgChild1Order", "ASC")
        .addOrderBy("orgChild2.orgChild2Order", "ASC")
        .addOrderBy("orgChild3.orgChild3Order", "ASC")
        .addOrderBy("orgChild4.orgChild4Order", "ASC")
        .addOrderBy("current_holders.posMasterOrder", "ASC")
        .addOrderBy("current_holders.posMasterCreatedAt", "ASC")
        .skip((body.page - 1) * body.pageSize)
        .take(body.pageSize)
        .getManyAndCount();
    } catch (error: any) {
      console.error('Error querying officer profiles:', {
        error: error.message,
        stack: error.stack,
        body: { ...body, keyword: body.keyword ? '[REDACTED]' : null },
      });
      throw new HttpError(
        HttpStatus.INTERNAL_SERVER_ERROR,
        'Failed to retrieve officer profiles'
      );
    }
  } else if (body.type.trim().toUpperCase() == "EMPLOYEE") {
    try {
      [profiles, total] = await this.profileEmpRepo
        .createQueryBuilder("profileEmployee")
        .leftJoinAndSelect("profileEmployee.roleKeycloaks", "roleKeycloaks")
        .leftJoinAndSelect("profileEmployee.current_holders", "current_holders")
        .leftJoinAndSelect("current_holders.orgRoot", "orgRoot")
        .leftJoinAndSelect("current_holders.orgChild1", "orgChild1")
        .leftJoinAndSelect("current_holders.orgChild2", "orgChild2")
        .leftJoinAndSelect("current_holders.orgChild3", "orgChild3")
        .leftJoinAndSelect("current_holders.orgChild4", "orgChild4")
        .where("profileEmployee.keycloak IS NOT NULL AND profileEmployee.keycloak != ''")
        .andWhere("profileEmployee.isDelete = :isDelete", { isDelete: false })
        .andWhere(checkChildFromRole)
        .andWhere(conditions)
        .andWhere({ employeeClass: "PERM" })
        .andWhere(
          new Brackets((qb) => {
            qb.orWhere(
              body.keyword != null && body.keyword != ""
                ? `profileEmployee.citizenId like :citizenKeyword`
                : "1=1",
              { citizenKeyword: body.keyword ? `%${body.keyword}%` : '' },
            )
              .orWhere(
                body.keyword != null && body.keyword != ""
                  ? `profileEmployee.email like :emailKeyword`
                  : "1=1",
                { emailKeyword: body.keyword ? `%${body.keyword}%` : '' },
              )
              .orWhere(
                body.keyword != null && body.keyword != ""
                  ? `CONCAT(profileEmployee.prefix, profileEmployee.firstName," ",profileEmployee.lastName) like :nameKeyword`
                  : "1=1",
                { nameKeyword: body.keyword ? `%${body.keyword}%` : '' },
              );
          }),
        )
        .orderBy("profileEmployee.citizenId", "ASC")
        .addOrderBy("orgRoot.orgRootOrder", "ASC")
        .addOrderBy("orgChild1.orgChild1Order", "ASC")
        .addOrderBy("orgChild2.orgChild2Order", "ASC")
        .addOrderBy("orgChild3.orgChild3Order", "ASC")
        .addOrderBy("orgChild4.orgChild4Order", "ASC")
        .addOrderBy("current_holders.posMasterOrder", "ASC")
        .addOrderBy("current_holders.posMasterCreatedAt", "ASC")
        .skip((body.page - 1) * body.pageSize)
        .take(body.pageSize)
        .getManyAndCount();
    } catch (error: any) {
      console.error('Error querying employee profiles:', {
        error: error.message,
        stack: error.stack,
        body: { ...body, keyword: body.keyword ? '[REDACTED]' : null },
      });
      throw new HttpError(
        HttpStatus.INTERNAL_SERVER_ERROR,
        'Failed to retrieve employee profiles'
      );
    }
  }
} catch (error) {
  if (error instanceof HttpError) {
    throw error;
  }
  throw new HttpError(
    HttpStatus.INTERNAL_SERVER_ERROR,
    'Failed to retrieve user data'
  );
}

const _profiles = profiles.map((_data: any) => ({
  id: _data.keycloak,
  firstname: _data.firstName,
  lastname: _data.lastName,
  email: _data.email,
  username: _data.citizenId,
  citizenId: _data.citizenId,
  roles: _data.roleKeycloaks,
  enabled: _data.isActive,
}));
return new HttpSuccess({ data: _profiles, total });
```

---

### 6. **SocketController** - No Error Handling for WebSocket Operations

**File & Location:** [SocketController.ts](src/controllers/SocketController.ts) - Method: `notify()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
The WebSocket send operation has no error handling. If the WebSocket service fails, the error will be unhandled.

**Affected Code Locations:**
- Line 7-24: Entire notify method

**Code Example:**

```typescript
@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  sendWebSocket(
    "socket-notification",
    { success: !payload.error, message: payload.message },
    {
      roles: payload.roles || [],
      userId: payload.userId || [],
    },
  );
}
```

**Recommended Fix:**

```typescript
@Post("notify")
async notify(
  @Body()
  payload: {
    message: string;
    userId?: string | string[];
    roles?: string | string[];
    error?: boolean;
  },
) {
  try {
    sendWebSocket(
      "socket-notification",
      { success: !payload.error, message: payload.message },
      {
        roles: payload.roles || [],
        userId: payload.userId || [],
      },
    );
    return new HttpSuccess({ message: 'Notification sent successfully' });
  } catch (error: any) {
    console.error('Error sending WebSocket notification:', {
      message: payload.message,
      userId: payload.userId,
      roles: payload.roles,
      error: error.message,
    });
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to send notification'
    );
  }
}
```

---

### 7. **RankController, RelationshipController, ReligionController** - No Error Handling

**File & Location:**
- [RankController.ts](src/controllers/RankController.ts)
- [RelationshipController.ts](src/controllers/RelationshipController.ts)
- [ReligionController.ts](src/controllers/ReligionController.ts)

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
All database operations in these controllers lack proper error handling. While they use HttpError for business logic validation, they don't handle database errors (connection issues, timeouts, etc.).

**Affected Code Locations:**
- All methods in all three controllers

**Recommended Fix:**

Add a generic error handling middleware or wrap each method with try-catch:

```typescript
// Example for RankController
@Post()
async createRank(
  @Body()
  requestBody: CreateRank,
  @Request() request: RequestWithUser,
) {
  try {
    const checkName = await this.rankRepository.findOne({
      where: { name: requestBody.name },
    });

    if (checkName) {
      throw new HttpError(HttpStatusCode.CONFLICT, "ชื่อนี้มีอยู่ในระบบแล้ว");
    }

    const before = null;
    const rank = Object.assign(new Rank(), requestBody);
    rank.createdUserId = request.user.sub;
    rank.createdFullName = request.user.name;
    rank.lastUpdateUserId = request.user.sub;
    rank.lastUpdateFullName = request.user.name;
    rank.createdAt = new Date();
    rank.lastUpdatedAt = new Date();

    await this.rankRepository.save(rank, { data: request });
    setLogDataDiff(request, { before, after: rank });
    return new HttpSuccess();
  } catch (error) {
    if (error instanceof HttpError) {
      throw error;
    }
    console.error('Error creating rank:', {
      name: requestBody.name,
      error: error instanceof Error ? error.message : error,
      stack: error instanceof Error ? error.stack : undefined,
    });
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to create rank'
    );
  }
}
```

---

### 8. **ViewWorkFlowController** - Sequential Async Operations in Loop

**File & Location:** [ViewWorkFlowController.ts](src/controllers/ViewWorkFlowController.ts) - Method: `getSystems()`

**Problem Type:** 2. Missing Error Handle

**Root Cause:**
Uses a for loop to process items sequentially, which could be slow and doesn't handle errors for individual items.

**Affected Code Locations:**
- Line 41-49: Sequential for loop processing

**Code Example:**

```typescript
const sys: any = [];

for (let index = 0; index < lists.length; index++) {
  const element = await lists[index];
  if (sys.findIndex((x: any) => x.sysName === element.sysName) === -1) {
    sys.push({
      sysName: element.sysName,
      name: element.name,
    });
  }
}
```

**Recommended Fix:**

```typescript
@Get("lists")
public async getSystems(@Request() req: RequestWithUser) {
  try {
    const lists = await this.metaWorkflowRepository
      .createQueryBuilder("metaWorkflow")
      .select(["metaWorkflow.name", "metaWorkflow.sysName"])
      .getMany();

    // Use Map for better performance and automatic deduplication
    const sysMap = new Map<string, { sysName: string; name: string }>();

    for (const element of lists) {
      if (!sysMap.has(element.sysName)) {
        sysMap.set(element.sysName, {
          sysName: element.sysName,
          name: element.name,
        });
      }
    }

    const sys = Array.from(sysMap.values());
    return new HttpSuccess(sys);
  } catch (error: any) {
    console.error('Error getting workflow systems:', {
      error: error.message,
      stack: error.stack,
    });
    throw new HttpError(
      HttpStatus.INTERNAL_SERVER_ERROR,
      'Failed to retrieve workflow systems'
    );
  }
}
```

---

## Summary Statistics

**Total Critical Issues Found:** 8

**Breakdown by Type:**
- **Unhandled Exception (forEach/for await with async):** 5 instances
- **Missing Error Handling (DB operations):** 10 instances
- **Transaction Issues:** 1 instance
- **External API Error Handling:** 2 instances
- **Generic Error Handling:** 3 instances

**Controllers with Issues:**
1. UserController - 5 critical issues
2. WorkflowController - 2 critical issues
3. ScriptProfileOrgController - 2 critical issues
4. SubDistrictController - 1 issue
5. SocketController - 1 issue
6. RankController - 1 issue
7. RelationshipController - 1 issue
8. ReligionController - 1 issue
9. ViewWorkFlowController - 1 issue
10. ReportController - Not fully analyzed (file too large)

**Risk Level: HIGH**

---

## Priority Recommendations

### Immediate Actions Required:

1. **Fix UserController methods** - Add comprehensive error handling to all `for await` loops and Keycloak operations
2. **Add transactions to WorkflowController** - Ensure data consistency during workflow creation
3. **Improve external API error handling** - Add proper logging and retry logic for external service calls
4. **Add global error handling middleware** - Catch unhandled errors at the application level
5. **Implement circuit breakers** - For external dependencies (Keycloak, leave service)

### Graceful Recovery Strategies:

1. **Implement request-level error boundaries** - Catch errors at the controller level
2. **Add operation timeouts** - Prevent indefinite hangs on external API calls
3. **Implement retry logic with exponential backoff** - For transient failures
4. **Add health checks** - Monitor Keycloak and database connectivity
5. **Use connection pooling** with proper error handling

### Long-term Improvements:

1. **Implement a centralized error handling middleware**
2. **Add structured logging** (e.g., Winston, Pino)
3. **Implement request tracing** for debugging distributed issues
4. **Add metrics/monitoring** for error rates and external API failures
5. **Implement graceful shutdown** procedures for batch operations

---

## Testing Recommendations

1. **Test Keycloak failure scenarios** - Simulate Keycloak unavailability during user operations
2. **Test with large datasets** - Ensure for await operations don't cause memory issues
3. **Test transaction rollback** - Verify data consistency on errors
4. **Test concurrent requests** - Ensure race conditions don't cause crashes
5. **Test external API failures** - Simulate leave service and notification failures
6. **Test database connection failures** - Ensure proper handling of connection issues
