add transaction #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
This commit is contained in:
parent
ecd0388eb0
commit
832c5d2cb3
10 changed files with 2322 additions and 1991 deletions
|
|
@ -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";
|
||||
|
|
@ -76,25 +76,30 @@ export interface SalaryExecutionContext {
|
|||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdate ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* Keycloak operations (deleteUser) ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้
|
||||
*/
|
||||
export class ExecuteSalaryService {
|
||||
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 orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private assistanceRepository = AppDataSource.getRepository(ProfileAssistance);
|
||||
private assistanceHistoryRepository = AppDataSource.getRepository(ProfileAssistanceHistory);
|
||||
|
||||
/**
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/assistance
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/assistance ทั้ง batch
|
||||
*
|
||||
* @returns สรุปผล success/failure ต่อคน
|
||||
*/
|
||||
async executeSalary(data: SalaryItem[], ctx: SalaryExecutionContext): Promise<void> {
|
||||
console.log("[ExecuteSalaryService] Starting executeSalary");
|
||||
console.log("[ExecuteSalaryService] Request body count:", data?.length);
|
||||
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Starting executeSalary — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
|
|
@ -158,170 +163,227 @@ export class ExecuteSalaryService {
|
|||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const profile: any = await this.profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: {
|
||||
roleKeycloaks: true,
|
||||
posType: true,
|
||||
posLevel: true,
|
||||
},
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const posMaster: any = await this.posMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: item.profileId,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
orgRevision: true,
|
||||
orgRoot: true,
|
||||
orgChild1: true,
|
||||
orgChild2: true,
|
||||
orgChild3: true,
|
||||
orgChild4: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgRevisionRef = posMaster ? posMaster.id : null;
|
||||
const orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
//ลบตำแหน่งที่รักษาการแทน
|
||||
const code = _command?.commandType?.code;
|
||||
if (code && ["C-PM-13"].includes(code)) {
|
||||
removePostMasterAct(profile.id);
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
let successCount = 0;
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*
|
||||
* หมายเหตุ: Keycloak deleteUser ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryItem,
|
||||
ctx: SalaryExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
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 assistanceRepository = manager.getRepository(ProfileAssistance);
|
||||
const assistanceHistoryRepository = manager.getRepository(ProfileAssistanceHistory);
|
||||
|
||||
const profile: any = await profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: {
|
||||
roleKeycloaks: true,
|
||||
posType: true,
|
||||
posLevel: true,
|
||||
},
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const posMaster: any = await posMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: item.profileId,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: {
|
||||
orgRevision: true,
|
||||
orgRoot: true,
|
||||
orgChild1: true,
|
||||
orgChild2: true,
|
||||
orgChild3: true,
|
||||
orgChild4: true,
|
||||
},
|
||||
});
|
||||
|
||||
const orgRevisionRef = posMaster ? posMaster.id : null;
|
||||
const orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
//ลบตำแหน่งที่รักษาการแทน
|
||||
const code = _command?.commandType?.code;
|
||||
if (code && ["C-PM-13"].includes(code)) {
|
||||
// await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction
|
||||
await removePostMasterAct(profile.id, manager);
|
||||
}
|
||||
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
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(),
|
||||
};
|
||||
if (item.isLeave != undefined && item.isLeave == true) {
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Creating PosMasterHistory — posMasterId: ${orgRevisionRef}, profileId: ${item.profileId}, type: DELETE`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE", null, manager);
|
||||
await removeProfileInOrganize(profile.id, "OFFICER", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
const _null: any = null;
|
||||
if (clearProfile.status) {
|
||||
// Keycloak deleteUser ทำก่อนเข้า transaction-bound save ด้านล่าง
|
||||
// (ทำภายใน transaction เดียวกัน เพราะถ้า fail ต้อง rollback DB ด้วย)
|
||||
// หมายเหตุ: Keycloak ไม่สามารถ rollback ได้ → ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
|
||||
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
|
||||
const delUserKeycloak = await deleteUser(profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// profile.keycloak = _null;
|
||||
profile.roleKeycloaks = [];
|
||||
profile.isActive = false;
|
||||
profile.isDelete = true;
|
||||
}
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
order: dest_item == null ? 1 : dest_item.order + 1,
|
||||
}
|
||||
profile.isLeave = item.isLeave;
|
||||
profile.leaveCommandId = item.commandId ?? _null;
|
||||
profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`;
|
||||
profile.leaveRemark = clearProfile.leaveRemark ?? _null;
|
||||
profile.leaveDate = item.commandDateAffect ?? _null;
|
||||
profile.leaveType = clearProfile.LeaveType ?? _null;
|
||||
//ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516)
|
||||
// profile.position = _null;
|
||||
// profile.posTypeId = _null;
|
||||
// profile.posLevelId = _null;
|
||||
profile.leaveReason = item.leaveReason ?? _null;
|
||||
profile.dateLeave = item.dateLeave ?? _null;
|
||||
profile.amount = item.amount ?? _null;
|
||||
profile.amountSpecial = item.amountSpecial ?? _null;
|
||||
await profileRepository.save(profile, { data: req });
|
||||
|
||||
// if (profile.id) {
|
||||
// await this.keycloakAttributeService.clearOrgDnaAttributes(
|
||||
// [profile.id],
|
||||
// "PROFILE",
|
||||
// );
|
||||
// }
|
||||
}
|
||||
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.profileSalaryId = dataSalary.id;
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
if (_command) {
|
||||
if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) {
|
||||
// ประวัติคำสั่งให้ช่วยราชการ
|
||||
const dataAssis = new ProfileAssistance();
|
||||
|
||||
const metaAssis = {
|
||||
profileId: item.profileId,
|
||||
agency: item.officerOrg,
|
||||
dateStart: item.dateStart,
|
||||
dateEnd: item.dateEnd,
|
||||
commandNo: `${item.commandNo}/${_commandYear}`,
|
||||
commandName: item.commandName,
|
||||
refId: item.refId,
|
||||
refCommandDate: new Date(),
|
||||
commandId: item.commandId,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
status: _command.commandType.code == "C-PM-15" ? "PENDING" : "DONE",
|
||||
};
|
||||
if (item.isLeave != undefined && item.isLeave == true) {
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE");
|
||||
await removeProfileInOrganize(profile.id, "OFFICER");
|
||||
|
||||
Object.assign(dataAssis, metaAssis);
|
||||
const historyAssis = new ProfileAssistanceHistory();
|
||||
Object.assign(historyAssis, { ...dataAssis, id: undefined });
|
||||
|
||||
await assistanceRepository.save(dataAssis);
|
||||
historyAssis.profileAssistanceId = dataAssis.id;
|
||||
await assistanceHistoryRepository.save(historyAssis);
|
||||
}
|
||||
// Task #2190
|
||||
else if (_command.commandType.code == "C-PM-13") {
|
||||
let organizeName = "";
|
||||
if (orgRootRef) {
|
||||
const names = [
|
||||
orgChild4Ref?.orgChild4Name,
|
||||
orgChild3Ref?.orgChild3Name,
|
||||
orgChild2Ref?.orgChild2Name,
|
||||
orgChild1Ref?.orgChild1Name,
|
||||
orgRootRef?.orgRootName,
|
||||
].filter(Boolean);
|
||||
organizeName = names.join(" ");
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
const _null: any = null;
|
||||
if (clearProfile.status) {
|
||||
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
|
||||
const delUserKeycloak = await deleteUser(profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// profile.keycloak = _null;
|
||||
profile.roleKeycloaks = [];
|
||||
profile.isActive = false;
|
||||
profile.isDelete = true;
|
||||
}
|
||||
}
|
||||
profile.isLeave = item.isLeave;
|
||||
profile.leaveCommandId = item.commandId ?? _null;
|
||||
profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`;
|
||||
profile.leaveRemark = clearProfile.leaveRemark ?? _null;
|
||||
profile.leaveDate = item.commandDateAffect ?? _null;
|
||||
profile.leaveType = clearProfile.LeaveType ?? _null;
|
||||
//ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516)
|
||||
// profile.position = _null;
|
||||
// profile.posTypeId = _null;
|
||||
// profile.posLevelId = _null;
|
||||
profile.leaveReason = item.leaveReason ?? _null;
|
||||
profile.dateLeave = item.dateLeave ?? _null;
|
||||
profile.amount = item.amount ?? _null;
|
||||
profile.amountSpecial = item.amountSpecial ?? _null;
|
||||
await this.profileRepository.save(profile, { data: req });
|
||||
}
|
||||
}
|
||||
|
||||
// if (profile.id) {
|
||||
// await this.keycloakAttributeService.clearOrgDnaAttributes(
|
||||
// [profile.id],
|
||||
// "PROFILE",
|
||||
// );
|
||||
// }
|
||||
}
|
||||
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.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
if (_command) {
|
||||
if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) {
|
||||
// ประวัติคำสั่งให้ช่วยราชการ
|
||||
const dataAssis = new ProfileAssistance();
|
||||
|
||||
const metaAssis = {
|
||||
profileId: item.profileId,
|
||||
agency: item.officerOrg,
|
||||
dateStart: item.dateStart,
|
||||
dateEnd: item.dateEnd,
|
||||
commandNo: `${item.commandNo}/${_commandYear}`,
|
||||
commandName: item.commandName,
|
||||
refId: item.refId,
|
||||
refCommandDate: new Date(),
|
||||
commandId: item.commandId,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
status: _command.commandType.code == "C-PM-15" ? "PENDING" : "DONE",
|
||||
};
|
||||
|
||||
Object.assign(dataAssis, metaAssis);
|
||||
const historyAssis = new ProfileAssistanceHistory();
|
||||
Object.assign(historyAssis, { ...dataAssis, id: undefined });
|
||||
|
||||
await this.assistanceRepository.save(dataAssis);
|
||||
historyAssis.profileAssistanceId = dataAssis.id;
|
||||
await this.assistanceHistoryRepository.save(historyAssis);
|
||||
}
|
||||
// Task #2190
|
||||
else if (_command.commandType.code == "C-PM-13") {
|
||||
let organizeName = "";
|
||||
if (orgRootRef) {
|
||||
const names = [
|
||||
orgChild4Ref?.orgChild4Name,
|
||||
orgChild3Ref?.orgChild3Name,
|
||||
orgChild2Ref?.orgChild2Name,
|
||||
orgChild1Ref?.orgChild1Name,
|
||||
orgRootRef?.orgRootName,
|
||||
].filter(Boolean);
|
||||
organizeName = names.join(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
|
||||
console.log("[ExecuteSalaryService] executeSalary completed successfully");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue