This commit is contained in:
parent
5acd485368
commit
ecd0388eb0
2 changed files with 79 additions and 64 deletions
|
|
@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue