From ecd0388eb0329114f481ff3c9966254263920db6 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 18:33:27 +0700 Subject: [PATCH] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1?= =?UTF-8?q?=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`);