Linear Flow discipline + organization #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s

This commit is contained in:
harid 2026-06-26 18:09:45 +07:00
parent 832c5d2cb3
commit 3d2fc5128a
7 changed files with 1591 additions and 1148 deletions

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