From 64be68d0a3a06e8ecbad95af1c4a1132ef15e4a7 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 13:34:06 +0700 Subject: [PATCH 01/12] =?UTF-8?q?[ExecuteSalaryCurrentService]=20=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=20transaction=20+=20respone=20succe?= =?UTF-8?q?ss/fail=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 15 +- src/services/ExecuteSalaryCurrentService.ts | 630 +++++++++++--------- src/services/rabbitmq.ts | 12 +- 3 files changed, 366 insertions(+), 291 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 1ea05633..d0dbbe51 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -106,7 +106,7 @@ import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; import { RetirementService } from "../services/RetirementService"; import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileService"; import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; -import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService"; +import { ExecuteSalaryCurrentService, ExecuteSalaryResult } from "../services/ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; @@ -3702,11 +3702,14 @@ export class CommandController extends Controller { }[]; }, ) { - await new ExecuteSalaryCurrentService().executeSalaryCurrent(body.data, { - user: { sub: req.user.sub, name: req.user.name }, - req, - }); - return new HttpSuccess(); + const result: ExecuteSalaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( + body.data, + { + user: { sub: req.user.sub, name: req.user.name }, + req, + }, + ); + return new HttpSuccess(result); } @Post("excexute/salary-employee-current") diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index 1fd5a54a..fdc2cf1d 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -1,4 +1,4 @@ -import { Double } from "typeorm"; +import { Double, EntityManager } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -60,6 +60,16 @@ export interface SalaryCurrentExecutionContext { req?: any; } +/** + * ผลลัพธ์การประมวลผล batch — แต่ละคนทำงานแบบ independent + * คนที่ fail จะ rollback เฉพาะตัว (per-item transaction) ไม่กระทบคนอื่น + */ +export interface ExecuteSalaryResult { + successCount: number; + failureCount: number; + failures: { profileId: string; reason: string }[]; +} + /** * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) * @@ -69,28 +79,29 @@ export interface SalaryCurrentExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateCurrent ต้นฉบับ + * + * Batch semantics: ประมวลผลทุกคนแบบ sequential (ทีละคน) แต่ละคนครอบด้วย + * transaction ของตัวเอง เพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง + * posMaster/position ตัวเดียวกัน — คนที่ throw จะ rollback เฉพาะตัว ไม่กระทบคนอื่น + * ผลลัพธ์รายงานเป็น success/failure count + รายชื่อคนที่ fail */ export class ExecuteSalaryCurrentService { private commandRepository = AppDataSource.getRepository(Command); private profileRepository = AppDataSource.getRepository(Profile); - private salaryRepo = AppDataSource.getRepository(ProfileSalary); - private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private posMasterRepository = AppDataSource.getRepository(PosMaster); - private positionRepository = AppDataSource.getRepository(Position); private orgRootRepository = AppDataSource.getRepository(OrgRoot); /** - * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของข้าราชการ + * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของข้าราชการทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน */ async executeSalaryCurrent( data: SalaryCurrentItem[], ctx: SalaryCurrentExecutionContext, - ): Promise { + ): Promise { console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); - const req = ctx.req; - // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // ───────────────────────────────────────────────────────────── @@ -149,283 +160,336 @@ export class ExecuteSalaryCurrentService { .orgRootShortName ?? ""; } } - await Promise.all( - data.map(async (item) => { - const profile: any = await this.profileRepository.findOneBy({ id: item.profileId }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - let _null: any = null; - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, + + // ───────────────────────────────────────────────────────────── + // Per-item transaction: แต่ละคนมี transaction ของตัวเอง (sequential) + // ประมวลทีละคนเพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง + // posMaster/position ตัวเดียวกัน คนที่ throw จะ rollback เฉพาะตัว (manager) + // และไม่กระทบคนอื่นใน batch + // ───────────────────────────────────────────────────────────── + const failures: ExecuteSalaryResult["failures"] = []; + let successCount = 0; + for (const item of data ?? []) { + try { + await AppDataSource.transaction(async (manager) => { + await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); }); - const before = null; - const dataSalary = new ProfileSalary(); + successCount++; + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryCurrentService] Failed profileId=${item.profileId}: ${reason}`, + err, + ); + failures.push({ profileId: item.profileId ?? "unknown", reason }); + } + } - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: ctx.user.sub, - createdFullName: ctx.user.name, - lastUpdateUserId: ctx.user.sub, - lastUpdateFullName: ctx.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - Object.assign(dataSalary, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); - await this.salaryRepo.save(dataSalary, { data: req }); - setLogDataDiff(req, { before, after: dataSalary }); - history.commandId = item.commandId ?? _null; - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster == null) { - console.error( - `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - const posMasterOld = await this.posMasterRepository.findOne({ - where: { - current_holderId: item.profileId, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - - const positionOld = await this.positionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { - posMasterId: posMasterOld?.id, - userId: ctx.user.sub, - endpoint: "updateMaster", - action: "command_change_reset_old_position", - }); - - positionOld.positionIsSelected = false; - await this.positionRepository.save(positionOld); - } - - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - console.log( - `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${ctx.user.sub}, endpoint: updateMaster)`, - ); - - const clearPosition = checkPosition.map((positions) => { - logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { - posMasterId: posMaster!.id, - userId: ctx.user.sub, - endpoint: "updateMaster", - action: "command_change_clear_positions", - }); - - return { - ...positions, - positionIsSelected: false, - }; - }); - await this.positionRepository.save(clearPosition); - } - - posMaster.current_holderId = item.profileId; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - if (posMasterOld != null) { - await this.posMasterRepository.save(posMasterOld); - await CreatePosMasterHistoryOfficer(posMasterOld.id, req); - } - await this.posMasterRepository.save(posMaster); - - // STEP 2: กำหนด position ใหม่ - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel - const posTypeId = item.positionTypeId || item.positionType; - const posLevelId = item.positionLevelId || item.positionLevel; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionName && posTypeId && posLevelId) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }; - - // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionName && posTypeId && posLevelId) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (positionNew != null) { - positionNew.positionIsSelected = true; - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - if (!posMaster.isSit) { - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.positionField = positionNew.positionField ?? null; - profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; - profile.positionArea = positionNew.positionArea ?? null; - profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; - } - profile.amount = item.amount ?? null; - profile.amountSpecial = item.amountSpecial ?? null; - await this.profileRepository.save(profile); - await this.positionRepository.save(positionNew); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - }), + console.log( + `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: ${failures.length}`, ); - console.log("[ExecuteSalaryCurrentService] executeSalaryCurrent completed successfully"); + return { successCount, failureCount: failures.length, failures }; + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ (กัน partial commit) + */ + private async processOne( + item: SalaryCurrentItem, + ctx: SalaryCurrentExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const posMasterRepository = manager.getRepository(PosMaster); + const positionRepository = manager.getRepository(Position); + + const profile: any = await profileRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + let _null: any = null; + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + Object.assign(dataSalary, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.commandId = item.commandId ?? _null; + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา + let posMaster = await posMasterRepository.findOne({ + where: { id: item.posmasterId }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + } + + if (posMaster == null) { + console.error( + `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + } + + const posMasterOld = await posMasterRepository.findOne({ + where: { + current_holderId: item.profileId, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + + const positionOld = await positionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { + posMasterId: posMasterOld?.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_reset_old_position", + }); + + positionOld.positionIsSelected = false; + await positionRepository.save(positionOld); + } + + const checkPosition = await positionRepository.find({ + where: { + posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + console.log( + `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${ctx.user.sub}, endpoint: updateMaster)`, + ); + + const clearPosition = checkPosition.map((positions) => { + logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { + posMasterId: posMaster!.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_clear_positions", + }); + + return { + ...positions, + positionIsSelected: false, + }; + }); + await positionRepository.save(clearPosition); + } + + posMaster.current_holderId = item.profileId; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + if (posMasterOld != null) { + await posMasterRepository.save(posMasterOld); + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + } + await posMasterRepository.save(posMaster); + + // STEP 2: กำหนด position ใหม่ + // Match position ตามลำดับ priority: + // Condition 1: match จาก positionId + // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) + // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) + // Fallback: เลือก position แรกใน posMaster + + let positionNew: Position | null = null; + + // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel + const posTypeId = item.positionTypeId || item.positionType; + const posLevelId = item.positionLevelId || item.positionLevel; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + if (item.positionId) { + const positionById = await positionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง + }, + relations: ["posExecutive"], + }); + + if (positionById) { + positionNew = positionById; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionName && posTypeId && posLevelId) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }; + + // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) + if (item.positionField) { + whereCondition.positionField = item.positionField; + } + if (item.posExecutiveId) { + whereCondition.posExecutiveId = item.posExecutiveId; + } + if (item.positionExecutiveField) { + whereCondition.positionExecutiveField = item.positionExecutiveField; + } + if (item.positionArea) { + whereCondition.positionArea = item.positionArea; + } + + const positionBy7Fields = await positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionName && posTypeId && posLevelId) { + const positionBy3Fields = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + } + } + + // // ═══════════════════════════════════════════════════════════ + // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster + // // ═══════════════════════════════════════════════════════════ + // if (!positionNew) { + // const fallbackPositions = await positionRepository.find({ + // where: { + // posMasterId: posMaster.id, + // }, + // relations: ["posExecutive"], + // order: { + // orderNo: "ASC", + // }, + // take: 1, + // }); + + // if (fallbackPositions.length > 0) { + // positionNew = fallbackPositions[0]; + // } + // } + + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (positionNew != null) { + positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + if (!posMaster.isSit) { + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; + } + profile.amount = item.amount ?? null; + profile.amountSpecial = item.amountSpecial ?? null; + await profileRepository.save(profile); + await positionRepository.save(positionNew); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); } } diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index d84a3eed..c1d6e778 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -388,8 +388,16 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); } else if (isSalaryCurrent) { - await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx); - console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`); + const salaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( + resultData, + ctx, + ); + console.log( + `[AMQ] Processed via ExecuteSalaryCurrentService — success: ${salaryResult.successCount}, failure: ${salaryResult.failureCount}`, + ); + for (const f of salaryResult.failures) { + console.error(`[AMQ] ExecuteSalaryCurrentService failed profileId=${f.profileId}: ${f.reason}`); + } } else if (isSalaryEmployeeCurrent) { await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); From 5acd4853681e633ad4d622aca9eb6f9ebeec0668 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 16:35:30 +0700 Subject: [PATCH 02/12] fix #62 --- src/controllers/ProfileEmployeeController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 660a36d6..21eb9a5d 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -3254,8 +3254,8 @@ export class ProfileEmployeeController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where(node && nodeId ? "current_holders.orgRevisionId = :orgRevisionId" : "1=1", { - orgRevisionId: node && nodeId ? findRevision.id : undefined, + .where("current_holders.orgRevisionId = :orgRevisionId", { + orgRevisionId: findRevision.id, }) .andWhere( _data.root != undefined && _data.root != null From ecd0388eb0329114f481ff3c9966254263920db6 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 18:33:27 +0700 Subject: [PATCH 03/12] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/ExecuteSalaryCurrentService.ts | 131 ++++++++++++-------- src/services/rabbitmq.ts | 12 +- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index fdc2cf1d..2f0871dc 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -61,8 +61,9 @@ export interface SalaryCurrentExecutionContext { } /** - * ผลลัพธ์การประมวลผล batch — แต่ละคนทำงานแบบ independent - * คนที่ fail จะ rollback เฉพาะตัว (per-item transaction) ไม่กระทบคนอื่น + * ผลลัพธ์การประมวลผล batch — all-or-nothing (single transaction ครอบทั้ง batch) + * ถ้าทุกคนสำเร็จจะ return result; ถ้ามีคนใด throw จะ rollback ทั้ง batch + * และ propagate error ออกไป (caller เห็นเป็น failure ทั้งหมด) */ export interface ExecuteSalaryResult { successCount: number; @@ -80,10 +81,9 @@ export interface ExecuteSalaryResult { * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateCurrent ต้นฉบับ * - * Batch semantics: ประมวลผลทุกคนแบบ sequential (ทีละคน) แต่ละคนครอบด้วย - * transaction ของตัวเอง เพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง - * posMaster/position ตัวเดียวกัน — คนที่ throw จะ rollback เฉพาะตัว ไม่กระทบคนอื่น - * ผลลัพธ์รายงานเป็น success/failure count + รายชื่อคนที่ fail + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count */ export class ExecuteSalaryCurrentService { private commandRepository = AppDataSource.getRepository(Command); @@ -99,8 +99,12 @@ export class ExecuteSalaryCurrentService { data: SalaryCurrentItem[], ctx: SalaryCurrentExecutionContext, ): Promise { - console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); - console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryCurrentService] Starting executeSalaryCurrent — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryCurrentService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -162,39 +166,37 @@ export class ExecuteSalaryCurrentService { } // ───────────────────────────────────────────────────────────── - // Per-item transaction: แต่ละคนมี transaction ของตัวเอง (sequential) - // ประมวลทีละคนเพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง - // posMaster/position ตัวเดียวกัน คนที่ throw จะ rollback เฉพาะตัว (manager) - // และไม่กระทบคนอื่นใน batch + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow // ───────────────────────────────────────────────────────────── - const failures: ExecuteSalaryResult["failures"] = []; let successCount = 0; - for (const item of data ?? []) { - try { - await AppDataSource.transaction(async (manager) => { + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); - }); - successCount++; - } catch (err) { - const reason = - err instanceof HttpError - ? err.message - : err instanceof Error + successCount++; + } catch (err) { + const reason = + err instanceof HttpError ? err.message - : "unexpected error"; - console.error( - `[ExecuteSalaryCurrentService] Failed profileId=${item.profileId}: ${reason}`, - err, - ); - failures.push({ profileId: item.profileId ?? "unknown", reason }); + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryCurrentService] Failed — commandCode: ${commandCode}, commandId: ${commandId}, profileId: ${item.profileId}, reason: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } } - } + }); console.log( - `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: ${failures.length}`, + `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: 0`, ); - return { successCount, failureCount: failures.length, failures }; + return { successCount, failureCount: 0, failures: [] }; } /** @@ -219,7 +221,7 @@ export class ExecuteSalaryCurrentService { const profile: any = await profileRepository.findOneBy({ id: item.profileId }); if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, `ไม่พบข้อมูลทะเบียนประวัตินี้ profileId: ${item.profileId}`); } let _null: any = null; const dest_item = await salaryRepo.findOne({ @@ -250,6 +252,9 @@ export class ExecuteSalaryCurrentService { await salaryHistoryRepo.save(history, { data: req }); // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา + console.log( + `[ExecuteSalaryCurrentService] STEP 1: Finding posMaster — posmasterId: ${item.posmasterId}, profileId: ${item.profileId}`, + ); let posMaster = await posMasterRepository.findOne({ where: { id: item.posmasterId }, relations: { @@ -261,14 +266,21 @@ export class ExecuteSalaryCurrentService { orgChild4: true, }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 1: posMaster found: ${!!posMaster}, ancestorDNA: ${posMaster?.ancestorDNA ?? "null"}, orgRevisionId: ${posMaster?.orgRevisionId ?? "null"}`, + ); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ const isCurrent = posMaster?.orgRevision?.orgRevisionIsCurrent === true && posMaster?.orgRevision?.orgRevisionIsDraft === false; + console.log(`[ExecuteSalaryCurrentService] STEP 1: isCurrent: ${isCurrent}`); // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA if (!isCurrent && posMaster?.ancestorDNA) { + console.log( + `[ExecuteSalaryCurrentService] STEP 1: Not current — re-resolving via ancestorDNA: ${posMaster.ancestorDNA}`, + ); posMaster = await posMasterRepository.findOne({ where: { ancestorDNA: posMaster.ancestorDNA, @@ -286,13 +298,16 @@ export class ExecuteSalaryCurrentService { orgChild4: true, }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 1: ancestorDNA re-resolve — found: ${!!posMaster}`, + ); } if (posMaster == null) { console.error( - `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, + `[ExecuteSalaryCurrentService] STEP 1: PosMaster not found — posmasterId: ${item.posmasterId}`, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, `ไม่พบข้อมูลตำแหน่งนี้ posMasterId: ${item.posmasterId}`); } const posMasterOld = await posMasterRepository.findOne({ @@ -359,6 +374,9 @@ export class ExecuteSalaryCurrentService { await posMasterRepository.save(posMasterOld); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + console.log( + `[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`, + ); } await posMasterRepository.save(posMaster); @@ -375,6 +393,10 @@ export class ExecuteSalaryCurrentService { const posTypeId = item.positionTypeId || item.positionType; const posLevelId = item.positionLevelId || item.positionLevel; + console.log( + `[ExecuteSalaryCurrentService] STEP 2: Resolving position — posMasterId: ${posMaster.id}, positionId: ${item.positionId ?? "null"}, positionName: ${item.positionName ?? "null"}, posTypeId: ${posTypeId ?? "null"}, posLevelId: ${posLevelId ?? "null"}`, + ); + // ═══════════════════════════════════════════════════════════ // CONDITION 1: เช็คจาก positionId ตรง // ═══════════════════════════════════════════════════════════ @@ -386,6 +408,9 @@ export class ExecuteSalaryCurrentService { }, relations: ["posExecutive"], }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 1: match: ${!!positionById}`, + ); if (positionById) { positionNew = positionById; @@ -423,6 +448,10 @@ export class ExecuteSalaryCurrentService { relations: ["posExecutive"], order: { orderNo: "ASC" }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 2: match: ${!!positionBy7Fields}`, + whereCondition, + ); if (positionBy7Fields) { positionNew = positionBy7Fields; @@ -443,31 +472,18 @@ export class ExecuteSalaryCurrentService { relations: ["posExecutive"], order: { orderNo: "ASC" }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 3: match: ${!!positionBy3Fields}`, + ); if (positionBy3Fields) { positionNew = positionBy3Fields; } } - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // if (!positionNew) { - // const fallbackPositions = await positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } + console.log( + `[ExecuteSalaryCurrentService] STEP 2: Resolved positionNew: ${positionNew ? positionNew.id : "null (no match — profile position not updated)"}`, + ); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { @@ -488,8 +504,15 @@ export class ExecuteSalaryCurrentService { profile.amountSpecial = item.amountSpecial ?? null; await profileRepository.save(profile); await positionRepository.save(positionNew); + console.log( + `[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`, + ); } // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); + + console.log( + `[ExecuteSalaryCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`, + ); } } diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index c1d6e778..d84a3eed 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -388,16 +388,8 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); } else if (isSalaryCurrent) { - const salaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( - resultData, - ctx, - ); - console.log( - `[AMQ] Processed via ExecuteSalaryCurrentService — success: ${salaryResult.successCount}, failure: ${salaryResult.failureCount}`, - ); - for (const f of salaryResult.failures) { - console.error(`[AMQ] ExecuteSalaryCurrentService failed profileId=${f.profileId}: ${f.reason}`); - } + await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`); } else if (isSalaryEmployeeCurrent) { await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); From 832c5d2cb36f2d46ef9c33f54e486624b2b9e485 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 24 Jun 2026 18:05:54 +0700 Subject: [PATCH 04/12] add transaction #224 --- src/controllers/CommandController.ts | 15 +- src/interfaces/utils.ts | 61 +- src/services/CommandService.ts | 28 +- src/services/ExecuteOfficerProfileService.ts | 2048 +++++++++-------- src/services/ExecuteSalaryCurrentService.ts | 31 +- .../ExecuteSalaryEmployeeCurrentService.ts | 282 ++- .../ExecuteSalaryEmployeeLeaveService.ts | 385 ++-- src/services/ExecuteSalaryLeaveService.ts | 904 ++++---- src/services/ExecuteSalaryService.ts | 386 ++-- src/services/PositionService.ts | 173 +- 10 files changed, 2322 insertions(+), 1991 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index d0dbbe51..1ea05633 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -106,7 +106,7 @@ import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; import { RetirementService } from "../services/RetirementService"; import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileService"; import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; -import { ExecuteSalaryCurrentService, ExecuteSalaryResult } from "../services/ExecuteSalaryCurrentService"; +import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; @@ -3702,14 +3702,11 @@ export class CommandController extends Controller { }[]; }, ) { - const result: ExecuteSalaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( - body.data, - { - user: { sub: req.user.sub, name: req.user.name }, - req, - }, - ); - return new HttpSuccess(result); + await new ExecuteSalaryCurrentService().executeSalaryCurrent(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, + }); + return new HttpSuccess(); } @Post("excexute/salary-employee-current") diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index 7b55fd0f..27b35585 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -4,7 +4,7 @@ import { PosMaster } from "../entities/PosMaster"; import { Position } from "../entities/Position"; import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { EmployeePosition } from "../entities/EmployeePosition"; -import { In, IsNull, MoreThan, Not } from "typeorm"; +import { EntityManager, In, IsNull, MoreThan, Not } from "typeorm"; import { RequestWithUser } from "../middlewares/user"; import { Command } from "../entities/Command"; import { ProfileSalary } from "../entities/ProfileSalary"; @@ -254,14 +254,23 @@ export function calculateRetireYear(birthDate: Date) { return yy + 61; } -export async function removeProfileInOrganize(profileId: string, type: string) { - const currentRevision = await AppDataSource.getRepository(OrgRevision) +export async function removeProfileInOrganize( + profileId: string, + type: string, + manager?: EntityManager, +) { + // ถ้าส่ง manager เข้ามา → ทุก query/update อยู่ใน transaction ของ caller (all-or-nothing) + // ถ้าไม่ส่ง → ใช้ global DataSource เหมือนเดิม (backward compatible) + const ds = manager ?? AppDataSource; + const currentRevision = await ds + .getRepository(OrgRevision) .createQueryBuilder("orgRevision") .where("orgRevision.orgRevisionIsDraft = false") .andWhere("orgRevision.orgRevisionIsCurrent = true") .getOne(); - const draftRevision = await AppDataSource.getRepository(OrgRevision) + const draftRevision = await ds + .getRepository(OrgRevision) .createQueryBuilder("orgRevision") .where("orgRevision.orgRevisionIsDraft = true") .andWhere("orgRevision.orgRevisionIsCurrent = false") @@ -271,26 +280,30 @@ export async function removeProfileInOrganize(profileId: string, type: string) { return; } if (type === "OFFICER") { - const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) + const findProfileInposMaster = await ds + .getRepository(PosMaster) .createQueryBuilder("posMaster") .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) .andWhere("posMaster.current_holderId = :profileId", { profileId }) .getOne(); - await AppDataSource.getRepository(PosMaster) + await ds + .getRepository(PosMaster) .createQueryBuilder() .update(PosMaster) .set({ current_holderId: null, isSit: false }) .where("id = :id", { id: findProfileInposMaster?.id }) .execute(); - const findProfileInposMasterDraft = await AppDataSource.getRepository(PosMaster) + const findProfileInposMasterDraft = await ds + .getRepository(PosMaster) .createQueryBuilder("posMaster") .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: draftRevision?.id }) .andWhere("posMaster.next_holderId = :profileId", { profileId }) .getOne(); - await AppDataSource.getRepository(PosMaster) + await ds + .getRepository(PosMaster) .createQueryBuilder() .update(PosMaster) .set({ next_holderId: null, isSit: false }) @@ -300,7 +313,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) { if (!findProfileInposMaster && !findProfileInposMasterDraft) { return; } - const findPosition = await AppDataSource.getRepository(Position) + const findPosition = await ds + .getRepository(Position) .createQueryBuilder("position") .where("position.posMasterId = :posMasterId", { posMasterId: findProfileInposMaster?.id }) .getMany(); @@ -308,7 +322,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) { if (!findPosition) { return; } - await AppDataSource.getRepository(Position) + await ds + .getRepository(Position) .createQueryBuilder() .update(Position) .set({ positionIsSelected: false }) @@ -316,14 +331,16 @@ export async function removeProfileInOrganize(profileId: string, type: string) { .execute(); } if (type === "EMPLOYEE") { - const findProfileInEmpPosMaster = await AppDataSource.getRepository(EmployeePosMaster) + const findProfileInEmpPosMaster = await ds + .getRepository(EmployeePosMaster) .createQueryBuilder("employeePosMaster") .where("employeePosMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id, }) .andWhere("employeePosMaster.current_holderId = :profileId", { profileId }) .getOne(); - await AppDataSource.getRepository(EmployeePosMaster) + await ds + .getRepository(EmployeePosMaster) .createQueryBuilder() .update(EmployeePosMaster) .set({ current_holderId: null, isSit: false }) @@ -333,7 +350,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) { if (!findProfileInEmpPosMaster) { return; } - const findEmpPosition = await AppDataSource.getRepository(EmployeePosition) + const findEmpPosition = await ds + .getRepository(EmployeePosition) .createQueryBuilder("employeePosition") .where("employeePosition.posMasterId = :posMasterId", { posMasterId: findProfileInEmpPosMaster?.id, @@ -344,7 +362,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) { return; } - await AppDataSource.getRepository(EmployeePosition) + await ds + .getRepository(EmployeePosition) .createQueryBuilder() .update(EmployeePosition) .set({ positionIsSelected: false }) @@ -353,8 +372,10 @@ export async function removeProfileInOrganize(profileId: string, type: string) { } } -export async function removePostMasterAct(profileId: string) { - const currentRevision = await AppDataSource.getRepository(OrgRevision) +export async function removePostMasterAct(profileId: string, manager?: EntityManager) { + const ds = manager ?? AppDataSource; + const currentRevision = await ds + .getRepository(OrgRevision) .createQueryBuilder("orgRevision") .where("orgRevision.orgRevisionIsDraft = false") .andWhere("orgRevision.orgRevisionIsCurrent = true") @@ -364,7 +385,8 @@ export async function removePostMasterAct(profileId: string) { return; } - const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) + const findProfileInposMaster = await ds + .getRepository(PosMaster) .createQueryBuilder("posMaster") .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) .andWhere("posMaster.current_holderId = :profileId", { profileId }) @@ -374,11 +396,12 @@ export async function removePostMasterAct(profileId: string) { return; } - const posMasterAct = await AppDataSource.getRepository(PosMasterAct) + const posMasterAct = await ds + .getRepository(PosMasterAct) .createQueryBuilder("posMasterAct") .where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id }) .getMany(); - await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct); + await ds.getRepository(PosMasterAct).remove(posMasterAct); } export async function checkReturnCommandType(commandId: string) { diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts index b002462c..87334a4b 100644 --- a/src/services/CommandService.ts +++ b/src/services/CommandService.ts @@ -3,6 +3,7 @@ import { CommandRecive } from "../entities/CommandRecive"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; import { Profile } from "../entities/Profile"; +import { EntityManager } from "typeorm"; export interface PosNumCodeSitResult { posNumCodeSit: string; @@ -17,43 +18,42 @@ export interface PosNumCodeSitResult { * เรียงลำดับผู้ได้รับคำสั่งใหม่หลังจากลบรายการ และอัพเดทสถานะคำสั่งถ้าไม่มีผู้ได้รับคำสั่งเหลือ * @param reciveId commandRecive.Id ของผู้ได้รับคำสั่ง * @param code ประเภทคำสั่ง + * @param manager ถ้าส่งเข้ามา → ทุก operation อยู่ใน transaction ของ caller (all-or-nothing) * @returns Promise */ export async function reOrderCommandRecivesAndDelete( - reciveId: string + reciveId: string, + manager?: EntityManager, ): Promise { - const commandReciveRepo = AppDataSource.getRepository(CommandRecive); - const commandRepo = AppDataSource.getRepository(Command); + const ds = manager ?? AppDataSource; + const commandReciveRepo = ds.getRepository(CommandRecive); + const commandRepo = ds.getRepository(Command); // ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId const commandRecive = await commandReciveRepo.findOne({ - where: { id: reciveId } + where: { id: reciveId }, }); - if (commandRecive == null) - return; + if (commandRecive == null) return; const commandId = commandRecive.commandId; // ลบตาม refId await commandReciveRepo.delete(commandRecive.id); - + const commandReciveList = await commandReciveRepo.find({ where: { commandId: commandId }, order: { order: "ASC" }, }); // ลำดับผู้ได้รับคำสั่งใหม่ if (commandReciveList.length > 0) { - await Promise.all( - commandReciveList.map(async (p, i) => { - p.order = i + 1; - await commandReciveRepo.save(p); - }) - ); + for (let i = 0; i < commandReciveList.length; i++) { + commandReciveList[i].order = i + 1; + await commandReciveRepo.save(commandReciveList[i]); + } } else { // ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง await commandRepo.update({ id: commandId }, { status: "CANCEL" }); } - } /** diff --git a/src/services/ExecuteOfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts index 0d77719f..c9e0bacb 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -1,4 +1,4 @@ -import { In, Like } from "typeorm"; +import { EntityManager, In, Like } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -177,8 +177,12 @@ export class ExecuteOfficerProfileService { data: OfficerProfileItem[], ctx: ExecutionContext, ): Promise { - console.log("[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile"); - console.log("[ExecuteOfficerProfileService] Request body count:", data?.length); + const commandId = + data?.find((x: any) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "unknown"; + console.log( + `[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile — commandId: ${commandId}`, + ); + console.log(`[ExecuteOfficerProfileService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields @@ -326,1025 +330,1085 @@ export class ExecuteOfficerProfileService { data.length, "profile(s)", ); - await Promise.all( - data.map(async (item, index) => { - console.log( - "[ExecuteOfficerProfileService] Processing item", - index + 1, - "of", - data.length, - ); - const _null: any = null; - if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; - if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; - if ( - item.bodyProfile.posLevelId && - !(await this.posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) - ) { + + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne( + item, + ctx, + manager, + roleKeycloak, + list, + _posNumCodeSit, + _posNumCodeSitAbb, + meta, + before, + commandId, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; console.error( - "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", - item.bodyProfile.posLevelId, + `[ExecuteOfficerProfileService] Failed commandId=${commandId}, citizenId=${item.bodyProfile?.citizenId}: ${reason}`, + err, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure } - if ( - item.bodyProfile.posTypeId && - !(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) - ) { + } + }); + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของ batch (กัน partial commit) + */ + private async processOne( + item: OfficerProfileItem, + ctx: ExecutionContext, + manager: EntityManager, + roleKeycloak: RoleKeycloak | null, + list: any[], + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + meta: any, + before: any, + commandId: string, + ): Promise { + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // repo ทั้งหมดสร้างจาก manager เพื่อให้อยู่ใน transaction เดียวกัน + // ───────────────────────────────────────────────────────────── + const profileRepository = manager.getRepository(Profile); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const posLevelRepo = manager.getRepository(PosLevel); + const posTypeRepo = manager.getRepository(PosType); + const provinceRepo = manager.getRepository(Province); + const districtRepo = manager.getRepository(District); + const subDistrictRepo = manager.getRepository(SubDistrict); + const posMasterRepository = manager.getRepository(PosMaster); + const positionRepository = manager.getRepository(Position); + const profileEducationRepo = manager.getRepository(ProfileEducation); + const profileEducationHistoryRepo = manager.getRepository(ProfileEducationHistory); + const certificateRepo = manager.getRepository(ProfileCertificate); + const certificateHistoryRepo = manager.getRepository(ProfileCertificateHistory); + const profileFamilyCoupleRepo = manager.getRepository(ProfileFamilyCouple); + const profileFamilyCoupleHistoryRepo = manager.getRepository(ProfileFamilyCoupleHistory); + const profileFamilyFatherRepo = manager.getRepository(ProfileFamilyFather); + const profileFamilyFatherHistoryRepo = manager.getRepository(ProfileFamilyFatherHistory); + const profileFamilyMotherRepo = manager.getRepository(ProfileFamilyMother); + const profileFamilyMotherHistoryRepo = manager.getRepository(ProfileFamilyMotherHistory); + const insigniaRepo = manager.getRepository(ProfileInsignia); + const insigniaHistoryRepo = manager.getRepository(ProfileInsigniaHistory); + const avatarRepository = manager.getRepository(ProfileAvatar); + + console.log( + "[ExecuteOfficerProfileService] Processing citizenId:", + item.bodyProfile.citizenId, + ); + const _null: any = null; + if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; + if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; + if ( + item.bodyProfile.posLevelId && + !(await posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) + ) { + console.error( + "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", + item.bodyProfile.posLevelId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); + } + if ( + item.bodyProfile.posTypeId && + !(await posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) + ) { + console.error( + "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", + item.bodyProfile.posTypeId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + } + + console.log( + "[ExecuteOfficerProfileService] Processing citizenId:", + item.bodyProfile.citizenId, + ); + let registrationProvinceId = await provinceRepo.findOneBy({ + id: item.bodyProfile.registrationProvinceId ?? "", + }); + let registrationDistrictId = await districtRepo.findOneBy({ + id: item.bodyProfile.registrationDistrictId ?? "", + }); + let registrationSubDistrictId = await subDistrictRepo.findOneBy({ + id: item.bodyProfile.registrationSubDistrictId ?? "", + }); + let currentProvinceId = await provinceRepo.findOneBy({ + id: item.bodyProfile.currentProvinceId ?? "", + }); + let currentDistrictId = await districtRepo.findOneBy({ + id: item.bodyProfile.currentDistrictId ?? "", + }); + let currentSubDistrictId = await subDistrictRepo.findOneBy({ + id: item.bodyProfile.currentSubDistrictId ?? "", + }); + console.log("[ExecuteOfficerProfileService] Address validation completed"); + + let _dateRetire = + item.bodyProfile.birthDate == null + ? _null + : calculateRetireDate(item.bodyProfile.birthDate); + let _dateRetireLaw = + item.bodyProfile.birthDate == null + ? _null + : calculateRetireLaw(item.bodyProfile.birthDate); + + let userKeycloakId: any; + let result: any; + console.log( + "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", + item.bodyProfile.citizenId, + ); + const checkUser = await getUserByUsername(item.bodyProfile.citizenId); + console.log( + "[ExecuteOfficerProfileService] Keycloak user exists:", + checkUser.length > 0, + ); + if (checkUser.length == 0) { + console.log("[ExecuteOfficerProfileService] Creating new Keycloak user"); + let password = item.bodyProfile.citizenId; + if (item.bodyProfile.birthDate != null) { + const _date = new Date(item.bodyProfile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = ( + new Date(item.bodyProfile.birthDate.toDateString()).getMonth() + 1 + ) + .toString() + .padStart(2, "0"); + const _year = new Date(item.bodyProfile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + console.log( + "[ExecuteOfficerProfileService] Calling createUser for:", + item.bodyProfile.citizenId, + ); + console.log( + "[ExecuteOfficerProfileService] createUser data - firstName:", + item.bodyProfile.firstName, + "lastName:", + item.bodyProfile.lastName, + ); + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak (ป้องกัน . หรืออักขระอื่นๆ) + const sanitizedFirstName = item.bodyProfile.firstName?.replace(/\./g, "") ?? ""; + userKeycloakId = await createUser(item.bodyProfile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: item.bodyProfile.lastName, + }); + if ( + userKeycloakId && + typeof userKeycloakId === "object" && + userKeycloakId.errorMessage + ) { + console.error( + "[ExecuteOfficerProfileService] createUser FAILED - field:", + userKeycloakId.field, + "errorMessage:", + userKeycloakId.errorMessage, + "params:", + userKeycloakId.params, + ); + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`, + ); + } + console.log( + "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", + userKeycloakId, + ); + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + console.log( + "[ExecuteOfficerProfileService] USER role assigned to new user, result:", + result, + ); + } else { + console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); + userKeycloakId = checkUser[0].id; + console.log( + "[ExecuteOfficerProfileService] Existing userKeycloakId:", + userKeycloakId, + ); + const rolesData = await getRoleMappings(userKeycloakId); + if (rolesData) { + const _delRole = rolesData.map((x: any) => ({ + id: x.id, + name: x.name, + })); + console.log( + "[ExecuteOfficerProfileService] Removing old roles:", + _delRole.length, + ); + await removeUserRoles(userKeycloakId, _delRole); + } + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + console.log( + "[ExecuteOfficerProfileService] USER role assigned to existing user", + ); + } + + let profile: any = await profileRepository.findOne({ + where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, + relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], + }); + console.log( + "[ExecuteOfficerProfileService] Profile found:", + !!profile, + "for citizenId:", + item.bodyProfile.citizenId, + ); + let _oldInsigniaIds: string[] = []; + let _oldSalaries: any[] = []; + //ลูกจ้างประจำ หรือ บุคคลภายนอก + if (!profile) { + console.log( + "[ExecuteOfficerProfileService] No existing profile found, creating new profile", + ); + //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม + let profileEmployee: any = await profileEmployeeRepository.findOne({ + where: { citizenId: item.bodyProfile.citizenId }, + relations: ["profileInsignias", "roleKeycloaks"], + }); + console.log( + "[ExecuteOfficerProfileService] Employee profile found:", + !!profileEmployee, + ); + if (profileEmployee) { + console.log( + "[ExecuteOfficerProfileService] Converting employee profile to officer profile", + ); + const _order = await salaryRepo.findOne({ + where: { profileEmployeeId: profileEmployee.id }, + order: { order: "DESC" }, + }); + const profileEmpSalary = new ProfileSalary(); + profileEmpSalary.posNumCodeSit = _posNumCodeSit; + profileEmpSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + profileEmpSalary.order = _order == null ? 1 : _order.order + 1; + Object.assign(profileEmpSalary, { + ...item.bodySalarys, + ...meta, + profileEmployeeId: profileEmployee.id, + profileId: undefined, + }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...profileEmpSalary, id: undefined }); + profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt; + (profileEmpSalary.profileId = _null), + await salaryRepo.save(profileEmpSalary, { data: req }); + setLogDataDiff(req, { before, after: profileEmpSalary }); + history.profileSalaryId = profileEmpSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + if (profileEmployee.profileInsignias.length > 0) { + _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; + } + await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE", manager); + if (profileEmployee.keycloak != null) { + // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); + // if (delUserKeycloak) { + // Task #228 + // profileEmployee.keycloak = _null; + profileEmployee.roleKeycloaks = []; + profileEmployee.isActive = false; + // } + } + profileEmployee.isLeave = true; + profileEmployee.leaveReason = "บรรจุข้าราชการ"; + profileEmployee.lastUpdateUserId = ctx.user.sub; + profileEmployee.lastUpdateFullName = ctx.user.name; + profileEmployee.lastUpdatedAt = new Date(); + await profileEmployeeRepository.save(profileEmployee); + setLogDataDiff(req, { before, after: profileEmployee }); + } + profile = Object.assign({ ...item.bodyProfile, ...meta }); + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.isProbation = item.bodyProfile.isProbation; + //เพิ่มใหม่จากรับโอน + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = item.bodyProfile.firstName ?? null; + profile.lastName = item.bodyProfile.lastName ?? null; + profile.birthDate = item.bodyProfile.birthDate ?? null; + profile.gender = item.bodyProfile.gender ?? null; + profile.relationship = item.bodyProfile.relationship ?? null; + profile.religion = item.bodyProfile.religion ?? null; + profile.ethnicity = item.bodyProfile.ethnicity; + profile.nationality = item.bodyProfile.nationality ?? null; + profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; + profile.phone = item.bodyProfile.phone ?? null; + + console.log("[ExecuteOfficerProfileService] Saving new profile"); + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] New profile saved, profileId:", + profile.id, + ); + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); + setLogDataDiff(req, { before, after: profile }); + } + //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ + else { + console.log( + "[ExecuteOfficerProfileService] Existing profile found, isLeave:", + profile.isLeave, + "leaveType:", + profile.leaveType, + ); + //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก + if ( + profile.isLeave && + ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) + ) { + console.log( + "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + ); + //ดึง profileSalary เดิม + _oldSalaries = await salaryRepo.find({ + where: { profileId: profile.id }, + order: { order: "ASC" }, + }); + if (profile.profileInsignias.length > 0) { + _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; + } + profile = Object.assign({ ...item.bodyProfile, ...meta }); + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.isProbation = item.bodyProfile.isProbation; + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = item.bodyProfile.firstName ?? null; + profile.lastName = item.bodyProfile.lastName ?? null; + profile.birthDate = item.bodyProfile.birthDate ?? null; + profile.gender = item.bodyProfile.gender ?? null; + profile.relationship = item.bodyProfile.relationship ?? null; + profile.religion = item.bodyProfile.religion ?? null; + profile.ethnicity = item.bodyProfile.ethnicity; + profile.nationality = item.bodyProfile.nationality ?? null; + profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; + profile.phone = item.bodyProfile.phone ?? null; + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } else { + console.log( + "[ExecuteOfficerProfileService] Updating existing active profile", + ); + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.isProbation = item.bodyProfile.isProbation; + profile.isLeave = item.bodyProfile.isLeave; + profile.isRetirement = false; + profile.isActive = true; + profile.isDelete = false; + profile.dateLeave = _null; + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.telephoneNumber = item.bodyProfile.telephoneNumber; + profile.phone = item.bodyProfile.phone; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.leaveCommandId = _null; + profile.leaveCommandNo = _null; + profile.leaveRemark = _null; + profile.leaveDate = _null; + profile.leaveType = _null; + profile.leaveReason = _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + //เพิ่มใหม่จากรับโอน + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = + item.bodyProfile.firstName && item.bodyProfile.firstName != "" + ? item.bodyProfile.firstName + : profile.firstName; + profile.lastName = + item.bodyProfile.lastName && item.bodyProfile.lastName != "" + ? item.bodyProfile.lastName + : profile.lastName; + profile.birthDate = item.bodyProfile.birthDate + ? item.bodyProfile.birthDate + : profile.birthDate; + profile.gender = + item.bodyProfile.gender && item.bodyProfile.gender != "" + ? item.bodyProfile.gender + : profile.gender; + profile.relationship = + item.bodyProfile.relationship && item.bodyProfile.relationship != "" + ? item.bodyProfile.relationship + : profile.relationship; + profile.religion = + item.bodyProfile.religion && item.bodyProfile.religion != "" + ? item.bodyProfile.religion + : profile.religion; + profile.ethnicity = + item.bodyProfile.ethnicity && item.bodyProfile.ethnicity != "" + ? item.bodyProfile.ethnicity + : profile.ethnicity; + profile.nationality = + item.bodyProfile.nationality && item.bodyProfile.nationality != "" + ? item.bodyProfile.nationality + : profile.nationality; + profile.bloodGroup = + item.bodyProfile.bloodGroup && item.bodyProfile.bloodGroup != "" + ? item.bodyProfile.bloodGroup + : profile.bloodGroup; + profile.phone = + item.bodyProfile.phone && item.bodyProfile.phone != "" + ? item.bodyProfile.phone + : profile.phone; + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] Existing active profile updated, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } + } + + if (profile && profile.id) { + console.log( + "[ExecuteOfficerProfileService] Processing additional data for profileId:", + profile.id, + ); + //Educations + if (item.bodyEducations && item.bodyEducations.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing educations, count:", + item.bodyEducations.length, + ); + for (const education of item.bodyEducations) { + const profileEdu = new ProfileEducation(); + Object.assign(profileEdu, { ...education, ...meta }); + const eduHistory = new ProfileEducationHistory(); + Object.assign(eduHistory, { ...profileEdu, id: undefined }); + profileEdu.profileId = profile.id; + const educationLevel = await profileEducationRepo.findOne({ + select: ["id", "level", "profileId"], + where: { profileId: profile.id, isDeleted: false }, + order: { level: "DESC" }, + }); + profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; + await profileEducationRepo.save(profileEdu, { data: req }); + setLogDataDiff(req, { before, after: profileEdu }); + eduHistory.profileEducationId = profileEdu.id; + await profileEducationHistoryRepo.save(eduHistory, { data: req }); + } + } + //Certificates + if (item.bodyCertificates && item.bodyCertificates.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing certificates, count:", + item.bodyCertificates.length, + ); + for (const cer of item.bodyCertificates) { + const profileCer = new ProfileCertificate(); + Object.assign(profileCer, { ...cer, ...meta }); + const cerHistory = new ProfileCertificateHistory(); + Object.assign(cerHistory, { ...profileCer, id: undefined }); + profileCer.profileId = profile.id; + await certificateRepo.save(profileCer, { data: req }); + setLogDataDiff(req, { before, after: profileCer }); + cerHistory.profileCertificateId = profileCer.id; + await certificateHistoryRepo.save(cerHistory, { data: req }); + } + } + //FamilyCouple + if (item.bodyMarry != null) { + console.log("[ExecuteOfficerProfileService] Processing couple/marry data"); + const profileCouple = new ProfileFamilyCouple(); + const data = { + profileId: profile.id, + couple: item.bodyMarry.marry, + couplePrefix: item.bodyMarry.marryPrefix, + coupleFirstName: item.bodyMarry.marryFirstName, + coupleLastName: item.bodyMarry.marryLastName, + coupleCareer: item.bodyMarry.marryOccupation, + coupleLive: true, + }; + Object.assign(profileCouple, { ...data, ...meta }); + const coupleHistory = new ProfileFamilyCoupleHistory(); + Object.assign(coupleHistory, { ...profileCouple, id: undefined }); + profileCouple.profileId = profile.id; + await profileFamilyCoupleRepo.save(profileCouple, { data: req }); + setLogDataDiff(req, { before, after: profileCouple }); + coupleHistory.profileFamilyCoupleId = profileCouple.id; + await profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); + } + //FamilyFather + if (item.bodyFather != null) { + console.log("[ExecuteOfficerProfileService] Processing father data"); + const profileFather = new ProfileFamilyFather(); + const data = { + profileId: profile.id, + fatherPrefix: item.bodyFather.fatherPrefix, + fatherFirstName: item.bodyFather.fatherFirstName, + fatherLastName: item.bodyFather.fatherLastName, + fatherCareer: item.bodyFather.fatherOccupation, + fatherLive: true, + }; + Object.assign(profileFather, { ...data, ...meta }); + const fatherHistory = new ProfileFamilyFatherHistory(); + Object.assign(fatherHistory, { ...profileFather, id: undefined }); + profileFather.profileId = profile.id; + await profileFamilyFatherRepo.save(profileFather, { data: req }); + setLogDataDiff(req, { before, after: profileFather }); + fatherHistory.profileFamilyFatherId = profileFather.id; + await profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); + } + //FamilyMother + if (item.bodyMother != null) { + console.log("[ExecuteOfficerProfileService] Processing mother data"); + const profileMother = new ProfileFamilyMother(); + const data = { + profileId: profile.id, + motherPrefix: item.bodyMother.motherPrefix, + motherFirstName: item.bodyMother.motherFirstName, + motherLastName: item.bodyMother.motherLastName, + motherCareer: item.bodyMother.motherOccupation, + motherLive: true, + }; + Object.assign(profileMother, { ...data, ...meta }); + const motherHistory = new ProfileFamilyMotherHistory(); + Object.assign(motherHistory, { ...profileMother, id: undefined }); + profileMother.profileId = profile.id; + await profileFamilyMotherRepo.save(profileMother, { data: req }); + setLogDataDiff(req, { before, after: profileMother }); + motherHistory.profileFamilyMotherId = profileMother.id; + await profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); + } + //Salary + //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ + if (_oldSalaries.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Restoring old salaries, count:", + _oldSalaries.length, + ); + for (const oldSal of _oldSalaries) { + const profileSal: any = new ProfileSalary(); + Object.assign(profileSal, { ...oldSal, ...meta }); + const salaryHistory = new ProfileSalaryHistory(); + Object.assign(salaryHistory, { ...profileSal, id: undefined }); + profileSal.profileId = profile.id; + await salaryRepo.save(profileSal, { data: req }); + setLogDataDiff(req, { before, after: profileSal }); + salaryHistory.profileSalaryId = profileSal.id; + await salaryHistoryRepo.save(salaryHistory, { data: req }); + } + } + //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว + if (item.bodySalarys && item.bodySalarys != null) { + console.log("[ExecuteOfficerProfileService] Processing new salary data"); + const dest_item = await salaryRepo.findOne({ + where: { profileId: profile.id }, + order: { order: "DESC" }, + }); + const profileSal: any = new ProfileSalary(); + profileSal.posNumCodeSit = _posNumCodeSit; + profileSal.posNumCodeSitAbb = _posNumCodeSitAbb; + Object.assign(profileSal, { ...item.bodySalarys, ...meta }); + const salaryHistory = new ProfileSalaryHistory(); + Object.assign(salaryHistory, { ...profileSal, id: undefined }); + profileSal.order = dest_item == null ? 1 : dest_item.order + 1; + profileSal.profileId = profile.id; + profileSal.dateGovernment = item.bodySalarys.commandDateAffect ?? meta.createdAt; + profileSal.amount = item.bodySalarys.amount ?? null; + profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null; + profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null; + profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? null; + await salaryRepo.save(profileSal, { data: req }); + setLogDataDiff(req, { before, after: profileSal }); + salaryHistory.profileSalaryId = profileSal.id; + await salaryHistoryRepo.save(salaryHistory, { data: req }); + } + //Position + if (item.bodyPosition && item.bodyPosition != null) { + console.log("[ExecuteOfficerProfileService] Processing position assignment"); + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) + console.log( + "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", + item.bodyPosition.posmasterId, + ); + let posMaster = await posMasterRepository.findOne({ + where: { + id: item.bodyPosition.posmasterId, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + console.log("[ExecuteOfficerProfileService] posMaster found:", !!posMaster); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + console.log("[ExecuteOfficerProfileService] posMaster isCurrent:", isCurrent); + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + console.log( + "[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA", + ); + posMaster = await posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + console.log( + "[ExecuteOfficerProfileService] Current posMaster from ancestorDNA found:", + !!posMaster, + ); + } + + if (posMaster == null) { console.error( - "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", - item.bodyProfile.posTypeId, + `[ExecuteOfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); } - console.log( - "[ExecuteOfficerProfileService] Processing citizenId:", - item.bodyProfile.citizenId, - ); - let registrationProvinceId = await this.provinceRepo.findOneBy({ - id: item.bodyProfile.registrationProvinceId ?? "", + // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน + console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); + const posMasterOld = await posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, }); - let registrationDistrictId = await this.districtRepo.findOneBy({ - id: item.bodyProfile.registrationDistrictId ?? "", - }); - let registrationSubDistrictId = await this.subDistrictRepo.findOneBy({ - id: item.bodyProfile.registrationSubDistrictId ?? "", - }); - let currentProvinceId = await this.provinceRepo.findOneBy({ - id: item.bodyProfile.currentProvinceId ?? "", - }); - let currentDistrictId = await this.districtRepo.findOneBy({ - id: item.bodyProfile.currentDistrictId ?? "", - }); - let currentSubDistrictId = await this.subDistrictRepo.findOneBy({ - id: item.bodyProfile.currentSubDistrictId ?? "", - }); - console.log("[ExecuteOfficerProfileService] Address validation completed"); - - let _dateRetire = - item.bodyProfile.birthDate == null - ? _null - : calculateRetireDate(item.bodyProfile.birthDate); - let _dateRetireLaw = - item.bodyProfile.birthDate == null - ? _null - : calculateRetireLaw(item.bodyProfile.birthDate); - - let userKeycloakId: any; - let result: any; - console.log( - "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", - item.bodyProfile.citizenId, - ); - const checkUser = await getUserByUsername(item.bodyProfile.citizenId); - console.log( - "[ExecuteOfficerProfileService] Keycloak user exists:", - checkUser.length > 0, - ); - if (checkUser.length == 0) { - console.log("[ExecuteOfficerProfileService] Creating new Keycloak user"); - let password = item.bodyProfile.citizenId; - if (item.bodyProfile.birthDate != null) { - const _date = new Date(item.bodyProfile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = ( - new Date(item.bodyProfile.birthDate.toDateString()).getMonth() + 1 - ) - .toString() - .padStart(2, "0"); - const _year = new Date(item.bodyProfile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - console.log( - "[ExecuteOfficerProfileService] Calling createUser for:", - item.bodyProfile.citizenId, - ); - console.log( - "[ExecuteOfficerProfileService] createUser data - firstName:", - item.bodyProfile.firstName, - "lastName:", - item.bodyProfile.lastName, - ); - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak (ป้องกัน . หรืออักขระอื่นๆ) - const sanitizedFirstName = item.bodyProfile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(item.bodyProfile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: item.bodyProfile.lastName, - }); - if ( - userKeycloakId && - typeof userKeycloakId === "object" && - userKeycloakId.errorMessage - ) { - console.error( - "[ExecuteOfficerProfileService] createUser FAILED - field:", - userKeycloakId.field, - "errorMessage:", - userKeycloakId.errorMessage, - "params:", - userKeycloakId.params, - ); - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`, - ); - } - console.log( - "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", - userKeycloakId, - ); - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to new user, result:", - result, - ); - } else { - console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); - userKeycloakId = checkUser[0].id; - console.log( - "[ExecuteOfficerProfileService] Existing userKeycloakId:", - userKeycloakId, - ); - const rolesData = await getRoleMappings(userKeycloakId); - if (rolesData) { - const _delRole = rolesData.map((x: any) => ({ - id: x.id, - name: x.name, - })); - console.log( - "[ExecuteOfficerProfileService] Removing old roles:", - _delRole.length, - ); - await removeUserRoles(userKeycloakId, _delRole); - } - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to existing user", - ); + if (posMasterOld != null) { + // เคลียร์คนครองเก่าออกจากตำแหน่งเดิม + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); } - let profile: any = await this.profileRepository.findOne({ - where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, - relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], + // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก + const positionOld = await positionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, }); - console.log( - "[ExecuteOfficerProfileService] Profile found:", - !!profile, - "for citizenId:", - item.bodyProfile.citizenId, - ); - let _oldInsigniaIds: string[] = []; - let _oldSalaries: any[] = []; - //ลูกจ้างประจำ หรือ บุคคลภายนอก - if (!profile) { - console.log( - "[ExecuteOfficerProfileService] No existing profile found, creating new profile", - ); - //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม - let profileEmployee: any = await this.profileEmployeeRepository.findOne({ - where: { citizenId: item.bodyProfile.citizenId }, - relations: ["profileInsignias", "roleKeycloaks"], - }); - console.log( - "[ExecuteOfficerProfileService] Employee profile found:", - !!profileEmployee, - ); - if (profileEmployee) { - console.log( - "[ExecuteOfficerProfileService] Converting employee profile to officer profile", - ); - const _order = await this.salaryRepo.findOne({ - where: { profileEmployeeId: profileEmployee.id }, - order: { order: "DESC" }, - }); - const profileEmpSalary = new ProfileSalary(); - profileEmpSalary.posNumCodeSit = _posNumCodeSit; - profileEmpSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - profileEmpSalary.order = _order == null ? 1 : _order.order + 1; - Object.assign(profileEmpSalary, { - ...item.bodySalarys, - ...meta, - profileEmployeeId: profileEmployee.id, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...profileEmpSalary, id: undefined }); - profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt; - (profileEmpSalary.profileId = _null), - await this.salaryRepo.save(profileEmpSalary, { data: req }); - setLogDataDiff(req, { before, after: profileEmpSalary }); - history.profileSalaryId = profileEmpSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - if (profileEmployee.profileInsignias.length > 0) { - _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; - } - await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); - if (profileEmployee.keycloak != null) { - // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); - // if (delUserKeycloak) { - // Task #228 - // profileEmployee.keycloak = _null; - profileEmployee.roleKeycloaks = []; - profileEmployee.isActive = false; - // } - } - profileEmployee.isLeave = true; - profileEmployee.leaveReason = "บรรจุข้าราชการ"; - profileEmployee.lastUpdateUserId = ctx.user.sub; - profileEmployee.lastUpdateFullName = ctx.user.name; - profileEmployee.lastUpdatedAt = new Date(); - await this.profileEmployeeRepository.save(profileEmployee); - setLogDataDiff(req, { before, after: profileEmployee }); - } - profile = Object.assign({ ...item.bodyProfile, ...meta }); - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.isProbation = item.bodyProfile.isProbation; - //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = item.bodyProfile.firstName ?? null; - profile.lastName = item.bodyProfile.lastName ?? null; - profile.birthDate = item.bodyProfile.birthDate ?? null; - profile.gender = item.bodyProfile.gender ?? null; - profile.relationship = item.bodyProfile.relationship ?? null; - profile.religion = item.bodyProfile.religion ?? null; - profile.ethnicity = item.bodyProfile.ethnicity; - profile.nationality = item.bodyProfile.nationality ?? null; - profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; - profile.phone = item.bodyProfile.phone ?? null; - - console.log("[ExecuteOfficerProfileService] Saving new profile"); - await this.profileRepository.save(profile); - console.log( - "[ExecuteOfficerProfileService] New profile saved, profileId:", - profile.id, - ); - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); - setLogDataDiff(req, { before, after: profile }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await positionRepository.save(positionOld); } - //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ - else { + + // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ + console.log( + "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster", + ); + const checkPosition = await positionRepository.find({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await positionRepository.save(clearPosition); + } + + // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster + console.log( + "[ExecuteOfficerProfileService] STEP 4: Assigning new holder to posMaster", + ); + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + if (posMasterOld != null) { + await posMasterRepository.save(posMasterOld); console.log( - "[ExecuteOfficerProfileService] Existing profile found, isLeave:", - profile.isLeave, - "leaveType:", - profile.leaveType, + `[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, citizenId: ${item.bodyProfile?.citizenId} (old)`, ); - //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก - if ( - profile.isLeave && - ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) - ) { + await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + } + await posMasterRepository.save(posMaster); + console.log("[ExecuteOfficerProfileService] posMaster saved with new holder"); + + // STEP 5: กำหนด position ใหม่ + console.log( + "[ExecuteOfficerProfileService] STEP 5: Determining position to assign", + ); + // Match position ตามลำดับ priority: + // Condition 1: match จาก positionId + // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) + // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) + // Fallback: เลือก position แรกใน posMaster + + let positionNew: Position | null = null; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + console.log( + "[ExecuteOfficerProfileService] CONDITION 1: Checking by positionId:", + item.bodyPosition?.positionId, + ); + if (item.bodyPosition?.positionId) { + const positionById = await positionRepository.findOne({ + where: { + id: item.bodyPosition.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง + }, + relations: ["posExecutive"], + }); + + if (positionById) { + positionNew = positionById; console.log( - "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + "[ExecuteOfficerProfileService] CONDITION 1 matched, positionId:", + positionById.id, ); - //ดึง profileSalary เดิม - _oldSalaries = await this.salaryRepo.find({ - where: { profileId: profile.id }, - order: { order: "ASC" }, - }); - if (profile.profileInsignias.length > 0) { - _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; - } - profile = Object.assign({ ...item.bodyProfile, ...meta }); - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.isProbation = item.bodyProfile.isProbation; - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = item.bodyProfile.firstName ?? null; - profile.lastName = item.bodyProfile.lastName ?? null; - profile.birthDate = item.bodyProfile.birthDate ?? null; - profile.gender = item.bodyProfile.gender ?? null; - profile.relationship = item.bodyProfile.relationship ?? null; - profile.religion = item.bodyProfile.religion ?? null; - profile.ethnicity = item.bodyProfile.ethnicity; - profile.nationality = item.bodyProfile.nationality ?? null; - profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; - profile.phone = item.bodyProfile.phone ?? null; - await this.profileRepository.save(profile); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[ExecuteOfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", + ); + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.bodyPosition.positionName, + posTypeId: item.bodyPosition.posTypeId, + posLevelId: item.bodyPosition.posLevelId, + }; + + if (item.bodyPosition.positionField) { + whereCondition.positionField = item.bodyPosition.positionField; + } + if (item.bodyPosition.posExecutiveId) { + whereCondition.posExecutiveId = item.bodyPosition.posExecutiveId; + } + if (item.bodyPosition.positionExecutiveField) { + whereCondition.positionExecutiveField = item.bodyPosition.positionExecutiveField; + } + if (item.bodyPosition.positionArea) { + whereCondition.positionArea = item.bodyPosition.positionArea; + } + + const positionBy7Fields = await positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; console.log( - "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", - profile.id, + "[ExecuteOfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", + positionBy7Fields.id, + ); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", + ); + const positionBy3Fields = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.bodyPosition.positionName, + posTypeId: item.bodyPosition.posTypeId, + posLevelId: item.bodyPosition.posLevelId, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + console.log( + "[ExecuteOfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", + positionBy3Fields.id, ); - setLogDataDiff(req, { before, after: profile }); } else { console.log( - "[ExecuteOfficerProfileService] Updating existing active profile", - ); - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.isProbation = item.bodyProfile.isProbation; - profile.isLeave = item.bodyProfile.isLeave; - profile.isRetirement = false; - profile.isActive = true; - profile.isDelete = false; - profile.dateLeave = _null; - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.telephoneNumber = item.bodyProfile.telephoneNumber; - profile.phone = item.bodyProfile.phone; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.leaveCommandId = _null; - profile.leaveCommandNo = _null; - profile.leaveRemark = _null; - profile.leaveDate = _null; - profile.leaveType = _null; - profile.leaveReason = _null; - profile.lastUpdateUserId = ctx.user.sub; - profile.lastUpdateFullName = ctx.user.name; - profile.lastUpdatedAt = new Date(); - //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = - item.bodyProfile.firstName && item.bodyProfile.firstName != "" - ? item.bodyProfile.firstName - : profile.firstName; - profile.lastName = - item.bodyProfile.lastName && item.bodyProfile.lastName != "" - ? item.bodyProfile.lastName - : profile.lastName; - profile.birthDate = item.bodyProfile.birthDate - ? item.bodyProfile.birthDate - : profile.birthDate; - profile.gender = - item.bodyProfile.gender && item.bodyProfile.gender != "" - ? item.bodyProfile.gender - : profile.gender; - profile.relationship = - item.bodyProfile.relationship && item.bodyProfile.relationship != "" - ? item.bodyProfile.relationship - : profile.relationship; - profile.religion = - item.bodyProfile.religion && item.bodyProfile.religion != "" - ? item.bodyProfile.religion - : profile.religion; - profile.ethnicity = - item.bodyProfile.ethnicity && item.bodyProfile.ethnicity != "" - ? item.bodyProfile.ethnicity - : profile.ethnicity; - profile.nationality = - item.bodyProfile.nationality && item.bodyProfile.nationality != "" - ? item.bodyProfile.nationality - : profile.nationality; - profile.bloodGroup = - item.bodyProfile.bloodGroup && item.bodyProfile.bloodGroup != "" - ? item.bodyProfile.bloodGroup - : profile.bloodGroup; - profile.phone = - item.bodyProfile.phone && item.bodyProfile.phone != "" - ? item.bodyProfile.phone - : profile.phone; - await this.profileRepository.save(profile); - console.log( - "[ExecuteOfficerProfileService] Existing active profile updated, profileId:", + "[ExecuteOfficerProfileService] No position matched for profileId:", profile.id, ); - setLogDataDiff(req, { before, after: profile }); } } - if (profile && profile.id) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (positionNew != null) { console.log( - "[ExecuteOfficerProfileService] Processing additional data for profileId:", - profile.id, + "[ExecuteOfficerProfileService] Final position assignment, isSit:", + posMaster.isSit, + "positionId:", + positionNew.id, ); - //Educations - if (item.bodyEducations && item.bodyEducations.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing educations, count:", - item.bodyEducations.length, - ); - await Promise.all( - item.bodyEducations.map(async (education) => { - const profileEdu = new ProfileEducation(); - Object.assign(profileEdu, { ...education, ...meta }); - const eduHistory = new ProfileEducationHistory(); - Object.assign(eduHistory, { ...profileEdu, id: undefined }); - profileEdu.profileId = profile.id; - const educationLevel = await this.profileEducationRepo.findOne({ - select: ["id", "level", "profileId"], - where: { profileId: profile.id, isDeleted: false }, - order: { level: "DESC" }, - }); - profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; - await this.profileEducationRepo.save(profileEdu, { data: req }); - setLogDataDiff(req, { before, after: profileEdu }); - eduHistory.profileEducationId = profileEdu.id; - await this.profileEducationHistoryRepo.save(eduHistory, { data: req }); - }), - ); + positionNew.positionIsSelected = true; + if (!posMaster.isSit) { + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; + // profile.dateStart = new Date(); } - //Certificates - if (item.bodyCertificates && item.bodyCertificates.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing certificates, count:", - item.bodyCertificates.length, - ); - await Promise.all( - item.bodyCertificates.map(async (cer) => { - const profileCer = new ProfileCertificate(); - Object.assign(profileCer, { ...cer, ...meta }); - const cerHistory = new ProfileCertificateHistory(); - Object.assign(cerHistory, { ...profileCer, id: undefined }); - profileCer.profileId = profile.id; - await this.certificateRepo.save(profileCer, { data: req }); - setLogDataDiff(req, { before, after: profileCer }); - cerHistory.profileCertificateId = profileCer.id; - await this.certificateHistoryRepo.save(cerHistory, { data: req }); - }), - ); - } - //FamilyCouple - if (item.bodyMarry != null) { - console.log("[ExecuteOfficerProfileService] Processing couple/marry data"); - const profileCouple = new ProfileFamilyCouple(); - const data = { - profileId: profile.id, - couple: item.bodyMarry.marry, - couplePrefix: item.bodyMarry.marryPrefix, - coupleFirstName: item.bodyMarry.marryFirstName, - coupleLastName: item.bodyMarry.marryLastName, - coupleCareer: item.bodyMarry.marryOccupation, - coupleLive: true, - }; - Object.assign(profileCouple, { ...data, ...meta }); - const coupleHistory = new ProfileFamilyCoupleHistory(); - Object.assign(coupleHistory, { ...profileCouple, id: undefined }); - profileCouple.profileId = profile.id; - await this.profileFamilyCoupleRepo.save(profileCouple, { data: req }); - setLogDataDiff(req, { before, after: profileCouple }); - coupleHistory.profileFamilyCoupleId = profileCouple.id; - await this.profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); - } - //FamilyFather - if (item.bodyFather != null) { - console.log("[ExecuteOfficerProfileService] Processing father data"); - const profileFather = new ProfileFamilyFather(); - const data = { - profileId: profile.id, - fatherPrefix: item.bodyFather.fatherPrefix, - fatherFirstName: item.bodyFather.fatherFirstName, - fatherLastName: item.bodyFather.fatherLastName, - fatherCareer: item.bodyFather.fatherOccupation, - fatherLive: true, - }; - Object.assign(profileFather, { ...data, ...meta }); - const fatherHistory = new ProfileFamilyFatherHistory(); - Object.assign(fatherHistory, { ...profileFather, id: undefined }); - profileFather.profileId = profile.id; - await this.profileFamilyFatherRepo.save(profileFather, { data: req }); - setLogDataDiff(req, { before, after: profileFather }); - fatherHistory.profileFamilyFatherId = profileFather.id; - await this.profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); - } - //FamilyMother - if (item.bodyMother != null) { - console.log("[ExecuteOfficerProfileService] Processing mother data"); - const profileMother = new ProfileFamilyMother(); - const data = { - profileId: profile.id, - motherPrefix: item.bodyMother.motherPrefix, - motherFirstName: item.bodyMother.motherFirstName, - motherLastName: item.bodyMother.motherLastName, - motherCareer: item.bodyMother.motherOccupation, - motherLive: true, - }; - Object.assign(profileMother, { ...data, ...meta }); - const motherHistory = new ProfileFamilyMotherHistory(); - Object.assign(motherHistory, { ...profileMother, id: undefined }); - profileMother.profileId = profile.id; - await this.profileFamilyMotherRepo.save(profileMother, { data: req }); - setLogDataDiff(req, { before, after: profileMother }); - motherHistory.profileFamilyMotherId = profileMother.id; - await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); - } - //Salary - //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ - if (_oldSalaries.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Restoring old salaries, count:", - _oldSalaries.length, - ); - await Promise.all( - _oldSalaries.map(async (oldSal) => { - const profileSal: any = new ProfileSalary(); - Object.assign(profileSal, { ...oldSal, ...meta }); - const salaryHistory = new ProfileSalaryHistory(); - Object.assign(salaryHistory, { ...profileSal, id: undefined }); - profileSal.profileId = profile.id; - await this.salaryRepo.save(profileSal, { data: req }); - setLogDataDiff(req, { before, after: profileSal }); - salaryHistory.profileSalaryId = profileSal.id; - await this.salaryHistoryRepo.save(salaryHistory, { data: req }); - }), - ); - } - //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว - if (item.bodySalarys && item.bodySalarys != null) { - console.log("[ExecuteOfficerProfileService] Processing new salary data"); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: profile.id }, - order: { order: "DESC" }, - }); - const profileSal: any = new ProfileSalary(); - profileSal.posNumCodeSit = _posNumCodeSit; - profileSal.posNumCodeSitAbb = _posNumCodeSitAbb; - Object.assign(profileSal, { ...item.bodySalarys, ...meta }); - const salaryHistory = new ProfileSalaryHistory(); - Object.assign(salaryHistory, { ...profileSal, id: undefined }); - profileSal.order = dest_item == null ? 1 : dest_item.order + 1; - profileSal.profileId = profile.id; - profileSal.dateGovernment = item.bodySalarys.commandDateAffect ?? meta.createdAt; - profileSal.amount = item.bodySalarys.amount ?? null; - profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null; - profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null; - profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? null; - await this.salaryRepo.save(profileSal, { data: req }); - setLogDataDiff(req, { before, after: profileSal }); - salaryHistory.profileSalaryId = profileSal.id; - await this.salaryHistoryRepo.save(salaryHistory, { data: req }); - } - //Position - if (item.bodyPosition && item.bodyPosition != null) { - console.log("[ExecuteOfficerProfileService] Processing position assignment"); - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) - console.log( - "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", - item.bodyPosition.posmasterId, - ); - let posMaster = await this.posMasterRepository.findOne({ - where: { - id: item.bodyPosition.posmasterId, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - console.log("[ExecuteOfficerProfileService] posMaster found:", !!posMaster); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - console.log("[ExecuteOfficerProfileService] posMaster isCurrent:", isCurrent); - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - console.log( - "[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA", - ); - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - console.log( - "[ExecuteOfficerProfileService] Current posMaster from ancestorDNA found:", - !!posMaster, - ); - } - - if (posMaster == null) { - console.error( - `[ExecuteOfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน - console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); - const posMasterOld = await this.posMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - // เคลียร์คนครองเก่าออกจากตำแหน่งเดิม - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - - // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก - const positionOld = await this.positionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.positionRepository.save(positionOld); - } - - // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ - console.log( - "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster", - ); - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.positionRepository.save(clearPosition); - } - - // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster - console.log( - "[ExecuteOfficerProfileService] STEP 4: Assigning new holder to posMaster", - ); - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - if (posMasterOld != null) { - await this.posMasterRepository.save(posMasterOld); - await CreatePosMasterHistoryOfficer(posMasterOld.id, req); - } - await this.posMasterRepository.save(posMaster); - console.log("[ExecuteOfficerProfileService] posMaster saved with new holder"); - - // STEP 5: กำหนด position ใหม่ - console.log( - "[ExecuteOfficerProfileService] STEP 5: Determining position to assign", - ); - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - console.log( - "[ExecuteOfficerProfileService] CONDITION 1: Checking by positionId:", - item.bodyPosition?.positionId, - ); - if (item.bodyPosition?.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.bodyPosition.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - console.log( - "[ExecuteOfficerProfileService] CONDITION 1 matched, positionId:", - positionById.id, - ); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log( - "[ExecuteOfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", - ); - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, - }; - - if (item.bodyPosition.positionField) { - whereCondition.positionField = item.bodyPosition.positionField; - } - if (item.bodyPosition.posExecutiveId) { - whereCondition.posExecutiveId = item.bodyPosition.posExecutiveId; - } - if (item.bodyPosition.positionExecutiveField) { - whereCondition.positionExecutiveField = item.bodyPosition.positionExecutiveField; - } - if (item.bodyPosition.positionArea) { - whereCondition.positionArea = item.bodyPosition.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - console.log( - "[ExecuteOfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", - positionBy7Fields.id, - ); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log( - "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", - ); - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - console.log( - "[ExecuteOfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", - positionBy3Fields.id, - ); - } else { - console.log( - "[ExecuteOfficerProfileService] No position matched for profileId:", - profile.id, - ); - } - } - - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (positionNew != null) { - console.log( - "[ExecuteOfficerProfileService] Final position assignment, isSit:", - posMaster.isSit, - "positionId:", - positionNew.id, - ); - positionNew.positionIsSelected = true; - if (!posMaster.isSit) { - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.positionField = positionNew.positionField ?? null; - profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; - profile.positionArea = positionNew.positionArea ?? null; - profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; - // profile.dateStart = new Date(); - } - await this.positionRepository.save(positionNew, { data: req }); - } else if (!posMaster.isSit) { - // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา - console.log( - "[ExecuteOfficerProfileService] positionNew is null, using bodyPosition data as fallback", - ); - profile.position = item.bodyPosition.positionName ?? null; - profile.posTypeId = item.bodyPosition.posTypeId ?? null; - profile.posLevelId = item.bodyPosition.posLevelId ?? null; - profile.positionField = item.bodyPosition.positionField ?? null; - profile.positionArea = item.bodyPosition.positionArea ?? null; - profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? null; - } - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); - // await CreatePosMasterHistoryOfficer(posMaster.id, req); - await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { - positionId: positionNew?.id, - }); - } - // Insignia - if (_oldInsigniaIds.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing old insignias, count:", - _oldInsigniaIds.length, - ); - const _insignias = await this.insigniaRepo.find({ - where: { id: In(_oldInsigniaIds), isDeleted: false }, - order: { createdAt: "ASC" }, - }); - for (const oldInsignia of _insignias) { - const newInsigniaData: CreateProfileInsignia = { - profileId: profile.id, - year: oldInsignia.year, - no: oldInsignia.no, - volume: oldInsignia.volume, - section: oldInsignia.section, - page: oldInsignia.page, - receiveDate: oldInsignia.receiveDate, - insigniaId: oldInsignia.insigniaId, - dateAnnounce: oldInsignia.dateAnnounce, - issue: oldInsignia.issue, - volumeNo: oldInsignia.volumeNo, - refCommandDate: oldInsignia.refCommandDate, - refCommandNo: oldInsignia.refCommandNo, - note: oldInsignia.note, - isUpload: oldInsignia.isUpload, - }; - const insignia = new ProfileInsignia(); - Object.assign(insignia, { ...newInsigniaData, ...meta }); - const history = new ProfileInsigniaHistory(); - Object.assign(history, { ...insignia, id: undefined }); - await this.insigniaRepo.save(insignia, { data: req }); - setLogDataDiff(req, { before, after: insignia }); - history.profileInsigniaId = insignia.id; - await this.insigniaHistoryRepo.save(history, { data: req }); - } - } - // เพิ่มรูปภาพโปรไฟล์ - if (item.bodyProfile.objectRefId) { - console.log( - "[ExecuteOfficerProfileService] Processing profile avatar image, objectRefId:", - item.bodyProfile.objectRefId, - ); - const _profileAvatar = new ProfileAvatar(); - Object.assign(_profileAvatar, { - ...meta, - profileId: profile.id, - profileEmployeeId: undefined, - }); - if (profile.profileAvatars && profile.profileAvatars.length > 0) { - await Promise.all( - profile.profileAvatars.map(async (item: any) => { - item.isActive = false; - await this.avatarRepository.save(item); - }), - ); - } - await this.avatarRepository.save(_profileAvatar); - let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`; - let fileName = `profile-${_profileAvatar.id}`; - _profileAvatar.isActive = true; - _profileAvatar.avatar = avatar; - _profileAvatar.avatarName = fileName; - await this.avatarRepository.save(_profileAvatar, { data: req }); - profile.avatar = avatar; - profile.avatarName = fileName; - await this.profileRepository.save(profile, { data: req }); - const checkAvatar = await this.avatarRepository.findOne({ - where: { avatar: avatar, avatarName: fileName }, - }); - if (checkAvatar && checkAvatar.profileId == null) { - checkAvatar.profileId = profile.id; - await this.avatarRepository.save(checkAvatar); - } - //duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม - await new CallAPI() - .PostData(req, `/salary/file/avatar/${item.bodyProfile.objectRefId}`, { - prefix: avatar, - fileName: fileName, - }) - .then(() => {}) - .catch(() => {}); + await positionRepository.save(positionNew, { data: req }); + } else if (!posMaster.isSit) { + // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา + console.log( + "[ExecuteOfficerProfileService] positionNew is null, using bodyPosition data as fallback", + ); + profile.position = item.bodyPosition.positionName ?? null; + profile.posTypeId = item.bodyPosition.posTypeId ?? null; + profile.posLevelId = item.bodyPosition.posLevelId ?? null; + profile.positionField = item.bodyPosition.positionField ?? null; + profile.positionArea = item.bodyPosition.positionArea ?? null; + profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? null; + } + await profileRepository.save(profile, { data: req }); + setLogDataDiff(req, { before, after: profile }); + // await CreatePosMasterHistoryOfficer(posMaster.id, req); + console.log( + `[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, citizenId: ${item.bodyProfile?.citizenId}`, + ); + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { + positionId: positionNew?.id, + }, manager); + } + // Insignia + if (_oldInsigniaIds.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing old insignias, count:", + _oldInsigniaIds.length, + ); + const _insignias = await insigniaRepo.find({ + where: { id: In(_oldInsigniaIds), isDeleted: false }, + order: { createdAt: "ASC" }, + }); + for (const oldInsignia of _insignias) { + const newInsigniaData: CreateProfileInsignia = { + profileId: profile.id, + year: oldInsignia.year, + no: oldInsignia.no, + volume: oldInsignia.volume, + section: oldInsignia.section, + page: oldInsignia.page, + receiveDate: oldInsignia.receiveDate, + insigniaId: oldInsignia.insigniaId, + dateAnnounce: oldInsignia.dateAnnounce, + issue: oldInsignia.issue, + volumeNo: oldInsignia.volumeNo, + refCommandDate: oldInsignia.refCommandDate, + refCommandNo: oldInsignia.refCommandNo, + note: oldInsignia.note, + isUpload: oldInsignia.isUpload, + }; + const insignia = new ProfileInsignia(); + Object.assign(insignia, { ...newInsigniaData, ...meta }); + const history = new ProfileInsigniaHistory(); + Object.assign(history, { ...insignia, id: undefined }); + await insigniaRepo.save(insignia, { data: req }); + setLogDataDiff(req, { before, after: insignia }); + history.profileInsigniaId = insignia.id; + await insigniaHistoryRepo.save(history, { data: req }); + } + } + // เพิ่มรูปภาพโปรไฟล์ + if (item.bodyProfile.objectRefId) { + console.log( + "[ExecuteOfficerProfileService] Processing profile avatar image, objectRefId:", + item.bodyProfile.objectRefId, + ); + const _profileAvatar = new ProfileAvatar(); + Object.assign(_profileAvatar, { + ...meta, + profileId: profile.id, + profileEmployeeId: undefined, + }); + if (profile.profileAvatars && profile.profileAvatars.length > 0) { + for (const avatarItem of profile.profileAvatars) { + avatarItem.isActive = false; + await avatarRepository.save(avatarItem); } } - }), + await avatarRepository.save(_profileAvatar); + let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`; + let fileName = `profile-${_profileAvatar.id}`; + _profileAvatar.isActive = true; + _profileAvatar.avatar = avatar; + _profileAvatar.avatarName = fileName; + await avatarRepository.save(_profileAvatar, { data: req }); + profile.avatar = avatar; + profile.avatarName = fileName; + await profileRepository.save(profile, { data: req }); + const checkAvatar = await avatarRepository.findOne({ + where: { avatar: avatar, avatarName: fileName }, + }); + if (checkAvatar && checkAvatar.profileId == null) { + checkAvatar.profileId = profile.id; + await avatarRepository.save(checkAvatar); + } + //duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม + await new CallAPI() + .PostData(req, `/salary/file/avatar/${item.bodyProfile.objectRefId}`, { + prefix: avatar, + fileName: fileName, + }) + .then(() => {}) + .catch(() => {}); + } + } + + console.log( + `[ExecuteOfficerProfileService] Completed processOne — citizenId: ${item.bodyProfile?.citizenId}`, ); - console.log("[ExecuteOfficerProfileService] executeCreateOfficerProfile completed successfully"); } } diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index 2f0871dc..d9039b30 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -60,17 +60,6 @@ export interface SalaryCurrentExecutionContext { req?: any; } -/** - * ผลลัพธ์การประมวลผล batch — all-or-nothing (single transaction ครอบทั้ง batch) - * ถ้าทุกคนสำเร็จจะ return result; ถ้ามีคนใด throw จะ rollback ทั้ง batch - * และ propagate error ออกไป (caller เห็นเป็น failure ทั้งหมด) - */ -export interface ExecuteSalaryResult { - successCount: number; - failureCount: number; - failures: { profileId: string; reason: string }[]; -} - /** * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) * @@ -98,7 +87,7 @@ export class ExecuteSalaryCurrentService { async executeSalaryCurrent( data: SalaryCurrentItem[], ctx: SalaryCurrentExecutionContext, - ): Promise { + ): Promise { const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; console.log( @@ -170,12 +159,10 @@ export class ExecuteSalaryCurrentService { // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow // ───────────────────────────────────────────────────────────── - let successCount = 0; await AppDataSource.transaction(async (manager) => { for (const item of data ?? []) { try { await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); - successCount++; } catch (err) { const reason = err instanceof HttpError @@ -191,12 +178,6 @@ export class ExecuteSalaryCurrentService { } } }); - - console.log( - `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: 0`, - ); - - return { successCount, failureCount: 0, failures: [] }; } /** @@ -373,10 +354,10 @@ export class ExecuteSalaryCurrentService { if (posMasterOld != null) { await posMasterRepository.save(posMasterOld); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน - await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); console.log( - `[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`, + `[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`, ); + await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); } await posMasterRepository.save(posMaster); @@ -504,11 +485,11 @@ export class ExecuteSalaryCurrentService { profile.amountSpecial = item.amountSpecial ?? null; await profileRepository.save(profile); await positionRepository.save(positionNew); - console.log( - `[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`, - ); } // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`, + ); await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); console.log( diff --git a/src/services/ExecuteSalaryEmployeeCurrentService.ts b/src/services/ExecuteSalaryEmployeeCurrentService.ts index 919144c1..93b6eaed 100644 --- a/src/services/ExecuteSalaryEmployeeCurrentService.ts +++ b/src/services/ExecuteSalaryEmployeeCurrentService.ts @@ -1,4 +1,4 @@ -import { Double } from "typeorm"; +import { Double, EntityManager } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -63,28 +63,29 @@ export interface SalaryEmployeeCurrentExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateCurrent ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count */ export class ExecuteSalaryEmployeeCurrentService { private commandRepository = AppDataSource.getRepository(Command); private profileRepository = AppDataSource.getRepository(Profile); - private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); - private salaryRepo = AppDataSource.getRepository(ProfileSalary); - private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); - private employeePositionRepository = AppDataSource.getRepository(EmployeePosition); private orgRootRepository = AppDataSource.getRepository(OrgRoot); /** - * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของลูกจ้าง + * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของลูกจ้างทั้ง batch */ async executeSalaryEmployeeCurrent( data: SalaryEmployeeCurrentItem[], ctx: SalaryEmployeeCurrentExecutionContext, ): Promise { - console.log("[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent"); - console.log("[ExecuteSalaryEmployeeCurrentService] Request body count:", data?.length); - - const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryEmployeeCurrentService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -144,119 +145,172 @@ export class ExecuteSalaryEmployeeCurrentService { .orgRootShortName ?? ""; } } - await Promise.all( - data.map(async (item) => { - const profile: any = await this.profileEmployeeRepository.findOneBy({ id: item.profileId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryEmployeeCurrentService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure } + } + }); + } - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const dataSalary = new ProfileSalary(); - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: ctx.user.sub, - createdFullName: ctx.user.name, - lastUpdateUserId: ctx.user.sub, - lastUpdateFullName: ctx.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit) + */ + private async processOne( + item: SalaryEmployeeCurrentItem, + ctx: SalaryEmployeeCurrentExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; - Object.assign(dataSalary, { - ...item, - ...meta, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + const employeePositionRepository = manager.getRepository(EmployeePosition); - await this.salaryRepo.save(dataSalary, { data: req }); - setLogDataDiff(req, { before, after: dataSalary }); - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); + const profile: any = await profileEmployeeRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } - const posMaster = await this.employeePosMasterRepository.findOne({ - where: { id: item.posmasterId }, - relations: ["orgRoot"], - }); - if (posMaster == null) - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; - const posMasterOld = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: item.profileId, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - // if (posMasterOld != null) posMasterOld.next_holderId = null; + Object.assign(dataSalary, { + ...item, + ...meta, + profileEmployeeId: item.profileId, + profileId: undefined, + }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); - const positionOld = await this.employeePositionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.employeePositionRepository.save(positionOld); - } + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); - const checkPosition = await this.employeePositionRepository.find({ - where: { - posMasterId: item.posmasterId, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.employeePositionRepository.save(clearPosition); - } + const posMaster = await employeePosMasterRepository.findOne({ + where: { id: item.posmasterId }, + relations: ["orgRoot"], + }); + if (posMaster == null) + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - posMaster.current_holderId = item.profileId; - posMaster.lastUpdatedAt = new Date(); - posMaster.next_holderId = null; - if (posMasterOld != null) { - await this.employeePosMasterRepository.save(posMasterOld); - await CreatePosMasterHistoryEmployee(posMasterOld.id, req); - } - await this.employeePosMasterRepository.save(posMaster); - const positionNew = await this.employeePositionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: item.posmasterId, - }, - }); - if (positionNew != null) { - positionNew.positionIsSelected = true; - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; - profile.positionEmployeePositionId = positionNew.positionName; - profile.amount = item.amount ?? null; - profile.amountSpecial = item.amountSpecial ?? null; - await this.profileEmployeeRepository.save(profile); - await this.employeePositionRepository.save(positionNew); - } - await CreatePosMasterHistoryEmployee(posMaster.id, req); - }), + const posMasterOld = await employeePosMasterRepository.findOne({ + where: { + current_holderId: item.profileId, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + // if (posMasterOld != null) posMasterOld.next_holderId = null; + + const positionOld = await employeePositionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await employeePositionRepository.save(positionOld); + } + + const checkPosition = await employeePositionRepository.find({ + where: { + posMasterId: item.posmasterId, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await employeePositionRepository.save(clearPosition); + } + + posMaster.current_holderId = item.profileId; + posMaster.lastUpdatedAt = new Date(); + posMaster.next_holderId = null; + if (posMasterOld != null) { + await employeePosMasterRepository.save(posMasterOld); + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`, + ); + await CreatePosMasterHistoryEmployee(posMasterOld.id, req, null, manager); + } + await employeePosMasterRepository.save(posMaster); + const positionNew = await employeePositionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: item.posmasterId, + }, + }); + if (positionNew != null) { + positionNew.positionIsSelected = true; + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; + profile.positionEmployeePositionId = positionNew.positionName; + profile.amount = item.amount ?? null; + profile.amountSpecial = item.amountSpecial ?? null; + await profileEmployeeRepository.save(profile); + await employeePositionRepository.save(positionNew); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`, ); + await CreatePosMasterHistoryEmployee(posMaster.id, req, null, manager); - console.log("[ExecuteSalaryEmployeeCurrentService] executeSalaryEmployeeCurrent completed successfully"); + console.log( + `[ExecuteSalaryEmployeeCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`, + ); } } diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts index bfb1af4c..46952847 100644 --- a/src/services/ExecuteSalaryEmployeeLeaveService.ts +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -1,4 +1,4 @@ -import { Double } from "typeorm"; +import { Double, EntityManager } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatus from "../interfaces/http-status"; @@ -71,29 +71,30 @@ export interface SalaryEmployeeLeaveExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateLeave ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count */ export class ExecuteSalaryEmployeeLeaveService { private commandRepository = AppDataSource.getRepository(Command); private commandReciveRepository = AppDataSource.getRepository(CommandRecive); private profileRepository = AppDataSource.getRepository(Profile); - private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); - private salaryRepo = AppDataSource.getRepository(ProfileSalary); - private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster); private orgRootRepository = AppDataSource.getRepository(OrgRoot); - private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); /** - * ประมวลผลสร้าง ProfileSalary + handle leave ของลูกจ้าง + * ประมวลผลสร้าง ProfileSalary + handle leave ของลูกจ้างทั้ง batch */ async executeSalaryEmployeeLeave( data: SalaryEmployeeLeaveItem[], ctx: SalaryEmployeeLeaveExecutionContext, ): Promise { - console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave"); - console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length); - - const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryEmployeeLeaveService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -156,166 +157,222 @@ export class ExecuteSalaryEmployeeLeaveService { } } const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - data.map(async (item) => { - const profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.profileId }, - relations: { - roleKeycloaks: true, - posType: true, - posLevel: true, - }, - }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - const code = _command?.commandType?.code; - //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก - if (item.resignId && code && ["C-PM-42"].includes(code)) { - const commandResign = await this.commandReciveRepository.findOne({ - where: { refId: item.resignId }, - relations: { command: true }, - }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) - : today; - if ( - commandResign && - _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate) - ) { - await reOrderCommandRecivesAndDelete(commandResign!.id); - } - } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const dataSalary = new ProfileSalary(); - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: ctx.user.sub, - createdFullName: ctx.user.name, - lastUpdateUserId: ctx.user.sub, - lastUpdateFullName: ctx.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - Object.assign(dataSalary, { - ...item, - ...meta, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); - dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt; - await this.salaryRepo.save(dataSalary, { data: req }); - setLogDataDiff(req, { before, after: dataSalary }); - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - const _null: any = null; - profile.isLeave = item.isLeave; - profile.leaveReason = item.leaveReason ?? _null; - profile.dateLeave = item.dateLeave ?? _null; - profile.lastUpdateUserId = ctx.user.sub; - profile.lastUpdateFullName = ctx.user.name; - profile.lastUpdatedAt = new Date(); - // บันทึกประวัติก่อนลบตำแหน่ง - const clearProfile = await checkCommandType(String(item.commandId)); - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: curRevision.id, - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - orgRootRef = curPosMaster?.orgRoot ?? null; - orgChild1Ref = curPosMaster?.orgChild1 ?? null; - orgChild2Ref = curPosMaster?.orgChild2 ?? null; - orgChild3Ref = curPosMaster?.orgChild3 ?? null; - orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb, today); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryEmployeeLeaveService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure } + } + }); + } - // ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "EMPLOYEE"); + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit) + */ + private async processOne( + item: SalaryEmployeeLeaveItem, + ctx: SalaryEmployeeLeaveExecutionContext, + manager: EntityManager, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + today: number, + ): Promise { + const req = ctx.req; + + const commandReciveRepository = manager.getRepository(CommandRecive); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + const orgRevisionRepo = manager.getRepository(OrgRevision); + + const profile = await profileEmployeeRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + posType: true, + posLevel: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const code = _command?.commandType?.code; + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก + if (item.resignId && code && ["C-PM-42"].includes(code)) { + const commandResign = await commandReciveRepository.findOne({ + where: { refId: item.resignId }, + relations: { command: true }, + }); + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) + : today; + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await reOrderCommandRecivesAndDelete(commandResign!.id, manager); + } + } + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + Object.assign(dataSalary, { + ...item, + ...meta, + profileEmployeeId: item.profileId, + profileId: undefined, + }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt; + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.isLeave = item.isLeave; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + // บันทึกประวัติก่อนลบตำแหน่ง + const clearProfile = await checkCommandType(String(item.commandId)); + const curRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + let orgRootRef = null; + let orgChild1Ref = null; + let orgChild2Ref = null; + let orgChild3Ref = null; + let orgChild4Ref = null; + if (curRevision) { + const curPosMaster = await employeePosMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: curRevision.id, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + orgRootRef = curPosMaster?.orgRoot ?? null; + orgChild1Ref = curPosMaster?.orgChild1 ?? null; + orgChild2Ref = curPosMaster?.orgChild2 ?? null; + orgChild3Ref = curPosMaster?.orgChild3 ?? null; + orgChild4Ref = curPosMaster?.orgChild4 ?? null; + if (curPosMaster) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryEmployeeLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`, + ); + await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager); + } + } + + // ลบตำแหน่ง + if (item.isLeave == true) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(profile.id, "EMPLOYEE", manager); + } + + if (clearProfile.status) { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + // Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว + // (Keycloak ไม่สามารถ rollback ได้) + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; } + } + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + } + await profileEmployeeRepository.save(profile); - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } - } - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - } - await this.profileEmployeeRepository.save(profile); + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE_EMPLOYEE", + // ); + // } - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE_EMPLOYEE", - // ); - // } + // Task #2190 + if (code && ["C-PM-23", "C-PM-43"].includes(code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } - // Task #2190 - if (code && ["C-PM-23", "C-PM-43"].includes(code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - } - }), + console.log( + `[ExecuteSalaryEmployeeLeaveService] Completed processOne — profileId: ${item.profileId}`, ); - - console.log("[ExecuteSalaryEmployeeLeaveService] executeSalaryEmployeeLeave completed successfully"); } } diff --git a/src/services/ExecuteSalaryLeaveService.ts b/src/services/ExecuteSalaryLeaveService.ts index 04d6c32c..98a1da41 100644 --- a/src/services/ExecuteSalaryLeaveService.ts +++ b/src/services/ExecuteSalaryLeaveService.ts @@ -1,4 +1,4 @@ -import { Double, In, Like } from "typeorm"; +import { Double, EntityManager, In, Like } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -103,27 +103,34 @@ export interface SalaryLeaveExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeave ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operations (deleteUser/createUser/addUserRoles/updateUserAttributes) + * ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว */ export class ExecuteSalaryLeaveService { private commandRepository = AppDataSource.getRepository(Command); private commandReciveRepository = AppDataSource.getRepository(CommandRecive); private profileRepository = AppDataSource.getRepository(Profile); - private salaryRepo = AppDataSource.getRepository(ProfileSalary); - private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private posMasterRepository = AppDataSource.getRepository(PosMaster); - private positionRepository = AppDataSource.getRepository(Position); private orgRootRepository = AppDataSource.getRepository(OrgRoot); - private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); /** - * ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการ + * ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน */ async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise { - console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave"); - console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length); - - const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryLeaveService] Starting executeSalaryLeave — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryLeaveService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -189,96 +196,242 @@ export class ExecuteSalaryLeaveService { } } const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, + + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne( + item, + ctx, + manager, + _command, + _posNumCodeSit, + _posNumCodeSitAbb, + today, + roleKeycloak, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryLeaveService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit) + */ + private async processOne( + item: SalaryLeaveItem, + ctx: SalaryLeaveExecutionContext, + manager: EntityManager, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + today: number, + roleKeycloak: RoleKeycloak | null, + ): Promise { + const req = ctx.req; + + const commandReciveRepository = manager.getRepository(CommandRecive); + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const posMasterRepository = manager.getRepository(PosMaster); + const positionRepository = manager.getRepository(Position); + const orgRevisionRepo = manager.getRepository(OrgRevision); + const roleKeycloakRepo = manager.getRepository(RoleKeycloak); + + const profile = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + //ลบตำแหน่งที่รักษาการแทน + const code = _command?.commandType?.code; + if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + // await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction + await removePostMasterAct(profile.id, manager); + } + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก + else if (item.resignId && code && ["C-PM-41"].includes(code)) { + const commandResign = await commandReciveRepository.findOne({ + where: { refId: item.resignId }, + relations: { command: true }, + }); + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) + : today; + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await reOrderCommandRecivesAndDelete(commandResign!.id, manager); + } + } + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const returnWork = await checkReturnCommandType(String(item.commandId)); + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? new Date(); + dataSalary.order = dest_item == null ? 1 : dest_item.order + 1; + const meta = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + if (!returnWork) { + Object.assign(dataSalary, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + } + const _null: any = null; + profile.isLeave = item.isLeave; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + const clearProfile = await checkCommandType(String(item.commandId)); + + //ปั๊มประวัติก่อนลบตำแหน่ง + const curRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + let orgRootRef = null; + let orgChild1Ref = null; + let orgChild2Ref = null; + let orgChild3Ref = null; + let orgChild4Ref = null; + if (curRevision) { + const curPosMaster = await posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: curRevision.id, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + orgRootRef = curPosMaster?.orgRoot ?? null; + orgChild1Ref = curPosMaster?.orgChild1 ?? null; + orgChild2Ref = curPosMaster?.orgChild2 ?? null; + orgChild3Ref = curPosMaster?.orgChild3 ?? null; + orgChild4Ref = curPosMaster?.orgChild4 ?? null; + if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`, + ); + await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE", null, manager); + } + } + + //ลบตำแหน่ง + if (item.isLeave == true) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(profile.id, "OFFICER", manager); + } + if (clearProfile.status) { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + // Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class) + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; + } + } + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + } + + if (item.isGovernment == true) { + if (returnWork) { + //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ + await removeProfileInOrganize(profile.id, "OFFICER", manager); + //ปั๊มตำแหน่งใหม่ + // หา posMaster และเช็ค orgRevisionIsCurrent + let posMaster = await posMasterRepository.findOne({ + where: { id: item.posmasterId?.toString() }, relations: { - roleKeycloaks: true, + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, }, }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - removePostMasterAct(profile.id); - } - //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก - else if (item.resignId && code && ["C-PM-41"].includes(code)) { - const commandResign = await this.commandReciveRepository.findOne({ - where: { refId: item.resignId }, - relations: { command: true }, - }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) - : today; - if ( - commandResign && - _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate) - ) { - await reOrderCommandRecivesAndDelete(commandResign!.id); - } - } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const returnWork = await checkReturnCommandType(String(item.commandId)); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const dataSalary = new ProfileSalary(); - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? new Date(); - dataSalary.order = dest_item == null ? 1 : dest_item.order + 1; - const meta = { - createdUserId: ctx.user.sub, - createdFullName: ctx.user.name, - lastUpdateUserId: ctx.user.sub, - lastUpdateFullName: ctx.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - if (!returnWork) { - Object.assign(dataSalary, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); - await this.salaryRepo.save(dataSalary, { data: req }); - setLogDataDiff(req, { before, after: dataSalary }); - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - } - const _null: any = null; - profile.isLeave = item.isLeave; - profile.leaveReason = item.leaveReason ?? _null; - profile.dateLeave = item.dateLeave ?? _null; - profile.lastUpdateUserId = ctx.user.sub; - profile.lastUpdateFullName = ctx.user.name; - profile.lastUpdatedAt = new Date(); - const clearProfile = await checkCommandType(String(item.commandId)); - //ปั๊มประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - if (curRevision) { - const curPosMaster = await this.posMasterRepository.findOne({ + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await posMasterRepository.findOne({ where: { - current_holderId: profile.id, - orgRevisionId: curRevision.id, + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, }, relations: { + orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, @@ -286,343 +439,270 @@ export class ExecuteSalaryLeaveService { orgChild4: true, }, }); - orgRootRef = curPosMaster?.orgRoot ?? null; - orgChild1Ref = curPosMaster?.orgChild1 ?? null; - orgChild2Ref = curPosMaster?.orgChild2 ?? null; - orgChild3Ref = curPosMaster?.orgChild3 ?? null; - orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { - await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); - } } - //ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "OFFICER"); - } - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } + if (posMaster) { + const checkPosition = await positionRepository.find({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await positionRepository.save(clearPosition); } - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - } + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + await posMasterRepository.save(posMaster); - if (item.isGovernment == true) { - if (returnWork) { - //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ - await removeProfileInOrganize(profile.id, "OFFICER"); - //ปั๊มตำแหน่งใหม่ - // หา posMaster และเช็ค orgRevisionIsCurrent - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId?.toString() }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, + // Match position ตามลำดับ priority: + // Condition 1: match จาก positionId + // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) + // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) + // Fallback: เลือก position แรกใน posMaster + + let positionNew: Position | null = null; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + if (item.positionId) { + const positionById = await positionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง }, + relations: ["posExecutive"], }); - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); + if (positionById) { + positionNew = positionById; } + } - if (posMaster) { - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.positionRepository.save(clearPosition); - } - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - await this.posMasterRepository.save(posMaster); - - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }; - - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - - if (positionNew) { - positionNew.positionIsSelected = true; - await this.positionRepository.save(positionNew, { data: req }); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - } - const newMapProfileSalary = { - profileId: profile.id, - commandId: item.commandId, - positionName: item.positionNameNew ?? null, - positionType: item.posTypeNameNew ?? null, - positionLevel: item.posLevelNameNew ?? null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - posNo: item.posNoNew, - posNoAbb: item.posNoAbbNew, - orgRoot: item.orgRootNew, - orgChild1: item.orgChild1New, - orgChild2: item.orgChild2New, - orgChild3: item.orgChild3New, - orgChild4: item.orgChild4New, - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, }; - Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); - await this.salaryRepo.save(dataSalary); - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history); - profile.leaveReason = _null; - profile.leaveCommandId = _null; - profile.leaveCommandNo = _null; - profile.leaveRemark = _null; - profile.leaveDate = _null; - profile.leaveType = _null; - profile.position = item.positionNameNew ?? _null; - profile.posTypeId = item.positionTypeNew ?? _null; - profile.posLevelId = item.positionLevelNew ?? _null; - } - let userKeycloakId; - const checkUser = await getUserByUsername(profile.citizenId); - //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ - if (checkUser.length == 0) { - let password = profile.citizenId; - if (profile.birthDate != null) { - const _date = new Date(profile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; + + if (item.positionField) { + whereCondition.positionField = item.positionField; } - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak - const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(profile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: profile.lastName, + if (item.posExecutiveId) { + whereCondition.posExecutiveId = item.posExecutiveId; + } + if (item.positionExecutiveField) { + whereCondition.positionExecutiveField = item.positionExecutiveField; + } + if (item.positionArea) { + whereCondition.positionArea = item.positionArea; + } + + const positionBy7Fields = await positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, }); - const list = await getRoles(); - let result = false; - if (Array.isArray(list) && userKeycloakId) { - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; } - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; } - //ถ้ามีอยู่แล้วให้ใช้อันเดิม - else { - const rolesData = await getRoleMappings(checkUser[0].id); - if (rolesData) { - const _roleKeycloak = await this.roleKeycloakRepo.find({ - where: { name: In(rolesData.map((x: any) => x.name)) }, - }); - profile.roleKeycloaks = - _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + const positionBy3Fields = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; } - profile.keycloak = checkUser[0].id; } - profile.amount = item.amount ?? _null; - profile.amountSpecial = item.amountSpecial ?? _null; - profile.isActive = true; - profile.isDelete = false; + + // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) + // if (!positionNew) { + // const fallbackPositions = await positionRepository.find({ + // where: { + // posMasterId: posMaster.id, + // }, + // relations: ["posExecutive"], + // order: { + // orderNo: "ASC", + // }, + // take: 1, + // }); + + // if (fallbackPositions.length > 0) { + // positionNew = fallbackPositions[0]; + // } + // } + + if (positionNew) { + positionNew.positionIsSelected = true; + await positionRepository.save(positionNew, { data: req }); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + console.log( + `[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`, + ); + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); } - await this.profileRepository.save(profile); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], + const newMapProfileSalary = { + profileId: profile.id, + commandId: item.commandId, + positionName: item.positionNameNew ?? null, + positionType: item.posTypeNameNew ?? null, + positionLevel: item.posLevelNameNew ?? null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + posNo: item.posNoNew, + posNoAbb: item.posNoAbbNew, + orgRoot: item.orgRootNew, + orgChild1: item.orgChild1New, + orgChild2: item.orgChild2New, + orgChild3: item.orgChild3New, + orgChild4: item.orgChild4New, + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await salaryRepo.save(dataSalary); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history); + profile.leaveReason = _null; + profile.leaveCommandId = _null; + profile.leaveCommandNo = _null; + profile.leaveRemark = _null; + profile.leaveDate = _null; + profile.leaveType = _null; + profile.position = item.positionNameNew ?? _null; + profile.posTypeId = item.positionTypeNew ?? _null; + profile.posLevelId = item.positionLevelNew ?? _null; + } + let userKeycloakId; + const checkUser = await getUserByUsername(profile.citizenId); + //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ + if (checkUser.length == 0) { + let password = profile.citizenId; + if (profile.birthDate != null) { + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + // Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class) + userKeycloakId = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName, }); - - // Task #2190 - if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } + const list = await getRoles(); + let result = false; + if (Array.isArray(list) && userKeycloakId) { + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); } - }), - ); + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + } + //ถ้ามีอยู่แล้วให้ใช้อันเดิม + else { + const rolesData = await getRoleMappings(checkUser[0].id); + if (rolesData) { + const _roleKeycloak = await roleKeycloakRepo.find({ + where: { name: In(rolesData.map((x: any) => x.name)) }, + }); + profile.roleKeycloaks = + _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + } + profile.keycloak = checkUser[0].id; + } + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.isActive = true; + profile.isDelete = false; + } + await profileRepository.save(profile); - console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully"); + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } + + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + + // Task #2190 + if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + + console.log( + `[ExecuteSalaryLeaveService] Completed processOne — profileId: ${item.profileId}`, + ); } } diff --git a/src/services/ExecuteSalaryService.ts b/src/services/ExecuteSalaryService.ts index bbdaa3a4..d375d9db 100644 --- a/src/services/ExecuteSalaryService.ts +++ b/src/services/ExecuteSalaryService.ts @@ -1,4 +1,4 @@ -import { Double } from "typeorm"; +import { Double, EntityManager } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -76,25 +76,30 @@ export interface SalaryExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdate ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * Keycloak operations (deleteUser) ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้ */ export class ExecuteSalaryService { private commandRepository = AppDataSource.getRepository(Command); private profileRepository = AppDataSource.getRepository(Profile); - private salaryRepo = AppDataSource.getRepository(ProfileSalary); - private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); - private posMasterRepository = AppDataSource.getRepository(PosMaster); private orgRootRepository = AppDataSource.getRepository(OrgRoot); - private assistanceRepository = AppDataSource.getRepository(ProfileAssistance); - private assistanceHistoryRepository = AppDataSource.getRepository(ProfileAssistanceHistory); /** - * ประมวลผลสร้าง ProfileSalary + handle leave/assistance + * ประมวลผลสร้าง ProfileSalary + handle leave/assistance ทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน */ async executeSalary(data: SalaryItem[], ctx: SalaryExecutionContext): Promise { - console.log("[ExecuteSalaryService] Starting executeSalary"); - console.log("[ExecuteSalaryService] Request body count:", data?.length); - - const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryService] Starting executeSalary — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -158,170 +163,227 @@ export class ExecuteSalaryService { .orgRootShortName ?? ""; } } - await Promise.all( - data.map(async (item) => { - const profile: any = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: { - roleKeycloaks: true, - posType: true, - posLevel: true, - }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const posMaster: any = await this.posMasterRepository.findOne({ - where: { - current_holderId: item.profileId, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const orgRevisionRef = posMaster ? posMaster.id : null; - const orgRootRef = orgRevisionRef?.orgRoot ?? null; - const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-13"].includes(code)) { - removePostMasterAct(profile.id); + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + let successCount = 0; + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure } + } + }); + } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit) + * + * หมายเหตุ: Keycloak deleteUser ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้ + */ + private async processOne( + item: SalaryItem, + ctx: SalaryExecutionContext, + manager: EntityManager, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const posMasterRepository = manager.getRepository(PosMaster); + const assistanceRepository = manager.getRepository(ProfileAssistance); + const assistanceHistoryRepository = manager.getRepository(ProfileAssistanceHistory); + + const profile: any = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + posType: true, + posLevel: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const posMaster: any = await posMasterRepository.findOne({ + where: { + current_holderId: item.profileId, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + const orgRevisionRef = posMaster ? posMaster.id : null; + const orgRootRef = orgRevisionRef?.orgRoot ?? null; + const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + //ลบตำแหน่งที่รักษาการแทน + const code = _command?.commandType?.code; + if (code && ["C-PM-13"].includes(code)) { + // await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction + await removePostMasterAct(profile.id, manager); + } + + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + if (item.isLeave != undefined && item.isLeave == true) { + console.log( + `[ExecuteSalaryService] Creating PosMasterHistory — posMasterId: ${orgRevisionRef}, profileId: ${item.profileId}, type: DELETE`, + ); + await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE", null, manager); + await removeProfileInOrganize(profile.id, "OFFICER", manager); + } + const clearProfile = await checkCommandType(String(item.commandId)); + const _null: any = null; + if (clearProfile.status) { + // Keycloak deleteUser ทำก่อนเข้า transaction-bound save ด้านล่าง + // (ทำภายใน transaction เดียวกัน เพราะถ้า fail ต้อง rollback DB ด้วย) + // หมายเหตุ: Keycloak ไม่สามารถ rollback ได้ → ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; } - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const dataSalary = new ProfileSalary(); - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, + } + profile.isLeave = item.isLeave; + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + await profileRepository.save(profile, { data: req }); + + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } + } + Object.assign(dataSalary, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + if (_command) { + if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) { + // ประวัติคำสั่งให้ช่วยราชการ + const dataAssis = new ProfileAssistance(); + + const metaAssis = { + profileId: item.profileId, + agency: item.officerOrg, + dateStart: item.dateStart, + dateEnd: item.dateEnd, + commandNo: `${item.commandNo}/${_commandYear}`, + commandName: item.commandName, + refId: item.refId, + refCommandDate: new Date(), + commandId: item.commandId, createdUserId: ctx.user.sub, createdFullName: ctx.user.name, lastUpdateUserId: ctx.user.sub, lastUpdateFullName: ctx.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), + status: _command.commandType.code == "C-PM-15" ? "PENDING" : "DONE", }; - if (item.isLeave != undefined && item.isLeave == true) { - await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE"); - await removeProfileInOrganize(profile.id, "OFFICER"); + + Object.assign(dataAssis, metaAssis); + const historyAssis = new ProfileAssistanceHistory(); + Object.assign(historyAssis, { ...dataAssis, id: undefined }); + + await assistanceRepository.save(dataAssis); + historyAssis.profileAssistanceId = dataAssis.id; + await assistanceHistoryRepository.save(historyAssis); + } + // Task #2190 + else if (_command.commandType.code == "C-PM-13") { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); } - const clearProfile = await checkCommandType(String(item.commandId)); - const _null: any = null; - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } - } - profile.isLeave = item.isLeave; - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - profile.leaveReason = item.leaveReason ?? _null; - profile.dateLeave = item.dateLeave ?? _null; - profile.amount = item.amount ?? _null; - profile.amountSpecial = item.amountSpecial ?? _null; - await this.profileRepository.save(profile, { data: req }); + } + } - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - } - Object.assign(dataSalary, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...dataSalary, id: undefined }); - - await this.salaryRepo.save(dataSalary, { data: req }); - setLogDataDiff(req, { before, after: dataSalary }); - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - if (_command) { - if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) { - // ประวัติคำสั่งให้ช่วยราชการ - const dataAssis = new ProfileAssistance(); - - const metaAssis = { - profileId: item.profileId, - agency: item.officerOrg, - dateStart: item.dateStart, - dateEnd: item.dateEnd, - commandNo: `${item.commandNo}/${_commandYear}`, - commandName: item.commandName, - refId: item.refId, - refCommandDate: new Date(), - commandId: item.commandId, - createdUserId: ctx.user.sub, - createdFullName: ctx.user.name, - lastUpdateUserId: ctx.user.sub, - lastUpdateFullName: ctx.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - status: _command.commandType.code == "C-PM-15" ? "PENDING" : "DONE", - }; - - Object.assign(dataAssis, metaAssis); - const historyAssis = new ProfileAssistanceHistory(); - Object.assign(historyAssis, { ...dataAssis, id: undefined }); - - await this.assistanceRepository.save(dataAssis); - historyAssis.profileAssistanceId = dataAssis.id; - await this.assistanceHistoryRepository.save(historyAssis); - } - // Task #2190 - else if (_command.commandType.code == "C-PM-13") { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - } - } - }), + console.log( + `[ExecuteSalaryService] Completed processOne — profileId: ${item.profileId}`, ); - - console.log("[ExecuteSalaryService] executeSalary completed successfully"); } } diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 144a5621..940991d1 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -217,92 +217,105 @@ export async function CreatePosMasterHistoryEmployee( posMasterId: string, request: RequestWithUser | null, type?: string | null, + manager?: EntityManager, ): Promise { - try { - await AppDataSource.transaction(async (manager) => { - const repoPosmaster = manager.getRepository(EmployeePosMaster); - const repoHistory = manager.getRepository(PosMasterEmployeeHistory); - const repoProfileEmployee = manager.getRepository(ProfileEmployee); + const execute = async (transactionManager: EntityManager) => { + const repoPosmaster = transactionManager.getRepository(EmployeePosMaster); + const repoHistory = transactionManager.getRepository(PosMasterEmployeeHistory); + const repoProfileEmployee = transactionManager.getRepository(ProfileEmployee); - const pm = await repoPosmaster.findOne({ - where: { id: posMasterId }, - relations: [ - "positions", - "positions.posLevel", - "positions.posType", - // "positions.posExecutive", - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - ], - }); - if (!pm) return false; - if (!pm.ancestorDNA) return false; - const _null: any = null; - const h = new PosMasterEmployeeHistory(); - const selectedPosition = - pm.positions.length > 0 - ? pm.positions.find((p) => p.positionIsSelected === true) ?? null - : null; - - let position = selectedPosition?.positionName ?? _null; - let posTypeName = selectedPosition?.posType?.posTypeName ?? _null; - let posLevelName = selectedPosition?.posType && selectedPosition?.posLevel - ? `${selectedPosition?.posType?.posTypeShortName ?? ""} ${selectedPosition?.posLevel?.posLevelName ?? ""}`.trim() - : _null; - if (pm.isSit && pm.current_holderId) { - const profile = await repoProfileEmployee.findOne({ - where: { id: pm.current_holderId }, - relations: ["posType", "posLevel"] - }); - position = profile?.position ?? _null; - posTypeName = profile?.posType?.posTypeName ?? _null; - posLevelName = profile?.posType && profile?.posLevel - ? `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim() - : _null; - } - h.ancestorDNA = pm.ancestorDNA; - if (!type || type != "DELETE") { - h.profileEmployeeId = pm.current_holder?.id || _null; - h.prefix = pm.current_holder?.prefix || _null; - h.firstName = pm.current_holder?.firstName || _null; - h.lastName = pm.current_holder?.lastName || _null; - h.position = position; - h.posType = posTypeName; - h.posLevel = posLevelName; - } - h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; - h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; - h.child2DnaId = pm.orgChild2?.ancestorDNA || _null; - h.child3DnaId = pm.orgChild3?.ancestorDNA || _null; - h.child4DnaId = pm.orgChild4?.ancestorDNA || _null; - h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null; - h.posMasterNo = pm.posMasterNo ?? _null; - h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null; - h.shortName = - [ - pm.orgChild4?.orgChild4ShortName, - pm.orgChild3?.orgChild3ShortName, - pm.orgChild2?.orgChild2ShortName, - pm.orgChild1?.orgChild1ShortName, - pm.orgRoot?.orgRootShortName, - ].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null; - const userId = request?.user?.sub ?? ""; - const userName = request?.user?.name ?? "system"; - h.createdUserId = userId; - h.createdFullName = userName; - h.lastUpdateUserId = userId; - h.lastUpdateFullName = userName; - h.createdAt = new Date(); - h.lastUpdatedAt = new Date(); - await repoHistory.save(h); + const pm = await repoPosmaster.findOne({ + where: { id: posMasterId }, + relations: [ + "positions", + "positions.posLevel", + "positions.posType", + // "positions.posExecutive", + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + ], }); + if (!pm) return; + if (!pm.ancestorDNA) return; + const _null: any = null; + const h = new PosMasterEmployeeHistory(); + const selectedPosition = + pm.positions.length > 0 + ? pm.positions.find((p) => p.positionIsSelected === true) ?? null + : null; + let position = selectedPosition?.positionName ?? _null; + let posTypeName = selectedPosition?.posType?.posTypeName ?? _null; + let posLevelName = selectedPosition?.posType && selectedPosition?.posLevel + ? `${selectedPosition?.posType?.posTypeShortName ?? ""} ${selectedPosition?.posLevel?.posLevelName ?? ""}`.trim() + : _null; + if (pm.isSit && pm.current_holderId) { + const profile = await repoProfileEmployee.findOne({ + where: { id: pm.current_holderId }, + relations: ["posType", "posLevel"] + }); + position = profile?.position ?? _null; + posTypeName = profile?.posType?.posTypeName ?? _null; + posLevelName = profile?.posType && profile?.posLevel + ? `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim() + : _null; + } + h.ancestorDNA = pm.ancestorDNA; + if (!type || type != "DELETE") { + h.profileEmployeeId = pm.current_holder?.id || _null; + h.prefix = pm.current_holder?.prefix || _null; + h.firstName = pm.current_holder?.firstName || _null; + h.lastName = pm.current_holder?.lastName || _null; + h.position = position; + h.posType = posTypeName; + h.posLevel = posLevelName; + } + h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; + h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; + h.child2DnaId = pm.orgChild2?.ancestorDNA || _null; + h.child3DnaId = pm.orgChild3?.ancestorDNA || _null; + h.child4DnaId = pm.orgChild4?.ancestorDNA || _null; + h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null; + h.posMasterNo = pm.posMasterNo ?? _null; + h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null; + h.shortName = + [ + pm.orgChild4?.orgChild4ShortName, + pm.orgChild3?.orgChild3ShortName, + pm.orgChild2?.orgChild2ShortName, + pm.orgChild1?.orgChild1ShortName, + pm.orgRoot?.orgRootShortName, + ].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null; + const userId = request?.user?.sub ?? ""; + const userName = request?.user?.name ?? "system"; + h.createdUserId = userId; + h.createdFullName = userName; + h.lastUpdateUserId = userId; + h.lastUpdateFullName = userName; + h.createdAt = new Date(); + h.lastUpdatedAt = new Date(); + await repoHistory.save(h); + }; + + try { + if (manager) { + await execute(manager); + return true; + } + + await AppDataSource.transaction(async (transactionManager) => { + await execute(transactionManager); + }); return true; } catch (err) { + if (manager) { + console.error("CreatePosMasterHistoryEmployee error (external transaction):", err); + throw err; + } console.error("CreatePosMasterHistoryEmployee transaction error:", err); return false; } From 3d2fc5128ab70ceca97d01985c9e261840172a73 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 26 Jun 2026 18:09:45 +0700 Subject: [PATCH 05/12] Linear Flow discipline + organization #224 --- src/controllers/CommandController.ts | 1170 +---------------- src/services/ExecuteOfficerProfileService.ts | 17 +- src/services/ExecuteOrgCommandService.ts | 860 ++++++++++++ .../ExecuteSalaryEmployeeLeaveService.ts | 4 + .../ExecuteSalaryLeaveDisciplineService.ts | 615 +++++++++ src/services/ExecuteSalaryService.ts | 6 +- src/services/rabbitmq.ts | 67 +- 7 files changed, 1591 insertions(+), 1148 deletions(-) create mode 100644 src/services/ExecuteOrgCommandService.ts create mode 100644 src/services/ExecuteSalaryLeaveDisciplineService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 1ea05633..7ca097fd 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -81,8 +81,6 @@ import { District } from "../entities/District"; import { Province } from "../entities/Province"; import { ProfileAssistance } from "../entities/ProfileAssistance"; import { ProfileAssistanceHistory } from "../entities/ProfileAssistanceHistory"; -import { ProfileActposition } from "../entities/ProfileActposition"; -import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory"; import { ProfileFamilyFather } from "../entities/ProfileFamilyFather"; import { ProfileFamilyFatherHistory } from "../entities/ProfileFamilyFatherHistory"; import { ProfileFamilyCouple } from "../entities/ProfileFamilyCouple"; @@ -110,9 +108,8 @@ import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentSer import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; -import { promisify } from "util"; -const REDIS_HOST = process.env.REDIS_HOST; -const REDIS_PORT = process.env.REDIS_PORT; +import { ExecuteSalaryLeaveDisciplineService } from "../services/ExecuteSalaryLeaveDisciplineService"; +import { ExecuteOrgCommandService } from "../services/ExecuteOrgCommandService"; @Route("api/v1/org/command") @Tags("Command") @@ -122,7 +119,6 @@ const REDIS_PORT = process.env.REDIS_PORT; "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง", ) export class CommandController extends Controller { - private redis = require("redis"); private commandRepository = AppDataSource.getRepository(Command); private commandTypeRepository = AppDataSource.getRepository(CommandType); private commandSendRepository = AppDataSource.getRepository(CommandSend); @@ -157,8 +153,6 @@ export class CommandController extends Controller { private subDistrictRepo = AppDataSource.getRepository(SubDistrict); private assistanceRepository = AppDataSource.getRepository(ProfileAssistance); private assistanceHistoryRepository = AppDataSource.getRepository(ProfileAssistanceHistory); - private actpositionRepository = AppDataSource.getRepository(ProfileActposition); - private actpositionHistoryRepository = AppDataSource.getRepository(ProfileActpositionHistory); private profileFamilyCoupleRepo = AppDataSource.getRepository(ProfileFamilyCouple); private profileFamilyCoupleHistoryRepo = AppDataSource.getRepository(ProfileFamilyCoupleHistory); private profileFamilyMotherRepo = AppDataSource.getRepository(ProfileFamilyMother); @@ -4035,6 +4029,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย + * + * Thin wrapper — เรียก ExecuteSalaryLeaveDisciplineService (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API คำสั่งวินัย (ข้าราชการ/ลูกจ้าง) + leave + */ @Post("excexute/salary-leave-discipline") public async newSalaryAndUpdateLeaveDiscipline( @Request() req: RequestWithUser, @@ -4072,472 +4074,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - relations: ["commandType"], - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - let profile; - let isEmployee: boolean = false; - let retireTypeName: string = ""; - // ขรก. - if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") { - profile = await this.profileRepository.findOne({ - relations: [ - // "profileSalary", - "posLevel", - "posType", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "current_holders.positions", - "current_holders.positions.posExecutive", - "roleKeycloaks", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-19", "C-PM-20"].includes(code)) { - removePostMasterAct(profile.id); - } - - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: profile.id, - commandId: item.commandId, - position: profile.position, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: item.orgRoot, - orgChild1: item.orgChild1, - orgChild2: item.orgChild2, - orgChild3: item.orgChild3, - orgChild4: item.orgChild4, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: item.posNo, - posNoAbb: item.posNoAbb, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - - // ประวัติวินัย - const dataDis = new ProfileDiscipline(); - - const metaDis = { - date: item.commandDateAffect, - refCommandDate: item.commandDateSign, - refCommandNo: `${item.commandNo}/${item.commandYear}`, - refCommandId: item.commandId, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(dataDis, { ...item, ...metaDis }); - const historyDis = new ProfileDisciplineHistory(); - Object.assign(historyDis, { ...dataDis, id: undefined }); - - await this.disciplineRepository.save(dataDis); - historyDis.profileDisciplineId = dataDis.id; - await this.disciplineHistoryRepository.save(historyDis); - - // ทะเบียนประวัติ - if (item.isLeave != null) { - const _profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const _null: any = null; - _profile.isLeave = item.isLeave; - _profile.leaveReason = item.leaveReason ?? _null; - _profile.dateLeave = item.dateLeave ?? _null; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - if (item.isLeave == true) { - if (orgRevisionRef) { - await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE"); - } - await removeProfileInOrganize(_profile.id, "OFFICER"); - } - const clearProfile = await checkCommandType(String(item.commandId)); - if (clearProfile.status) { - retireTypeName = clearProfile.retireTypeName ?? ""; - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { - const delUserKeycloak = await deleteUser(_profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // _profile.keycloak = _null; - _profile.roleKeycloaks = []; - _profile.isActive = false; - _profile.isDelete = true; - } - } - _profile.leaveCommandId = item.commandId ?? _null; - _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - _profile.leaveRemark = clearProfile.leaveRemark ?? _null; - _profile.leaveDate = item.commandDateAffect ?? _null; - _profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // _profile.position = _null; - // _profile.posTypeId = _null; - // _profile.posLevelId = _null; - } - await this.profileRepository.save(_profile); - // if (_profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [_profile.id], - // "PROFILE", - // ); - // } - } - } - // ลูกจ้าง - else { - isEmployee = true; - profile = await this.profileEmployeeRepository.findOne({ - relations: [ - // "profileSalary", - "posLevel", - "posType", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "roleKeycloaks", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileEmployeeId: profile.id, - commandId: item.commandId, - position: profile.position, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: - profile?.posType && profile?.posLevel - ? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}` - : null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: item.orgRoot, - orgChild1: item.orgChild1, - orgChild2: item.orgChild2, - orgChild3: item.orgChild3, - orgChild4: item.orgChild4, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: item.posNo, - posNoAbb: item.posNoAbb, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - - // ประวัติวินัย - const dataDis = new ProfileDiscipline(); - - const metaDis = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(dataDis, { - ...item, - ...metaDis, - date: item.commandDateAffect, - refCommandDate: item.commandDateSign, - refCommandNo: item.commandNo, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const historyDis = new ProfileDisciplineHistory(); - Object.assign(historyDis, { ...dataDis, id: undefined }); - - await this.disciplineRepository.save(dataDis); - historyDis.profileDisciplineId = dataDis.id; - await this.disciplineHistoryRepository.save(historyDis); - - // ทะเบียนประวัติ - if (item.isLeave != null) { - const _profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const _null: any = null; - _profile.isLeave = item.isLeave; - _profile.leaveReason = item.leaveReason ?? _null; - _profile.dateLeave = item.dateLeave ?? _null; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - if (item.isLeave == true) { - // บันทึกประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: _profile.id, - orgRevisionId: curRevision.id, - }, - }); - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } - await removeProfileInOrganize(_profile.id, "EMPLOYEE"); - } - const clearProfile = await checkCommandType(String(item.commandId)); - if (clearProfile.status) { - retireTypeName = clearProfile.retireTypeName ?? ""; - if ( - _profile.keycloak != null && - _profile.keycloak != "" && - _profile.isDelete === false - ) { - const delUserKeycloak = await deleteUser(_profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // _profile.keycloak = _null; - _profile.roleKeycloaks = []; - _profile.isActive = false; - _profile.isDelete = true; - } - } - _profile.leaveCommandId = item.commandId ?? _null; - _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - _profile.leaveRemark = clearProfile.leaveRemark ?? _null; - _profile.leaveDate = item.commandDateAffect ?? _null; - _profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // _profile.position = _null; - // _profile.posTypeId = _null; - // _profile.posLevelId = _null; - } - await this.profileEmployeeRepository.save(_profile); - // if (_profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [_profile.id], - // "PROFILE_EMPLOYEE", - // ); - // } - } - } - // Task #2190 - if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - let _posLevelName: string = !isEmployee - ? `${profile.posLevel?.posLevelName}` - : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; - } - }), - ); - return new HttpSuccess(); } @@ -5459,365 +4999,14 @@ export class CommandController extends Controller { }[]; }, ) { - let _reqBody: any[] = []; - const roleKeycloak = await this.roleKeycloakRepo.findOne({ - where: { name: Like("USER") }, + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand21Employee (C-PM-21) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand21Employee(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.refIds.find((x) => x.commandId)?.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.refIds.map(async (item) => { - const profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.refId }, - relations: ["roleKeycloaks"], - }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - const orgRevision = await this.orgRevisionRepository.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - const _posMaster = await this.employeePosMasterRepository.findOne({ - where: { - orgRevisionId: orgRevision?.id, - id: profile.posmasterIdTemp, - // current_holderId: profile.id - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const orgRootRef = _posMaster?.orgRoot ?? null; - const orgChild1Ref = _posMaster?.orgChild1 ?? null; - const orgChild2Ref = _posMaster?.orgChild2 ?? null; - const orgChild3Ref = _posMaster?.orgChild3 ?? null; - const orgChild4Ref = _posMaster?.orgChild4 ?? null; - let orgShortName = ""; - if (_posMaster != null) { - if (_posMaster.orgChild1Id === null) { - orgShortName = _posMaster.orgRoot?.orgRootShortName; - } else if (_posMaster.orgChild2Id === null) { - orgShortName = _posMaster.orgChild1?.orgChild1ShortName; - } else if (_posMaster.orgChild3Id === null) { - orgShortName = _posMaster.orgChild2?.orgChild2ShortName; - } else if (_posMaster.orgChild4Id === null) { - orgShortName = _posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = _posMaster.orgChild4?.orgChild4ShortName; - } - } - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.refId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileEmployeeId: profile.id, - amount: item.amount, - amountSpecial: item.amountSpecial, - commandId: item.commandId, - positionSalaryAmount: item.positionSalaryAmount, - mouthSalaryAmount: item.mouthSalaryAmount, - position: profile.positionTemp, - positionName: profile.positionTemp, - positionType: profile.posTypeNameTemp, - positionLevel: profile.posLevelNameTemp, - order: dest_item == null ? 1 : dest_item.order + 1, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: profile.posMasterNoTemp ?? "", - posNoAbb: orgShortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - const posMaster = await this.employeePosMasterRepository.findOne({ - where: { id: profile.posmasterIdTemp }, - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], - }); - if (posMaster == null) - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - - const posMasterOld = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - // if (posMasterOld != null) posMasterOld.next_holderId = null; - - const positionOld = await this.employeePositionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.employeePositionRepository.save(positionOld); - } - - const checkPosition = await this.employeePositionRepository.find({ - where: { - posMasterId: profile.posmasterIdTemp, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.employeePositionRepository.save(clearPosition); - } - - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - posMaster.next_holderId = null; - if (posMasterOld != null) { - await this.employeePosMasterRepository.save(posMasterOld); - await CreatePosMasterHistoryEmployee(posMasterOld.id, req); - } - await this.employeePosMasterRepository.save(posMaster); - await CreatePosMasterHistoryEmployee(posMaster.id, req); - - const clsTempPosmaster = await this.employeeTempPosMasterRepository.find({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - - if (clsTempPosmaster.length > 0) { - const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({ - ...posMasterTemp, - current_holderId: null, - next_holderId: null, - })); - await this.employeeTempPosMasterRepository.save(clearTempPosmaster); - - const checkTempPosition = await this.employeePositionRepository.find({ - where: { - posMasterTempId: In(clearTempPosmaster.map((x) => x.id)), - positionIsSelected: true, - }, - }); - if (checkTempPosition.length > 0) { - const clearTempPosition = checkTempPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.employeePositionRepository.save(clearTempPosition); - } - await Promise.all( - clsTempPosmaster.map( - async (posMasterTemp) => - await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req), - ), - ); - } - - const positionNew = await this.employeePositionRepository.findOne({ - where: { - id: profile.positionIdTemp, - posMasterId: profile.posmasterIdTemp, - }, - }); - - if (positionNew != null) { - // Create Keycloak - const checkUser = await getUserByUsername(profile.citizenId); - if (checkUser.length == 0) { - let password = profile.citizenId; - if (profile.birthDate != null) { - // const gregorianYear = profile.birthDate.getFullYear() + 543; - - // const formattedDate = - // profile.birthDate.toISOString().slice(8, 10) + - // profile.birthDate.toISOString().slice(5, 7) + - // gregorianYear; - // password = formattedDate; - const _date = new Date(profile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak - const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; - const userKeycloakId = await createUser(profile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: profile.lastName, - // email: profile.email, - }); - // if (typeof userKeycloakId !== "string") { - // throw new Error(userKeycloakId.errorMessage); - // } - const list = await getRoles(); - if (!Array.isArray(list)) - throw new Error("Failed. Cannot get role(s) data from the server."); - const result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - // if (!result) throw new Error("Failed. Cannot set user's role."); - profile.keycloak = - userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : ""; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - // End Create Keycloak - } else { - const rolesData = await getRoleMappings(checkUser[0].id); - if (rolesData) { - const _roleKeycloak = await this.roleKeycloakRepo.find({ - where: { name: In(rolesData.map((x: any) => x.name)) }, - }); - profile.roleKeycloaks = - _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; - } - profile.keycloak = checkUser[0].id; - } - positionNew.positionIsSelected = true; - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; - profile.positionEmployeePositionId = positionNew.positionName; - profile.statusTemp = "DONE"; - profile.employeeClass = "PERM"; - const _null: any = null; - profile.employeeWage = item.amount == null ? _null : item.amount.toString(); - profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); - profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date(); - profile.amount = item.amount == null ? _null : item.amount; - profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; - _reqBody.push({ - profileId: profile.id, - prefix: profile.prefix, - firstName: profile.firstName, - lastName: profile.lastName, - citizenId: profile.citizenId, - root: posMaster.orgRoot.orgRootName, - rootId: posMaster.orgRootId, - rootShortName: posMaster.orgRoot.orgRootShortName, - rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null, - child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null, - child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null, - child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null, - child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null, - }); - await this.profileEmployeeRepository.save(profile); - await this.employeePositionRepository.save(positionNew); - await CreatePosMasterHistoryEmployee(posMaster.id, req); - //ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว - const posMasterTemp = await this.employeeTempPosMasterRepository.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - }); - if (posMasterTemp) { - await this.employeeTempPosMasterRepository.update(posMasterTemp.id, { - current_holderId: _null, - }); - await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req); - } - } - }), - ); - await new CallAPI() - .PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", { - profileEmps: _reqBody, - }) - .catch((error) => { - throw new Error("Failed. Cannot update status. ", error); - }); return new HttpSuccess(); } @@ -5860,153 +5049,14 @@ export class CommandController extends Controller { }[]; }, ) { - // 1. Bulk update status - await this.posMasterActRepository.update( - { id: In(body.refIds.map((x) => x.refId)) }, - { statusReport: "DONE" }, - ); - - // 2. ดึงข้อมูลครบทุก relation ที่จำเป็น - const posMasters = await this.posMasterActRepository.find({ - where: { id: In(body.refIds.map((x) => x.refId)) }, - relations: [ - "posMasterChild", - "posMasterChild.current_holder", - "posMaster", - "posMaster.current_holder", - "posMaster.positions", - "posMaster.orgRoot", - "posMaster.orgChild1", - "posMaster.orgChild2", - "posMaster.orgChild3", - "posMaster.orgChild4", - ], + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand40Officer (C-PM-40) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand40Officer(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - - // 3. ตรวจสอบว่ามี body.refIds[0] หรือไม่ - const firstRef = body.refIds[0]; - if (!firstRef) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบข้อมูล refIds"); - } - - const profileIdsToClearCache = new Set(); - - await Promise.all( - posMasters.map(async (item) => { - // 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด - if (!item.posMasterChild?.current_holderId || !item.posMaster) { - console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`); - return; - } - - if (item.posMasterChild.current_holderId) { - profileIdsToClearCache.add(item.posMasterChild.current_holderId); - } - - // 5. สร้าง orgShortName แบบปลอดภัย - const orgShortName = - [ - item.posMaster?.orgChild4?.orgChild4ShortName, - item.posMaster?.orgChild3?.orgChild3ShortName, - item.posMaster?.orgChild2?.orgChild2ShortName, - item.posMaster?.orgChild1?.orgChild1ShortName, - item.posMaster?.orgRoot?.orgRootShortName, - ].find(Boolean) ?? ""; - - // 6. หา position ที่ถูกเลือกแบบปลอดภัย - const selectedPosition = item.posMaster?.positions; - const positionName = - selectedPosition - ?.map((pos) => pos.positionName) - .filter(Boolean) - .join(", ") ?? "-"; - - // 7. สร้าง metaAct แบบปลอดภัย - const metaAct = { - profileId: item.posMasterChild.current_holderId, - dateStart: firstRef.commandDateAffect ?? null, - dateEnd: null, - position: positionName, - status: true, - commandId: firstRef.commandId ?? null, - createdUserId: req.user?.sub ?? null, - createdFullName: req.user?.name ?? null, - lastUpdateUserId: req.user?.sub ?? null, - lastUpdateFullName: req.user?.name ?? null, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: firstRef.commandNo ?? null, - refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`, - commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null, - posNo: - orgShortName && item.posMaster?.posMasterNo - ? `${orgShortName} ${item.posMaster.posMasterNo}` - : item.posMaster?.posMasterNo ?? "-", - posNoAbb: orgShortName, - commandDateAffect: firstRef.commandDateAffect ?? null, - commandDateSign: firstRef.commandDateSign ?? null, - commandCode: firstRef.commandCode ?? null, - commandName: firstRef.commandName ?? null, - remark: firstRef.remark ?? null, - }; - - try { - // 8. ปิดสถานะรักษาการ - const existingActPositions = await this.actpositionRepository.find({ - where: { - profileId: item.posMasterChild.current_holderId, - status: true, - isDeleted: false, - }, - }); - - if (existingActPositions.length > 0) { - const updatedActPositions = existingActPositions.map((_data) => ({ - ..._data, - status: false, - dateEnd: new Date(), - })); - - await this.actpositionRepository.save(updatedActPositions); - } - - // 9. บันทึกข้อมูลใหม่ - const dataAct = new ProfileActposition(); - Object.assign(dataAct, metaAct); - - const historyAct = new ProfileActpositionHistory(); - Object.assign(historyAct, { ...dataAct, id: undefined }); - - await this.actpositionRepository.save(dataAct); - historyAct.profileActpositionId = dataAct.id; - await this.actpositionHistoryRepository.save(historyAct); - } catch (error) { - console.error(`Error processing item ${item.id}:`, error); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`, - ); - } - }), - ); - - if (profileIdsToClearCache.size > 0) { - await Promise.all( - Array.from(profileIdsToClearCache).map(async (profileId) => { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - - const delAsync = promisify(redisClient.del).bind(redisClient); - await delAsync("role_" + profileId); - await delAsync("menu_" + profileId); - - redisClient.quit(); - }), - ); - } - return new HttpSuccess(); } @@ -6157,166 +5207,14 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.refIds.find((x) => x.commandId)?.commandId ?? "" }, + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand38Officer (C-PM-38) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand38Officer(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.refIds.map(async (item) => { - const posMaster = await this.posMasterRepository.findOne({ - where: { id: item.refId }, - relations: [ - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - "current_holder.posLevel", - "current_holder.posType", - ], - }); - if (!posMaster) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว"); - } - if (posMaster.next_holderId != null) { - const orgRootRef = posMaster?.orgRoot ?? null; - const orgChild1Ref = posMaster?.orgChild1 ?? null; - const orgChild2Ref = posMaster?.orgChild2 ?? null; - const orgChild3Ref = posMaster?.orgChild3 ?? null; - const orgChild4Ref = posMaster?.orgChild4 ?? null; - const shortName = - posMaster != null && posMaster.orgChild4 != null - ? `${posMaster.orgChild4.orgChild4ShortName}` - : posMaster != null && posMaster.orgChild3 != null - ? `${posMaster.orgChild3.orgChild3ShortName}` - : posMaster != null && posMaster.orgChild2 != null - ? `${posMaster.orgChild2.orgChild2ShortName}` - : posMaster != null && posMaster.orgChild1 != null - ? `${posMaster.orgChild1.orgChild1ShortName}` - : posMaster != null && posMaster?.orgRoot != null - ? `${posMaster.orgRoot.orgRootShortName}` - : null; - const profile = await this.profileRepository.findOne({ - where: { id: posMaster.next_holderId }, - }); - const position = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - relations: ["posType", "posLevel"], - }); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: profile?.id }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: profile?.id, - date: new Date(), - amount: item.amount, - commandId: item.commandId, - positionSalaryAmount: item.positionSalaryAmount, - mouthSalaryAmount: item.mouthSalaryAmount, - position: position?.positionName ?? null, - positionType: position?.posType?.posTypeName ?? null, - positionLevel: position?.posLevel?.posLevelName ?? null, - order: dest_item == null ? 1 : dest_item.order + 1, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posMaster.posMasterNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - // if (profile != null) { - // profile.position = position?.positionName ?? ""; - // profile.posTypeId = position?.posTypeId ?? ""; - // profile.posLevelId = position?.posLevelId ?? ""; - // profile.lastUpdateUserId = req.user.sub; - // profile.lastUpdateFullName = req.user.name; - // profile.lastUpdatedAt = new Date(); - // await this.profileRepository.save(profile); - // } - } - }), - ); - // const posMasters = await this.posMasterRepository.find({ - // where: { id: In(body.refIds.map((x) => x.refId)) }, - // }); - // const data = posMasters.map((_data) => ({ - // ..._data, - // current_holderId: _data.next_holderId, - // next_holderId: null, - // statusReport: "PENDING", - // })); - // await this.posMasterRepository.save(data); return new HttpSuccess(); } diff --git a/src/services/ExecuteOfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts index c9e0bacb..f06159dc 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -129,14 +129,23 @@ export interface ExecutionContext { /** * Service สำหรับสร้าง/อัปเดตทะเบียนประวัติข้าราชการ (Profile) หลังออกคำสั่งบรรจุ * - * ถูกออกแบบมาเพื่อแก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ - * โดยให้ฝั่งบรรจุส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เอง - * ที่ต้นทาง (Linear Flow) แทนการเรียกซ้อนกันกลับไปมา + * ใช้กับ commandType: C-PM-01, 02, 14 * * - endpoint /org/command/excexute/create-officer-profile เรียกผ่าน service นี้ (thin wrapper) - * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (no HTTP loopback) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes) + * ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว + * + * Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ + * ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน */ export class ExecuteOfficerProfileService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts new file mode 100644 index 00000000..62ed231f --- /dev/null +++ b/src/services/ExecuteOrgCommandService.ts @@ -0,0 +1,860 @@ +import { Double, EntityManager, In, Like } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import Extension from "../interfaces/extension"; +import CallAPI from "../interfaces/call-api"; +import { setLogDataDiff } from "../interfaces/utils"; +import { + CreatePosMasterHistoryEmployee, + CreatePosMasterHistoryEmployeeTemp, +} from "./PositionService"; +import { + addUserRoles, + createUser, + getRoles, + getUserByUsername, + getRoleMappings, +} from "../keycloak"; +import { Command } from "../entities/Command"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { RoleKeycloak } from "../entities/RoleKeycloak"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster"; +import { EmployeePosition } from "../entities/EmployeePosition"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { PosMasterAct } from "../entities/PosMasterAct"; +import { ProfileActposition } from "../entities/ProfileActposition"; +import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory"; +import { promisify } from "util"; + +const redis = require("redis"); +const REDIS_HOST = process.env.REDIS_HOST; +const REDIS_PORT = process.env.REDIS_PORT; + +/** + * Input: refIds ที่ consumer ใน rabbitmq build ขึ้น (เดิมคือ body.refIds ของ endpoint /excecute) + * ใช้กับ C-PM-21, C-PM-38, C-PM-40 + */ +export interface CommandRefItem { + refId: string; + commandId?: string | null; + amount: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount: Double | null; + mouthSalaryAmount: Double | null; + commandNo: string | null; + commandYear: number; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; +} + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface OrgCommandExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งที่เดิม "ยิงเข้าตัว" (HTTP loopback เข้า org เอง) + * + * ใช้กับ commandType: + * - C-PM-21 : command21/employee/report/excecute (ลูกจ้าง → พนักงานประจำ) + * - C-PM-38 : command38/officer/report/excecute (เงินเดือน next_holder ข้าราชการ) + * - C-PM-40 : command40/officer/report/excecute (รักษาการ) + * + * - endpoint commandXX/.../excecute ทั้ง 3 เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow / ทำต่อ) + * แทนการ PostData(path + "/excecute") ที่เป็น HTTP loopback เข้า org ตัวเอง + * + * Behavior ทั้งหมด preserve จาก CommandController ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * + * ⚠️ หมายเหตุ side-effect ที่อยู่นอก DB transaction: + * - Keycloak operations (createUser/addUserRoles/getRoleMappings) ใน C-PM-21 ทำภายใน transaction + * เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจากนี้ + * Keycloak จะถูกเปลี่ยนไปแล้ว + * - .NET call (C-PM-21) ทำหลัง transaction commit แล้ว เพราะ .NET ไม่สามารถ rollback ได้ + * - Redis cache clear (C-PM-40) ทำหลัง transaction commit (เป็นการ del cache key — idempotent) + * - CreatePosMasterHistoryEmployeeTemp สร้าง nested transaction ของตัวเอง (ไม่รับ manager) + */ +export class ExecuteOrgCommandService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); + private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); + + // ───────────────────────────────────────────────────────────── + // แก้ปัญหา _posNumCodeSit/_command resolution ที่ซ้ำกันในทุก endpoint + // (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ command: Command | null; posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + let profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { command, posNumCodeSit, posNumCodeSitAbb }; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-21 : command21/employee/report/excecute + // ลูกจ้างชั่วคราว → พนักงานประจำ (บรรจุ) + // ═══════════════════════════════════════════════════════════════ + /** + * @returns profileEmps ที่จะส่งต่อให้ .NET (สำหรับ consumer rabbitmq เรียก .NET เอง) + * ถ้าเรียกจาก thin-wrapper endpoint จะเรียก .NET ภายใน method นี้เอง + */ + async executeCommand21Employee( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + options?: { callDotNet?: boolean }, + ): Promise<{ profileEmps: any[] }> { + const req = ctx.req; + const callDotNet = options?.callDotNet ?? true; + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand21Employee — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const roleKeycloak = await this.roleKeycloakRepo.findOne({ + where: { name: Like("USER") }, + }); + const { command: _command, posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + const profileEmps: any[] = []; + + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOneCommand21( + item, + ctx, + manager, + roleKeycloak, + _command, + _posNumCodeSit, + _posNumCodeSitAbb, + profileEmps, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteOrgCommandService] Failed C-PM-21, commandId=${commandId}, refId=${item.refId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + // .NET call ทำหลัง commit (เหมือน endpoint เดิมที่เรียกหลัง Promise.all) — .NET ไม่ rollback ได้ + if (callDotNet && profileEmps.length > 0) { + await new CallAPI() + .PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", { + profileEmps, + }) + .catch((error) => { + throw new Error(`Failed. Cannot update status. ${error?.message ?? ""}`); + }); + } + + console.log( + `[ExecuteOrgCommandService] Completed C-PM-21 — ${profileEmps.length} profiles sent to .NET`, + ); + return { profileEmps }; + } + + private async processOneCommand21( + item: CommandRefItem, + ctx: OrgCommandExecutionContext, + manager: EntityManager, + roleKeycloak: RoleKeycloak | null, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileEmps: any[], + ): Promise { + const req = ctx.req; + + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + const employeeTempPosMasterRepository = manager.getRepository(EmployeeTempPosMaster); + const employeePositionRepository = manager.getRepository(EmployeePosition); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + const profile = await profileEmployeeRepository.findOne({ + where: { id: item.refId }, + relations: ["roleKeycloaks"], + }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const orgRevision = await this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + const _posMaster = await employeePosMasterRepository.findOne({ + where: { + orgRevisionId: orgRevision?.id, + id: profile.posmasterIdTemp, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + const orgRootRef = _posMaster?.orgRoot ?? null; + const orgChild1Ref = _posMaster?.orgChild1 ?? null; + const orgChild2Ref = _posMaster?.orgChild2 ?? null; + const orgChild3Ref = _posMaster?.orgChild3 ?? null; + const orgChild4Ref = _posMaster?.orgChild4 ?? null; + let orgShortName = ""; + if (_posMaster != null) { + if (_posMaster.orgChild1Id === null) { + orgShortName = _posMaster.orgRoot?.orgRootShortName; + } else if (_posMaster.orgChild2Id === null) { + orgShortName = _posMaster.orgChild1?.orgChild1ShortName; + } else if (_posMaster.orgChild3Id === null) { + orgShortName = _posMaster.orgChild2?.orgChild2ShortName; + } else if (_posMaster.orgChild4Id === null) { + orgShortName = _posMaster.orgChild3?.orgChild3ShortName; + } else { + orgShortName = _posMaster.orgChild4?.orgChild4ShortName; + } + } + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.refId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileEmployeeId: profile.id, + amount: item.amount, + amountSpecial: item.amountSpecial, + positionSalaryAmount: item.positionSalaryAmount, + mouthSalaryAmount: item.mouthSalaryAmount, + position: profile.positionTemp, + positionName: profile.positionTemp, + positionType: profile.posTypeNameTemp, + positionLevel: profile.posLevelNameTemp, + order: dest_item == null ? 1 : dest_item.order + 1, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: profile.posMasterNoTemp ?? "", + posNoAbb: orgShortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + const posMaster = await employeePosMasterRepository.findOne({ + where: { id: profile.posmasterIdTemp }, + relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], + }); + if (posMaster == null) + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + + const posMasterOld = await employeePosMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + + const positionOld = await employeePositionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await employeePositionRepository.save(positionOld); + } + + const checkPosition = await employeePositionRepository.find({ + where: { + posMasterId: profile.posmasterIdTemp, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await employeePositionRepository.save(clearPosition); + } + + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + posMaster.next_holderId = null; + if (posMasterOld != null) { + await employeePosMasterRepository.save(posMasterOld); + await CreatePosMasterHistoryEmployee(posMasterOld.id, req, undefined, manager); + } + await employeePosMasterRepository.save(posMaster); + await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager); + + const clsTempPosmaster = await employeeTempPosMasterRepository.find({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + + if (clsTempPosmaster.length > 0) { + const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({ + ...posMasterTemp, + current_holderId: null, + next_holderId: null, + })); + await employeeTempPosMasterRepository.save(clearTempPosmaster); + + const checkTempPosition = await employeePositionRepository.find({ + where: { + posMasterTempId: In(clearTempPosmaster.map((x) => x.id)), + positionIsSelected: true, + }, + }); + if (checkTempPosition.length > 0) { + const clearTempPosition = checkTempPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await employeePositionRepository.save(clearTempPosition); + } + await Promise.all( + clsTempPosmaster.map( + async (posMasterTemp) => + await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req), + ), + ); + } + + const positionNew = await employeePositionRepository.findOne({ + where: { + id: profile.positionIdTemp, + posMasterId: profile.posmasterIdTemp, + }, + }); + + if (positionNew != null) { + // Create Keycloak + const checkUser = await getUserByUsername(profile.citizenId); + if (checkUser.length == 0) { + let password = profile.citizenId; + if (profile.birthDate != null) { + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + const userKeycloakId = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName, + }); + const list = await getRoles(); + if (!Array.isArray(list)) + throw new Error("Failed. Cannot get role(s) data from the server."); + const result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + profile.keycloak = + userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : ""; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + // End Create Keycloak + } else { + const rolesData = await getRoleMappings(checkUser[0].id); + if (rolesData) { + const _roleKeycloak = await this.roleKeycloakRepo.find({ + where: { name: In(rolesData.map((x: any) => x.name)) }, + }); + profile.roleKeycloaks = + _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + } + profile.keycloak = checkUser[0].id; + } + positionNew.positionIsSelected = true; + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; + profile.positionEmployeePositionId = positionNew.positionName; + profile.statusTemp = "DONE"; + profile.employeeClass = "PERM"; + const _null: any = null; + profile.employeeWage = item.amount == null ? _null : item.amount.toString(); + profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); + profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date(); + profile.amount = item.amount == null ? _null : item.amount; + profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; + profileEmps.push({ + profileId: profile.id, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + root: posMaster.orgRoot.orgRootName, + rootId: posMaster.orgRootId, + rootShortName: posMaster.orgRoot.orgRootShortName, + rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null, + child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null, + child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null, + child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null, + child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null, + }); + await profileEmployeeRepository.save(profile); + await employeePositionRepository.save(positionNew); + await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager); + //ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว + const posMasterTemp = await employeeTempPosMasterRepository.findOne({ + where: { + orgRevisionId: orgRevision?.id, + current_holderId: profile.id, + }, + }); + if (posMasterTemp) { + await employeeTempPosMasterRepository.update(posMasterTemp.id, { + current_holderId: _null, + }); + await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req); + } + } + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-38 : command38/officer/report/excecute + // เงินเดือน next_holder ของข้าราชการ + // ═══════════════════════════════════════════════════════════════ + async executeCommand38Officer( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + ): Promise { + const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand38Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOneCommand38( + item, + ctx, + manager, + _posNumCodeSit, + _posNumCodeSitAbb, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteOrgCommandService] Failed C-PM-38, commandId=${commandId}, refId=${item.refId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteOrgCommandService] Completed C-PM-38 — ${data?.length ?? 0} items`); + } + + private async processOneCommand38( + item: CommandRefItem, + ctx: OrgCommandExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const posMasterRepository = manager.getRepository(PosMaster); + const profileRepository = manager.getRepository(Profile); + const positionRepository = manager.getRepository(Position); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + const posMaster = await posMasterRepository.findOne({ + where: { id: item.refId }, + relations: [ + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + "current_holder.posLevel", + "current_holder.posType", + ], + }); + if (!posMaster) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว"); + } + if (posMaster.next_holderId != null) { + const orgRootRef = posMaster?.orgRoot ?? null; + const orgChild1Ref = posMaster?.orgChild1 ?? null; + const orgChild2Ref = posMaster?.orgChild2 ?? null; + const orgChild3Ref = posMaster?.orgChild3 ?? null; + const orgChild4Ref = posMaster?.orgChild4 ?? null; + const shortName = + posMaster != null && posMaster.orgChild4 != null + ? `${posMaster.orgChild4.orgChild4ShortName}` + : posMaster != null && posMaster.orgChild3 != null + ? `${posMaster.orgChild3.orgChild3ShortName}` + : posMaster != null && posMaster.orgChild2 != null + ? `${posMaster.orgChild2.orgChild2ShortName}` + : posMaster != null && posMaster.orgChild1 != null + ? `${posMaster.orgChild1.orgChild1ShortName}` + : posMaster != null && posMaster?.orgRoot != null + ? `${posMaster.orgRoot.orgRootShortName}` + : null; + const profile = await profileRepository.findOne({ + where: { id: posMaster.next_holderId }, + }); + const position = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + relations: ["posType", "posLevel"], + }); + const dest_item = await salaryRepo.findOne({ + where: { profileId: profile?.id }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: profile?.id, + date: new Date(), + amount: item.amount, + commandId: item.commandId, + positionSalaryAmount: item.positionSalaryAmount, + mouthSalaryAmount: item.mouthSalaryAmount, + position: position?.positionName ?? null, + positionType: position?.posType?.posTypeName ?? null, + positionLevel: position?.posLevel?.posLevelName ?? null, + order: dest_item == null ? 1 : dest_item.order + 1, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posMaster.posMasterNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + } + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-40 : command40/officer/report/excecute + // รักษาการ (ProfileActposition) + // ═══════════════════════════════════════════════════════════════ + async executeCommand40Officer( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand40Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + // 3. ตรวจสอบว่ามี data[0] หรือไม่ + const firstRef = data[0]; + if (!firstRef) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบข้อมูล refIds"); + } + + const profileIdsToClearCache = new Set(); + + await AppDataSource.transaction(async (manager) => { + // 1. Bulk update status + await manager.getRepository(PosMasterAct).update( + { id: In(data.map((x) => x.refId)) }, + { statusReport: "DONE" }, + ); + + // 2. ดึงข้อมูลครบทุก relation ที่จำเป็น + const posMasters = await manager.getRepository(PosMasterAct).find({ + where: { id: In(data.map((x) => x.refId)) }, + relations: [ + "posMasterChild", + "posMasterChild.current_holder", + "posMaster", + "posMaster.current_holder", + "posMaster.positions", + "posMaster.orgRoot", + "posMaster.orgChild1", + "posMaster.orgChild2", + "posMaster.orgChild3", + "posMaster.orgChild4", + ], + }); + + for (const item of posMasters) { + try { + // 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด + if (!item.posMasterChild?.current_holderId || !item.posMaster) { + console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`); + continue; + } + + if (item.posMasterChild.current_holderId) { + profileIdsToClearCache.add(item.posMasterChild.current_holderId); + } + + // 5. สร้าง orgShortName แบบปลอดภัย + const orgShortName = + [ + item.posMaster?.orgChild4?.orgChild4ShortName, + item.posMaster?.orgChild3?.orgChild3ShortName, + item.posMaster?.orgChild2?.orgChild2ShortName, + item.posMaster?.orgChild1?.orgChild1ShortName, + item.posMaster?.orgRoot?.orgRootShortName, + ].find(Boolean) ?? ""; + + // 6. หา position ที่ถูกเลือกแบบปลอดภัย + const selectedPosition = item.posMaster?.positions; + const positionName = + selectedPosition + ?.map((pos) => pos.positionName) + .filter(Boolean) + .join(", ") ?? "-"; + + // 7. สร้าง metaAct แบบปลอดภัย + const metaAct = { + profileId: item.posMasterChild.current_holderId, + dateStart: firstRef.commandDateAffect ?? null, + dateEnd: null, + position: positionName, + status: true, + commandId: firstRef.commandId ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: firstRef.commandNo ?? null, + refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`, + commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null, + posNo: + orgShortName && item.posMaster?.posMasterNo + ? `${orgShortName} ${item.posMaster.posMasterNo}` + : item.posMaster?.posMasterNo ?? "-", + posNoAbb: orgShortName, + commandDateAffect: firstRef.commandDateAffect ?? null, + commandDateSign: firstRef.commandDateSign ?? null, + commandCode: firstRef.commandCode ?? null, + commandName: firstRef.commandName ?? null, + remark: firstRef.remark ?? null, + }; + + // 8. ปิดสถานะรักษาการ + const actpositionRepository = manager.getRepository(ProfileActposition); + const actpositionHistoryRepository = manager.getRepository(ProfileActpositionHistory); + + const existingActPositions = await actpositionRepository.find({ + where: { + profileId: item.posMasterChild.current_holderId, + status: true, + isDeleted: false, + }, + }); + + if (existingActPositions.length > 0) { + const updatedActPositions = existingActPositions.map((_data) => ({ + ..._data, + status: false, + dateEnd: new Date(), + })); + + await actpositionRepository.save(updatedActPositions); + } + + // 9. บันทึกข้อมูลใหม่ + const dataAct = new ProfileActposition(); + Object.assign(dataAct, metaAct); + + const historyAct = new ProfileActpositionHistory(); + Object.assign(historyAct, { ...dataAct, id: undefined }); + + await actpositionRepository.save(dataAct); + historyAct.profileActpositionId = dataAct.id; + await actpositionHistoryRepository.save(historyAct); + } catch (error) { + console.error(`Error processing item ${item.id}:`, error); + throw new HttpError( + HttpStatusCode.INTERNAL_SERVER_ERROR, + `เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`, + ); + } + } + }); + + // Redis cache clear ทำหลัง commit (del cache key — idempotent) + if (profileIdsToClearCache.size > 0) { + await Promise.all( + Array.from(profileIdsToClearCache).map(async (profileId) => { + const redisClient = await redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + + const delAsync = promisify(redisClient.del).bind(redisClient); + await delAsync("role_" + profileId); + await delAsync("menu_" + profileId); + + redisClient.quit(); + }), + ); + } + + console.log(`[ExecuteOrgCommandService] Completed C-PM-40 — ${data?.length ?? 0} items`); + } +} diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts index 46952847..95bfa948 100644 --- a/src/services/ExecuteSalaryEmployeeLeaveService.ts +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -75,6 +75,10 @@ export interface SalaryEmployeeLeaveExecutionContext { * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว */ export class ExecuteSalaryEmployeeLeaveService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/ExecuteSalaryLeaveDisciplineService.ts b/src/services/ExecuteSalaryLeaveDisciplineService.ts new file mode 100644 index 00000000..99a1dffc --- /dev/null +++ b/src/services/ExecuteSalaryLeaveDisciplineService.ts @@ -0,0 +1,615 @@ +import { Double, EntityManager } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { ProfileDiscipline } from "../entities/ProfileDiscipline"; +import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { Command } from "../entities/Command"; +import { + checkCommandType, + removePostMasterAct, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { + CreatePosMasterHistoryEmployee, + CreatePosMasterHistoryOfficer, +} from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave-discipline + * (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 — คำสั่งวินัย ข้าราชการ/ลูกจ้าง) + * + * profileType "OFFICER" → ข้าราชการ, ค่าอื่น/null → ลูกจ้าง + */ +export interface SalaryLeaveDisciplineItem { + profileId: string; + profileType?: string | null; + isLeave: boolean | null; + leaveReason?: string | null; + dateLeave?: Date | string | null; + detail?: string | null; + level?: string | null; + unStigma?: string | null; + commandId?: string | null; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + isGovernment?: boolean | null; + commandNo: string | null; + commandYear: number | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + posNo?: string | null; + posNoAbb?: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryLeaveDisciplineExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย + * + * ใช้กับ commandType: C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 + * + * - endpoint /org/command/excexute/salary-leave-discipline เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeaveDiscipline ต้นฉบับ + * รวมถึงกรณี OFFICER ที่การ save ProfileSalary + ProfileDiscipline ถูก comment out ไว้ + * (เก็บไว้เพื่อ preserve behavior เดิม — มีเพียง EMPLOYEE เท่านั้นที่ save จริง) + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว + */ +export class ExecuteSalaryLeaveDisciplineService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + /** + * ประมวลผลคำสั่งวินัยทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน + */ + async executeSalaryLeaveDiscipline( + data: SalaryLeaveDisciplineItem[], + ctx: SalaryLeaveDisciplineExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryLeaveDisciplineService] Starting executeSalaryLeaveDiscipline — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryLeaveDisciplineService] Request body count: ${data?.length ?? 0}`); + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + relations: ["commandType"], + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + + // ───────────────────────────────────────────────────────────── + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow + // ───────────────────────────────────────────────────────────── + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryLeaveDisciplineService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit) + */ + private async processOne( + item: SalaryLeaveDisciplineItem, + ctx: SalaryLeaveDisciplineExecutionContext, + manager: EntityManager, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const profileRepository = manager.getRepository(Profile); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const disciplineRepository = manager.getRepository(ProfileDiscipline); + const disciplineHistoryRepository = manager.getRepository(ProfileDisciplineHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + + const orgRevision = await orgRevisionRepo.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + + let orgRootRef: any = null; + let orgChild1Ref: any = null; + let orgChild2Ref: any = null; + let orgChild3Ref: any = null; + let orgChild4Ref: any = null; + + const code = _command?.commandType?.code; + + // ═══════════════════════════════════════════════════════════ + // OFFICER (ข้าราชการ) + // ═══════════════════════════════════════════════════════════ + if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") { + const profile: any = await profileRepository.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "current_holders.positions", + "current_holders.positions.posExecutive", + "roleKeycloaks", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + + //ลบตำแหน่งที่รักษาการแทน (await + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน) + if (code && ["C-PM-19", "C-PM-20"].includes(code)) { + await removePostMasterAct(profile.id, manager); + } + + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + const position = + profile.current_holders + .filter((x: any) => x.orgRevisionId == orgRevision?.id)[0] + ?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + // ประวัติตำแหน่ง + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: profile.id, + commandId: item.commandId, + position: profile.position, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: item.orgRoot, + orgChild1: item.orgChild1, + orgChild2: item.orgChild2, + orgChild3: item.orgChild3, + orgChild4: item.orgChild4, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: item.posNo, + posNoAbb: item.posNoAbb, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(data, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + // ── preserve: OFFICER branch ไม่ save ProfileSalary (comment ตามต้นฉบับ) ── + // await salaryRepo.save(data, { data: req }); + // history.profileSalaryId = data.id; + // await salaryHistoryRepo.save(history, { data: req }); + + // ประวัติวินัย + const dataDis = new ProfileDiscipline(); + const metaDis = { + date: item.commandDateAffect, + refCommandDate: item.commandDateSign, + refCommandNo: `${item.commandNo}/${item.commandYear}`, + refCommandId: item.commandId, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + Object.assign(dataDis, { ...item, ...metaDis }); + const historyDis = new ProfileDisciplineHistory(); + Object.assign(historyDis, { ...dataDis, id: undefined }); + // ── preserve: OFFICER branch ไม่ save ProfileDiscipline (comment ตามต้นฉบับ) ── + // await disciplineRepository.save(dataDis, { data: req }); + // historyDis.profileDisciplineId = dataDis.id; + // await disciplineHistoryRepository.save(historyDis, { data: req }); + + // ทะเบียนประวัติ + if (item.isLeave != null) { + const _profile: any = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const _null: any = null; + _profile.isLeave = item.isLeave; + _profile.leaveReason = item.leaveReason ?? _null; + _profile.dateLeave = item.dateLeave ?? _null; + _profile.lastUpdateUserId = ctx.user.sub; + _profile.lastUpdateFullName = ctx.user.name; + _profile.lastUpdatedAt = new Date(); + if (item.isLeave == true) { + if (orgRevisionRef) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(_profile.id, "OFFICER", manager); + } + const clearProfile = await checkCommandType(String(item.commandId)); + if (clearProfile.status) { + if ( + _profile.keycloak != null && + _profile.keycloak != "" && + _profile.isDelete === false + ) { + // Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class) + const delUserKeycloak = await deleteUser(_profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // _profile.keycloak = _null; + _profile.roleKeycloaks = []; + _profile.isActive = false; + _profile.isDelete = true; + } + } + _profile.leaveCommandId = item.commandId ?? _null; + _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + _profile.leaveRemark = clearProfile.leaveRemark ?? _null; + _profile.leaveDate = item.commandDateAffect ?? _null; + _profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // _profile.position = _null; + // _profile.posTypeId = _null; + // _profile.posLevelId = _null; + } + await profileRepository.save(_profile, { data: req }); + setLogDataDiff(req, { before: null, after: _profile }); + } + } + // ═══════════════════════════════════════════════════════════ + // EMPLOYEE (ลูกจ้าง) + // ═══════════════════════════════════════════════════════════ + else { + const profile: any = await profileEmployeeRepository.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "roleKeycloaks", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const lastSalary = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + // ประวัติตำแหน่ง + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileEmployeeId: profile.id, + commandId: item.commandId, + position: profile.position, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: + profile?.posType && profile?.posLevel + ? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}` + : null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: item.orgRoot, + orgChild1: item.orgChild1, + orgChild2: item.orgChild2, + orgChild3: item.orgChild3, + orgChild4: item.orgChild4, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: item.posNo, + posNoAbb: item.posNoAbb, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(data, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before: null, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + // ประวัติวินัย + const dataDis = new ProfileDiscipline(); + const metaDis = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + Object.assign(dataDis, { + ...item, + ...metaDis, + date: item.commandDateAffect, + refCommandDate: item.commandDateSign, + refCommandNo: item.commandNo, + profileEmployeeId: item.profileId, + profileId: undefined, + }); + const historyDis = new ProfileDisciplineHistory(); + Object.assign(historyDis, { ...dataDis, id: undefined }); + await disciplineRepository.save(dataDis, { data: req }); + setLogDataDiff(req, { before: null, after: dataDis }); + historyDis.profileDisciplineId = dataDis.id; + await disciplineHistoryRepository.save(historyDis, { data: req }); + + // ทะเบียนประวัติ + if (item.isLeave != null) { + const _profile: any = await profileEmployeeRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const _null: any = null; + _profile.isLeave = item.isLeave; + _profile.leaveReason = item.leaveReason ?? _null; + _profile.dateLeave = item.dateLeave ?? _null; + _profile.lastUpdateUserId = ctx.user.sub; + _profile.lastUpdateFullName = ctx.user.name; + _profile.lastUpdatedAt = new Date(); + if (item.isLeave == true) { + // บันทึกประวัติก่อนลบตำแหน่ง + const curRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + if (curRevision) { + const curPosMaster = await employeePosMasterRepository.findOne({ + where: { + current_holderId: _profile.id, + orgRevisionId: curRevision.id, + }, + }); + if (curPosMaster) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager); + } + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(_profile.id, "EMPLOYEE", manager); + } + const clearProfile = await checkCommandType(String(item.commandId)); + if (clearProfile.status) { + if ( + _profile.keycloak != null && + _profile.keycloak != "" && + _profile.isDelete === false + ) { + // Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว + // (Keycloak ไม่สามารถ rollback ได้) + const delUserKeycloak = await deleteUser(_profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // _profile.keycloak = _null; + _profile.roleKeycloaks = []; + _profile.isActive = false; + _profile.isDelete = true; + } + } + _profile.leaveCommandId = item.commandId ?? _null; + _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + _profile.leaveRemark = clearProfile.leaveRemark ?? _null; + _profile.leaveDate = item.commandDateAffect ?? _null; + _profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // _profile.position = _null; + // _profile.posTypeId = _null; + // _profile.posLevelId = _null; + } + await profileEmployeeRepository.save(_profile, { data: req }); + setLogDataDiff(req, { before: null, after: _profile }); + } + } + + // Task #2190 (preserve: organizeName computed แต่ยังไม่ได้ใช้ในต้นฉบับ — เก็บไว้ตาม behavior เดิม) + if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + + console.log( + `[ExecuteSalaryLeaveDisciplineService] Completed processOne — profileId: ${item.profileId}`, + ); + } +} diff --git a/src/services/ExecuteSalaryService.ts b/src/services/ExecuteSalaryService.ts index d375d9db..7bc07823 100644 --- a/src/services/ExecuteSalaryService.ts +++ b/src/services/ExecuteSalaryService.ts @@ -70,7 +70,7 @@ export interface SalaryExecutionContext { /** * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ * - * ใช้กับ commandType: C-PM-13 (โอน), C-PM-15 (ช่วยราชการ), C-PM-16 (เกษียณ) + * ใช้กับ commandType: C-PM-13, 15, 16 * * - endpoint /org/command/excexute/salary เรียกผ่าน service นี้ (thin wrapper) * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) @@ -81,7 +81,9 @@ export interface SalaryExecutionContext { * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) * ถ้าทุกคนสำเร็จจะ return result รายงาน success count * - * Keycloak operations (deleteUser) ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้ + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว */ export class ExecuteSalaryService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index d84a3eed..f662e247 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -35,6 +35,8 @@ import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; +import { ExecuteSalaryLeaveDisciplineService } from "./ExecuteSalaryLeaveDisciplineService"; +import { ExecuteOrgCommandService } from "./ExecuteOrgCommandService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -335,6 +337,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) // - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ) // - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave) + // - ExecuteSalaryLeaveDisciplineService : C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 (คำสั่งวินัย) + // - ExecuteOrgCommandService : C-PM-21, 38, 40 (org-self — path ชี้กลับ org เอง + // เรียก Service ตรงๆ ไม่ผ่าน HTTP loopback เพราะ PostData(path+"/excecute") = ยิงเข้าตัว) // - คำสั่งอื่น ยังใช้ Circular Flow เดิม // ───────────────────────────────────────────────────────────── const code = command.commandType?.code; @@ -344,17 +349,46 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code); const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code); const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code); + const isSalaryLeaveDiscipline = ["C-PM-19", "C-PM-20", "C-PM-25", "C-PM-26", "C-PM-27", "C-PM-28", + "C-PM-29", "C-PM-30", "C-PM-31", "C-PM-32", + ].includes(code); + // C-PM-21/38/40: path ชี้กลับ org เอง (ไม่ใช่ .NET) → ต้องเรียก Service ตรงๆ ไม่ผ่าน loopback + const isCommand21 = code === "C-PM-21"; + const isCommand38 = code === "C-PM-38"; + const isCommand40 = code === "C-PM-40"; + const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40; const isLinearFlow = isOfficerProfile || isSalaryCurrent || isSalaryEmployeeCurrent || isSalary || isSalaryLeave || - isSalaryEmployeeLeave; + isSalaryEmployeeLeave || + isSalaryLeaveDiscipline; - if (isLinearFlow) { + // Org-self (C-PM-21/38/40): เรียก Service ตรงๆ (Linear Flow / ทำต่อ) ไม่ผ่าน HTTP loopback + // เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว + if (isOrgSelfLinear) { + console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`); + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + const flatRefIds = chunks.flat(); + if (isCommand21) { + await new ExecuteOrgCommandService().executeCommand21Employee(flatRefIds, ctx); + } else if (isCommand38) { + await new ExecuteOrgCommandService().executeCommand38Officer(flatRefIds, ctx); + } else if (isCommand40) { + await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx); + } + console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`); + } else if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); + const isCpm32 = code === "C-PM-32"; let resultData: any[] = []; + let resultData1: any[] = []; //เฉพาะ C-PM-32 (ฝั่ง "การพิจารณาลงโทษ") for (const chunk of chunks) { const res = await new CallAPI().PostData( @@ -363,9 +397,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { { refIds: chunk }, false, ); - // response (resultData) จาก .NET - if (Array.isArray(res)) { - console.log(`[AMQ] Push result data`); + if (isCpm32 && res && !Array.isArray(res)) { + // C-PM-32: response เป็น object { data, data1 } → แยก 2 track + console.log( + `[AMQ] C-PM-32 split response — data: ${res.data?.length ?? 0}, data1: ${res.data1?.length ?? 0}`, + ); + if (Array.isArray(res.data)) resultData.push(...res.data); + if (Array.isArray(res.data1)) resultData1.push(...res.data1); + } else if (Array.isArray(res)) { + console.log(`[AMQ] Push result data (${res.length})`); resultData.push(...res); } } @@ -373,7 +413,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`); // Route ไป service ที่ถูกต้องตาม commandType - if (resultData.length > 0) { + if (resultData.length > 0 || resultData1.length > 0) { // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) const pseudoReq = { headers: { authorization: token }, @@ -402,6 +442,21 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } else if (isSalaryEmployeeLeave) { await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`); + } else if (isSalaryLeaveDiscipline) { + // C-PM-32 (คำสั่งยุติเรื่อง): response เป็น object { data, data1 } + // profileId เดียวกันอาจอยู่ในทั้ง 2 track → ต้องส่งให้ org แยก 2 ครั้ง ห้าม merge + if (resultData.length > 0) { + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveDisciplineService`, + ); + } + if (isCpm32 && resultData1.length > 0) { + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData1, ctx); + console.log( + `[AMQ] Processed resultData1: ${resultData1.length} profiles via ExecuteSalaryLeaveDisciplineService`, + ); + } } } } else { From 7b37cc37db274351b84a57f22ebf29340e3e548c Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 29 Jun 2026 14:28:50 +0700 Subject: [PATCH 06/12] Linear Flow Probation+Salary #224 --- src/controllers/CommandController.ts | 408 +------------ src/controllers/ProfileSalaryController.ts | 88 +-- .../ProfileSalaryEmployeeController.ts | 91 +-- src/services/ExecuteSalaryProbationService.ts | 539 ++++++++++++++++++ src/services/ExecuteSalaryReportService.ts | 318 +++++++++++ src/services/rabbitmq.ts | 104 ++++ 6 files changed, 976 insertions(+), 572 deletions(-) create mode 100644 src/services/ExecuteSalaryProbationService.ts create mode 100644 src/services/ExecuteSalaryReportService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 7ca097fd..19dd6a3f 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -109,6 +109,7 @@ import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEm import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; import { ExecuteSalaryLeaveDisciplineService } from "../services/ExecuteSalaryLeaveDisciplineService"; +import { ExecuteSalaryProbationService } from "../services/ExecuteSalaryProbationService"; import { ExecuteOrgCommandService } from "../services/ExecuteOrgCommandService"; @Route("api/v1/org/command") @@ -4424,170 +4425,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - let commandType: any = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryProbationService().executeProbationPass(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - commandType = await this.commandTypeRepository.findOne({ - select: { code: true }, - where: { id: _command.commandTypeId }, - }); - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - // const leaveType = await this.leaveType.findOne({ - // select: { id: true, limit: true, code: true }, - // where: { code: "LV-005" } - // }); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - relations: [ - "posType", - "posLevel", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - ], - where: { id: item.profileId }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - const shortName = - orgRevisionRef?.orgChild4?.orgChild4ShortName ?? - orgRevisionRef?.orgChild3?.orgChild3ShortName ?? - orgRevisionRef?.orgChild2?.orgChild2ShortName ?? - orgRevisionRef?.orgChild1?.orgChild1ShortName ?? - orgRevisionRef?.orgRoot?.orgRootShortName ?? - null; - const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null; - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: item.profileId, - commandId: item.commandId, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - amount: item.amount ? item.amount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - order: nextOrder, - orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null, - orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null, - orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null, - orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null, - orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - }), - ); - - if (commandType && String(commandType.code) == "C-PM-11") { - const profileIds = body.data.map((x) => x.profileId); - await this.profileRepository.update({ id: In(profileIds) }, { isProbation: false }); - // // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ - // if (leaveType != null) { - // await Promise.all( - // body.data.map((item) => - // new CallAPI().PutData(req, `/leave-beginning/schedule`, { - // profileId: item.profileId, - // leaveTypeId: leaveType.id, - // leaveYear: item.commandYear, - // leaveDays: leaveType.limit, - // leaveDaysUsed: 0, - // leaveCount: 0, - // beginningLeaveDays: 0, - // beginningLeaveCount: 0, - // }) - // .then(() => {}) - // .catch(() => {}) - // ) - // ); - // } - } return new HttpSuccess(); } @@ -4614,245 +4455,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryProbationService().executeProbationLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - relations: [ - // "profileSalary", - "posType", - "posLevel", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "current_holders.positions", - "current_holders.positions.posExecutive", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const _profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); - } - let dateLeave_: any = item.commandDateAffect; - _profile.isLeave = true; - _profile.leaveReason = - "คำสั่งให้ข้าราชการออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐานที่กำหนด"; - _profile.dateLeave = dateLeave_; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - const orgRootRef = orgRevisionRef?.orgRoot ?? null; - const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - const shortName = - !profile.current_holders || profile.current_holders.length == 0 - ? null - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` - : null; - const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - const profileSalary: ProfileSalary = Object.assign(new ProfileSalary(), { - profileId: item.profileId, - commandId: item.commandId, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - amount: item.amount ? item.amount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - posNumCodeSit: _posNumCodeSit, - posNumCodeSitAbb: _posNumCodeSitAbb, - }); - if (orgRevisionRef) { - await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE"); - } - await removeProfileInOrganize(profile.id, "OFFICER"); - const clearProfile = await checkCommandType(String(item.commandId)); - const _null: any = null; - if (clearProfile.status) { - if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { - const delUserKeycloak = await deleteUser(_profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // _profile.keycloak = _null; - _profile.roleKeycloaks = []; - _profile.isActive = false; - _profile.isDelete = true; - } - } - _profile.leaveCommandId = item.commandId ?? _null; - _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - _profile.leaveRemark = clearProfile.leaveRemark ?? _null; - _profile.leaveDate = item.commandDateAffect ?? _null; - _profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // _profile.position = _null; - // _profile.posTypeId = _null; - // _profile.posLevelId = _null; - } - await Promise.all([ - this.profileRepository.save(_profile), - this.salaryRepo.save(profileSalary), - ]); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...profileSalary, id: undefined }); - history.profileSalaryId = profileSalary.id; - await this.salaryHistoryRepo.save(history); - // Task #2190 - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - }), - ); return new HttpSuccess(); } diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 8abe9aa2..c294cb25 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,6 +23,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService"; import { normalizeDurationSumSimple } from "../utils/tenure"; import { TenurePositionOfficer, @@ -1380,91 +1381,10 @@ export class ProfileSalaryController extends Controller { @Post("update") public async updateSalary(@Request() req: RequestWithUser, @Body() body: CreateProfileSalary) { - if (!body.profileId) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileId"); - } - - const profile = await this.profileRepo.findOneBy({ id: body.profileId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id); - - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: body.profileId }, - order: { order: "DESC" }, + await new ExecuteSalaryReportService().executeOfficerSalaryUpdate([body], { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - const before = null; - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepo.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { ...body, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - let _null: any = null; - profile.amount = body.amount ?? _null; - profile.amountSpecial = body.amountSpecial ?? _null; - profile.positionSalaryAmount = body.positionSalaryAmount ?? _null; - profile.mouthSalaryAmount = body.mouthSalaryAmount ?? _null; - await this.profileRepo.save(profile); return new HttpSuccess(); } diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 5f2ea997..6f6847f7 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,6 +27,7 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService"; import { normalizeDurationSumSimple } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; @@ -507,94 +508,10 @@ export class ProfileSalaryEmployeeController extends Controller { @Request() req: RequestWithUser, @Body() body: CreateProfileSalaryEmployee, ) { - if (!body.profileEmployeeId) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); - } - - const profile = await this.profileRepo.findOneBy({ id: body.profileEmployeeId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id); - - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: body.profileEmployeeId }, - order: { order: "DESC" }, + await new ExecuteSalaryReportService().executeEmployeeSalaryUpdate([body], { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - const before = null; - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileGovementRepo.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { ...body, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - let _null: any = null; - profile.amount = body.amount ?? _null; - profile.amountSpecial = body.amountSpecial ?? _null; - profile.positionSalaryAmount = body.positionSalaryAmount ?? _null; - profile.mouthSalaryAmount = body.mouthSalaryAmount ?? _null; - profile.salaryLevel = body.salaryLevel ?? _null; - profile.group = body.group ?? _null; - await this.profileRepo.save(profile); return new HttpSuccess(); } diff --git a/src/services/ExecuteSalaryProbationService.ts b/src/services/ExecuteSalaryProbationService.ts new file mode 100644 index 00000000..41dfb7b6 --- /dev/null +++ b/src/services/ExecuteSalaryProbationService.ts @@ -0,0 +1,539 @@ +import { Double, EntityManager, In, Repository } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { Command } from "../entities/Command"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { checkCommandType, removeProfileInOrganize } from "../interfaces/utils"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนที่ probation return กลับมา (หลัง Linear Flow refactor) + * - C-PM-11 : excexute/salary-probation (ผ่านทดลองงาน) + * - C-PM-12 : excexute/salary-probation-leave (ออกเพราะผลทดลองฯ ต่ำกว่ามาตรฐาน) + * + * shape เดียวกับ body.data ของ endpoint /org/command/excexute/salary-probation(-leave) เดิม + */ +export interface ProbationSalaryItem { + profileId: string; + commandId?: string | null; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + commandNo: string | null; + commandYear: number | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName?: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + isGovernment?: boolean | null; // C-PM-12 เท่านั้น +} + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface ProbationExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งทดลองปฏิบัติหน้าที่ราชการ (probation) — C-PM-11, C-PM-12 + * + * เดิมเป็น circular callback: org AMQ → probation → PostData("/org/command/excexute/salary-probation(-leave)") + * หลัง Linear Flow: org AMQ → probation (return salary data) → เรียก service นี้โดยตรง (no callback) + * + * - C-PM-11 : executeProbationPass — สร้าง ProfileSalary + history, isProbation=false + * - C-PM-12 : executeProbationLeave — leave logic + deleteUser(Keycloak) + สร้าง ProfileSalary + history + * + * - endpoint /org/command/excexute/salary-probation(-leave) เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController + * (newSalaryAndUpdateLeaveDisciplinefgh / ExecuteCommand12Async ต้นฉบับ) + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * + * ⚠️ Keycloak: deleteUser (C-PM-12) ทำภายใน transaction เพื่อ preserve behavior เดิม + * (consistent กับ ExecuteSalaryService C-PM-13/15/16) — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก deleteUser สำเร็จ → user จะถูกลบใน Keycloak ไปแล้ว + */ +export class ExecuteSalaryProbationService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + // ───────────────────────────────────────────────────────────── + // แก้ปัญหา _posNumCodeSit resolution ที่ซ้ำกันในทุก endpoint + // (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + const profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { posNumCodeSit, posNumCodeSitAbb }; + } + + // normalize date (AMQ path ส่ง string มา → แปลงเป็น Date / null ถ้า invalid) + private toDate(v: any): Date | null { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-11 : ผ่านทดลองปฏิบัติหน้าที่ราชการ + // สร้าง ProfileSalary + history แล้ว set isProbation=false + // ═══════════════════════════════════════════════════════════════ + async executeProbationPass( + data: ProbationSalaryItem[], + ctx: ProbationExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryProbationService] executeProbationPass (C-PM-11) — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน AMQ handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = this.toDate(it.commandDateAffect); + it.commandDateSign = this.toDate(it.commandDateSign); + } + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + const profileIds = (data ?? []).map((x) => x.profileId).filter(Boolean); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + + for (const item of data ?? []) { + try { + await this.processOneProbationPass( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + salaryRepo, + salaryHistoryRepo, + profileRepository, + orgRevisionRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryProbationService] Failed C-PM-11, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + + // C-PM-11: ผ่านทดลองงาน → isProbation = false (bulk update ใน transaction เดียวกัน) + if (profileIds.length > 0) { + await profileRepository.update({ id: In(profileIds) }, { isProbation: false }); + } + }); + + console.log(`[ExecuteSalaryProbationService] Completed C-PM-11 — ${data?.length ?? 0} items`); + } + + private async processOneProbationPass( + item: ProbationSalaryItem, + ctx: ProbationExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + profileRepository: Repository, + orgRevisionRepo: Repository, + ): Promise { + // current orgRevision (อ่านครั้งเดียวต่อคน — preserve query pattern ของ endpoint เดิม) + const orgRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + + const profile: any = await profileRepository.findOne({ + relations: [ + "posType", + "posLevel", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + const shortName = + orgRevisionRef?.orgChild4?.orgChild4ShortName ?? + orgRevisionRef?.orgChild3?.orgChild3ShortName ?? + orgRevisionRef?.orgChild2?.orgChild2ShortName ?? + orgRevisionRef?.orgChild1?.orgChild1ShortName ?? + orgRevisionRef?.orgRoot?.orgRootShortName ?? + null; + const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null; + // NOTE: endpoint เดิมไม่ได้ load relation "current_holders.positions" → position เป็น null (preserve) + const position = + profile.current_holders + ?.filter((x: any) => x.orgRevisionId == orgRevision?.id)[0] + ?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: item.profileId, + commandId: item.commandId, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + amount: item.amount ? item.amount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null, + orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null, + orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null, + orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null, + orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history); + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-12 : ออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐาน + // leave logic (removeProfileInOrganize + deleteUser Keycloak) + สร้าง ProfileSalary + history + // ═══════════════════════════════════════════════════════════════ + async executeProbationLeave( + data: ProbationSalaryItem[], + ctx: ProbationExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryProbationService] executeProbationLeave (C-PM-12) — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = this.toDate(it.commandDateAffect); + it.commandDateSign = this.toDate(it.commandDateSign); + } + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + + for (const item of data ?? []) { + try { + await this.processOneProbationLeave( + item, + ctx, + manager, + _posNumCodeSit, + _posNumCodeSitAbb, + salaryRepo, + salaryHistoryRepo, + profileRepository, + orgRevisionRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryProbationService] Failed C-PM-12, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryProbationService] Completed C-PM-12 — ${data?.length ?? 0} items`); + } + + private async processOneProbationLeave( + item: ProbationSalaryItem, + ctx: ProbationExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + profileRepository: Repository, + orgRevisionRepo: Repository, + ): Promise { + const req = ctx.req; + + const profile: any = await profileRepository.findOne({ + relations: [ + "posType", + "posLevel", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "current_holders.positions", + "current_holders.positions.posExecutive", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); + } + + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + + // _profile (load แยกสำหรับ mutation เกี่ยวกับ Keycloak/leave — preserve pattern เดิม) + const _profile: any = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); + } + + let dateLeave_: any = item.commandDateAffect; + _profile.isLeave = true; + _profile.leaveReason = + "คำสั่งให้ข้าราชการออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐานที่กำหนด"; + _profile.dateLeave = dateLeave_; + _profile.lastUpdateUserId = ctx.user.sub; + _profile.lastUpdateFullName = ctx.user.name; + _profile.lastUpdatedAt = new Date(); + + const orgRevision = await orgRevisionRepo.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + const orgRootRef = orgRevisionRef?.orgRoot ?? null; + const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + const matchHolder = profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id); + const shortName = + !profile.current_holders || profile.current_holders.length == 0 + ? null + : matchHolder != null && matchHolder?.orgChild4 != null + ? `${matchHolder.orgChild4.orgChild4ShortName}` + : matchHolder != null && matchHolder?.orgChild3 != null + ? `${matchHolder.orgChild3.orgChild3ShortName}` + : matchHolder != null && matchHolder?.orgChild2 != null + ? `${matchHolder.orgChild2.orgChild2ShortName}` + : matchHolder != null && matchHolder?.orgChild1 != null + ? `${matchHolder.orgChild1.orgChild1ShortName}` + : matchHolder != null && matchHolder?.orgRoot != null + ? `${matchHolder.orgRoot.orgRootShortName}` + : null; + const posNo = `${matchHolder?.posMasterNo}`; + const position = + matchHolder?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + const profileSalary: ProfileSalary = Object.assign(new ProfileSalary(), { + profileId: item.profileId, + commandId: item.commandId, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + amount: item.amount ? item.amount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + posNumCodeSit: _posNumCodeSit, + posNumCodeSitAbb: _posNumCodeSitAbb, + }); + + if (orgRevisionRef) { + await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager); + } + await removeProfileInOrganize(profile.id, "OFFICER", manager); + + const clearProfile = await checkCommandType(String(item.commandId)); + const _null: any = null; + if (clearProfile.status) { + // Keycloak deleteUser ทำภายใน transaction (preserve behavior เดิม — Keycloak ไม่ rollback ได้) + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { + const delUserKeycloak = await deleteUser(_profile.keycloak); + if (delUserKeycloak) { + // Task #228 + _profile.roleKeycloaks = []; + _profile.isActive = false; + _profile.isDelete = true; + } + } + _profile.leaveCommandId = item.commandId ?? _null; + _profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + _profile.leaveRemark = clearProfile.leaveRemark ?? _null; + _profile.leaveDate = item.commandDateAffect ?? _null; + _profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // _profile.position = _null; + // _profile.posTypeId = _null; + // _profile.posLevelId = _null; + } + await Promise.all([ + profileRepository.save(_profile), + salaryRepo.save(profileSalary), + ]); + + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...profileSalary, id: undefined }); + history.profileSalaryId = profileSalary.id; + await salaryHistoryRepo.save(history); + + console.log( + `[ExecuteSalaryProbationService] processOneProbationLeave done — profileId: ${item.profileId}`, + ); + } +} diff --git a/src/services/ExecuteSalaryReportService.ts b/src/services/ExecuteSalaryReportService.ts new file mode 100644 index 00000000..7c72c367 --- /dev/null +++ b/src/services/ExecuteSalaryReportService.ts @@ -0,0 +1,318 @@ +import { EntityManager, Repository } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import permission from "../interfaces/permission"; +import { setLogDataDiff } from "../interfaces/utils"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { + CreateProfileSalary, + CreateProfileSalaryEmployee, + ProfileSalary, +} from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { Command } from "../entities/Command"; +import { OrgRoot } from "../entities/OrgRoot"; + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface SalaryReportExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งเงินเดือนที่ยิงมาจาก salary service + * + * - C-PM-33, C-PM-34, C-PM-35, C-PM-45 : officer → /org/profile/salary/update + * - C-PM-36, C-PM-37, C-PM-46 : employee → /org/profile-employee/salary/update + * + * เดิมเป็น circular callback: org AMQ → salary service → PostData("/org/profile/(employee/)salary/update") + * หลัง Linear Flow: org AMQ → salary service (return salary data) → เรียก service นี้โดยตรง (no callback) + * + * - executeOfficerSalaryUpdate : สร้าง ProfileSalary + history + อัปเดต Profile (amount*) + * - executeEmployeeSalaryUpdate : สร้าง ProfileSalary + history + อัปเดต ProfileEmployee (amount* + salaryLevel/group) + * + * Behavior ทั้งหมด preserve จาก + * ProfileSalaryController.updateSalary / ProfileSalaryEmployeeController.updateSalary ต้นฉบับ + * (รวม permission check + setLogDataDiff + save({data: req})) + * + * Batch semantics: all-or-nothing — transaction เดียวครอบทั้ง batch + * ถ้าคนใด throw (validation/permission) จะ rollback ทั้ง batch และ propagate error + * + * ⚠️ หมายเหตุ permission check: ทำ per-item ภายใน transaction (preserve behavior เดิมที่เช็คทุกคน) + * ทำ HTTP loopback ไป /org/permission/user/... ด้วย token ใน ctx.req — หาก batch ใหญ่อาจช้า + * (เหมือนเดิม เพราะ salary เดิมก็ยิงเข้า endpoint ทีละคนพร้อม permission check) + */ +export class ExecuteSalaryReportService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + // ───────────────────────────────────────────────────────────── + // resolve _posNumCodeSit/Abb จาก command (ทำครั้งเดียวก่อนเข้า transaction) + // admin-lookup ใช้ Profile (officer: profileRepo / employee: profileGovementRepo — ทั้งคู่คือ Profile) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + const profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { posNumCodeSit, posNumCodeSitAbb }; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-33/34/35/45 : officer salary update + // ═══════════════════════════════════════════════════════════════ + async executeOfficerSalaryUpdate( + data: CreateProfileSalary[], + ctx: SalaryReportExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryReportService] executeOfficerSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + for (const item of data ?? []) { + try { + await this.processOneOfficer( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + profileRepository, + salaryRepo, + salaryHistoryRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryReportService] Failed officer, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryReportService] Completed officer — ${data?.length ?? 0} items`); + } + + private async processOneOfficer( + item: CreateProfileSalary, + ctx: SalaryReportExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileRepository: Repository, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + ): Promise { + const req = ctx.req; + + if (!item.profileId) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileId"); + } + const profile = await profileRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id); + + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + Object.assign(data, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.positionSalaryAmount = item.positionSalaryAmount ?? _null; + profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null; + await profileRepository.save(profile); + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-36/37/46 : employee salary update + // ═══════════════════════════════════════════════════════════════ + async executeEmployeeSalaryUpdate( + data: CreateProfileSalaryEmployee[], + ctx: SalaryReportExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryReportService] executeEmployeeSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + for (const item of data ?? []) { + try { + await this.processOneEmployee( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + profileEmployeeRepository, + salaryRepo, + salaryHistoryRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryReportService] Failed employee, commandId=${commandId}, profileEmployeeId=${item.profileEmployeeId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryReportService] Completed employee — ${data?.length ?? 0} items`); + } + + private async processOneEmployee( + item: CreateProfileSalaryEmployee, + ctx: SalaryReportExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileEmployeeRepository: Repository, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + ): Promise { + const req = ctx.req; + + if (!item.profileEmployeeId) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); + } + const profile = await profileEmployeeRepository.findOneBy({ id: item.profileEmployeeId }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id); + + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileEmployeeId }, + order: { order: "DESC" }, + }); + const before = null; + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + order: dest_item == null ? 1 : dest_item.order + 1, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + + Object.assign(data, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.positionSalaryAmount = item.positionSalaryAmount ?? _null; + profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null; + profile.salaryLevel = item.salaryLevel ?? _null; + profile.group = item.group ?? _null; + await profileEmployeeRepository.save(profile); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index f662e247..227c431c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -37,6 +37,8 @@ import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; import { ExecuteSalaryLeaveDisciplineService } from "./ExecuteSalaryLeaveDisciplineService"; import { ExecuteOrgCommandService } from "./ExecuteOrgCommandService"; +import { ExecuteSalaryProbationService } from "./ExecuteSalaryProbationService"; +import { ExecuteSalaryReportService } from "./ExecuteSalaryReportService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -357,6 +359,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isCommand38 = code === "C-PM-38"; const isCommand40 = code === "C-PM-40"; const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40; + // C-PM-10/11/12: ยิงไป probation service — เป็น branch แยก (ไม่ใช่ .NET linear flow) + // - C-PM-10: fire-only (probation update ในตัวเอง ไม่มี org-side action) + // - C-PM-11/12: probation return salary data → เรียก ExecuteSalaryProbationService ตรงๆ + const isProbation = ["C-PM-10", "C-PM-11", "C-PM-12"].includes(code); + // C-PM-33/34/35/45 (officer) + C-PM-36/37/46 (employee): ยิงไป salary service + // เป็น branch แยก — salary return salary data → เรียก ExecuteSalaryReportService ตรงๆ + const isSalaryServiceOfficer = ["C-PM-33", "C-PM-34", "C-PM-35", "C-PM-45"].includes(code); + const isSalaryServiceEmployee = ["C-PM-36", "C-PM-37", "C-PM-46"].includes(code); + const isSalaryService = isSalaryServiceOfficer || isSalaryServiceEmployee; const isLinearFlow = isOfficerProfile || isSalaryCurrent || @@ -384,6 +395,99 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx); } console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`); + } else if (isProbation) { + // Probation Linear Flow (C-PM-10/11/12) + // - C-PM-10: fire-only — probation อัปเดต appoint ในตัวเอง ไม่มี org-side action + // - C-PM-11/12: fire → probation return salary data → route ไป ExecuteSalaryProbationService + // แทนการ callback เข้า org (Circular Flow เดิม) + console.log(`[AMQ] Probation Linear Flow (${code})`); + if (code === "C-PM-10") { + for (const chunk of chunks) { + await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + } + console.log(`[AMQ] C-PM-10 fire-only — no org-side action`); + } else { + let resultData: any[] = []; + for (const chunk of chunks) { + const res = await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + // รองรับทั้ง array และ { data: [...] } (contract ของ probation หลัง Linear Flow) + if (res && Array.isArray(res.data)) { + resultData.push(...res.data); + } else if (Array.isArray(res)) { + resultData.push(...res); + } + } + + if (resultData.length > 0) { + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + + if (code === "C-PM-11") { + await new ExecuteSalaryProbationService().executeProbationPass(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryProbationService (C-PM-11)`, + ); + } else if (code === "C-PM-12") { + await new ExecuteSalaryProbationService().executeProbationLeave(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryProbationService (C-PM-12)`, + ); + } + } + } + } else if (isSalaryService) { + // Salary Service Linear Flow (C-PM-33/34/35/45 officer, C-PM-36/37/46 employee) + // fire → salary service return salary data → route ไป ExecuteSalaryReportService + // แทนการ callback เข้า /org/profile(/-employee)/salary/update (Circular Flow เดิม) + console.log(`[AMQ] Salary Service Linear Flow (${code})`); + let resultData: any[] = []; + for (const chunk of chunks) { + const res = await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + // รองรับทั้ง array และ { data: [...] } (contract ของ salary service หลัง Linear Flow) + if (res && Array.isArray(res.data)) { + resultData.push(...res.data); + } else if (Array.isArray(res)) { + resultData.push(...res); + } + } + + if (resultData.length > 0) { + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + + if (isSalaryServiceOfficer) { + await new ExecuteSalaryReportService().executeOfficerSalaryUpdate(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryReportService (officer)`, + ); + } else { + await new ExecuteSalaryReportService().executeEmployeeSalaryUpdate(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryReportService (employee)`, + ); + } + } } else if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); const isCpm32 = code === "C-PM-32"; From 2f7ec40e783b1b5172fded9bb49bfdca9f85a8b7 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 29 Jun 2026 16:01:16 +0700 Subject: [PATCH 07/12] add pass redis test --- src/controllers/AuthRoleController.ts | 2 ++ src/controllers/OrganizationController.ts | 1 + src/controllers/PermissionController.ts | 7 +++++++ src/controllers/PosMasterActController.ts | 2 ++ src/interfaces/extension.ts | 1 + src/interfaces/permission.ts | 13 ++++++++----- src/services/ExecuteOrgCommandService.ts | 1 + src/services/rabbitmq.ts | 1 + 8 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index 13f5ce04..a549dfb5 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -128,6 +128,7 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { @@ -296,6 +297,7 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); await redisClient.flushdb(function (err: any, succeeded: any) { diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8358313b..605b2fa5 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8830,6 +8830,7 @@ export class OrganizationController extends Controller { const redisClient = redis.createClient({ host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), + password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 44747b09..324a42b4 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -42,6 +42,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -293,6 +294,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -691,6 +693,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -807,6 +810,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -902,6 +906,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1155,6 +1160,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1413,6 +1419,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); // const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index fbc09201..9ccf1d9c 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -328,6 +328,7 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); @@ -865,6 +866,7 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index 405ed849..a2a55256 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -384,6 +384,7 @@ class Extension { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); const deleteKey = delAsync(type + id); diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index 5d22d274..aa1903ec 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -217,17 +217,20 @@ class CheckAuth { // Create Redis client const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: REDIS_PORT, - }, + // socket: { + // host: REDIS_HOST, + // port: REDIS_PORT, + // }, + host: REDIS_HOST, + port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); redisClient.on("error", (err: any) => { console.error("[REDIS] Connection error:", err.message); }); - await redisClient.connect(); + // await redisClient.connect(); console.log("[REDIS] Connected successfully!"); const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts index 62ed231f..31a7d317 100644 --- a/src/services/ExecuteOrgCommandService.ts +++ b/src/services/ExecuteOrgCommandService.ts @@ -844,6 +844,7 @@ export class ExecuteOrgCommandService { const redisClient = await redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 227c431c..b4e81c41 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1987,6 +1987,7 @@ async function clearMenuAndRoleCache(): Promise { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); From 8807ee32262231de2e00442de68abf6683994b96 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 30 Jun 2026 11:04:19 +0700 Subject: [PATCH 08/12] revert add password redis --- src/controllers/AuthRoleController.ts | 2 -- src/controllers/OrganizationController.ts | 1 - src/controllers/PermissionController.ts | 7 ------- src/controllers/PosMasterActController.ts | 2 -- src/interfaces/extension.ts | 1 - src/interfaces/permission.ts | 13 +++++-------- src/services/ExecuteOrgCommandService.ts | 1 - src/services/rabbitmq.ts | 1 - 8 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index a549dfb5..13f5ce04 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -128,7 +128,6 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { @@ -297,7 +296,6 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); await redisClient.flushdb(function (err: any, succeeded: any) { diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 605b2fa5..8358313b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8830,7 +8830,6 @@ export class OrganizationController extends Controller { const redisClient = redis.createClient({ host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), - password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 324a42b4..44747b09 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -42,7 +42,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -294,7 +293,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -693,7 +691,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -810,7 +807,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -906,7 +902,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1160,7 +1155,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1419,7 +1413,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); // const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 9ccf1d9c..fbc09201 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -328,7 +328,6 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); @@ -866,7 +865,6 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index a2a55256..405ed849 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -384,7 +384,6 @@ class Extension { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); const deleteKey = delAsync(type + id); diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index aa1903ec..5d22d274 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -217,20 +217,17 @@ class CheckAuth { // Create Redis client const redisClient = this.redis.createClient({ - // socket: { - // host: REDIS_HOST, - // port: REDIS_PORT, - // }, - host: REDIS_HOST, - port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, + socket: { + host: REDIS_HOST, + port: REDIS_PORT, + }, }); redisClient.on("error", (err: any) => { console.error("[REDIS] Connection error:", err.message); }); - // await redisClient.connect(); + await redisClient.connect(); console.log("[REDIS] Connected successfully!"); const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts index 31a7d317..62ed231f 100644 --- a/src/services/ExecuteOrgCommandService.ts +++ b/src/services/ExecuteOrgCommandService.ts @@ -844,7 +844,6 @@ export class ExecuteOrgCommandService { const redisClient = await redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index b4e81c41..227c431c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1987,7 +1987,6 @@ async function clearMenuAndRoleCache(): Promise { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); From 005f3f1334ada42f5b84d2d509dd818c29b77d18 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 2 Jul 2026 14:46:57 +0700 Subject: [PATCH 09/12] =?UTF-8?q?fix=20=E0=B9=80=E0=B8=9E=E0=B8=B4?= =?UTF-8?q?=E0=B9=88=E0=B8=A1=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=81=E0=B8=B2?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=AD=E0=B8=81=E0=B8=84=E0=B8=B3=E0=B8=AA?= =?UTF-8?q?=E0=B8=B1=E0=B9=88=E0=B8=87=20=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=E0=B9=88=E0=B8=AD=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=A7?= =?UTF-8?q?=E0=B8=A2=E0=B8=87=E0=B8=B2=E0=B8=99=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B9=81=E0=B8=AA=E0=B8=94=E0=B8=87=20#2582?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 173 +++++---------------------- src/services/CommandService.ts | 101 ++++++++++++++++ 2 files changed, 132 insertions(+), 142 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 19dd6a3f..b401aa31 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -100,7 +100,7 @@ import { } from "../services/PositionService"; import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; -import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; +import { reOrderCommandRecivesAndDelete, ensureCommandOperator } from "../services/CommandService"; import { RetirementService } from "../services/RetirementService"; import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileService"; import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; @@ -415,6 +415,7 @@ export class CommandController extends Controller { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบประเภทคำสั่งนี้ในระบบ"); } const now = new Date(); + let userProfile: any = null; command.detailHeader = commandType.detailHeader; command.detailBody = commandType.detailBody; command.detailFooter = commandType.detailFooter; @@ -428,78 +429,39 @@ export class CommandController extends Controller { command.lastUpdateUserId = request.user.sub; command.lastUpdateFullName = request.user.name; command.lastUpdatedAt = now; - await this.commandRepository.save(command); - // insert commandOperator - if (request.user.sub) { - const profile = await this.profileRepository.findOne({ - where: { keycloak: request.user.sub }, - relations: { - posLevel: true, - posType: true, - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, + // Query profile ครั้งเดียว ใช้ร่วมกันทั้ง shortName และ CommandOperator + userProfile = await this.profileRepository.findOne({ + where: { keycloak: request.user.sub }, + relations: { + posLevel: true, + posType: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, }, - }); - if (profile) { - const currentHolder = profile!.current_holders?.find( - (x) => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, - ); + }, + }); - const posNo = - currentHolder != null && currentHolder.orgChild4 != null - ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild3 != null - ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild2 != null - ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild1 != null - ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder?.orgRoot != null - ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` - : null; + // เช็คถ้าไม่ใช่ กสจ. ดึง root.shortName มาปั๊ม + if (userProfile) { + const currentHolder = userProfile.current_holders?.find( + (x: any) => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); - const position = await this.positionRepository.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: currentHolder?.orgRevisionId, - current_holderId: profile!.id, - }, - }, - order: { createdAt: "DESC" }, - relations: { posExecutive: true }, - }); - const operator = Object.assign(new CommandOperator(), { - profileId: profile?.id, - prefix: profile?.prefix, - firstName: profile?.firstName, - lastName: profile?.lastName, - posNo: posNo, - posType: profile?.posType?.posTypeName ?? null, - posLevel: profile?.posLevel?.posLevelName ?? null, - position: position?.positionName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - roleName: "เจ้าหน้าที่ดำเนินการ", - orderNo: 1, - commandId: command.id, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - }); - await this.commandOperatorRepository.save(operator); + if (currentHolder && !currentHolder.orgChild1?.isOfficer) { + command.shortName = currentHolder.orgRoot?.orgRootShortName ?? null; } } + + await this.commandRepository.save(command); + // insert commandOperator + await ensureCommandOperator(userProfile, command.id, request, now); return new HttpSuccess(command.id); } @@ -2720,80 +2682,7 @@ export class CommandController extends Controller { where: { commandId: command.id, roleName: "เจ้าหน้าที่ดำเนินการ" }, }); if (!checkCommandOperator) { - if (request.user.sub) { - // ใช้ userProfile ที่ query ไปแล้วถ้ามี ถ้าไม่มีค่อย query ใหม่ - let profile = userProfile; - if (!profile) { - profile = await this.profileRepository.findOne({ - where: { keycloak: request.user.sub }, - relations: { - posLevel: true, - posType: true, - current_holders: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }, - }); - } - if (profile) { - const currentHolder = profile!.current_holders?.find( - (x: any) => - x.orgRevision?.orgRevisionIsDraft === false && - x.orgRevision?.orgRevisionIsCurrent === true, - ); - - const posNo = - currentHolder != null && currentHolder.orgChild4 != null - ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild3 != null - ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild2 != null - ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder.orgChild1 != null - ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` - : currentHolder != null && currentHolder?.orgRoot != null - ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` - : null; - - const position = await this.positionRepository.findOne({ - where: { - positionIsSelected: true, - posMaster: { - orgRevisionId: currentHolder?.orgRevisionId, - current_holderId: profile!.id, - }, - }, - order: { createdAt: "DESC" }, - relations: { posExecutive: true }, - }); - const operator = Object.assign(new CommandOperator(), { - profileId: profile?.id, - prefix: profile?.prefix, - firstName: profile?.firstName, - lastName: profile?.lastName, - posNo: posNo, - posType: profile?.posType?.posTypeName ?? null, - posLevel: profile?.posLevel?.posLevelName ?? null, - position: position?.positionName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - roleName: "เจ้าหน้าที่ดำเนินการ", - orderNo: 1, - commandId: command.id, - createdUserId: request.user.sub, - createdFullName: request.user.name, - createdAt: now, - lastUpdateUserId: request.user.sub, - lastUpdateFullName: request.user.name, - lastUpdatedAt: now, - }); - await this.commandOperatorRepository.save(operator); - } - } + await ensureCommandOperator(userProfile, command.id, request, now); } const path = commandTypePath(commandCode); diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts index 87334a4b..0c0d7fe9 100644 --- a/src/services/CommandService.ts +++ b/src/services/CommandService.ts @@ -1,8 +1,11 @@ import { AppDataSource } from "../database/data-source"; import { CommandRecive } from "../entities/CommandRecive"; import { Command } from "../entities/Command"; +import { CommandOperator } from "../entities/CommandOperator"; import { OrgRoot } from "../entities/OrgRoot"; +import { Position } from "../entities/Position"; import { Profile } from "../entities/Profile"; +import { RequestWithUser } from "../middlewares/user"; import { EntityManager } from "typeorm"; export interface PosNumCodeSitResult { @@ -144,3 +147,101 @@ export async function getPosNumCodeSit( commandExcecuteDate, }; } + +/** + * สร้าง/insert CommandOperator "เจ้าหน้าที่ดำเนินการ" สำหรับ command + * ใช้ userProfile ที่ query ไปแล้วถ้ามี ถ้าไม่มีค่อย query ใหม่ + * @param userProfile profile ที่ query ไปแล้ว (หรือ null ถ้ายังไม่ได้ query) + * @param commandId command id ที่จะผูกกับ operator + * @param request request context (สำหรับ user.sub / user.name) + * @param now timestamp สำหรับ audit fields + * @param manager ถ้าส่งเข้ามา → ทุก operation อยู่ใน transaction ของ caller (all-or-nothing) + * @returns Promise + */ +export async function ensureCommandOperator( + userProfile: Profile | null, + commandId: string, + request: RequestWithUser, + now: Date, + manager?: EntityManager, +): Promise { + const ds = manager ?? AppDataSource; + const profileRepo = ds.getRepository(Profile); + const positionRepo = ds.getRepository(Position); + const commandOperatorRepo = ds.getRepository(CommandOperator); + + if (!request.user.sub) return; + // ใช้ userProfile ที่ query ไปแล้วถ้ามี ถ้าไม่มีค่อย query ใหม่ + let profile = userProfile; + if (!profile) { + profile = await profileRepo.findOne({ + where: { keycloak: request.user.sub }, + relations: { + posLevel: true, + posType: true, + current_holders: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }, + }); + } + if (!profile) return; + + const currentHolder = profile.current_holders?.find( + (x) => + x.orgRevision?.orgRevisionIsDraft === false && + x.orgRevision?.orgRevisionIsCurrent === true, + ); + + const posNo = + currentHolder != null && currentHolder.orgChild4 != null + ? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild3 != null + ? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild2 != null + ? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder.orgChild1 != null + ? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}` + : currentHolder != null && currentHolder?.orgRoot != null + ? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}` + : null; + + const position = await positionRepo.findOne({ + where: { + positionIsSelected: true, + posMaster: { + orgRevisionId: currentHolder?.orgRevisionId, + current_holderId: profile.id, + }, + }, + order: { createdAt: "DESC" }, + relations: { posExecutive: true }, + }); + + const operator = Object.assign(new CommandOperator(), { + profileId: profile?.id, + prefix: profile?.prefix, + firstName: profile?.firstName, + lastName: profile?.lastName, + posNo: posNo, + posType: profile?.posType?.posTypeName ?? null, + posLevel: profile?.posLevel?.posLevelName ?? null, + position: position?.positionName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + roleName: "เจ้าหน้าที่ดำเนินการ", + orderNo: 1, + commandId: commandId, + createdUserId: request.user.sub, + createdFullName: request.user.name, + createdAt: now, + lastUpdateUserId: request.user.sub, + lastUpdateFullName: request.user.name, + lastUpdatedAt: now, + }); + await commandOperatorRepo.save(operator); +} From 30ddb4e3f38067425b25c1f224a9a676d27039cd Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 2 Jul 2026 18:11:59 +0700 Subject: [PATCH 10/12] fix #240 --- src/controllers/OrganizationDotnetController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 8e803569..9d4e006b 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -8918,7 +8918,7 @@ export class OrganizationDotnetController extends Controller { dateStart: profile?.dateStart ?? null, dateAppoint: profile?.dateAppoint ?? null, keycloak: profile?.keycloak ?? null, - posNo: `${item.shortName} ${item.posMasterNo}`, + posNo: profile?.posMasterNo ?? `${item.shortName} ${item.posMasterNo}`, position: item.position, positionLevel: item.posLevel, positionType: item.posType, From cf161f98096a58813bbd4909885b812eb48122b5 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 3 Jul 2026 10:34:20 +0700 Subject: [PATCH 11/12] fix #240 --- src/controllers/OrganizationDotnetController.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/controllers/OrganizationDotnetController.ts b/src/controllers/OrganizationDotnetController.ts index 9d4e006b..0814d8a2 100644 --- a/src/controllers/OrganizationDotnetController.ts +++ b/src/controllers/OrganizationDotnetController.ts @@ -7227,7 +7227,9 @@ export class OrganizationDotnetController extends Controller { dateStart: profileEmp?.dateStart ?? null, dateAppoint: profileEmp?.dateAppoint ?? null, keycloak: profileEmp?.keycloak ?? null, - posNo: `${item.shortName} ${item.posMasterNo}`, + posNo: `${item.shortName} ${[item.posMasterNoPrefix, item.posMasterNo, item.posMasterNoSuffix] + .filter((p) => p !== null && p !== undefined && p !== "") + .join(" ")}`, position: item.position, positionLevel: item.posLevel, positionType: item.posType, @@ -8900,7 +8902,7 @@ export class OrganizationDotnetController extends Controller { const profiles = await this.profileRepo.find({ where: { id: In(profileIds) }, - select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak"], + select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak", "posMasterNo"], }); const profileMap = new Map(profiles.map((p) => [p.id, p])); @@ -8918,7 +8920,11 @@ export class OrganizationDotnetController extends Controller { dateStart: profile?.dateStart ?? null, dateAppoint: profile?.dateAppoint ?? null, keycloak: profile?.keycloak ?? null, - posNo: profile?.posMasterNo ?? `${item.shortName} ${item.posMasterNo}`, + posNo: + profile?.posMasterNo ?? + `${item.shortName} ${[item.posMasterNoPrefix, item.posMasterNo, item.posMasterNoSuffix] + .filter((p) => p !== null && p !== undefined && p !== "") + .join(" ")}`, position: item.position, positionLevel: item.posLevel, positionType: item.posType, From 759e69d44d604a3cc32d9dc8c127ee62b16e7cab Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 3 Jul 2026 17:30:44 +0700 Subject: [PATCH 12/12] fix --- src/controllers/PosMasterActController.ts | 6 ++--- src/services/rabbitmq.ts | 28 +++++++++++++++++------ 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index fbc09201..3b5d3196 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -621,13 +621,13 @@ export class PosMasterActController extends Controller { let conditionGroup = ""; if (body.type.trim().toUpperCase() == "GROUP1.1") { conditionGroup = - "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ชำนาญงาน') OR (posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ปฏิบัติงาน') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ปฏิบัติการ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการ')"; + "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ชำนาญงาน') OR (posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ปฏิบัติงาน') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ปฏิบัติการ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการ')"; } else if (body.type.trim().toUpperCase() == "GROUP1.2") { conditionGroup = - "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'อาวุโส') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการพิเศษ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'ต้น')"; + "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'อาวุโส') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการพิเศษ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'ต้น')"; } else if (body.type.trim().toUpperCase() == "GROUP2") { conditionGroup = - "(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ทักษะพิเศษ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'เชี่ยวชาญ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ทรงคุณวุฒิ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'สูง') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'ต้น') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'สูง')"; + "(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ทักษะพิเศษ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'เชี่ยวชาญ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ทรงคุณวุฒิ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'สูง') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'ต้น') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'สูง')"; } else { throw new HttpError(HttpStatusCode.NOT_FOUND, "กลุ่มเป้าหมายไม่ถูกต้อง"); } diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 227c431c..3a989a5f 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -176,6 +176,23 @@ function createConsumer( //----> consumer ); } +/** + * สร้าง pseudo Express request สำหรับ service ที่ถูกเรียกจาก RabbitMQ (ไม่ใช่ HTTP) + * ต้องมี `.app.locals.logData.sequence` เพราะ addLogSequence (ใน CallAPI) อ่านค่านี้ + * และ `.headers.authorization` + `.user` สำหรับ audit/auth + */ +function buildPseudoReq(token: string, user: any) { + return { + headers: { authorization: token }, + user, + app: { + locals: { + logData: { sequence: [] as any[] }, + }, + }, + }; +} + async function handler(msg: amqp.ConsumeMessage): Promise { //----> condition before process consumer // const repo = AppDataSource.getRepository(Command); @@ -381,7 +398,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว if (isOrgSelfLinear) { console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`); - const pseudoReq = { headers: { authorization: token }, user }; + const pseudoReq = buildPseudoReq(token, user); const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -429,7 +446,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } if (resultData.length > 0) { - const pseudoReq = { headers: { authorization: token }, user }; + const pseudoReq = buildPseudoReq(token, user); const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -470,7 +487,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } if (resultData.length > 0) { - const pseudoReq = { headers: { authorization: token }, user }; + const pseudoReq = buildPseudoReq(token, user); const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq, @@ -519,10 +536,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // Route ไป service ที่ถูกต้องตาม commandType if (resultData.length > 0 || resultData1.length > 0) { // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) - const pseudoReq = { - headers: { authorization: token }, - user, - }; + const pseudoReq = buildPseudoReq(token, user); const ctx = { user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, req: pseudoReq,