import { Double, In, Like } 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 { ProfileSalary } from "../entities/ProfileSalary"; import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; import { OrgRoot } from "../entities/OrgRoot"; import { OrgRevision } from "../entities/OrgRevision"; import { PosMaster } from "../entities/PosMaster"; import { Position } from "../entities/Position"; import { RoleKeycloak } from "../entities/RoleKeycloak"; import { CommandRecive } from "../entities/CommandRecive"; import { Command } from "../entities/Command"; import { checkCommandType, checkReturnCommandType, removePostMasterAct, removeProfileInOrganize, setLogDataDiff, } from "../interfaces/utils"; import { reOrderCommandRecivesAndDelete } from "./CommandService"; import { CreatePosMasterHistoryOfficer } from "./PositionService"; import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; import { addUserRoles, createUser, deleteUser, getRoleMappings, getRoles, getUserByUsername, updateUserAttributes, } from "../keycloak"; /** * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave * (C-PM-08, 09, 17, 18, 41, 48 — ลาออก/พักราชการ/กลับเข้าราชการ ของข้าราชการ) */ export interface SalaryLeaveItem { profileId: string; amount?: Double | null; amountSpecial?: Double | null; positionSalaryAmount?: Double | null; mouthSalaryAmount?: Double | null; positionExecutive: string | null; positionExecutiveField?: string | null; positionArea?: string | null; positionType: string | null; positionLevel: string | null; isLeave: boolean; leaveReason?: string | null; dateLeave?: Date | string | null; posExecutiveId?: string | null; positionField?: string | null; commandId?: string | null; isGovernment?: boolean | null; orgRoot?: string | null; orgChild1?: string | null; orgChild2?: string | null; orgChild3?: string | null; orgChild4?: string | null; commandNo: string | null; commandYear: number | null; posNo: string | null; posNoAbb: string | null; commandDateAffect?: Date | string | null; commandDateSign?: Date | string | null; positionName: string | null; commandCode?: string | null; commandName?: string | null; remark: string | null; positionId?: string | null; positionTypeNew?: string | null; positionLevelNew?: string | null; positionNameNew?: string | null; posmasterId?: string | null; posTypeNameNew?: string | null; posLevelNameNew?: string | null; posNoNew?: string | null; posNoAbbNew?: string | null; orgRootNew?: string | null; orgChild1New?: string | null; orgChild2New?: string | null; orgChild3New?: string | null; orgChild4New?: string | null; resignId?: string | null; } /** * Context สำหรับ audit/log */ export interface SalaryLeaveExecutionContext { user: { sub: string; name: string }; req?: any; } /** * Service สำหรับสร้าง ProfileSalary ข้าราชการ + handle leave/กลับเข้าราชการ * * ใช้กับ commandType: C-PM-08, 09, 17, 18, 41, 48 * * - endpoint /org/command/excexute/salary-leave เรียกผ่าน service นี้ (thin wrapper) * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeave ต้นฉบับ */ export class ExecuteSalaryLeaveService { private commandRepository = AppDataSource.getRepository(Command); private commandReciveRepository = AppDataSource.getRepository(CommandRecive); private profileRepository = AppDataSource.getRepository(Profile); private salaryRepo = AppDataSource.getRepository(ProfileSalary); private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); private posMasterRepository = AppDataSource.getRepository(PosMaster); private positionRepository = AppDataSource.getRepository(Position); private orgRootRepository = AppDataSource.getRepository(OrgRoot); private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); /** * ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการ */ async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise { console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave"); console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length); const req = ctx.req; // ───────────────────────────────────────────────────────────── // 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); } const roleKeycloak = await this.roleKeycloakRepo.findOne({ where: { name: Like("USER") }, }); 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 ?? ""; } } const today = new Date().setHours(0, 0, 0, 0); await Promise.all( data.map(async (item) => { const profile = await this.profileRepository.findOne({ where: { id: item.profileId }, relations: { roleKeycloaks: true, }, }); if (!profile) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); } //ลบตำแหน่งที่รักษาการแทน const code = _command?.commandType?.code; if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { removePostMasterAct(profile.id); } //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก else if (item.resignId && code && ["C-PM-41"].includes(code)) { const commandResign = await this.commandReciveRepository.findOne({ where: { refId: item.resignId }, relations: { command: true }, }); const executeDate = commandResign ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) : today; if ( commandResign && _command.status !== "REPORTED" && (_command.status !== "WAITING" || today < executeDate) ) { await reOrderCommandRecivesAndDelete(commandResign!.id); } } let _commandYear = item.commandYear; if (item.commandYear) { _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; } const returnWork = await checkReturnCommandType(String(item.commandId)); 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; dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? new Date(); dataSalary.order = dest_item == null ? 1 : dest_item.order + 1; const meta = { createdUserId: ctx.user.sub, createdFullName: ctx.user.name, lastUpdateUserId: ctx.user.sub, lastUpdateFullName: ctx.user.name, createdAt: new Date(), lastUpdatedAt: new Date(), }; if (!returnWork) { 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 }); } 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(); const clearProfile = await checkCommandType(String(item.commandId)); //ปั๊มประวัติก่อนลบตำแหน่ง const curRevision = await this.orgRevisionRepo.findOne({ where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, }); let orgRootRef = null; let orgChild1Ref = null; let orgChild2Ref = null; let orgChild3Ref = null; let orgChild4Ref = null; if (curRevision) { const curPosMaster = await this.posMasterRepository.findOne({ where: { current_holderId: profile.id, orgRevisionId: curRevision.id, }, relations: { orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true, }, }); orgRootRef = curPosMaster?.orgRoot ?? null; orgChild1Ref = curPosMaster?.orgChild1 ?? null; orgChild2Ref = curPosMaster?.orgChild2 ?? null; orgChild3Ref = curPosMaster?.orgChild3 ?? null; orgChild4Ref = curPosMaster?.orgChild4 ?? null; if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); } } //ลบตำแหน่ง if (item.isLeave == true) { await removeProfileInOrganize(profile.id, "OFFICER"); } 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.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; } if (item.isGovernment == true) { if (returnWork) { //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ await removeProfileInOrganize(profile.id, "OFFICER"); //ปั๊มตำแหน่งใหม่ // หา posMaster และเช็ค orgRevisionIsCurrent let posMaster = await this.posMasterRepository.findOne({ where: { id: item.posmasterId?.toString() }, relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true, }, }); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ const isCurrent = posMaster?.orgRevision?.orgRevisionIsCurrent === true && posMaster?.orgRevision?.orgRevisionIsDraft === false; // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA if (!isCurrent && posMaster?.ancestorDNA) { posMaster = await this.posMasterRepository.findOne({ where: { ancestorDNA: posMaster.ancestorDNA, orgRevision: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false, }, }, relations: { orgRevision: true, orgRoot: true, orgChild1: true, orgChild2: true, orgChild3: true, orgChild4: true, }, }); } if (posMaster) { const checkPosition = await this.positionRepository.find({ where: { posMasterId: posMaster.id, positionIsSelected: true, }, }); if (checkPosition.length > 0) { const clearPosition = checkPosition.map((positions) => ({ ...positions, positionIsSelected: false, })); await this.positionRepository.save(clearPosition); } posMaster.current_holderId = profile.id; posMaster.lastUpdatedAt = new Date(); // posMaster.conditionReason = _null; // posMaster.isCondition = false; await this.posMasterRepository.save(posMaster); // Match position ตามลำดับ priority: // Condition 1: match จาก positionId // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) // Fallback: เลือก position แรกใน posMaster let positionNew: Position | null = null; // ═══════════════════════════════════════════════════════════ // CONDITION 1: เช็คจาก positionId ตรง // ═══════════════════════════════════════════════════════════ if (item.positionId) { const positionById = await this.positionRepository.findOne({ where: { id: item.positionId, posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง }, relations: ["posExecutive"], }); if (positionById) { positionNew = positionById; } } // ═══════════════════════════════════════════════════════════ // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) // ═══════════════════════════════════════════════════════════ if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า const whereCondition: any = { posMasterId: posMaster.id, positionName: item.positionNameNew, posTypeId: item.positionTypeNew, posLevelId: item.positionLevelNew, }; if (item.positionField) { whereCondition.positionField = item.positionField; } if (item.posExecutiveId) { whereCondition.posExecutiveId = item.posExecutiveId; } if (item.positionExecutiveField) { whereCondition.positionExecutiveField = item.positionExecutiveField; } if (item.positionArea) { whereCondition.positionArea = item.positionArea; } const positionBy7Fields = await this.positionRepository.findOne({ where: whereCondition, relations: ["posExecutive"], order: { orderNo: "ASC" }, }); if (positionBy7Fields) { positionNew = positionBy7Fields; } } // ═══════════════════════════════════════════════════════════ // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) // ═══════════════════════════════════════════════════════════ if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { const positionBy3Fields = await this.positionRepository.findOne({ where: { posMasterId: posMaster.id, positionName: item.positionNameNew, posTypeId: item.positionTypeNew, posLevelId: item.positionLevelNew, }, relations: ["posExecutive"], order: { orderNo: "ASC" }, }); if (positionBy3Fields) { positionNew = positionBy3Fields; } } // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) // if (!positionNew) { // const fallbackPositions = await this.positionRepository.find({ // where: { // posMasterId: posMaster.id, // }, // relations: ["posExecutive"], // order: { // orderNo: "ASC", // }, // take: 1, // }); // if (fallbackPositions.length > 0) { // positionNew = fallbackPositions[0]; // } // } if (positionNew) { positionNew.positionIsSelected = true; await this.positionRepository.save(positionNew, { data: req }); } await CreatePosMasterHistoryOfficer(posMaster.id, req); profile.posMasterNo = getPosMasterNo(posMaster); profile.org = getOrgFullName(posMaster); } const newMapProfileSalary = { profileId: profile.id, commandId: item.commandId, positionName: item.positionNameNew ?? null, positionType: item.posTypeNameNew ?? null, positionLevel: item.posLevelNameNew ?? 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, posNo: item.posNoNew, posNoAbb: item.posNoAbbNew, orgRoot: item.orgRootNew, orgChild1: item.orgChild1New, orgChild2: item.orgChild2New, orgChild3: item.orgChild3New, orgChild4: item.orgChild4New, isGovernment: item.isGovernment, commandNo: item.commandNo, commandYear: item.commandYear, commandDateAffect: item.commandDateAffect, commandDateSign: item.commandDateSign, commandCode: item.commandCode, commandName: item.commandName, remark: item.remark, }; Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); const history = new ProfileSalaryHistory(); Object.assign(history, { ...dataSalary, id: undefined }); await this.salaryRepo.save(dataSalary); history.profileSalaryId = dataSalary.id; await this.salaryHistoryRepo.save(history); profile.leaveReason = _null; profile.leaveCommandId = _null; profile.leaveCommandNo = _null; profile.leaveRemark = _null; profile.leaveDate = _null; profile.leaveType = _null; profile.position = item.positionNameNew ?? _null; profile.posTypeId = item.positionTypeNew ?? _null; profile.posLevelId = item.positionLevelNew ?? _null; } let userKeycloakId; const checkUser = await getUserByUsername(profile.citizenId); //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ 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, "") ?? ""; userKeycloakId = await createUser(profile.citizenId, password, { firstName: sanitizedFirstName, lastName: profile.lastName, }); const list = await getRoles(); let result = false; if (Array.isArray(list) && userKeycloakId) { result = await addUserRoles( userKeycloakId, list .filter((v) => v.name === "USER") .map((x) => ({ id: x.id, name: x.name, })), ); } profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; profile.keycloak = userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; } //ถ้ามีอยู่แล้วให้ใช้อันเดิม 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; } profile.amount = item.amount ?? _null; profile.amountSpecial = item.amountSpecial ?? _null; profile.isActive = true; profile.isDelete = false; } await this.profileRepository.save(profile); // if (profile.id) { // await this.keycloakAttributeService.clearOrgDnaAttributes( // [profile.id], // "PROFILE", // ); // } // update user attribute in keycloak await updateUserAttributes(profile.keycloak ?? "", { profileId: [profile.id], prefix: [profile.prefix || ""], }); // Task #2190 if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { let organizeName = ""; if (orgRootRef) { const names = [ orgChild4Ref?.orgChild4Name, orgChild3Ref?.orgChild3Name, orgChild2Ref?.orgChild2Name, orgChild1Ref?.orgChild1Name, orgRootRef?.orgRootName, ].filter(Boolean); organizeName = names.join(" "); } } }), ); console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully"); } }