add transaction #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s

This commit is contained in:
harid 2026-06-24 18:05:54 +07:00
parent ecd0388eb0
commit 832c5d2cb3
10 changed files with 2322 additions and 1991 deletions

View file

@ -106,7 +106,7 @@ import { reOrderCommandRecivesAndDelete } from "../services/CommandService";
import { RetirementService } from "../services/RetirementService";
import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileService";
import { ExecuteSalaryService } from "../services/ExecuteSalaryService";
import { ExecuteSalaryCurrentService, ExecuteSalaryResult } from "../services/ExecuteSalaryCurrentService";
import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService";
import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService";
import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService";
import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService";
@ -3702,14 +3702,11 @@ export class CommandController extends Controller {
}[];
},
) {
const result: ExecuteSalaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent(
body.data,
{
user: { sub: req.user.sub, name: req.user.name },
req,
},
);
return new HttpSuccess(result);
await new ExecuteSalaryCurrentService().executeSalaryCurrent(body.data, {
user: { sub: req.user.sub, name: req.user.name },
req,
});
return new HttpSuccess();
}
@Post("excexute/salary-employee-current")

View file

@ -4,7 +4,7 @@ import { PosMaster } from "../entities/PosMaster";
import { Position } from "../entities/Position";
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { EmployeePosition } from "../entities/EmployeePosition";
import { In, IsNull, MoreThan, Not } from "typeorm";
import { EntityManager, In, IsNull, MoreThan, Not } from "typeorm";
import { RequestWithUser } from "../middlewares/user";
import { Command } from "../entities/Command";
import { ProfileSalary } from "../entities/ProfileSalary";
@ -254,14 +254,23 @@ export function calculateRetireYear(birthDate: Date) {
return yy + 61;
}
export async function removeProfileInOrganize(profileId: string, type: string) {
const currentRevision = await AppDataSource.getRepository(OrgRevision)
export async function removeProfileInOrganize(
profileId: string,
type: string,
manager?: EntityManager,
) {
// ถ้าส่ง manager เข้ามา → ทุก query/update อยู่ใน transaction ของ caller (all-or-nothing)
// ถ้าไม่ส่ง → ใช้ global DataSource เหมือนเดิม (backward compatible)
const ds = manager ?? AppDataSource;
const currentRevision = await ds
.getRepository(OrgRevision)
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne();
const draftRevision = await AppDataSource.getRepository(OrgRevision)
const draftRevision = await ds
.getRepository(OrgRevision)
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = true")
.andWhere("orgRevision.orgRevisionIsCurrent = false")
@ -271,26 +280,30 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
return;
}
if (type === "OFFICER") {
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster)
const findProfileInposMaster = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
.andWhere("posMaster.current_holderId = :profileId", { profileId })
.getOne();
await AppDataSource.getRepository(PosMaster)
await ds
.getRepository(PosMaster)
.createQueryBuilder()
.update(PosMaster)
.set({ current_holderId: null, isSit: false })
.where("id = :id", { id: findProfileInposMaster?.id })
.execute();
const findProfileInposMasterDraft = await AppDataSource.getRepository(PosMaster)
const findProfileInposMasterDraft = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: draftRevision?.id })
.andWhere("posMaster.next_holderId = :profileId", { profileId })
.getOne();
await AppDataSource.getRepository(PosMaster)
await ds
.getRepository(PosMaster)
.createQueryBuilder()
.update(PosMaster)
.set({ next_holderId: null, isSit: false })
@ -300,7 +313,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findProfileInposMaster && !findProfileInposMasterDraft) {
return;
}
const findPosition = await AppDataSource.getRepository(Position)
const findPosition = await ds
.getRepository(Position)
.createQueryBuilder("position")
.where("position.posMasterId = :posMasterId", { posMasterId: findProfileInposMaster?.id })
.getMany();
@ -308,7 +322,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findPosition) {
return;
}
await AppDataSource.getRepository(Position)
await ds
.getRepository(Position)
.createQueryBuilder()
.update(Position)
.set({ positionIsSelected: false })
@ -316,14 +331,16 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
.execute();
}
if (type === "EMPLOYEE") {
const findProfileInEmpPosMaster = await AppDataSource.getRepository(EmployeePosMaster)
const findProfileInEmpPosMaster = await ds
.getRepository(EmployeePosMaster)
.createQueryBuilder("employeePosMaster")
.where("employeePosMaster.orgRevisionId = :orgRevisionId", {
orgRevisionId: currentRevision?.id,
})
.andWhere("employeePosMaster.current_holderId = :profileId", { profileId })
.getOne();
await AppDataSource.getRepository(EmployeePosMaster)
await ds
.getRepository(EmployeePosMaster)
.createQueryBuilder()
.update(EmployeePosMaster)
.set({ current_holderId: null, isSit: false })
@ -333,7 +350,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findProfileInEmpPosMaster) {
return;
}
const findEmpPosition = await AppDataSource.getRepository(EmployeePosition)
const findEmpPosition = await ds
.getRepository(EmployeePosition)
.createQueryBuilder("employeePosition")
.where("employeePosition.posMasterId = :posMasterId", {
posMasterId: findProfileInEmpPosMaster?.id,
@ -344,7 +362,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
return;
}
await AppDataSource.getRepository(EmployeePosition)
await ds
.getRepository(EmployeePosition)
.createQueryBuilder()
.update(EmployeePosition)
.set({ positionIsSelected: false })
@ -353,8 +372,10 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
}
}
export async function removePostMasterAct(profileId: string) {
const currentRevision = await AppDataSource.getRepository(OrgRevision)
export async function removePostMasterAct(profileId: string, manager?: EntityManager) {
const ds = manager ?? AppDataSource;
const currentRevision = await ds
.getRepository(OrgRevision)
.createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true")
@ -364,7 +385,8 @@ export async function removePostMasterAct(profileId: string) {
return;
}
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster)
const findProfileInposMaster = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
.andWhere("posMaster.current_holderId = :profileId", { profileId })
@ -374,11 +396,12 @@ export async function removePostMasterAct(profileId: string) {
return;
}
const posMasterAct = await AppDataSource.getRepository(PosMasterAct)
const posMasterAct = await ds
.getRepository(PosMasterAct)
.createQueryBuilder("posMasterAct")
.where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id })
.getMany();
await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct);
await ds.getRepository(PosMasterAct).remove(posMasterAct);
}
export async function checkReturnCommandType(commandId: string) {

View file

@ -3,6 +3,7 @@ import { CommandRecive } from "../entities/CommandRecive";
import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
import { Profile } from "../entities/Profile";
import { EntityManager } from "typeorm";
export interface PosNumCodeSitResult {
posNumCodeSit: string;
@ -17,43 +18,42 @@ export interface PosNumCodeSitResult {
*
* @param reciveId commandRecive.Id
* @param code
* @param manager operation transaction caller (all-or-nothing)
* @returns Promise<void>
*/
export async function reOrderCommandRecivesAndDelete(
reciveId: string
reciveId: string,
manager?: EntityManager,
): Promise<void> {
const commandReciveRepo = AppDataSource.getRepository(CommandRecive);
const commandRepo = AppDataSource.getRepository(Command);
const ds = manager ?? AppDataSource;
const commandReciveRepo = ds.getRepository(CommandRecive);
const commandRepo = ds.getRepository(Command);
// ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId
const commandRecive = await commandReciveRepo.findOne({
where: { id: reciveId }
where: { id: reciveId },
});
if (commandRecive == null)
return;
if (commandRecive == null) return;
const commandId = commandRecive.commandId;
// ลบตาม refId
await commandReciveRepo.delete(commandRecive.id);
const commandReciveList = await commandReciveRepo.find({
where: { commandId: commandId },
order: { order: "ASC" },
});
// ลำดับผู้ได้รับคำสั่งใหม่
if (commandReciveList.length > 0) {
await Promise.all(
commandReciveList.map(async (p, i) => {
p.order = i + 1;
await commandReciveRepo.save(p);
})
);
for (let i = 0; i < commandReciveList.length; i++) {
commandReciveList[i].order = i + 1;
await commandReciveRepo.save(commandReciveList[i]);
}
} else {
// ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง
await commandRepo.update({ id: commandId }, { status: "CANCEL" });
}
}
/**

File diff suppressed because it is too large Load diff

View file

@ -60,17 +60,6 @@ export interface SalaryCurrentExecutionContext {
req?: any;
}
/**
* batch all-or-nothing (single transaction batch)
* return result; throw rollback batch
* propagate error (caller failure )
*/
export interface ExecuteSalaryResult {
successCount: number;
failureCount: number;
failures: { profileId: string; reason: string }[];
}
/**
* Service ProfileSalary + ()
*
@ -98,7 +87,7 @@ export class ExecuteSalaryCurrentService {
async executeSalaryCurrent(
data: SalaryCurrentItem[],
ctx: SalaryCurrentExecutionContext,
): Promise<ExecuteSalaryResult> {
): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
@ -170,12 +159,10 @@ export class ExecuteSalaryCurrentService {
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
// ─────────────────────────────────────────────────────────────
let successCount = 0;
await AppDataSource.transaction(async (manager) => {
for (const item of data ?? []) {
try {
await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb);
successCount++;
} catch (err) {
const reason =
err instanceof HttpError
@ -191,12 +178,6 @@ export class ExecuteSalaryCurrentService {
}
}
});
console.log(
`[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: 0`,
);
return { successCount, failureCount: 0, failures: [] };
}
/**
@ -373,10 +354,10 @@ export class ExecuteSalaryCurrentService {
if (posMasterOld != null) {
await posMasterRepository.save(posMasterOld);
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
console.log(
`[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`,
`[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`,
);
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
}
await posMasterRepository.save(posMaster);
@ -504,11 +485,11 @@ export class ExecuteSalaryCurrentService {
profile.amountSpecial = item.amountSpecial ?? null;
await profileRepository.save(profile);
await positionRepository.save(positionNew);
console.log(
`[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`,
);
}
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
);
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
console.log(

View file

@ -1,4 +1,4 @@
import { Double } from "typeorm";
import { Double, EntityManager } from "typeorm";
import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
@ -63,28 +63,29 @@ export interface SalaryEmployeeCurrentExecutionContext {
* - consumer rabbitmq handler service (Linear Flow)
*
* Behavior preserve CommandController.newSalaryEmployeeAndUpdateCurrent
*
* Batch semantics: all-or-nothing transaction (sequential)
* throw rollback batch propagate error ()
* return result success count
*/
export class ExecuteSalaryEmployeeCurrentService {
private commandRepository = AppDataSource.getRepository(Command);
private profileRepository = AppDataSource.getRepository(Profile);
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
private employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
/**
* ProfileSalary +
* ProfileSalary + batch
*/
async executeSalaryEmployeeCurrent(
data: SalaryEmployeeCurrentItem[],
ctx: SalaryEmployeeCurrentExecutionContext,
): Promise<void> {
console.log("[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent");
console.log("[ExecuteSalaryEmployeeCurrentService] Request body count:", data?.length);
const req = ctx.req;
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
`[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryEmployeeCurrentService] Request body count: ${data?.length ?? 0}`);
// ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -144,119 +145,172 @@ export class ExecuteSalaryEmployeeCurrentService {
.orgRootShortName ?? "";
}
}
await Promise.all(
data.map(async (item) => {
const profile: any = await this.profileEmployeeRepository.findOneBy({ id: item.profileId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
// ─────────────────────────────────────────────────────────────
// 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, _posNumCodeSit, _posNumCodeSitAbb);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryEmployeeCurrentService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
});
}
const dest_item = await this.salaryRepo.findOne({
where: { profileEmployeeId: item.profileId },
order: { order: "DESC" },
});
const before = null;
const dataSalary = new ProfileSalary();
dataSalary.posNumCodeSit = _posNumCodeSit;
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
/**
* 1 transaction (manager)
* save manager.getRepository(...) transaction
* throw rollback + batch ( partial commit)
*/
private async processOne(
item: SalaryEmployeeCurrentItem,
ctx: SalaryEmployeeCurrentExecutionContext,
manager: EntityManager,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
): Promise<void> {
const req = ctx.req;
Object.assign(dataSalary, {
...item,
...meta,
profileEmployeeId: item.profileId,
profileId: undefined,
});
const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined });
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
const employeePositionRepository = manager.getRepository(EmployeePosition);
await this.salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id;
await this.salaryHistoryRepo.save(history, { data: req });
const profile: any = await profileEmployeeRepository.findOneBy({ id: item.profileId });
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
const posMaster = await this.employeePosMasterRepository.findOne({
where: { id: item.posmasterId },
relations: ["orgRoot"],
});
if (posMaster == null)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
const dest_item = await salaryRepo.findOne({
where: { profileEmployeeId: item.profileId },
order: { order: "DESC" },
});
const before = null;
const dataSalary = new ProfileSalary();
dataSalary.posNumCodeSit = _posNumCodeSit;
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
const posMasterOld = await this.employeePosMasterRepository.findOne({
where: {
current_holderId: item.profileId,
orgRevisionId: posMaster.orgRevisionId,
},
});
if (posMasterOld != null) {
posMasterOld.current_holderId = null;
posMasterOld.lastUpdatedAt = new Date();
}
// if (posMasterOld != null) posMasterOld.next_holderId = null;
Object.assign(dataSalary, {
...item,
...meta,
profileEmployeeId: item.profileId,
profileId: undefined,
});
const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined });
const positionOld = await this.employeePositionRepository.findOne({
where: {
posMasterId: posMasterOld?.id,
positionIsSelected: true,
},
});
if (positionOld != null) {
positionOld.positionIsSelected = false;
await this.employeePositionRepository.save(positionOld);
}
await salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id;
await salaryHistoryRepo.save(history, { data: req });
const checkPosition = await this.employeePositionRepository.find({
where: {
posMasterId: item.posmasterId,
positionIsSelected: true,
},
});
if (checkPosition.length > 0) {
const clearPosition = checkPosition.map((positions) => ({
...positions,
positionIsSelected: false,
}));
await this.employeePositionRepository.save(clearPosition);
}
const posMaster = await employeePosMasterRepository.findOne({
where: { id: item.posmasterId },
relations: ["orgRoot"],
});
if (posMaster == null)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
posMaster.current_holderId = item.profileId;
posMaster.lastUpdatedAt = new Date();
posMaster.next_holderId = null;
if (posMasterOld != null) {
await this.employeePosMasterRepository.save(posMasterOld);
await CreatePosMasterHistoryEmployee(posMasterOld.id, req);
}
await this.employeePosMasterRepository.save(posMaster);
const positionNew = await this.employeePositionRepository.findOne({
where: {
id: item.positionId,
posMasterId: item.posmasterId,
},
});
if (positionNew != null) {
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.amount = item.amount ?? null;
profile.amountSpecial = item.amountSpecial ?? null;
await this.profileEmployeeRepository.save(profile);
await this.employeePositionRepository.save(positionNew);
}
await CreatePosMasterHistoryEmployee(posMaster.id, req);
}),
const posMasterOld = await employeePosMasterRepository.findOne({
where: {
current_holderId: item.profileId,
orgRevisionId: posMaster.orgRevisionId,
},
});
if (posMasterOld != null) {
posMasterOld.current_holderId = null;
posMasterOld.lastUpdatedAt = new Date();
}
// if (posMasterOld != null) posMasterOld.next_holderId = null;
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: item.posmasterId,
positionIsSelected: true,
},
});
if (checkPosition.length > 0) {
const clearPosition = checkPosition.map((positions) => ({
...positions,
positionIsSelected: false,
}));
await employeePositionRepository.save(clearPosition);
}
posMaster.current_holderId = item.profileId;
posMaster.lastUpdatedAt = new Date();
posMaster.next_holderId = null;
if (posMasterOld != null) {
await employeePosMasterRepository.save(posMasterOld);
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`,
);
await CreatePosMasterHistoryEmployee(posMasterOld.id, req, null, manager);
}
await employeePosMasterRepository.save(posMaster);
const positionNew = await employeePositionRepository.findOne({
where: {
id: item.positionId,
posMasterId: item.posmasterId,
},
});
if (positionNew != null) {
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.amount = item.amount ?? null;
profile.amountSpecial = item.amountSpecial ?? null;
await profileEmployeeRepository.save(profile);
await employeePositionRepository.save(positionNew);
}
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
);
await CreatePosMasterHistoryEmployee(posMaster.id, req, null, manager);
console.log("[ExecuteSalaryEmployeeCurrentService] executeSalaryEmployeeCurrent completed successfully");
console.log(
`[ExecuteSalaryEmployeeCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`,
);
}
}

View file

@ -1,4 +1,4 @@
import { Double } from "typeorm";
import { Double, EntityManager } from "typeorm";
import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status";
@ -71,29 +71,30 @@ export interface SalaryEmployeeLeaveExecutionContext {
* - consumer rabbitmq handler service (Linear Flow)
*
* Behavior preserve CommandController.newSalaryEmployeeAndUpdateLeave
*
* Batch semantics: all-or-nothing transaction (sequential)
* throw rollback batch propagate error ()
* return result success count
*/
export class ExecuteSalaryEmployeeLeaveService {
private commandRepository = AppDataSource.getRepository(Command);
private commandReciveRepository = AppDataSource.getRepository(CommandRecive);
private profileRepository = AppDataSource.getRepository(Profile);
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
/**
* ProfileSalary + handle leave
* ProfileSalary + handle leave batch
*/
async executeSalaryEmployeeLeave(
data: SalaryEmployeeLeaveItem[],
ctx: SalaryEmployeeLeaveExecutionContext,
): Promise<void> {
console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave");
console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length);
const req = ctx.req;
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
`[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryEmployeeLeaveService] Request body count: ${data?.length ?? 0}`);
// ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -156,166 +157,222 @@ export class ExecuteSalaryEmployeeLeaveService {
}
}
const today = new Date().setHours(0, 0, 0, 0);
await Promise.all(
data.map(async (item) => {
const profile = await this.profileEmployeeRepository.findOne({
where: { id: item.profileId },
relations: {
roleKeycloaks: true,
posType: true,
posLevel: true,
},
});
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
const code = _command?.commandType?.code;
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
if (item.resignId && code && ["C-PM-42"].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 dest_item = await this.salaryRepo.findOne({
where: { profileEmployeeId: item.profileId },
order: { order: "DESC" },
});
const before = null;
const dataSalary = new ProfileSalary();
dataSalary.posNumCodeSit = _posNumCodeSit;
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(dataSalary, {
...item,
...meta,
profileEmployeeId: item.profileId,
profileId: undefined,
});
const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined });
dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt;
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.employeePosMasterRepository.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) {
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE");
}
// ─────────────────────────────────────────────────────────────
// 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, today);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryEmployeeLeaveService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
});
}
// ลบตำแหน่ง
if (item.isLeave == true) {
await removeProfileInOrganize(profile.id, "EMPLOYEE");
/**
* 1 transaction (manager)
* save manager.getRepository(...) transaction
* throw rollback + batch ( partial commit)
*/
private async processOne(
item: SalaryEmployeeLeaveItem,
ctx: SalaryEmployeeLeaveExecutionContext,
manager: EntityManager,
_command: Command | null,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
today: number,
): Promise<void> {
const req = ctx.req;
const commandReciveRepository = manager.getRepository(CommandRecive);
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
const orgRevisionRepo = manager.getRepository(OrgRevision);
const profile = await profileEmployeeRepository.findOne({
where: { id: item.profileId },
relations: {
roleKeycloaks: true,
posType: true,
posLevel: true,
},
});
if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
const code = _command?.commandType?.code;
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
if (item.resignId && code && ["C-PM-42"].includes(code)) {
const commandResign = await 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)
) {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
}
}
let _commandYear = item.commandYear;
if (item.commandYear) {
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
}
const dest_item = await salaryRepo.findOne({
where: { profileEmployeeId: item.profileId },
order: { order: "DESC" },
});
const before = null;
const dataSalary = new ProfileSalary();
dataSalary.posNumCodeSit = _posNumCodeSit;
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(dataSalary, {
...item,
...meta,
profileEmployeeId: item.profileId,
profileId: undefined,
});
const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined });
dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt;
await salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id;
await 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 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 employeePosMasterRepository.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) {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryEmployeeLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`,
);
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager);
}
}
// ลบตำแหน่ง
if (item.isLeave == true) {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await removeProfileInOrganize(profile.id, "EMPLOYEE", manager);
}
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);
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;
}
await this.profileEmployeeRepository.save(profile);
// if (profile.id) {
// await this.keycloakAttributeService.clearOrgDnaAttributes(
// [profile.id],
// "PROFILE_EMPLOYEE",
// );
// }
// if (profile.id) {
// await this.keycloakAttributeService.clearOrgDnaAttributes(
// [profile.id],
// "PROFILE_EMPLOYEE",
// );
// }
// Task #2190
if (code && ["C-PM-23", "C-PM-43"].includes(code)) {
let organizeName = "";
if (orgRootRef) {
const names = [
orgChild4Ref?.orgChild4Name,
orgChild3Ref?.orgChild3Name,
orgChild2Ref?.orgChild2Name,
orgChild1Ref?.orgChild1Name,
orgRootRef?.orgRootName,
].filter(Boolean);
organizeName = names.join(" ");
}
}
// Task #2190
if (code && ["C-PM-23", "C-PM-43"].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(
`[ExecuteSalaryEmployeeLeaveService] Completed processOne — profileId: ${item.profileId}`,
);
console.log("[ExecuteSalaryEmployeeLeaveService] executeSalaryEmployeeLeave completed successfully");
}
}

View file

@ -1,4 +1,4 @@
import { Double, In, Like } from "typeorm";
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";
@ -103,27 +103,34 @@ export interface SalaryLeaveExecutionContext {
* - consumer rabbitmq handler service (Linear Flow)
*
* Behavior preserve CommandController.newSalaryAndUpdateLeave
*
* Batch semantics: all-or-nothing transaction (sequential)
* throw rollback batch propagate error ()
* return result success count
*
* Keycloak: operations (deleteUser/createUser/addUserRoles/updateUserAttributes)
* transaction preserve behavior Keycloak rollback
* DB rollback Keycloak operation Keycloak
*/
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/
* ProfileSalary + handle leave/ batch
*
* @returns success/failure
*/
async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise<void> {
console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave");
console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length);
const req = ctx.req;
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
`[ExecuteSalaryLeaveService] Starting executeSalaryLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryLeaveService] Request body count: ${data?.length ?? 0}`);
// ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -189,96 +196,242 @@ export class ExecuteSalaryLeaveService {
}
}
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 },
// ─────────────────────────────────────────────────────────────
// 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,
today,
roleKeycloak,
);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryLeaveService] 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: SalaryLeaveItem,
ctx: SalaryLeaveExecutionContext,
manager: EntityManager,
_command: Command | null,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
today: number,
roleKeycloak: RoleKeycloak | null,
): Promise<void> {
const req = ctx.req;
const commandReciveRepository = manager.getRepository(CommandRecive);
const profileRepository = manager.getRepository(Profile);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
const posMasterRepository = manager.getRepository(PosMaster);
const positionRepository = manager.getRepository(Position);
const orgRevisionRepo = manager.getRepository(OrgRevision);
const roleKeycloakRepo = manager.getRepository(RoleKeycloak);
const profile = await 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)) {
// await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction
await removePostMasterAct(profile.id, manager);
}
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
else if (item.resignId && code && ["C-PM-41"].includes(code)) {
const commandResign = await 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)
) {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
}
}
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 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 salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id;
await 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 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 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") {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`,
);
await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE", null, manager);
}
}
//ลบตำแหน่ง
if (item.isLeave == true) {
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await removeProfileInOrganize(profile.id, "OFFICER", manager);
}
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;
}
if (item.isGovernment == true) {
if (returnWork) {
//ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้
await removeProfileInOrganize(profile.id, "OFFICER", manager);
//ปั๊มตำแหน่งใหม่
// หา posMaster และเช็ค orgRevisionIsCurrent
let posMaster = await posMasterRepository.findOne({
where: { id: item.posmasterId?.toString() },
relations: {
roleKeycloaks: true,
orgRevision: true,
orgRoot: true,
orgChild1: true,
orgChild2: true,
orgChild3: true,
orgChild4: 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({
// เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่
const isCurrent =
posMaster?.orgRevision?.orgRevisionIsCurrent === true &&
posMaster?.orgRevision?.orgRevisionIsDraft === false;
// ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA
if (!isCurrent && posMaster?.ancestorDNA) {
posMaster = await posMasterRepository.findOne({
where: {
current_holderId: profile.id,
orgRevisionId: curRevision.id,
ancestorDNA: posMaster.ancestorDNA,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
relations: {
orgRevision: true,
orgRoot: true,
orgChild1: true,
orgChild2: true,
@ -286,343 +439,270 @@ export class ExecuteSalaryLeaveService {
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;
}
if (posMaster) {
const checkPosition = await positionRepository.find({
where: {
posMasterId: posMaster.id,
positionIsSelected: true,
},
});
if (checkPosition.length > 0) {
const clearPosition = checkPosition.map((positions) => ({
...positions,
positionIsSelected: false,
}));
await positionRepository.save(clearPosition);
}
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;
}
posMaster.current_holderId = profile.id;
posMaster.lastUpdatedAt = new Date();
// posMaster.conditionReason = _null;
// posMaster.isCondition = false;
await posMasterRepository.save(posMaster);
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,
// 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 positionRepository.findOne({
where: {
id: item.positionId,
posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง
},
relations: ["posExecutive"],
});
// เช็คว่า 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 (positionById) {
positionNew = positionById;
}
}
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,
// ═══════════════════════════════════════════════════════════
// 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,
};
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}`;
if (item.positionField) {
whereCondition.positionField = item.positionField;
}
// กรอง "." ออกจาก firstName ก่อนส่งไป keycloak
const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? "";
userKeycloakId = await createUser(profile.citizenId, password, {
firstName: sanitizedFirstName,
lastName: profile.lastName,
if (item.posExecutiveId) {
whereCondition.posExecutiveId = item.posExecutiveId;
}
if (item.positionExecutiveField) {
whereCondition.positionExecutiveField = item.positionExecutiveField;
}
if (item.positionArea) {
whereCondition.positionArea = item.positionArea;
}
const positionBy7Fields = await positionRepository.findOne({
where: whereCondition,
relations: ["posExecutive"],
order: { orderNo: "ASC" },
});
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,
})),
);
if (positionBy7Fields) {
positionNew = positionBy7Fields;
}
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 : [];
// ═══════════════════════════════════════════════════════════
// CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match)
// ═══════════════════════════════════════════════════════════
if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) {
const positionBy3Fields = await positionRepository.findOne({
where: {
posMasterId: posMaster.id,
positionName: item.positionNameNew,
posTypeId: item.positionTypeNew,
posLevelId: item.positionLevelNew,
},
relations: ["posExecutive"],
order: { orderNo: "ASC" },
});
if (positionBy3Fields) {
positionNew = positionBy3Fields;
}
profile.keycloak = checkUser[0].id;
}
profile.amount = item.amount ?? _null;
profile.amountSpecial = item.amountSpecial ?? _null;
profile.isActive = true;
profile.isDelete = false;
// // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition)
// if (!positionNew) {
// const fallbackPositions = await 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 positionRepository.save(positionNew, { data: req });
}
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
);
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
profile.posMasterNo = getPosMasterNo(posMaster);
profile.org = getOrgFullName(posMaster);
}
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 || ""],
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 salaryRepo.save(dataSalary);
history.profileSalaryId = dataSalary.id;
await 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, "") ?? "";
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
userKeycloakId = await createUser(profile.citizenId, password, {
firstName: sanitizedFirstName,
lastName: profile.lastName,
});
// 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(" ");
}
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 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 profileRepository.save(profile);
console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully");
// 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] Completed processOne — profileId: ${item.profileId}`,
);
}
}

View file

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

View file

@ -217,92 +217,105 @@ export async function CreatePosMasterHistoryEmployee(
posMasterId: string,
request: RequestWithUser | null,
type?: string | null,
manager?: EntityManager,
): Promise<boolean> {
try {
await AppDataSource.transaction(async (manager) => {
const repoPosmaster = manager.getRepository(EmployeePosMaster);
const repoHistory = manager.getRepository(PosMasterEmployeeHistory);
const repoProfileEmployee = manager.getRepository(ProfileEmployee);
const execute = async (transactionManager: EntityManager) => {
const repoPosmaster = transactionManager.getRepository(EmployeePosMaster);
const repoHistory = transactionManager.getRepository(PosMasterEmployeeHistory);
const repoProfileEmployee = transactionManager.getRepository(ProfileEmployee);
const pm = await repoPosmaster.findOne({
where: { id: posMasterId },
relations: [
"positions",
"positions.posLevel",
"positions.posType",
// "positions.posExecutive",
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
"current_holder",
],
});
if (!pm) return false;
if (!pm.ancestorDNA) return false;
const _null: any = null;
const h = new PosMasterEmployeeHistory();
const selectedPosition =
pm.positions.length > 0
? pm.positions.find((p) => p.positionIsSelected === true) ?? null
: null;
let position = selectedPosition?.positionName ?? _null;
let posTypeName = selectedPosition?.posType?.posTypeName ?? _null;
let posLevelName = selectedPosition?.posType && selectedPosition?.posLevel
? `${selectedPosition?.posType?.posTypeShortName ?? ""} ${selectedPosition?.posLevel?.posLevelName ?? ""}`.trim()
: _null;
if (pm.isSit && pm.current_holderId) {
const profile = await repoProfileEmployee.findOne({
where: { id: pm.current_holderId },
relations: ["posType", "posLevel"]
});
position = profile?.position ?? _null;
posTypeName = profile?.posType?.posTypeName ?? _null;
posLevelName = profile?.posType && profile?.posLevel
? `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim()
: _null;
}
h.ancestorDNA = pm.ancestorDNA;
if (!type || type != "DELETE") {
h.profileEmployeeId = pm.current_holder?.id || _null;
h.prefix = pm.current_holder?.prefix || _null;
h.firstName = pm.current_holder?.firstName || _null;
h.lastName = pm.current_holder?.lastName || _null;
h.position = position;
h.posType = posTypeName;
h.posLevel = posLevelName;
}
h.rootDnaId = pm.orgRoot?.ancestorDNA || _null;
h.child1DnaId = pm.orgChild1?.ancestorDNA || _null;
h.child2DnaId = pm.orgChild2?.ancestorDNA || _null;
h.child3DnaId = pm.orgChild3?.ancestorDNA || _null;
h.child4DnaId = pm.orgChild4?.ancestorDNA || _null;
h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null;
h.posMasterNo = pm.posMasterNo ?? _null;
h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null;
h.shortName =
[
pm.orgChild4?.orgChild4ShortName,
pm.orgChild3?.orgChild3ShortName,
pm.orgChild2?.orgChild2ShortName,
pm.orgChild1?.orgChild1ShortName,
pm.orgRoot?.orgRootShortName,
].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null;
const userId = request?.user?.sub ?? "";
const userName = request?.user?.name ?? "system";
h.createdUserId = userId;
h.createdFullName = userName;
h.lastUpdateUserId = userId;
h.lastUpdateFullName = userName;
h.createdAt = new Date();
h.lastUpdatedAt = new Date();
await repoHistory.save(h);
const pm = await repoPosmaster.findOne({
where: { id: posMasterId },
relations: [
"positions",
"positions.posLevel",
"positions.posType",
// "positions.posExecutive",
"orgRoot",
"orgChild1",
"orgChild2",
"orgChild3",
"orgChild4",
"current_holder",
],
});
if (!pm) return;
if (!pm.ancestorDNA) return;
const _null: any = null;
const h = new PosMasterEmployeeHistory();
const selectedPosition =
pm.positions.length > 0
? pm.positions.find((p) => p.positionIsSelected === true) ?? null
: null;
let position = selectedPosition?.positionName ?? _null;
let posTypeName = selectedPosition?.posType?.posTypeName ?? _null;
let posLevelName = selectedPosition?.posType && selectedPosition?.posLevel
? `${selectedPosition?.posType?.posTypeShortName ?? ""} ${selectedPosition?.posLevel?.posLevelName ?? ""}`.trim()
: _null;
if (pm.isSit && pm.current_holderId) {
const profile = await repoProfileEmployee.findOne({
where: { id: pm.current_holderId },
relations: ["posType", "posLevel"]
});
position = profile?.position ?? _null;
posTypeName = profile?.posType?.posTypeName ?? _null;
posLevelName = profile?.posType && profile?.posLevel
? `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim()
: _null;
}
h.ancestorDNA = pm.ancestorDNA;
if (!type || type != "DELETE") {
h.profileEmployeeId = pm.current_holder?.id || _null;
h.prefix = pm.current_holder?.prefix || _null;
h.firstName = pm.current_holder?.firstName || _null;
h.lastName = pm.current_holder?.lastName || _null;
h.position = position;
h.posType = posTypeName;
h.posLevel = posLevelName;
}
h.rootDnaId = pm.orgRoot?.ancestorDNA || _null;
h.child1DnaId = pm.orgChild1?.ancestorDNA || _null;
h.child2DnaId = pm.orgChild2?.ancestorDNA || _null;
h.child3DnaId = pm.orgChild3?.ancestorDNA || _null;
h.child4DnaId = pm.orgChild4?.ancestorDNA || _null;
h.posMasterNoPrefix = pm.posMasterNoPrefix ?? _null;
h.posMasterNo = pm.posMasterNo ?? _null;
h.posMasterNoSuffix = pm.posMasterNoSuffix ?? _null;
h.shortName =
[
pm.orgChild4?.orgChild4ShortName,
pm.orgChild3?.orgChild3ShortName,
pm.orgChild2?.orgChild2ShortName,
pm.orgChild1?.orgChild1ShortName,
pm.orgRoot?.orgRootShortName,
].find((s) => typeof s === "string" && s.trim().length > 0) ?? _null;
const userId = request?.user?.sub ?? "";
const userName = request?.user?.name ?? "system";
h.createdUserId = userId;
h.createdFullName = userName;
h.lastUpdateUserId = userId;
h.lastUpdateFullName = userName;
h.createdAt = new Date();
h.lastUpdatedAt = new Date();
await repoHistory.save(h);
};
try {
if (manager) {
await execute(manager);
return true;
}
await AppDataSource.transaction(async (transactionManager) => {
await execute(transactionManager);
});
return true;
} catch (err) {
if (manager) {
console.error("CreatePosMasterHistoryEmployee error (external transaction):", err);
throw err;
}
console.error("CreatePosMasterHistoryEmployee transaction error:", err);
return false;
}