เพิ่ม log
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m4s

This commit is contained in:
harid 2026-06-23 18:33:27 +07:00
parent 5acd485368
commit ecd0388eb0
2 changed files with 79 additions and 64 deletions

View file

@ -61,8 +61,9 @@ export interface SalaryCurrentExecutionContext {
} }
/** /**
* batch independent * batch all-or-nothing (single transaction batch)
* fail rollback (per-item transaction) * return result; throw rollback batch
* propagate error (caller failure )
*/ */
export interface ExecuteSalaryResult { export interface ExecuteSalaryResult {
successCount: number; successCount: number;
@ -80,10 +81,9 @@ export interface ExecuteSalaryResult {
* *
* Behavior preserve CommandController.newSalaryAndUpdateCurrent * Behavior preserve CommandController.newSalaryAndUpdateCurrent
* *
* Batch semantics: ประมวลผลทุกคนแบบ sequential () * Batch semantics: all-or-nothing transaction (sequential)
* transaction race condition batch * throw rollback batch propagate error ()
* posMaster/position throw rollback * return result success count
* success/failure count + fail
*/ */
export class ExecuteSalaryCurrentService { export class ExecuteSalaryCurrentService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
@ -99,8 +99,12 @@ export class ExecuteSalaryCurrentService {
data: SalaryCurrentItem[], data: SalaryCurrentItem[],
ctx: SalaryCurrentExecutionContext, ctx: SalaryCurrentExecutionContext,
): Promise<ExecuteSalaryResult> { ): Promise<ExecuteSalaryResult> {
console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); 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) // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -162,39 +166,37 @@ export class ExecuteSalaryCurrentService {
} }
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Per-item transaction: แต่ละคนมี transaction ของตัวเอง (sequential) // Single transaction ครอบทั้ง batch (all-or-nothing)
// ประมวลทีละคนเพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
// posMaster/position ตัวเดียวกัน คนที่ throw จะ rollback เฉพาะตัว (manager) // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
// และไม่กระทบคนอื่นใน batch
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
const failures: ExecuteSalaryResult["failures"] = [];
let successCount = 0; let successCount = 0;
for (const item of data ?? []) { await AppDataSource.transaction(async (manager) => {
try { for (const item of data ?? []) {
await AppDataSource.transaction(async (manager) => { try {
await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb);
}); successCount++;
successCount++; } catch (err) {
} catch (err) { const reason =
const reason = err instanceof HttpError
err instanceof HttpError
? err.message
: err instanceof Error
? err.message ? err.message
: "unexpected error"; : err instanceof Error
console.error( ? err.message
`[ExecuteSalaryCurrentService] Failed profileId=${item.profileId}: ${reason}`, : "unexpected error";
err, console.error(
); `[ExecuteSalaryCurrentService] Failed — commandCode: ${commandCode}, commandId: ${commandId}, profileId: ${item.profileId}, reason: ${reason}`,
failures.push({ profileId: item.profileId ?? "unknown", reason }); err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
} }
} });
console.log( 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 }); const profile: any = await profileRepository.findOneBy({ id: item.profileId });
if (!profile) { if (!profile) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, `ไม่พบข้อมูลทะเบียนประวัตินี้ profileId: ${item.profileId}`);
} }
let _null: any = null; let _null: any = null;
const dest_item = await salaryRepo.findOne({ const dest_item = await salaryRepo.findOne({
@ -250,6 +252,9 @@ export class ExecuteSalaryCurrentService {
await salaryHistoryRepo.save(history, { data: req }); await salaryHistoryRepo.save(history, { data: req });
// STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา
console.log(
`[ExecuteSalaryCurrentService] STEP 1: Finding posMaster — posmasterId: ${item.posmasterId}, profileId: ${item.profileId}`,
);
let posMaster = await posMasterRepository.findOne({ let posMaster = await posMasterRepository.findOne({
where: { id: item.posmasterId }, where: { id: item.posmasterId },
relations: { relations: {
@ -261,14 +266,21 @@ export class ExecuteSalaryCurrentService {
orgChild4: true, orgChild4: true,
}, },
}); });
console.log(
`[ExecuteSalaryCurrentService] STEP 1: posMaster found: ${!!posMaster}, ancestorDNA: ${posMaster?.ancestorDNA ?? "null"}, orgRevisionId: ${posMaster?.orgRevisionId ?? "null"}`,
);
// เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่
const isCurrent = const isCurrent =
posMaster?.orgRevision?.orgRevisionIsCurrent === true && posMaster?.orgRevision?.orgRevisionIsCurrent === true &&
posMaster?.orgRevision?.orgRevisionIsDraft === false; posMaster?.orgRevision?.orgRevisionIsDraft === false;
console.log(`[ExecuteSalaryCurrentService] STEP 1: isCurrent: ${isCurrent}`);
// ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA
if (!isCurrent && posMaster?.ancestorDNA) { if (!isCurrent && posMaster?.ancestorDNA) {
console.log(
`[ExecuteSalaryCurrentService] STEP 1: Not current — re-resolving via ancestorDNA: ${posMaster.ancestorDNA}`,
);
posMaster = await posMasterRepository.findOne({ posMaster = await posMasterRepository.findOne({
where: { where: {
ancestorDNA: posMaster.ancestorDNA, ancestorDNA: posMaster.ancestorDNA,
@ -286,13 +298,16 @@ export class ExecuteSalaryCurrentService {
orgChild4: true, orgChild4: true,
}, },
}); });
console.log(
`[ExecuteSalaryCurrentService] STEP 1: ancestorDNA re-resolve — found: ${!!posMaster}`,
);
} }
if (posMaster == null) { if (posMaster == null) {
console.error( 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({ const posMasterOld = await posMasterRepository.findOne({
@ -359,6 +374,9 @@ export class ExecuteSalaryCurrentService {
await posMasterRepository.save(posMasterOld); await posMasterRepository.save(posMasterOld);
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
console.log(
`[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`,
);
} }
await posMasterRepository.save(posMaster); await posMasterRepository.save(posMaster);
@ -375,6 +393,10 @@ export class ExecuteSalaryCurrentService {
const posTypeId = item.positionTypeId || item.positionType; const posTypeId = item.positionTypeId || item.positionType;
const posLevelId = item.positionLevelId || item.positionLevel; 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 ตรง // CONDITION 1: เช็คจาก positionId ตรง
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
@ -386,6 +408,9 @@ export class ExecuteSalaryCurrentService {
}, },
relations: ["posExecutive"], relations: ["posExecutive"],
}); });
console.log(
`[ExecuteSalaryCurrentService] STEP 2 / Condition 1: match: ${!!positionById}`,
);
if (positionById) { if (positionById) {
positionNew = positionById; positionNew = positionById;
@ -423,6 +448,10 @@ export class ExecuteSalaryCurrentService {
relations: ["posExecutive"], relations: ["posExecutive"],
order: { orderNo: "ASC" }, order: { orderNo: "ASC" },
}); });
console.log(
`[ExecuteSalaryCurrentService] STEP 2 / Condition 2: match: ${!!positionBy7Fields}`,
whereCondition,
);
if (positionBy7Fields) { if (positionBy7Fields) {
positionNew = positionBy7Fields; positionNew = positionBy7Fields;
@ -443,31 +472,18 @@ export class ExecuteSalaryCurrentService {
relations: ["posExecutive"], relations: ["posExecutive"],
order: { orderNo: "ASC" }, order: { orderNo: "ASC" },
}); });
console.log(
`[ExecuteSalaryCurrentService] STEP 2 / Condition 3: match: ${!!positionBy3Fields}`,
);
if (positionBy3Fields) { if (positionBy3Fields) {
positionNew = positionBy3Fields; positionNew = positionBy3Fields;
} }
} }
// // ═══════════════════════════════════════════════════════════ console.log(
// // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster `[ExecuteSalaryCurrentService] STEP 2: Resolved positionNew: ${positionNew ? positionNew.id : "null (no match — profile position not updated)"}`,
// // ═══════════════════════════════════════════════════════════ );
// 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) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ
if (positionNew != null) { if (positionNew != null) {
@ -488,8 +504,15 @@ export class ExecuteSalaryCurrentService {
profile.amountSpecial = item.amountSpecial ?? null; profile.amountSpecial = item.amountSpecial ?? null;
await profileRepository.save(profile); await profileRepository.save(profile);
await positionRepository.save(positionNew); await positionRepository.save(positionNew);
console.log(
`[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`,
);
} }
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
console.log(
`[ExecuteSalaryCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`,
);
} }
} }

View file

@ -388,16 +388,8 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx);
console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`);
} else if (isSalaryCurrent) { } else if (isSalaryCurrent) {
const salaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx);
resultData, console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`);
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) { } else if (isSalaryEmployeeCurrent) {
await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx);
console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`);