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 { 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 { 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 { 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 { 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(); 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`); } }