From 64be68d0a3a06e8ecbad95af1c4a1132ef15e4a7 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 13:34:06 +0700 Subject: [PATCH] =?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`);