Linear Flow discipline + organization #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s
This commit is contained in:
parent
832c5d2cb3
commit
3d2fc5128a
7 changed files with 1591 additions and 1148 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -129,14 +129,23 @@ export interface ExecutionContext {
|
|||
/**
|
||||
* Service สำหรับสร้าง/อัปเดตทะเบียนประวัติข้าราชการ (Profile) หลังออกคำสั่งบรรจุ
|
||||
*
|
||||
* ถูกออกแบบมาเพื่อแก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ
|
||||
* โดยให้ฝั่งบรรจุส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เอง
|
||||
* ที่ต้นทาง (Linear Flow) แทนการเรียกซ้อนกันกลับไปมา
|
||||
* ใช้กับ commandType: C-PM-01, 02, 14
|
||||
*
|
||||
* - endpoint /org/command/excexute/create-officer-profile เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (no HTTP loopback)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes)
|
||||
* ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้
|
||||
* ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*
|
||||
* Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ
|
||||
* ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน
|
||||
*/
|
||||
export class ExecuteOfficerProfileService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
|
|
|
|||
860
src/services/ExecuteOrgCommandService.ts
Normal file
860
src/services/ExecuteOrgCommandService.ts
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
import { Double, EntityManager, In, Like } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import Extension from "../interfaces/extension";
|
||||
import CallAPI from "../interfaces/call-api";
|
||||
import { setLogDataDiff } from "../interfaces/utils";
|
||||
import {
|
||||
CreatePosMasterHistoryEmployee,
|
||||
CreatePosMasterHistoryEmployeeTemp,
|
||||
} from "./PositionService";
|
||||
import {
|
||||
addUserRoles,
|
||||
createUser,
|
||||
getRoles,
|
||||
getUserByUsername,
|
||||
getRoleMappings,
|
||||
} from "../keycloak";
|
||||
import { Command } from "../entities/Command";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
||||
import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
import { RoleKeycloak } from "../entities/RoleKeycloak";
|
||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster";
|
||||
import { EmployeePosition } from "../entities/EmployeePosition";
|
||||
import { PosMaster } from "../entities/PosMaster";
|
||||
import { Position } from "../entities/Position";
|
||||
import { ProfileSalary } from "../entities/ProfileSalary";
|
||||
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
|
||||
import { PosMasterAct } from "../entities/PosMasterAct";
|
||||
import { ProfileActposition } from "../entities/ProfileActposition";
|
||||
import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory";
|
||||
import { promisify } from "util";
|
||||
|
||||
const redis = require("redis");
|
||||
const REDIS_HOST = process.env.REDIS_HOST;
|
||||
const REDIS_PORT = process.env.REDIS_PORT;
|
||||
|
||||
/**
|
||||
* Input: refIds ที่ consumer ใน rabbitmq build ขึ้น (เดิมคือ body.refIds ของ endpoint /excecute)
|
||||
* ใช้กับ C-PM-21, C-PM-38, C-PM-40
|
||||
*/
|
||||
export interface CommandRefItem {
|
||||
refId: string;
|
||||
commandId?: string | null;
|
||||
amount: Double | null;
|
||||
amountSpecial?: Double | null;
|
||||
positionSalaryAmount: Double | null;
|
||||
mouthSalaryAmount: Double | null;
|
||||
commandNo: string | null;
|
||||
commandYear: number;
|
||||
commandDateAffect?: Date | string | null;
|
||||
commandDateSign?: Date | string | null;
|
||||
commandCode?: string | null;
|
||||
commandName?: string | null;
|
||||
remark: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context สำหรับ audit/log (เหมือน ExecuteSalaryService)
|
||||
*/
|
||||
export interface OrgCommandExecutionContext {
|
||||
user: { sub: string; name: string };
|
||||
req?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service สำหรับคำสั่งที่เดิม "ยิงเข้าตัว" (HTTP loopback เข้า org เอง)
|
||||
*
|
||||
* ใช้กับ commandType:
|
||||
* - C-PM-21 : command21/employee/report/excecute (ลูกจ้าง → พนักงานประจำ)
|
||||
* - C-PM-38 : command38/officer/report/excecute (เงินเดือน next_holder ข้าราชการ)
|
||||
* - C-PM-40 : command40/officer/report/excecute (รักษาการ)
|
||||
*
|
||||
* - endpoint commandXX/.../excecute ทั้ง 3 เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow / ทำต่อ)
|
||||
* แทนการ PostData(path + "/excecute") ที่เป็น HTTP loopback เข้า org ตัวเอง
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
*
|
||||
* ⚠️ หมายเหตุ side-effect ที่อยู่นอก DB transaction:
|
||||
* - Keycloak operations (createUser/addUserRoles/getRoleMappings) ใน C-PM-21 ทำภายใน transaction
|
||||
* เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจากนี้
|
||||
* Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
* - .NET call (C-PM-21) ทำหลัง transaction commit แล้ว เพราะ .NET ไม่สามารถ rollback ได้
|
||||
* - Redis cache clear (C-PM-40) ทำหลัง transaction commit (เป็นการ del cache key — idempotent)
|
||||
* - CreatePosMasterHistoryEmployeeTemp สร้าง nested transaction ของตัวเอง (ไม่รับ manager)
|
||||
*/
|
||||
export class ExecuteOrgCommandService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
|
||||
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
|
||||
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
||||
private posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// แก้ปัญหา _posNumCodeSit/_command resolution ที่ซ้ำกันในทุก endpoint
|
||||
// (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
private async resolvePosNumCodeSit(
|
||||
commandId: string | null | undefined,
|
||||
): Promise<{ command: Command | null; posNumCodeSit: string; posNumCodeSitAbb: string }> {
|
||||
let posNumCodeSit = "";
|
||||
let posNumCodeSitAbb = "";
|
||||
const command = commandId
|
||||
? await this.commandRepository.findOne({ where: { id: commandId } })
|
||||
: null;
|
||||
if (command) {
|
||||
if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
|
||||
const orgRootDeputy = await this.orgRootRepository.findOne({
|
||||
where: {
|
||||
isDeputy: true,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: ["orgRevision"],
|
||||
});
|
||||
posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
|
||||
posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป.";
|
||||
} else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
|
||||
posNumCodeSit = "กรุงเทพมหานคร";
|
||||
posNumCodeSitAbb = "กทม.";
|
||||
} else {
|
||||
let profileAdmin = await this.profileRepository.findOne({
|
||||
where: {
|
||||
keycloak: command?.createdUserId.toString(),
|
||||
current_holders: {
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
|
||||
});
|
||||
posNumCodeSit =
|
||||
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
|
||||
"";
|
||||
posNumCodeSitAbb =
|
||||
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
|
||||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
return { command, posNumCodeSit, posNumCodeSitAbb };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-21 : command21/employee/report/excecute
|
||||
// ลูกจ้างชั่วคราว → พนักงานประจำ (บรรจุ)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
/**
|
||||
* @returns profileEmps ที่จะส่งต่อให้ .NET (สำหรับ consumer rabbitmq เรียก .NET เอง)
|
||||
* ถ้าเรียกจาก thin-wrapper endpoint จะเรียก .NET ภายใน method นี้เอง
|
||||
*/
|
||||
async executeCommand21Employee(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
options?: { callDotNet?: boolean },
|
||||
): Promise<{ profileEmps: any[] }> {
|
||||
const req = ctx.req;
|
||||
const callDotNet = options?.callDotNet ?? true;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand21Employee — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
const roleKeycloak = await this.roleKeycloakRepo.findOne({
|
||||
where: { name: Like("USER") },
|
||||
});
|
||||
const { command: _command, posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
|
||||
await this.resolvePosNumCodeSit(commandId);
|
||||
|
||||
const profileEmps: any[] = [];
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOneCommand21(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
roleKeycloak,
|
||||
_command,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
profileEmps,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteOrgCommandService] Failed C-PM-21, commandId=${commandId}, refId=${item.refId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// .NET call ทำหลัง commit (เหมือน endpoint เดิมที่เรียกหลัง Promise.all) — .NET ไม่ rollback ได้
|
||||
if (callDotNet && profileEmps.length > 0) {
|
||||
await new CallAPI()
|
||||
.PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", {
|
||||
profileEmps,
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`Failed. Cannot update status. ${error?.message ?? ""}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] Completed C-PM-21 — ${profileEmps.length} profiles sent to .NET`,
|
||||
);
|
||||
return { profileEmps };
|
||||
}
|
||||
|
||||
private async processOneCommand21(
|
||||
item: CommandRefItem,
|
||||
ctx: OrgCommandExecutionContext,
|
||||
manager: EntityManager,
|
||||
roleKeycloak: RoleKeycloak | null,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
profileEmps: any[],
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
const employeeTempPosMasterRepository = manager.getRepository(EmployeeTempPosMaster);
|
||||
const employeePositionRepository = manager.getRepository(EmployeePosition);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
|
||||
const profile = await profileEmployeeRepository.findOne({
|
||||
where: { id: item.refId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
|
||||
}
|
||||
const orgRevision = await this.orgRevisionRepository.findOne({
|
||||
where: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
});
|
||||
const _posMaster = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
orgRevisionId: orgRevision?.id,
|
||||
id: profile.posmasterIdTemp,
|
||||
},
|
||||
relations: {
|
||||
orgRoot: true,
|
||||
orgChild1: true,
|
||||
orgChild2: true,
|
||||
orgChild3: true,
|
||||
orgChild4: true,
|
||||
},
|
||||
});
|
||||
const orgRootRef = _posMaster?.orgRoot ?? null;
|
||||
const orgChild1Ref = _posMaster?.orgChild1 ?? null;
|
||||
const orgChild2Ref = _posMaster?.orgChild2 ?? null;
|
||||
const orgChild3Ref = _posMaster?.orgChild3 ?? null;
|
||||
const orgChild4Ref = _posMaster?.orgChild4 ?? null;
|
||||
let orgShortName = "";
|
||||
if (_posMaster != null) {
|
||||
if (_posMaster.orgChild1Id === null) {
|
||||
orgShortName = _posMaster.orgRoot?.orgRootShortName;
|
||||
} else if (_posMaster.orgChild2Id === null) {
|
||||
orgShortName = _posMaster.orgChild1?.orgChild1ShortName;
|
||||
} else if (_posMaster.orgChild3Id === null) {
|
||||
orgShortName = _posMaster.orgChild2?.orgChild2ShortName;
|
||||
} else if (_posMaster.orgChild4Id === null) {
|
||||
orgShortName = _posMaster.orgChild3?.orgChild3ShortName;
|
||||
} else {
|
||||
orgShortName = _posMaster.orgChild4?.orgChild4ShortName;
|
||||
}
|
||||
}
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.refId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileEmployeeId: profile.id,
|
||||
amount: item.amount,
|
||||
amountSpecial: item.amountSpecial,
|
||||
positionSalaryAmount: item.positionSalaryAmount,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount,
|
||||
position: profile.positionTemp,
|
||||
positionName: profile.positionTemp,
|
||||
positionType: profile.posTypeNameTemp,
|
||||
positionLevel: profile.posLevelNameTemp,
|
||||
order: dest_item == null ? 1 : dest_item.order + 1,
|
||||
orgRoot: orgRootRef?.orgRootName ?? null,
|
||||
orgChild1: orgChild1Ref?.orgChild1Name ?? null,
|
||||
orgChild2: orgChild2Ref?.orgChild2Name ?? null,
|
||||
orgChild3: orgChild3Ref?.orgChild3Name ?? null,
|
||||
orgChild4: orgChild4Ref?.orgChild4Name ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: profile.posMasterNoTemp ?? "",
|
||||
posNoAbb: orgShortName,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
|
||||
Object.assign(dataSalary, 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 });
|
||||
|
||||
const posMaster = await employeePosMasterRepository.findOne({
|
||||
where: { id: profile.posmasterIdTemp },
|
||||
relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"],
|
||||
});
|
||||
if (posMaster == null)
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
|
||||
|
||||
const posMasterOld = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
},
|
||||
});
|
||||
if (posMasterOld != null) {
|
||||
posMasterOld.current_holderId = null;
|
||||
posMasterOld.lastUpdatedAt = new Date();
|
||||
}
|
||||
|
||||
const positionOld = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMasterOld?.id,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (positionOld != null) {
|
||||
positionOld.positionIsSelected = false;
|
||||
await employeePositionRepository.save(positionOld);
|
||||
}
|
||||
|
||||
const checkPosition = await employeePositionRepository.find({
|
||||
where: {
|
||||
posMasterId: profile.posmasterIdTemp,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (checkPosition.length > 0) {
|
||||
const clearPosition = checkPosition.map((positions) => ({
|
||||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await employeePositionRepository.save(clearPosition);
|
||||
}
|
||||
|
||||
posMaster.current_holderId = profile.id;
|
||||
posMaster.lastUpdatedAt = new Date();
|
||||
posMaster.next_holderId = null;
|
||||
if (posMasterOld != null) {
|
||||
await employeePosMasterRepository.save(posMasterOld);
|
||||
await CreatePosMasterHistoryEmployee(posMasterOld.id, req, undefined, manager);
|
||||
}
|
||||
await employeePosMasterRepository.save(posMaster);
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager);
|
||||
|
||||
const clsTempPosmaster = await employeeTempPosMasterRepository.find({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (clsTempPosmaster.length > 0) {
|
||||
const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({
|
||||
...posMasterTemp,
|
||||
current_holderId: null,
|
||||
next_holderId: null,
|
||||
}));
|
||||
await employeeTempPosMasterRepository.save(clearTempPosmaster);
|
||||
|
||||
const checkTempPosition = await employeePositionRepository.find({
|
||||
where: {
|
||||
posMasterTempId: In(clearTempPosmaster.map((x) => x.id)),
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (checkTempPosition.length > 0) {
|
||||
const clearTempPosition = checkTempPosition.map((positions) => ({
|
||||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await employeePositionRepository.save(clearTempPosition);
|
||||
}
|
||||
await Promise.all(
|
||||
clsTempPosmaster.map(
|
||||
async (posMasterTemp) =>
|
||||
await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const positionNew = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
id: profile.positionIdTemp,
|
||||
posMasterId: profile.posmasterIdTemp,
|
||||
},
|
||||
});
|
||||
|
||||
if (positionNew != null) {
|
||||
// Create Keycloak
|
||||
const checkUser = await getUserByUsername(profile.citizenId);
|
||||
if (checkUser.length == 0) {
|
||||
let password = profile.citizenId;
|
||||
if (profile.birthDate != null) {
|
||||
const _date = new Date(profile.birthDate.toDateString())
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543;
|
||||
password = `${_date}${_month}${_year}`;
|
||||
}
|
||||
// กรอง "." ออกจาก firstName ก่อนส่งไป keycloak
|
||||
const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? "";
|
||||
const userKeycloakId = await createUser(profile.citizenId, password, {
|
||||
firstName: sanitizedFirstName,
|
||||
lastName: profile.lastName,
|
||||
});
|
||||
const list = await getRoles();
|
||||
if (!Array.isArray(list))
|
||||
throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||
const result = await addUserRoles(
|
||||
userKeycloakId,
|
||||
list
|
||||
.filter((v) => v.name === "USER")
|
||||
.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
})),
|
||||
);
|
||||
profile.keycloak =
|
||||
userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : "";
|
||||
profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : [];
|
||||
// End Create Keycloak
|
||||
} else {
|
||||
const rolesData = await getRoleMappings(checkUser[0].id);
|
||||
if (rolesData) {
|
||||
const _roleKeycloak = await this.roleKeycloakRepo.find({
|
||||
where: { name: In(rolesData.map((x: any) => x.name)) },
|
||||
});
|
||||
profile.roleKeycloaks =
|
||||
_roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : [];
|
||||
}
|
||||
profile.keycloak = checkUser[0].id;
|
||||
}
|
||||
positionNew.positionIsSelected = true;
|
||||
profile.posLevelId = positionNew.posLevelId;
|
||||
profile.posTypeId = positionNew.posTypeId;
|
||||
profile.position = positionNew.positionName;
|
||||
profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null;
|
||||
profile.positionEmployeePositionId = positionNew.positionName;
|
||||
profile.statusTemp = "DONE";
|
||||
profile.employeeClass = "PERM";
|
||||
const _null: any = null;
|
||||
profile.employeeWage = item.amount == null ? _null : item.amount.toString();
|
||||
profile.dateStart = _command ? _command.commandExcecuteDate : new Date();
|
||||
profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date();
|
||||
profile.amount = item.amount == null ? _null : item.amount;
|
||||
profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial;
|
||||
profileEmps.push({
|
||||
profileId: profile.id,
|
||||
prefix: profile.prefix,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
citizenId: profile.citizenId,
|
||||
root: posMaster.orgRoot.orgRootName,
|
||||
rootId: posMaster.orgRootId,
|
||||
rootShortName: posMaster.orgRoot.orgRootShortName,
|
||||
rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null,
|
||||
child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null,
|
||||
child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null,
|
||||
child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null,
|
||||
child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null,
|
||||
});
|
||||
await profileEmployeeRepository.save(profile);
|
||||
await employeePositionRepository.save(positionNew);
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager);
|
||||
//ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว
|
||||
const posMasterTemp = await employeeTempPosMasterRepository.findOne({
|
||||
where: {
|
||||
orgRevisionId: orgRevision?.id,
|
||||
current_holderId: profile.id,
|
||||
},
|
||||
});
|
||||
if (posMasterTemp) {
|
||||
await employeeTempPosMasterRepository.update(posMasterTemp.id, {
|
||||
current_holderId: _null,
|
||||
});
|
||||
await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-38 : command38/officer/report/excecute
|
||||
// เงินเดือน next_holder ของข้าราชการ
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
async executeCommand38Officer(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand38Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
|
||||
await this.resolvePosNumCodeSit(commandId);
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOneCommand38(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteOrgCommandService] Failed C-PM-38, commandId=${commandId}, refId=${item.refId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[ExecuteOrgCommandService] Completed C-PM-38 — ${data?.length ?? 0} items`);
|
||||
}
|
||||
|
||||
private async processOneCommand38(
|
||||
item: CommandRefItem,
|
||||
ctx: OrgCommandExecutionContext,
|
||||
manager: EntityManager,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const posMasterRepository = manager.getRepository(PosMaster);
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const positionRepository = manager.getRepository(Position);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
|
||||
const posMaster = await posMasterRepository.findOne({
|
||||
where: { id: item.refId },
|
||||
relations: [
|
||||
"orgRoot",
|
||||
"orgChild1",
|
||||
"orgChild2",
|
||||
"orgChild3",
|
||||
"orgChild4",
|
||||
"current_holder",
|
||||
"current_holder.posLevel",
|
||||
"current_holder.posType",
|
||||
],
|
||||
});
|
||||
if (!posMaster) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว");
|
||||
}
|
||||
if (posMaster.next_holderId != null) {
|
||||
const orgRootRef = posMaster?.orgRoot ?? null;
|
||||
const orgChild1Ref = posMaster?.orgChild1 ?? null;
|
||||
const orgChild2Ref = posMaster?.orgChild2 ?? null;
|
||||
const orgChild3Ref = posMaster?.orgChild3 ?? null;
|
||||
const orgChild4Ref = posMaster?.orgChild4 ?? null;
|
||||
const shortName =
|
||||
posMaster != null && posMaster.orgChild4 != null
|
||||
? `${posMaster.orgChild4.orgChild4ShortName}`
|
||||
: posMaster != null && posMaster.orgChild3 != null
|
||||
? `${posMaster.orgChild3.orgChild3ShortName}`
|
||||
: posMaster != null && posMaster.orgChild2 != null
|
||||
? `${posMaster.orgChild2.orgChild2ShortName}`
|
||||
: posMaster != null && posMaster.orgChild1 != null
|
||||
? `${posMaster.orgChild1.orgChild1ShortName}`
|
||||
: posMaster != null && posMaster?.orgRoot != null
|
||||
? `${posMaster.orgRoot.orgRootShortName}`
|
||||
: null;
|
||||
const profile = await profileRepository.findOne({
|
||||
where: { id: posMaster.next_holderId },
|
||||
});
|
||||
const position = await positionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
relations: ["posType", "posLevel"],
|
||||
});
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: profile?.id },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileId: profile?.id,
|
||||
date: new Date(),
|
||||
amount: item.amount,
|
||||
commandId: item.commandId,
|
||||
positionSalaryAmount: item.positionSalaryAmount,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount,
|
||||
position: position?.positionName ?? null,
|
||||
positionType: position?.posType?.posTypeName ?? null,
|
||||
positionLevel: position?.posLevel?.posLevelName ?? null,
|
||||
order: dest_item == null ? 1 : dest_item.order + 1,
|
||||
orgRoot: orgRootRef?.orgRootName ?? null,
|
||||
orgChild1: orgChild1Ref?.orgChild1Name ?? null,
|
||||
orgChild2: orgChild2Ref?.orgChild2Name ?? null,
|
||||
orgChild3: orgChild3Ref?.orgChild3Name ?? null,
|
||||
orgChild4: orgChild4Ref?.orgChild4Name ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: posMaster.posMasterNo,
|
||||
posNoAbb: shortName,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(dataSalary, 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 });
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-40 : command40/officer/report/excecute
|
||||
// รักษาการ (ProfileActposition)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
async executeCommand40Officer(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
): Promise<void> {
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand40Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
// 3. ตรวจสอบว่ามี data[0] หรือไม่
|
||||
const firstRef = data[0];
|
||||
if (!firstRef) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบข้อมูล refIds");
|
||||
}
|
||||
|
||||
const profileIdsToClearCache = new Set<string>();
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
// 1. Bulk update status
|
||||
await manager.getRepository(PosMasterAct).update(
|
||||
{ id: In(data.map((x) => x.refId)) },
|
||||
{ statusReport: "DONE" },
|
||||
);
|
||||
|
||||
// 2. ดึงข้อมูลครบทุก relation ที่จำเป็น
|
||||
const posMasters = await manager.getRepository(PosMasterAct).find({
|
||||
where: { id: In(data.map((x) => x.refId)) },
|
||||
relations: [
|
||||
"posMasterChild",
|
||||
"posMasterChild.current_holder",
|
||||
"posMaster",
|
||||
"posMaster.current_holder",
|
||||
"posMaster.positions",
|
||||
"posMaster.orgRoot",
|
||||
"posMaster.orgChild1",
|
||||
"posMaster.orgChild2",
|
||||
"posMaster.orgChild3",
|
||||
"posMaster.orgChild4",
|
||||
],
|
||||
});
|
||||
|
||||
for (const item of posMasters) {
|
||||
try {
|
||||
// 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด
|
||||
if (!item.posMasterChild?.current_holderId || !item.posMaster) {
|
||||
console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.posMasterChild.current_holderId) {
|
||||
profileIdsToClearCache.add(item.posMasterChild.current_holderId);
|
||||
}
|
||||
|
||||
// 5. สร้าง orgShortName แบบปลอดภัย
|
||||
const orgShortName =
|
||||
[
|
||||
item.posMaster?.orgChild4?.orgChild4ShortName,
|
||||
item.posMaster?.orgChild3?.orgChild3ShortName,
|
||||
item.posMaster?.orgChild2?.orgChild2ShortName,
|
||||
item.posMaster?.orgChild1?.orgChild1ShortName,
|
||||
item.posMaster?.orgRoot?.orgRootShortName,
|
||||
].find(Boolean) ?? "";
|
||||
|
||||
// 6. หา position ที่ถูกเลือกแบบปลอดภัย
|
||||
const selectedPosition = item.posMaster?.positions;
|
||||
const positionName =
|
||||
selectedPosition
|
||||
?.map((pos) => pos.positionName)
|
||||
.filter(Boolean)
|
||||
.join(", ") ?? "-";
|
||||
|
||||
// 7. สร้าง metaAct แบบปลอดภัย
|
||||
const metaAct = {
|
||||
profileId: item.posMasterChild.current_holderId,
|
||||
dateStart: firstRef.commandDateAffect ?? null,
|
||||
dateEnd: null,
|
||||
position: positionName,
|
||||
status: true,
|
||||
commandId: firstRef.commandId ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: firstRef.commandNo ?? null,
|
||||
refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`,
|
||||
commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null,
|
||||
posNo:
|
||||
orgShortName && item.posMaster?.posMasterNo
|
||||
? `${orgShortName} ${item.posMaster.posMasterNo}`
|
||||
: item.posMaster?.posMasterNo ?? "-",
|
||||
posNoAbb: orgShortName,
|
||||
commandDateAffect: firstRef.commandDateAffect ?? null,
|
||||
commandDateSign: firstRef.commandDateSign ?? null,
|
||||
commandCode: firstRef.commandCode ?? null,
|
||||
commandName: firstRef.commandName ?? null,
|
||||
remark: firstRef.remark ?? null,
|
||||
};
|
||||
|
||||
// 8. ปิดสถานะรักษาการ
|
||||
const actpositionRepository = manager.getRepository(ProfileActposition);
|
||||
const actpositionHistoryRepository = manager.getRepository(ProfileActpositionHistory);
|
||||
|
||||
const existingActPositions = await actpositionRepository.find({
|
||||
where: {
|
||||
profileId: item.posMasterChild.current_holderId,
|
||||
status: true,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingActPositions.length > 0) {
|
||||
const updatedActPositions = existingActPositions.map((_data) => ({
|
||||
..._data,
|
||||
status: false,
|
||||
dateEnd: new Date(),
|
||||
}));
|
||||
|
||||
await actpositionRepository.save(updatedActPositions);
|
||||
}
|
||||
|
||||
// 9. บันทึกข้อมูลใหม่
|
||||
const dataAct = new ProfileActposition();
|
||||
Object.assign(dataAct, metaAct);
|
||||
|
||||
const historyAct = new ProfileActpositionHistory();
|
||||
Object.assign(historyAct, { ...dataAct, id: undefined });
|
||||
|
||||
await actpositionRepository.save(dataAct);
|
||||
historyAct.profileActpositionId = dataAct.id;
|
||||
await actpositionHistoryRepository.save(historyAct);
|
||||
} catch (error) {
|
||||
console.error(`Error processing item ${item.id}:`, error);
|
||||
throw new HttpError(
|
||||
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
||||
`เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Redis cache clear ทำหลัง commit (del cache key — idempotent)
|
||||
if (profileIdsToClearCache.size > 0) {
|
||||
await Promise.all(
|
||||
Array.from(profileIdsToClearCache).map(async (profileId) => {
|
||||
const redisClient = await redis.createClient({
|
||||
host: REDIS_HOST,
|
||||
port: REDIS_PORT,
|
||||
});
|
||||
|
||||
const delAsync = promisify(redisClient.del).bind(redisClient);
|
||||
await delAsync("role_" + profileId);
|
||||
await delAsync("menu_" + profileId);
|
||||
|
||||
redisClient.quit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[ExecuteOrgCommandService] Completed C-PM-40 — ${data?.length ?? 0} items`);
|
||||
}
|
||||
}
|
||||
|
|
@ -75,6 +75,10 @@ export interface SalaryEmployeeLeaveExecutionContext {
|
|||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryEmployeeLeaveService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
|
|
|
|||
615
src/services/ExecuteSalaryLeaveDisciplineService.ts
Normal file
615
src/services/ExecuteSalaryLeaveDisciplineService.ts
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
import { Double, EntityManager } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
||||
import { ProfileSalary } from "../entities/ProfileSalary";
|
||||
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
|
||||
import { ProfileDiscipline } from "../entities/ProfileDiscipline";
|
||||
import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory";
|
||||
import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
import { Command } from "../entities/Command";
|
||||
import {
|
||||
checkCommandType,
|
||||
removePostMasterAct,
|
||||
removeProfileInOrganize,
|
||||
setLogDataDiff,
|
||||
} from "../interfaces/utils";
|
||||
import {
|
||||
CreatePosMasterHistoryEmployee,
|
||||
CreatePosMasterHistoryOfficer,
|
||||
} from "./PositionService";
|
||||
import { deleteUser } from "../keycloak";
|
||||
|
||||
/**
|
||||
* Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave-discipline
|
||||
* (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 — คำสั่งวินัย ข้าราชการ/ลูกจ้าง)
|
||||
*
|
||||
* profileType "OFFICER" → ข้าราชการ, ค่าอื่น/null → ลูกจ้าง
|
||||
*/
|
||||
export interface SalaryLeaveDisciplineItem {
|
||||
profileId: string;
|
||||
profileType?: string | null;
|
||||
isLeave: boolean | null;
|
||||
leaveReason?: string | null;
|
||||
dateLeave?: Date | string | null;
|
||||
detail?: string | null;
|
||||
level?: string | null;
|
||||
unStigma?: string | null;
|
||||
commandId?: string | null;
|
||||
amount?: Double | null;
|
||||
amountSpecial?: Double | null;
|
||||
positionSalaryAmount?: Double | null;
|
||||
mouthSalaryAmount?: Double | null;
|
||||
isGovernment?: boolean | null;
|
||||
commandNo: string | null;
|
||||
commandYear: number | null;
|
||||
commandDateAffect?: Date | string | null;
|
||||
commandDateSign?: Date | string | null;
|
||||
commandCode?: string | null;
|
||||
commandName?: string | null;
|
||||
remark: string | null;
|
||||
orgRoot?: string | null;
|
||||
orgChild1?: string | null;
|
||||
orgChild2?: string | null;
|
||||
orgChild3?: string | null;
|
||||
orgChild4?: string | null;
|
||||
posNo?: string | null;
|
||||
posNoAbb?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context สำหรับ audit/log
|
||||
*/
|
||||
export interface SalaryLeaveDisciplineExecutionContext {
|
||||
user: { sub: string; name: string };
|
||||
req?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service สำหรับสร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย
|
||||
*
|
||||
* ใช้กับ commandType: C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32
|
||||
*
|
||||
* - endpoint /org/command/excexute/salary-leave-discipline เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeaveDiscipline ต้นฉบับ
|
||||
* รวมถึงกรณี OFFICER ที่การ save ProfileSalary + ProfileDiscipline ถูก comment out ไว้
|
||||
* (เก็บไว้เพื่อ preserve behavior เดิม — มีเพียง EMPLOYEE เท่านั้นที่ save จริง)
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryLeaveDisciplineService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
|
||||
/**
|
||||
* ประมวลผลคำสั่งวินัยทั้ง batch
|
||||
*
|
||||
* @returns สรุปผล success/failure ต่อคน
|
||||
*/
|
||||
async executeSalaryLeaveDiscipline(
|
||||
data: SalaryLeaveDisciplineItem[],
|
||||
ctx: SalaryLeaveDisciplineExecutionContext,
|
||||
): Promise<void> {
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Starting executeSalaryLeaveDiscipline — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryLeaveDisciplineService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const toDate = (v: any): Date | null => {
|
||||
if (v == null || v === "") return null;
|
||||
if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
|
||||
const d = new Date(v);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
for (const item of data ?? []) {
|
||||
const it = item as any;
|
||||
it.dateLeave = toDate(it.dateLeave);
|
||||
it.commandDateAffect = toDate(it.commandDateAffect);
|
||||
it.commandDateSign = toDate(it.commandDateSign);
|
||||
}
|
||||
|
||||
let _posNumCodeSit: string = "";
|
||||
let _posNumCodeSitAbb: string = "";
|
||||
const _command = await this.commandRepository.findOne({
|
||||
relations: ["commandType"],
|
||||
where: { id: data.find((x) => x.commandId)?.commandId ?? "" },
|
||||
});
|
||||
if (_command) {
|
||||
if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
|
||||
const orgRootDeputy = await this.orgRootRepository.findOne({
|
||||
where: {
|
||||
isDeputy: true,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: ["orgRevision"],
|
||||
});
|
||||
_posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
|
||||
_posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป.";
|
||||
} else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
|
||||
_posNumCodeSit = "กรุงเทพมหานคร";
|
||||
_posNumCodeSitAbb = "กทม.";
|
||||
} else {
|
||||
let _profileAdmin = await this.profileRepository.findOne({
|
||||
where: {
|
||||
keycloak: _command?.createdUserId.toString(),
|
||||
current_holders: {
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
|
||||
});
|
||||
_posNumCodeSit =
|
||||
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
|
||||
"";
|
||||
_posNumCodeSitAbb =
|
||||
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
|
||||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
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(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryLeaveDisciplineItem,
|
||||
ctx: SalaryLeaveDisciplineExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const disciplineRepository = manager.getRepository(ProfileDiscipline);
|
||||
const disciplineHistoryRepository = manager.getRepository(ProfileDisciplineHistory);
|
||||
const orgRevisionRepo = manager.getRepository(OrgRevision);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
|
||||
const orgRevision = await orgRevisionRepo.findOne({
|
||||
where: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
});
|
||||
|
||||
let orgRootRef: any = null;
|
||||
let orgChild1Ref: any = null;
|
||||
let orgChild2Ref: any = null;
|
||||
let orgChild3Ref: any = null;
|
||||
let orgChild4Ref: any = null;
|
||||
|
||||
const code = _command?.commandType?.code;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// OFFICER (ข้าราชการ)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") {
|
||||
const profile: any = await profileRepository.findOne({
|
||||
relations: [
|
||||
"posLevel",
|
||||
"posType",
|
||||
"current_holders",
|
||||
"current_holders.orgRoot",
|
||||
"current_holders.orgChild1",
|
||||
"current_holders.orgChild2",
|
||||
"current_holders.orgChild3",
|
||||
"current_holders.orgChild4",
|
||||
"current_holders.positions",
|
||||
"current_holders.positions.posExecutive",
|
||||
"roleKeycloaks",
|
||||
],
|
||||
where: { id: item.profileId },
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const lastSalary = await salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
select: ["order"],
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
|
||||
|
||||
//ลบตำแหน่งที่รักษาการแทน (await + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน)
|
||||
if (code && ["C-PM-19", "C-PM-20"].includes(code)) {
|
||||
await removePostMasterAct(profile.id, manager);
|
||||
}
|
||||
|
||||
const orgRevisionRef =
|
||||
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
|
||||
orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
const position =
|
||||
profile.current_holders
|
||||
.filter((x: any) => x.orgRevisionId == orgRevision?.id)[0]
|
||||
?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null;
|
||||
|
||||
// ประวัติตำแหน่ง
|
||||
const data = new ProfileSalary();
|
||||
data.posNumCodeSit = _posNumCodeSit;
|
||||
data.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileId: profile.id,
|
||||
commandId: item.commandId,
|
||||
position: profile.position,
|
||||
positionName: profile.position,
|
||||
positionType: profile?.posType?.posTypeName ?? null,
|
||||
positionLevel: profile?.posLevel?.posLevelName ?? null,
|
||||
positionExecutive: position?.posExecutive?.posExecutiveName ?? null,
|
||||
amount: item.amount ? item.amount : null,
|
||||
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
|
||||
amountSpecial: item.amountSpecial ? item.amountSpecial : null,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
|
||||
order: nextOrder,
|
||||
orgRoot: item.orgRoot,
|
||||
orgChild1: item.orgChild1,
|
||||
orgChild2: item.orgChild2,
|
||||
orgChild3: item.orgChild3,
|
||||
orgChild4: item.orgChild4,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
dateGovernment: item.commandDateAffect ?? new Date(),
|
||||
isGovernment: item.isGovernment,
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: item.posNo,
|
||||
posNoAbb: item.posNoAbb,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(data, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...data, id: undefined });
|
||||
// ── preserve: OFFICER branch ไม่ save ProfileSalary (comment ตามต้นฉบับ) ──
|
||||
// await salaryRepo.save(data, { data: req });
|
||||
// history.profileSalaryId = data.id;
|
||||
// await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
// ประวัติวินัย
|
||||
const dataDis = new ProfileDiscipline();
|
||||
const metaDis = {
|
||||
date: item.commandDateAffect,
|
||||
refCommandDate: item.commandDateSign,
|
||||
refCommandNo: `${item.commandNo}/${item.commandYear}`,
|
||||
refCommandId: item.commandId,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
};
|
||||
Object.assign(dataDis, { ...item, ...metaDis });
|
||||
const historyDis = new ProfileDisciplineHistory();
|
||||
Object.assign(historyDis, { ...dataDis, id: undefined });
|
||||
// ── preserve: OFFICER branch ไม่ save ProfileDiscipline (comment ตามต้นฉบับ) ──
|
||||
// await disciplineRepository.save(dataDis, { data: req });
|
||||
// historyDis.profileDisciplineId = dataDis.id;
|
||||
// await disciplineHistoryRepository.save(historyDis, { data: req });
|
||||
|
||||
// ทะเบียนประวัติ
|
||||
if (item.isLeave != null) {
|
||||
const _profile: any = await profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!_profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const _null: any = null;
|
||||
_profile.isLeave = item.isLeave;
|
||||
_profile.leaveReason = item.leaveReason ?? _null;
|
||||
_profile.dateLeave = item.dateLeave ?? _null;
|
||||
_profile.lastUpdateUserId = ctx.user.sub;
|
||||
_profile.lastUpdateFullName = ctx.user.name;
|
||||
_profile.lastUpdatedAt = new Date();
|
||||
if (item.isLeave == true) {
|
||||
if (orgRevisionRef) {
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager);
|
||||
}
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(_profile.id, "OFFICER", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
if (clearProfile.status) {
|
||||
if (
|
||||
_profile.keycloak != null &&
|
||||
_profile.keycloak != "" &&
|
||||
_profile.isDelete === false
|
||||
) {
|
||||
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
|
||||
const delUserKeycloak = await deleteUser(_profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// _profile.keycloak = _null;
|
||||
_profile.roleKeycloaks = [];
|
||||
_profile.isActive = false;
|
||||
_profile.isDelete = true;
|
||||
}
|
||||
}
|
||||
_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;
|
||||
}
|
||||
await profileRepository.save(_profile, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: _profile });
|
||||
}
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// EMPLOYEE (ลูกจ้าง)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
else {
|
||||
const profile: any = await profileEmployeeRepository.findOne({
|
||||
relations: [
|
||||
"posLevel",
|
||||
"posType",
|
||||
"current_holders",
|
||||
"current_holders.orgRoot",
|
||||
"current_holders.orgChild1",
|
||||
"current_holders.orgChild2",
|
||||
"current_holders.orgChild3",
|
||||
"current_holders.orgChild4",
|
||||
"roleKeycloaks",
|
||||
],
|
||||
where: { id: item.profileId },
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const lastSalary = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.profileId },
|
||||
select: ["order"],
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
|
||||
const orgRevisionRef =
|
||||
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
|
||||
orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
// ประวัติตำแหน่ง
|
||||
const data = new ProfileSalary();
|
||||
data.posNumCodeSit = _posNumCodeSit;
|
||||
data.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileEmployeeId: profile.id,
|
||||
commandId: item.commandId,
|
||||
position: profile.position,
|
||||
positionName: profile.position,
|
||||
positionType: profile?.posType?.posTypeName ?? null,
|
||||
positionLevel:
|
||||
profile?.posType && profile?.posLevel
|
||||
? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}`
|
||||
: null,
|
||||
amount: item.amount ? item.amount : null,
|
||||
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
|
||||
order: nextOrder,
|
||||
orgRoot: item.orgRoot,
|
||||
orgChild1: item.orgChild1,
|
||||
orgChild2: item.orgChild2,
|
||||
orgChild3: item.orgChild3,
|
||||
orgChild4: item.orgChild4,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
dateGovernment: item.commandDateAffect ?? new Date(),
|
||||
isGovernment: item.isGovernment,
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: item.posNo,
|
||||
posNoAbb: item.posNoAbb,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(data, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...data, id: undefined });
|
||||
await salaryRepo.save(data, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: data });
|
||||
history.profileSalaryId = data.id;
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
// ประวัติวินัย
|
||||
const dataDis = new ProfileDiscipline();
|
||||
const metaDis = {
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
};
|
||||
Object.assign(dataDis, {
|
||||
...item,
|
||||
...metaDis,
|
||||
date: item.commandDateAffect,
|
||||
refCommandDate: item.commandDateSign,
|
||||
refCommandNo: item.commandNo,
|
||||
profileEmployeeId: item.profileId,
|
||||
profileId: undefined,
|
||||
});
|
||||
const historyDis = new ProfileDisciplineHistory();
|
||||
Object.assign(historyDis, { ...dataDis, id: undefined });
|
||||
await disciplineRepository.save(dataDis, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: dataDis });
|
||||
historyDis.profileDisciplineId = dataDis.id;
|
||||
await disciplineHistoryRepository.save(historyDis, { data: req });
|
||||
|
||||
// ทะเบียนประวัติ
|
||||
if (item.isLeave != null) {
|
||||
const _profile: any = await profileEmployeeRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!_profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const _null: any = null;
|
||||
_profile.isLeave = item.isLeave;
|
||||
_profile.leaveReason = item.leaveReason ?? _null;
|
||||
_profile.dateLeave = item.dateLeave ?? _null;
|
||||
_profile.lastUpdateUserId = ctx.user.sub;
|
||||
_profile.lastUpdateFullName = ctx.user.name;
|
||||
_profile.lastUpdatedAt = new Date();
|
||||
if (item.isLeave == true) {
|
||||
// บันทึกประวัติก่อนลบตำแหน่ง
|
||||
const curRevision = await orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
||||
});
|
||||
if (curRevision) {
|
||||
const curPosMaster = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: _profile.id,
|
||||
orgRevisionId: curRevision.id,
|
||||
},
|
||||
});
|
||||
if (curPosMaster) {
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager);
|
||||
}
|
||||
}
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(_profile.id, "EMPLOYEE", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
if (clearProfile.status) {
|
||||
if (
|
||||
_profile.keycloak != null &&
|
||||
_profile.keycloak != "" &&
|
||||
_profile.isDelete === false
|
||||
) {
|
||||
// Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
|
||||
// (Keycloak ไม่สามารถ rollback ได้)
|
||||
const delUserKeycloak = await deleteUser(_profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// _profile.keycloak = _null;
|
||||
_profile.roleKeycloaks = [];
|
||||
_profile.isActive = false;
|
||||
_profile.isDelete = true;
|
||||
}
|
||||
}
|
||||
_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;
|
||||
}
|
||||
await profileEmployeeRepository.save(_profile, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: _profile });
|
||||
}
|
||||
}
|
||||
|
||||
// Task #2190 (preserve: organizeName computed แต่ยังไม่ได้ใช้ในต้นฉบับ — เก็บไว้ตาม behavior เดิม)
|
||||
if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) {
|
||||
let organizeName = "";
|
||||
if (orgRootRef) {
|
||||
const names = [
|
||||
orgChild4Ref?.orgChild4Name,
|
||||
orgChild3Ref?.orgChild3Name,
|
||||
orgChild2Ref?.orgChild2Name,
|
||||
orgChild1Ref?.orgChild1Name,
|
||||
orgRootRef?.orgRootName,
|
||||
].filter(Boolean);
|
||||
organizeName = names.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +70,7 @@ export interface SalaryExecutionContext {
|
|||
/**
|
||||
* Service สำหรับสร้าง ProfileSalary ของข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ
|
||||
*
|
||||
* ใช้กับ commandType: C-PM-13 (โอน), C-PM-15 (ช่วยราชการ), C-PM-16 (เกษียณ)
|
||||
* ใช้กับ commandType: C-PM-13, 15, 16
|
||||
*
|
||||
* - endpoint /org/command/excexute/salary เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
|
|
@ -81,7 +81,9 @@ export interface SalaryExecutionContext {
|
|||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* Keycloak operations (deleteUser) ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService";
|
|||
import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService";
|
||||
import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService";
|
||||
import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService";
|
||||
import { ExecuteSalaryLeaveDisciplineService } from "./ExecuteSalaryLeaveDisciplineService";
|
||||
import { ExecuteOrgCommandService } from "./ExecuteOrgCommandService";
|
||||
|
||||
const redis = require("redis");
|
||||
const REDIS_HOST = process.env.REDIS_HOST;
|
||||
|
|
@ -335,6 +337,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
// - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ)
|
||||
// - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ)
|
||||
// - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave)
|
||||
// - ExecuteSalaryLeaveDisciplineService : C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 (คำสั่งวินัย)
|
||||
// - ExecuteOrgCommandService : C-PM-21, 38, 40 (org-self — path ชี้กลับ org เอง
|
||||
// เรียก Service ตรงๆ ไม่ผ่าน HTTP loopback เพราะ PostData(path+"/excecute") = ยิงเข้าตัว)
|
||||
// - คำสั่งอื่น ยังใช้ Circular Flow เดิม
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const code = command.commandType?.code;
|
||||
|
|
@ -344,17 +349,46 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code);
|
||||
const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code);
|
||||
const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code);
|
||||
const isSalaryLeaveDiscipline = ["C-PM-19", "C-PM-20", "C-PM-25", "C-PM-26", "C-PM-27", "C-PM-28",
|
||||
"C-PM-29", "C-PM-30", "C-PM-31", "C-PM-32",
|
||||
].includes(code);
|
||||
// C-PM-21/38/40: path ชี้กลับ org เอง (ไม่ใช่ .NET) → ต้องเรียก Service ตรงๆ ไม่ผ่าน loopback
|
||||
const isCommand21 = code === "C-PM-21";
|
||||
const isCommand38 = code === "C-PM-38";
|
||||
const isCommand40 = code === "C-PM-40";
|
||||
const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40;
|
||||
const isLinearFlow =
|
||||
isOfficerProfile ||
|
||||
isSalaryCurrent ||
|
||||
isSalaryEmployeeCurrent ||
|
||||
isSalary ||
|
||||
isSalaryLeave ||
|
||||
isSalaryEmployeeLeave;
|
||||
isSalaryEmployeeLeave ||
|
||||
isSalaryLeaveDiscipline;
|
||||
|
||||
if (isLinearFlow) {
|
||||
// Org-self (C-PM-21/38/40): เรียก Service ตรงๆ (Linear Flow / ทำต่อ) ไม่ผ่าน HTTP loopback
|
||||
// เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว
|
||||
if (isOrgSelfLinear) {
|
||||
console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`);
|
||||
const pseudoReq = { headers: { authorization: token }, user };
|
||||
const ctx = {
|
||||
user: { sub: user?.sub ?? "system", name: user?.name ?? "System" },
|
||||
req: pseudoReq,
|
||||
};
|
||||
const flatRefIds = chunks.flat();
|
||||
if (isCommand21) {
|
||||
await new ExecuteOrgCommandService().executeCommand21Employee(flatRefIds, ctx);
|
||||
} else if (isCommand38) {
|
||||
await new ExecuteOrgCommandService().executeCommand38Officer(flatRefIds, ctx);
|
||||
} else if (isCommand40) {
|
||||
await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx);
|
||||
}
|
||||
console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`);
|
||||
} else if (isLinearFlow) {
|
||||
console.log(`[AMQ] Linear Flow (${code})`);
|
||||
const isCpm32 = code === "C-PM-32";
|
||||
let resultData: any[] = [];
|
||||
let resultData1: any[] = []; //เฉพาะ C-PM-32 (ฝั่ง "การพิจารณาลงโทษ")
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const res = await new CallAPI().PostData(
|
||||
|
|
@ -363,9 +397,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
{ refIds: chunk },
|
||||
false,
|
||||
);
|
||||
// response (resultData) จาก .NET
|
||||
if (Array.isArray(res)) {
|
||||
console.log(`[AMQ] Push result data`);
|
||||
if (isCpm32 && res && !Array.isArray(res)) {
|
||||
// C-PM-32: response เป็น object { data, data1 } → แยก 2 track
|
||||
console.log(
|
||||
`[AMQ] C-PM-32 split response — data: ${res.data?.length ?? 0}, data1: ${res.data1?.length ?? 0}`,
|
||||
);
|
||||
if (Array.isArray(res.data)) resultData.push(...res.data);
|
||||
if (Array.isArray(res.data1)) resultData1.push(...res.data1);
|
||||
} else if (Array.isArray(res)) {
|
||||
console.log(`[AMQ] Push result data (${res.length})`);
|
||||
resultData.push(...res);
|
||||
}
|
||||
}
|
||||
|
|
@ -373,7 +413,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`);
|
||||
|
||||
// Route ไป service ที่ถูกต้องตาม commandType
|
||||
if (resultData.length > 0) {
|
||||
if (resultData.length > 0 || resultData1.length > 0) {
|
||||
// สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req})
|
||||
const pseudoReq = {
|
||||
headers: { authorization: token },
|
||||
|
|
@ -402,6 +442,21 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
} else if (isSalaryEmployeeLeave) {
|
||||
await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx);
|
||||
console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`);
|
||||
} else if (isSalaryLeaveDiscipline) {
|
||||
// C-PM-32 (คำสั่งยุติเรื่อง): response เป็น object { data, data1 }
|
||||
// profileId เดียวกันอาจอยู่ในทั้ง 2 track → ต้องส่งให้ org แยก 2 ครั้ง ห้าม merge
|
||||
if (resultData.length > 0) {
|
||||
await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData, ctx);
|
||||
console.log(
|
||||
`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveDisciplineService`,
|
||||
);
|
||||
}
|
||||
if (isCpm32 && resultData1.length > 0) {
|
||||
await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData1, ctx);
|
||||
console.log(
|
||||
`[AMQ] Processed resultData1: ${resultData1.length} profiles via ExecuteSalaryLeaveDisciplineService`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue