เพิ่ม 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
* 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<ExecuteSalaryResult> {
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}`,
);
}
}

View file

@ -388,16 +388,8 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
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`);