From 832c5d2cb36f2d46ef9c33f54e486624b2b9e485 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 24 Jun 2026 18:05:54 +0700 Subject: [PATCH] add transaction #224 --- src/controllers/CommandController.ts | 15 +- src/interfaces/utils.ts | 61 +- src/services/CommandService.ts | 28 +- src/services/ExecuteOfficerProfileService.ts | 2048 +++++++++-------- src/services/ExecuteSalaryCurrentService.ts | 31 +- .../ExecuteSalaryEmployeeCurrentService.ts | 282 ++- .../ExecuteSalaryEmployeeLeaveService.ts | 385 ++-- src/services/ExecuteSalaryLeaveService.ts | 904 ++++---- src/services/ExecuteSalaryService.ts | 386 ++-- src/services/PositionService.ts | 173 +- 10 files changed, 2322 insertions(+), 1991 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index d0dbbe51..1ea05633 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -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") diff --git a/src/interfaces/utils.ts b/src/interfaces/utils.ts index 7b55fd0f..27b35585 100644 --- a/src/interfaces/utils.ts +++ b/src/interfaces/utils.ts @@ -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) { diff --git a/src/services/CommandService.ts b/src/services/CommandService.ts index b002462c..87334a4b 100644 --- a/src/services/CommandService.ts +++ b/src/services/CommandService.ts @@ -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 */ export async function reOrderCommandRecivesAndDelete( - reciveId: string + reciveId: string, + manager?: EntityManager, ): Promise { - 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" }); } - } /** diff --git a/src/services/ExecuteOfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts index 0d77719f..c9e0bacb 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -1,4 +1,4 @@ -import { In, Like } from "typeorm"; +import { EntityManager, In, Like } from "typeorm"; import { AppDataSource } from "../database/data-source"; import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; @@ -177,8 +177,12 @@ export class ExecuteOfficerProfileService { data: OfficerProfileItem[], ctx: ExecutionContext, ): Promise { - console.log("[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile"); - console.log("[ExecuteOfficerProfileService] Request body count:", data?.length); + const commandId = + data?.find((x: any) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "unknown"; + console.log( + `[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile — commandId: ${commandId}`, + ); + console.log(`[ExecuteOfficerProfileService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields @@ -326,1025 +330,1085 @@ export class ExecuteOfficerProfileService { data.length, "profile(s)", ); - await Promise.all( - data.map(async (item, index) => { - console.log( - "[ExecuteOfficerProfileService] Processing item", - index + 1, - "of", - data.length, - ); - const _null: any = null; - if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; - if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; - if ( - item.bodyProfile.posLevelId && - !(await this.posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) - ) { + + // ───────────────────────────────────────────────────────────── + // 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, + roleKeycloak, + list, + _posNumCodeSit, + _posNumCodeSitAbb, + meta, + before, + commandId, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; console.error( - "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", - item.bodyProfile.posLevelId, + `[ExecuteOfficerProfileService] Failed commandId=${commandId}, citizenId=${item.bodyProfile?.citizenId}: ${reason}`, + err, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure } - if ( - item.bodyProfile.posTypeId && - !(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) - ) { + } + }); + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของ batch (กัน partial commit) + */ + private async processOne( + item: OfficerProfileItem, + ctx: ExecutionContext, + manager: EntityManager, + roleKeycloak: RoleKeycloak | null, + list: any[], + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + meta: any, + before: any, + commandId: string, + ): Promise { + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // repo ทั้งหมดสร้างจาก manager เพื่อให้อยู่ใน transaction เดียวกัน + // ───────────────────────────────────────────────────────────── + const profileRepository = manager.getRepository(Profile); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const posLevelRepo = manager.getRepository(PosLevel); + const posTypeRepo = manager.getRepository(PosType); + const provinceRepo = manager.getRepository(Province); + const districtRepo = manager.getRepository(District); + const subDistrictRepo = manager.getRepository(SubDistrict); + const posMasterRepository = manager.getRepository(PosMaster); + const positionRepository = manager.getRepository(Position); + const profileEducationRepo = manager.getRepository(ProfileEducation); + const profileEducationHistoryRepo = manager.getRepository(ProfileEducationHistory); + const certificateRepo = manager.getRepository(ProfileCertificate); + const certificateHistoryRepo = manager.getRepository(ProfileCertificateHistory); + const profileFamilyCoupleRepo = manager.getRepository(ProfileFamilyCouple); + const profileFamilyCoupleHistoryRepo = manager.getRepository(ProfileFamilyCoupleHistory); + const profileFamilyFatherRepo = manager.getRepository(ProfileFamilyFather); + const profileFamilyFatherHistoryRepo = manager.getRepository(ProfileFamilyFatherHistory); + const profileFamilyMotherRepo = manager.getRepository(ProfileFamilyMother); + const profileFamilyMotherHistoryRepo = manager.getRepository(ProfileFamilyMotherHistory); + const insigniaRepo = manager.getRepository(ProfileInsignia); + const insigniaHistoryRepo = manager.getRepository(ProfileInsigniaHistory); + const avatarRepository = manager.getRepository(ProfileAvatar); + + console.log( + "[ExecuteOfficerProfileService] Processing citizenId:", + item.bodyProfile.citizenId, + ); + const _null: any = null; + if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; + if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; + if ( + item.bodyProfile.posLevelId && + !(await posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) + ) { + console.error( + "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", + item.bodyProfile.posLevelId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); + } + if ( + item.bodyProfile.posTypeId && + !(await posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) + ) { + console.error( + "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", + item.bodyProfile.posTypeId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + } + + console.log( + "[ExecuteOfficerProfileService] Processing citizenId:", + item.bodyProfile.citizenId, + ); + let registrationProvinceId = await provinceRepo.findOneBy({ + id: item.bodyProfile.registrationProvinceId ?? "", + }); + let registrationDistrictId = await districtRepo.findOneBy({ + id: item.bodyProfile.registrationDistrictId ?? "", + }); + let registrationSubDistrictId = await subDistrictRepo.findOneBy({ + id: item.bodyProfile.registrationSubDistrictId ?? "", + }); + let currentProvinceId = await provinceRepo.findOneBy({ + id: item.bodyProfile.currentProvinceId ?? "", + }); + let currentDistrictId = await districtRepo.findOneBy({ + id: item.bodyProfile.currentDistrictId ?? "", + }); + let currentSubDistrictId = await subDistrictRepo.findOneBy({ + id: item.bodyProfile.currentSubDistrictId ?? "", + }); + console.log("[ExecuteOfficerProfileService] Address validation completed"); + + let _dateRetire = + item.bodyProfile.birthDate == null + ? _null + : calculateRetireDate(item.bodyProfile.birthDate); + let _dateRetireLaw = + item.bodyProfile.birthDate == null + ? _null + : calculateRetireLaw(item.bodyProfile.birthDate); + + let userKeycloakId: any; + let result: any; + console.log( + "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", + item.bodyProfile.citizenId, + ); + const checkUser = await getUserByUsername(item.bodyProfile.citizenId); + console.log( + "[ExecuteOfficerProfileService] Keycloak user exists:", + checkUser.length > 0, + ); + if (checkUser.length == 0) { + console.log("[ExecuteOfficerProfileService] Creating new Keycloak user"); + let password = item.bodyProfile.citizenId; + if (item.bodyProfile.birthDate != null) { + const _date = new Date(item.bodyProfile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = ( + new Date(item.bodyProfile.birthDate.toDateString()).getMonth() + 1 + ) + .toString() + .padStart(2, "0"); + const _year = new Date(item.bodyProfile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + console.log( + "[ExecuteOfficerProfileService] Calling createUser for:", + item.bodyProfile.citizenId, + ); + console.log( + "[ExecuteOfficerProfileService] createUser data - firstName:", + item.bodyProfile.firstName, + "lastName:", + item.bodyProfile.lastName, + ); + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak (ป้องกัน . หรืออักขระอื่นๆ) + const sanitizedFirstName = item.bodyProfile.firstName?.replace(/\./g, "") ?? ""; + userKeycloakId = await createUser(item.bodyProfile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: item.bodyProfile.lastName, + }); + if ( + userKeycloakId && + typeof userKeycloakId === "object" && + userKeycloakId.errorMessage + ) { + console.error( + "[ExecuteOfficerProfileService] createUser FAILED - field:", + userKeycloakId.field, + "errorMessage:", + userKeycloakId.errorMessage, + "params:", + userKeycloakId.params, + ); + throw new HttpError( + HttpStatusCode.BAD_REQUEST, + `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`, + ); + } + console.log( + "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", + userKeycloakId, + ); + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + console.log( + "[ExecuteOfficerProfileService] USER role assigned to new user, result:", + result, + ); + } else { + console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); + userKeycloakId = checkUser[0].id; + console.log( + "[ExecuteOfficerProfileService] Existing userKeycloakId:", + userKeycloakId, + ); + const rolesData = await getRoleMappings(userKeycloakId); + if (rolesData) { + const _delRole = rolesData.map((x: any) => ({ + id: x.id, + name: x.name, + })); + console.log( + "[ExecuteOfficerProfileService] Removing old roles:", + _delRole.length, + ); + await removeUserRoles(userKeycloakId, _delRole); + } + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + console.log( + "[ExecuteOfficerProfileService] USER role assigned to existing user", + ); + } + + let profile: any = await profileRepository.findOne({ + where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, + relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], + }); + console.log( + "[ExecuteOfficerProfileService] Profile found:", + !!profile, + "for citizenId:", + item.bodyProfile.citizenId, + ); + let _oldInsigniaIds: string[] = []; + let _oldSalaries: any[] = []; + //ลูกจ้างประจำ หรือ บุคคลภายนอก + if (!profile) { + console.log( + "[ExecuteOfficerProfileService] No existing profile found, creating new profile", + ); + //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม + let profileEmployee: any = await profileEmployeeRepository.findOne({ + where: { citizenId: item.bodyProfile.citizenId }, + relations: ["profileInsignias", "roleKeycloaks"], + }); + console.log( + "[ExecuteOfficerProfileService] Employee profile found:", + !!profileEmployee, + ); + if (profileEmployee) { + console.log( + "[ExecuteOfficerProfileService] Converting employee profile to officer profile", + ); + const _order = await salaryRepo.findOne({ + where: { profileEmployeeId: profileEmployee.id }, + order: { order: "DESC" }, + }); + const profileEmpSalary = new ProfileSalary(); + profileEmpSalary.posNumCodeSit = _posNumCodeSit; + profileEmpSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + profileEmpSalary.order = _order == null ? 1 : _order.order + 1; + Object.assign(profileEmpSalary, { + ...item.bodySalarys, + ...meta, + profileEmployeeId: profileEmployee.id, + profileId: undefined, + }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...profileEmpSalary, id: undefined }); + profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt; + (profileEmpSalary.profileId = _null), + await salaryRepo.save(profileEmpSalary, { data: req }); + setLogDataDiff(req, { before, after: profileEmpSalary }); + history.profileSalaryId = profileEmpSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + if (profileEmployee.profileInsignias.length > 0) { + _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; + } + await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE", manager); + if (profileEmployee.keycloak != null) { + // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); + // if (delUserKeycloak) { + // Task #228 + // profileEmployee.keycloak = _null; + profileEmployee.roleKeycloaks = []; + profileEmployee.isActive = false; + // } + } + profileEmployee.isLeave = true; + profileEmployee.leaveReason = "บรรจุข้าราชการ"; + profileEmployee.lastUpdateUserId = ctx.user.sub; + profileEmployee.lastUpdateFullName = ctx.user.name; + profileEmployee.lastUpdatedAt = new Date(); + await profileEmployeeRepository.save(profileEmployee); + setLogDataDiff(req, { before, after: profileEmployee }); + } + profile = Object.assign({ ...item.bodyProfile, ...meta }); + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.isProbation = item.bodyProfile.isProbation; + //เพิ่มใหม่จากรับโอน + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = item.bodyProfile.firstName ?? null; + profile.lastName = item.bodyProfile.lastName ?? null; + profile.birthDate = item.bodyProfile.birthDate ?? null; + profile.gender = item.bodyProfile.gender ?? null; + profile.relationship = item.bodyProfile.relationship ?? null; + profile.religion = item.bodyProfile.religion ?? null; + profile.ethnicity = item.bodyProfile.ethnicity; + profile.nationality = item.bodyProfile.nationality ?? null; + profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; + profile.phone = item.bodyProfile.phone ?? null; + + console.log("[ExecuteOfficerProfileService] Saving new profile"); + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] New profile saved, profileId:", + profile.id, + ); + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); + setLogDataDiff(req, { before, after: profile }); + } + //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ + else { + console.log( + "[ExecuteOfficerProfileService] Existing profile found, isLeave:", + profile.isLeave, + "leaveType:", + profile.leaveType, + ); + //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก + if ( + profile.isLeave && + ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) + ) { + console.log( + "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + ); + //ดึง profileSalary เดิม + _oldSalaries = await salaryRepo.find({ + where: { profileId: profile.id }, + order: { order: "ASC" }, + }); + if (profile.profileInsignias.length > 0) { + _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; + } + profile = Object.assign({ ...item.bodyProfile, ...meta }); + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.isProbation = item.bodyProfile.isProbation; + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = item.bodyProfile.firstName ?? null; + profile.lastName = item.bodyProfile.lastName ?? null; + profile.birthDate = item.bodyProfile.birthDate ?? null; + profile.gender = item.bodyProfile.gender ?? null; + profile.relationship = item.bodyProfile.relationship ?? null; + profile.religion = item.bodyProfile.religion ?? null; + profile.ethnicity = item.bodyProfile.ethnicity; + profile.nationality = item.bodyProfile.nationality ?? null; + profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; + profile.phone = item.bodyProfile.phone ?? null; + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } else { + console.log( + "[ExecuteOfficerProfileService] Updating existing active profile", + ); + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + profile.isProbation = item.bodyProfile.isProbation; + profile.isLeave = item.bodyProfile.isLeave; + profile.isRetirement = false; + profile.isActive = true; + profile.isDelete = false; + profile.dateLeave = _null; + profile.dateRetire = _dateRetire; + profile.dateRetireLaw = _dateRetireLaw; + profile.registrationAddress = item.bodyProfile.registrationAddress; + profile.registrationProvinceId = registrationProvinceId + ? registrationProvinceId.id + : _null; + profile.registrationDistrictId = registrationDistrictId + ? registrationDistrictId.id + : _null; + profile.registrationSubDistrictId = registrationSubDistrictId + ? registrationSubDistrictId.id + : _null; + profile.registrationZipCode = item.bodyProfile.registrationZipCode; + profile.currentAddress = item.bodyProfile.currentAddress; + profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; + profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; + profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; + profile.currentZipCode = item.bodyProfile.currentZipCode; + profile.email = item.bodyProfile.email; + profile.telephoneNumber = item.bodyProfile.telephoneNumber; + profile.phone = item.bodyProfile.phone; + profile.dateStart = item.bodyProfile.dateStart; + profile.amount = item.bodyProfile.amount ?? null; + profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; + profile.leaveCommandId = _null; + profile.leaveCommandNo = _null; + profile.leaveRemark = _null; + profile.leaveDate = _null; + profile.leaveType = _null; + profile.leaveReason = _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + //เพิ่มใหม่จากรับโอน + profile.rank = item?.bodyProfile?.rank || null; + profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; + profile.prefixMain = item?.bodyProfile?.prefix ?? null; + profile.firstName = + item.bodyProfile.firstName && item.bodyProfile.firstName != "" + ? item.bodyProfile.firstName + : profile.firstName; + profile.lastName = + item.bodyProfile.lastName && item.bodyProfile.lastName != "" + ? item.bodyProfile.lastName + : profile.lastName; + profile.birthDate = item.bodyProfile.birthDate + ? item.bodyProfile.birthDate + : profile.birthDate; + profile.gender = + item.bodyProfile.gender && item.bodyProfile.gender != "" + ? item.bodyProfile.gender + : profile.gender; + profile.relationship = + item.bodyProfile.relationship && item.bodyProfile.relationship != "" + ? item.bodyProfile.relationship + : profile.relationship; + profile.religion = + item.bodyProfile.religion && item.bodyProfile.religion != "" + ? item.bodyProfile.religion + : profile.religion; + profile.ethnicity = + item.bodyProfile.ethnicity && item.bodyProfile.ethnicity != "" + ? item.bodyProfile.ethnicity + : profile.ethnicity; + profile.nationality = + item.bodyProfile.nationality && item.bodyProfile.nationality != "" + ? item.bodyProfile.nationality + : profile.nationality; + profile.bloodGroup = + item.bodyProfile.bloodGroup && item.bodyProfile.bloodGroup != "" + ? item.bodyProfile.bloodGroup + : profile.bloodGroup; + profile.phone = + item.bodyProfile.phone && item.bodyProfile.phone != "" + ? item.bodyProfile.phone + : profile.phone; + await profileRepository.save(profile); + console.log( + "[ExecuteOfficerProfileService] Existing active profile updated, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } + } + + if (profile && profile.id) { + console.log( + "[ExecuteOfficerProfileService] Processing additional data for profileId:", + profile.id, + ); + //Educations + if (item.bodyEducations && item.bodyEducations.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing educations, count:", + item.bodyEducations.length, + ); + for (const education of item.bodyEducations) { + const profileEdu = new ProfileEducation(); + Object.assign(profileEdu, { ...education, ...meta }); + const eduHistory = new ProfileEducationHistory(); + Object.assign(eduHistory, { ...profileEdu, id: undefined }); + profileEdu.profileId = profile.id; + const educationLevel = await profileEducationRepo.findOne({ + select: ["id", "level", "profileId"], + where: { profileId: profile.id, isDeleted: false }, + order: { level: "DESC" }, + }); + profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; + await profileEducationRepo.save(profileEdu, { data: req }); + setLogDataDiff(req, { before, after: profileEdu }); + eduHistory.profileEducationId = profileEdu.id; + await profileEducationHistoryRepo.save(eduHistory, { data: req }); + } + } + //Certificates + if (item.bodyCertificates && item.bodyCertificates.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing certificates, count:", + item.bodyCertificates.length, + ); + for (const cer of item.bodyCertificates) { + const profileCer = new ProfileCertificate(); + Object.assign(profileCer, { ...cer, ...meta }); + const cerHistory = new ProfileCertificateHistory(); + Object.assign(cerHistory, { ...profileCer, id: undefined }); + profileCer.profileId = profile.id; + await certificateRepo.save(profileCer, { data: req }); + setLogDataDiff(req, { before, after: profileCer }); + cerHistory.profileCertificateId = profileCer.id; + await certificateHistoryRepo.save(cerHistory, { data: req }); + } + } + //FamilyCouple + if (item.bodyMarry != null) { + console.log("[ExecuteOfficerProfileService] Processing couple/marry data"); + const profileCouple = new ProfileFamilyCouple(); + const data = { + profileId: profile.id, + couple: item.bodyMarry.marry, + couplePrefix: item.bodyMarry.marryPrefix, + coupleFirstName: item.bodyMarry.marryFirstName, + coupleLastName: item.bodyMarry.marryLastName, + coupleCareer: item.bodyMarry.marryOccupation, + coupleLive: true, + }; + Object.assign(profileCouple, { ...data, ...meta }); + const coupleHistory = new ProfileFamilyCoupleHistory(); + Object.assign(coupleHistory, { ...profileCouple, id: undefined }); + profileCouple.profileId = profile.id; + await profileFamilyCoupleRepo.save(profileCouple, { data: req }); + setLogDataDiff(req, { before, after: profileCouple }); + coupleHistory.profileFamilyCoupleId = profileCouple.id; + await profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); + } + //FamilyFather + if (item.bodyFather != null) { + console.log("[ExecuteOfficerProfileService] Processing father data"); + const profileFather = new ProfileFamilyFather(); + const data = { + profileId: profile.id, + fatherPrefix: item.bodyFather.fatherPrefix, + fatherFirstName: item.bodyFather.fatherFirstName, + fatherLastName: item.bodyFather.fatherLastName, + fatherCareer: item.bodyFather.fatherOccupation, + fatherLive: true, + }; + Object.assign(profileFather, { ...data, ...meta }); + const fatherHistory = new ProfileFamilyFatherHistory(); + Object.assign(fatherHistory, { ...profileFather, id: undefined }); + profileFather.profileId = profile.id; + await profileFamilyFatherRepo.save(profileFather, { data: req }); + setLogDataDiff(req, { before, after: profileFather }); + fatherHistory.profileFamilyFatherId = profileFather.id; + await profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); + } + //FamilyMother + if (item.bodyMother != null) { + console.log("[ExecuteOfficerProfileService] Processing mother data"); + const profileMother = new ProfileFamilyMother(); + const data = { + profileId: profile.id, + motherPrefix: item.bodyMother.motherPrefix, + motherFirstName: item.bodyMother.motherFirstName, + motherLastName: item.bodyMother.motherLastName, + motherCareer: item.bodyMother.motherOccupation, + motherLive: true, + }; + Object.assign(profileMother, { ...data, ...meta }); + const motherHistory = new ProfileFamilyMotherHistory(); + Object.assign(motherHistory, { ...profileMother, id: undefined }); + profileMother.profileId = profile.id; + await profileFamilyMotherRepo.save(profileMother, { data: req }); + setLogDataDiff(req, { before, after: profileMother }); + motherHistory.profileFamilyMotherId = profileMother.id; + await profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); + } + //Salary + //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ + if (_oldSalaries.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Restoring old salaries, count:", + _oldSalaries.length, + ); + for (const oldSal of _oldSalaries) { + const profileSal: any = new ProfileSalary(); + Object.assign(profileSal, { ...oldSal, ...meta }); + const salaryHistory = new ProfileSalaryHistory(); + Object.assign(salaryHistory, { ...profileSal, id: undefined }); + profileSal.profileId = profile.id; + await salaryRepo.save(profileSal, { data: req }); + setLogDataDiff(req, { before, after: profileSal }); + salaryHistory.profileSalaryId = profileSal.id; + await salaryHistoryRepo.save(salaryHistory, { data: req }); + } + } + //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว + if (item.bodySalarys && item.bodySalarys != null) { + console.log("[ExecuteOfficerProfileService] Processing new salary data"); + const dest_item = await salaryRepo.findOne({ + where: { profileId: profile.id }, + order: { order: "DESC" }, + }); + const profileSal: any = new ProfileSalary(); + profileSal.posNumCodeSit = _posNumCodeSit; + profileSal.posNumCodeSitAbb = _posNumCodeSitAbb; + Object.assign(profileSal, { ...item.bodySalarys, ...meta }); + const salaryHistory = new ProfileSalaryHistory(); + Object.assign(salaryHistory, { ...profileSal, id: undefined }); + profileSal.order = dest_item == null ? 1 : dest_item.order + 1; + profileSal.profileId = profile.id; + profileSal.dateGovernment = item.bodySalarys.commandDateAffect ?? meta.createdAt; + profileSal.amount = item.bodySalarys.amount ?? null; + profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null; + profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null; + profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? null; + await salaryRepo.save(profileSal, { data: req }); + setLogDataDiff(req, { before, after: profileSal }); + salaryHistory.profileSalaryId = profileSal.id; + await salaryHistoryRepo.save(salaryHistory, { data: req }); + } + //Position + if (item.bodyPosition && item.bodyPosition != null) { + console.log("[ExecuteOfficerProfileService] Processing position assignment"); + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) + console.log( + "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", + item.bodyPosition.posmasterId, + ); + let posMaster = await posMasterRepository.findOne({ + where: { + id: item.bodyPosition.posmasterId, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + console.log("[ExecuteOfficerProfileService] posMaster found:", !!posMaster); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + console.log("[ExecuteOfficerProfileService] posMaster isCurrent:", isCurrent); + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + console.log( + "[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA", + ); + posMaster = await posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + console.log( + "[ExecuteOfficerProfileService] Current posMaster from ancestorDNA found:", + !!posMaster, + ); + } + + if (posMaster == null) { console.error( - "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", - item.bodyProfile.posTypeId, + `[ExecuteOfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); } - console.log( - "[ExecuteOfficerProfileService] Processing citizenId:", - item.bodyProfile.citizenId, - ); - let registrationProvinceId = await this.provinceRepo.findOneBy({ - id: item.bodyProfile.registrationProvinceId ?? "", + // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน + console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); + const posMasterOld = await posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, }); - let registrationDistrictId = await this.districtRepo.findOneBy({ - id: item.bodyProfile.registrationDistrictId ?? "", - }); - let registrationSubDistrictId = await this.subDistrictRepo.findOneBy({ - id: item.bodyProfile.registrationSubDistrictId ?? "", - }); - let currentProvinceId = await this.provinceRepo.findOneBy({ - id: item.bodyProfile.currentProvinceId ?? "", - }); - let currentDistrictId = await this.districtRepo.findOneBy({ - id: item.bodyProfile.currentDistrictId ?? "", - }); - let currentSubDistrictId = await this.subDistrictRepo.findOneBy({ - id: item.bodyProfile.currentSubDistrictId ?? "", - }); - console.log("[ExecuteOfficerProfileService] Address validation completed"); - - let _dateRetire = - item.bodyProfile.birthDate == null - ? _null - : calculateRetireDate(item.bodyProfile.birthDate); - let _dateRetireLaw = - item.bodyProfile.birthDate == null - ? _null - : calculateRetireLaw(item.bodyProfile.birthDate); - - let userKeycloakId: any; - let result: any; - console.log( - "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", - item.bodyProfile.citizenId, - ); - const checkUser = await getUserByUsername(item.bodyProfile.citizenId); - console.log( - "[ExecuteOfficerProfileService] Keycloak user exists:", - checkUser.length > 0, - ); - if (checkUser.length == 0) { - console.log("[ExecuteOfficerProfileService] Creating new Keycloak user"); - let password = item.bodyProfile.citizenId; - if (item.bodyProfile.birthDate != null) { - const _date = new Date(item.bodyProfile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = ( - new Date(item.bodyProfile.birthDate.toDateString()).getMonth() + 1 - ) - .toString() - .padStart(2, "0"); - const _year = new Date(item.bodyProfile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - console.log( - "[ExecuteOfficerProfileService] Calling createUser for:", - item.bodyProfile.citizenId, - ); - console.log( - "[ExecuteOfficerProfileService] createUser data - firstName:", - item.bodyProfile.firstName, - "lastName:", - item.bodyProfile.lastName, - ); - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak (ป้องกัน . หรืออักขระอื่นๆ) - const sanitizedFirstName = item.bodyProfile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(item.bodyProfile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: item.bodyProfile.lastName, - }); - if ( - userKeycloakId && - typeof userKeycloakId === "object" && - userKeycloakId.errorMessage - ) { - console.error( - "[ExecuteOfficerProfileService] createUser FAILED - field:", - userKeycloakId.field, - "errorMessage:", - userKeycloakId.errorMessage, - "params:", - userKeycloakId.params, - ); - throw new HttpError( - HttpStatusCode.BAD_REQUEST, - `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`, - ); - } - console.log( - "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", - userKeycloakId, - ); - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to new user, result:", - result, - ); - } else { - console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); - userKeycloakId = checkUser[0].id; - console.log( - "[ExecuteOfficerProfileService] Existing userKeycloakId:", - userKeycloakId, - ); - const rolesData = await getRoleMappings(userKeycloakId); - if (rolesData) { - const _delRole = rolesData.map((x: any) => ({ - id: x.id, - name: x.name, - })); - console.log( - "[ExecuteOfficerProfileService] Removing old roles:", - _delRole.length, - ); - await removeUserRoles(userKeycloakId, _delRole); - } - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - console.log( - "[ExecuteOfficerProfileService] USER role assigned to existing user", - ); + if (posMasterOld != null) { + // เคลียร์คนครองเก่าออกจากตำแหน่งเดิม + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); } - let profile: any = await this.profileRepository.findOne({ - where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, - relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], + // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก + const positionOld = await positionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, }); - console.log( - "[ExecuteOfficerProfileService] Profile found:", - !!profile, - "for citizenId:", - item.bodyProfile.citizenId, - ); - let _oldInsigniaIds: string[] = []; - let _oldSalaries: any[] = []; - //ลูกจ้างประจำ หรือ บุคคลภายนอก - if (!profile) { - console.log( - "[ExecuteOfficerProfileService] No existing profile found, creating new profile", - ); - //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม - let profileEmployee: any = await this.profileEmployeeRepository.findOne({ - where: { citizenId: item.bodyProfile.citizenId }, - relations: ["profileInsignias", "roleKeycloaks"], - }); - console.log( - "[ExecuteOfficerProfileService] Employee profile found:", - !!profileEmployee, - ); - if (profileEmployee) { - console.log( - "[ExecuteOfficerProfileService] Converting employee profile to officer profile", - ); - const _order = await this.salaryRepo.findOne({ - where: { profileEmployeeId: profileEmployee.id }, - order: { order: "DESC" }, - }); - const profileEmpSalary = new ProfileSalary(); - profileEmpSalary.posNumCodeSit = _posNumCodeSit; - profileEmpSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - profileEmpSalary.order = _order == null ? 1 : _order.order + 1; - Object.assign(profileEmpSalary, { - ...item.bodySalarys, - ...meta, - profileEmployeeId: profileEmployee.id, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...profileEmpSalary, id: undefined }); - profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt; - (profileEmpSalary.profileId = _null), - await this.salaryRepo.save(profileEmpSalary, { data: req }); - setLogDataDiff(req, { before, after: profileEmpSalary }); - history.profileSalaryId = profileEmpSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - if (profileEmployee.profileInsignias.length > 0) { - _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; - } - await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); - if (profileEmployee.keycloak != null) { - // const delUserKeycloak = await deleteUser(profileEmployee.keycloak); - // if (delUserKeycloak) { - // Task #228 - // profileEmployee.keycloak = _null; - profileEmployee.roleKeycloaks = []; - profileEmployee.isActive = false; - // } - } - profileEmployee.isLeave = true; - profileEmployee.leaveReason = "บรรจุข้าราชการ"; - profileEmployee.lastUpdateUserId = ctx.user.sub; - profileEmployee.lastUpdateFullName = ctx.user.name; - profileEmployee.lastUpdatedAt = new Date(); - await this.profileEmployeeRepository.save(profileEmployee); - setLogDataDiff(req, { before, after: profileEmployee }); - } - profile = Object.assign({ ...item.bodyProfile, ...meta }); - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.isProbation = item.bodyProfile.isProbation; - //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = item.bodyProfile.firstName ?? null; - profile.lastName = item.bodyProfile.lastName ?? null; - profile.birthDate = item.bodyProfile.birthDate ?? null; - profile.gender = item.bodyProfile.gender ?? null; - profile.relationship = item.bodyProfile.relationship ?? null; - profile.religion = item.bodyProfile.religion ?? null; - profile.ethnicity = item.bodyProfile.ethnicity; - profile.nationality = item.bodyProfile.nationality ?? null; - profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; - profile.phone = item.bodyProfile.phone ?? null; - - console.log("[ExecuteOfficerProfileService] Saving new profile"); - await this.profileRepository.save(profile); - console.log( - "[ExecuteOfficerProfileService] New profile saved, profileId:", - profile.id, - ); - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); - setLogDataDiff(req, { before, after: profile }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await positionRepository.save(positionOld); } - //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ - else { + + // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ + console.log( + "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new 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); + } + + // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster + console.log( + "[ExecuteOfficerProfileService] STEP 4: Assigning new holder to posMaster", + ); + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + if (posMasterOld != null) { + await posMasterRepository.save(posMasterOld); console.log( - "[ExecuteOfficerProfileService] Existing profile found, isLeave:", - profile.isLeave, - "leaveType:", - profile.leaveType, + `[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, citizenId: ${item.bodyProfile?.citizenId} (old)`, ); - //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก - if ( - profile.isLeave && - ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) - ) { + await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + } + await posMasterRepository.save(posMaster); + console.log("[ExecuteOfficerProfileService] posMaster saved with new holder"); + + // STEP 5: กำหนด position ใหม่ + console.log( + "[ExecuteOfficerProfileService] STEP 5: Determining position to assign", + ); + // 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 ตรง + // ═══════════════════════════════════════════════════════════ + console.log( + "[ExecuteOfficerProfileService] CONDITION 1: Checking by positionId:", + item.bodyPosition?.positionId, + ); + if (item.bodyPosition?.positionId) { + const positionById = await positionRepository.findOne({ + where: { + id: item.bodyPosition.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง + }, + relations: ["posExecutive"], + }); + + if (positionById) { + positionNew = positionById; console.log( - "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + "[ExecuteOfficerProfileService] CONDITION 1 matched, positionId:", + positionById.id, ); - //ดึง profileSalary เดิม - _oldSalaries = await this.salaryRepo.find({ - where: { profileId: profile.id }, - order: { order: "ASC" }, - }); - if (profile.profileInsignias.length > 0) { - _oldInsigniaIds = profile.profileInsignias?.map((x: any) => x.id) ?? []; - } - profile = Object.assign({ ...item.bodyProfile, ...meta }); - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.isProbation = item.bodyProfile.isProbation; - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = item.bodyProfile.firstName ?? null; - profile.lastName = item.bodyProfile.lastName ?? null; - profile.birthDate = item.bodyProfile.birthDate ?? null; - profile.gender = item.bodyProfile.gender ?? null; - profile.relationship = item.bodyProfile.relationship ?? null; - profile.religion = item.bodyProfile.religion ?? null; - profile.ethnicity = item.bodyProfile.ethnicity; - profile.nationality = item.bodyProfile.nationality ?? null; - profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; - profile.phone = item.bodyProfile.phone ?? null; - await this.profileRepository.save(profile); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[ExecuteOfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", + ); + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.bodyPosition.positionName, + posTypeId: item.bodyPosition.posTypeId, + posLevelId: item.bodyPosition.posLevelId, + }; + + if (item.bodyPosition.positionField) { + whereCondition.positionField = item.bodyPosition.positionField; + } + if (item.bodyPosition.posExecutiveId) { + whereCondition.posExecutiveId = item.bodyPosition.posExecutiveId; + } + if (item.bodyPosition.positionExecutiveField) { + whereCondition.positionExecutiveField = item.bodyPosition.positionExecutiveField; + } + if (item.bodyPosition.positionArea) { + whereCondition.positionArea = item.bodyPosition.positionArea; + } + + const positionBy7Fields = await positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; console.log( - "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", - profile.id, + "[ExecuteOfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", + positionBy7Fields.id, + ); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", + ); + const positionBy3Fields = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.bodyPosition.positionName, + posTypeId: item.bodyPosition.posTypeId, + posLevelId: item.bodyPosition.posLevelId, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + console.log( + "[ExecuteOfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", + positionBy3Fields.id, ); - setLogDataDiff(req, { before, after: profile }); } else { console.log( - "[ExecuteOfficerProfileService] Updating existing active profile", - ); - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - profile.isProbation = item.bodyProfile.isProbation; - profile.isLeave = item.bodyProfile.isLeave; - profile.isRetirement = false; - profile.isActive = true; - profile.isDelete = false; - profile.dateLeave = _null; - profile.dateRetire = _dateRetire; - profile.dateRetireLaw = _dateRetireLaw; - profile.registrationAddress = item.bodyProfile.registrationAddress; - profile.registrationProvinceId = registrationProvinceId - ? registrationProvinceId.id - : _null; - profile.registrationDistrictId = registrationDistrictId - ? registrationDistrictId.id - : _null; - profile.registrationSubDistrictId = registrationSubDistrictId - ? registrationSubDistrictId.id - : _null; - profile.registrationZipCode = item.bodyProfile.registrationZipCode; - profile.currentAddress = item.bodyProfile.currentAddress; - profile.currentProvinceId = currentProvinceId ? currentProvinceId.id : _null; - profile.currentDistrictId = currentDistrictId ? currentDistrictId.id : _null; - profile.currentSubDistrictId = currentSubDistrictId ? currentSubDistrictId.id : _null; - profile.currentZipCode = item.bodyProfile.currentZipCode; - profile.email = item.bodyProfile.email; - profile.telephoneNumber = item.bodyProfile.telephoneNumber; - profile.phone = item.bodyProfile.phone; - profile.dateStart = item.bodyProfile.dateStart; - profile.amount = item.bodyProfile.amount ?? null; - profile.amountSpecial = item.bodyProfile.amountSpecial ?? null; - profile.leaveCommandId = _null; - profile.leaveCommandNo = _null; - profile.leaveRemark = _null; - profile.leaveDate = _null; - profile.leaveType = _null; - profile.leaveReason = _null; - profile.lastUpdateUserId = ctx.user.sub; - profile.lastUpdateFullName = ctx.user.name; - profile.lastUpdatedAt = new Date(); - //เพิ่มใหม่จากรับโอน - profile.rank = item?.bodyProfile?.rank || null; - profile.prefix = item?.bodyProfile?.rank || item?.bodyProfile?.prefix || null; - profile.prefixMain = item?.bodyProfile?.prefix ?? null; - profile.firstName = - item.bodyProfile.firstName && item.bodyProfile.firstName != "" - ? item.bodyProfile.firstName - : profile.firstName; - profile.lastName = - item.bodyProfile.lastName && item.bodyProfile.lastName != "" - ? item.bodyProfile.lastName - : profile.lastName; - profile.birthDate = item.bodyProfile.birthDate - ? item.bodyProfile.birthDate - : profile.birthDate; - profile.gender = - item.bodyProfile.gender && item.bodyProfile.gender != "" - ? item.bodyProfile.gender - : profile.gender; - profile.relationship = - item.bodyProfile.relationship && item.bodyProfile.relationship != "" - ? item.bodyProfile.relationship - : profile.relationship; - profile.religion = - item.bodyProfile.religion && item.bodyProfile.religion != "" - ? item.bodyProfile.religion - : profile.religion; - profile.ethnicity = - item.bodyProfile.ethnicity && item.bodyProfile.ethnicity != "" - ? item.bodyProfile.ethnicity - : profile.ethnicity; - profile.nationality = - item.bodyProfile.nationality && item.bodyProfile.nationality != "" - ? item.bodyProfile.nationality - : profile.nationality; - profile.bloodGroup = - item.bodyProfile.bloodGroup && item.bodyProfile.bloodGroup != "" - ? item.bodyProfile.bloodGroup - : profile.bloodGroup; - profile.phone = - item.bodyProfile.phone && item.bodyProfile.phone != "" - ? item.bodyProfile.phone - : profile.phone; - await this.profileRepository.save(profile); - console.log( - "[ExecuteOfficerProfileService] Existing active profile updated, profileId:", + "[ExecuteOfficerProfileService] No position matched for profileId:", profile.id, ); - setLogDataDiff(req, { before, after: profile }); } } - if (profile && profile.id) { + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (positionNew != null) { console.log( - "[ExecuteOfficerProfileService] Processing additional data for profileId:", - profile.id, + "[ExecuteOfficerProfileService] Final position assignment, isSit:", + posMaster.isSit, + "positionId:", + positionNew.id, ); - //Educations - if (item.bodyEducations && item.bodyEducations.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing educations, count:", - item.bodyEducations.length, - ); - await Promise.all( - item.bodyEducations.map(async (education) => { - const profileEdu = new ProfileEducation(); - Object.assign(profileEdu, { ...education, ...meta }); - const eduHistory = new ProfileEducationHistory(); - Object.assign(eduHistory, { ...profileEdu, id: undefined }); - profileEdu.profileId = profile.id; - const educationLevel = await this.profileEducationRepo.findOne({ - select: ["id", "level", "profileId"], - where: { profileId: profile.id, isDeleted: false }, - order: { level: "DESC" }, - }); - profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; - await this.profileEducationRepo.save(profileEdu, { data: req }); - setLogDataDiff(req, { before, after: profileEdu }); - eduHistory.profileEducationId = profileEdu.id; - await this.profileEducationHistoryRepo.save(eduHistory, { data: req }); - }), - ); + positionNew.positionIsSelected = true; + if (!posMaster.isSit) { + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.positionField = positionNew.positionField ?? null; + profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; + profile.positionArea = positionNew.positionArea ?? null; + profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; + // profile.dateStart = new Date(); } - //Certificates - if (item.bodyCertificates && item.bodyCertificates.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing certificates, count:", - item.bodyCertificates.length, - ); - await Promise.all( - item.bodyCertificates.map(async (cer) => { - const profileCer = new ProfileCertificate(); - Object.assign(profileCer, { ...cer, ...meta }); - const cerHistory = new ProfileCertificateHistory(); - Object.assign(cerHistory, { ...profileCer, id: undefined }); - profileCer.profileId = profile.id; - await this.certificateRepo.save(profileCer, { data: req }); - setLogDataDiff(req, { before, after: profileCer }); - cerHistory.profileCertificateId = profileCer.id; - await this.certificateHistoryRepo.save(cerHistory, { data: req }); - }), - ); - } - //FamilyCouple - if (item.bodyMarry != null) { - console.log("[ExecuteOfficerProfileService] Processing couple/marry data"); - const profileCouple = new ProfileFamilyCouple(); - const data = { - profileId: profile.id, - couple: item.bodyMarry.marry, - couplePrefix: item.bodyMarry.marryPrefix, - coupleFirstName: item.bodyMarry.marryFirstName, - coupleLastName: item.bodyMarry.marryLastName, - coupleCareer: item.bodyMarry.marryOccupation, - coupleLive: true, - }; - Object.assign(profileCouple, { ...data, ...meta }); - const coupleHistory = new ProfileFamilyCoupleHistory(); - Object.assign(coupleHistory, { ...profileCouple, id: undefined }); - profileCouple.profileId = profile.id; - await this.profileFamilyCoupleRepo.save(profileCouple, { data: req }); - setLogDataDiff(req, { before, after: profileCouple }); - coupleHistory.profileFamilyCoupleId = profileCouple.id; - await this.profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); - } - //FamilyFather - if (item.bodyFather != null) { - console.log("[ExecuteOfficerProfileService] Processing father data"); - const profileFather = new ProfileFamilyFather(); - const data = { - profileId: profile.id, - fatherPrefix: item.bodyFather.fatherPrefix, - fatherFirstName: item.bodyFather.fatherFirstName, - fatherLastName: item.bodyFather.fatherLastName, - fatherCareer: item.bodyFather.fatherOccupation, - fatherLive: true, - }; - Object.assign(profileFather, { ...data, ...meta }); - const fatherHistory = new ProfileFamilyFatherHistory(); - Object.assign(fatherHistory, { ...profileFather, id: undefined }); - profileFather.profileId = profile.id; - await this.profileFamilyFatherRepo.save(profileFather, { data: req }); - setLogDataDiff(req, { before, after: profileFather }); - fatherHistory.profileFamilyFatherId = profileFather.id; - await this.profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); - } - //FamilyMother - if (item.bodyMother != null) { - console.log("[ExecuteOfficerProfileService] Processing mother data"); - const profileMother = new ProfileFamilyMother(); - const data = { - profileId: profile.id, - motherPrefix: item.bodyMother.motherPrefix, - motherFirstName: item.bodyMother.motherFirstName, - motherLastName: item.bodyMother.motherLastName, - motherCareer: item.bodyMother.motherOccupation, - motherLive: true, - }; - Object.assign(profileMother, { ...data, ...meta }); - const motherHistory = new ProfileFamilyMotherHistory(); - Object.assign(motherHistory, { ...profileMother, id: undefined }); - profileMother.profileId = profile.id; - await this.profileFamilyMotherRepo.save(profileMother, { data: req }); - setLogDataDiff(req, { before, after: profileMother }); - motherHistory.profileFamilyMotherId = profileMother.id; - await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); - } - //Salary - //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ - if (_oldSalaries.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Restoring old salaries, count:", - _oldSalaries.length, - ); - await Promise.all( - _oldSalaries.map(async (oldSal) => { - const profileSal: any = new ProfileSalary(); - Object.assign(profileSal, { ...oldSal, ...meta }); - const salaryHistory = new ProfileSalaryHistory(); - Object.assign(salaryHistory, { ...profileSal, id: undefined }); - profileSal.profileId = profile.id; - await this.salaryRepo.save(profileSal, { data: req }); - setLogDataDiff(req, { before, after: profileSal }); - salaryHistory.profileSalaryId = profileSal.id; - await this.salaryHistoryRepo.save(salaryHistory, { data: req }); - }), - ); - } - //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว - if (item.bodySalarys && item.bodySalarys != null) { - console.log("[ExecuteOfficerProfileService] Processing new salary data"); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: profile.id }, - order: { order: "DESC" }, - }); - const profileSal: any = new ProfileSalary(); - profileSal.posNumCodeSit = _posNumCodeSit; - profileSal.posNumCodeSitAbb = _posNumCodeSitAbb; - Object.assign(profileSal, { ...item.bodySalarys, ...meta }); - const salaryHistory = new ProfileSalaryHistory(); - Object.assign(salaryHistory, { ...profileSal, id: undefined }); - profileSal.order = dest_item == null ? 1 : dest_item.order + 1; - profileSal.profileId = profile.id; - profileSal.dateGovernment = item.bodySalarys.commandDateAffect ?? meta.createdAt; - profileSal.amount = item.bodySalarys.amount ?? null; - profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null; - profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null; - profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? null; - await this.salaryRepo.save(profileSal, { data: req }); - setLogDataDiff(req, { before, after: profileSal }); - salaryHistory.profileSalaryId = profileSal.id; - await this.salaryHistoryRepo.save(salaryHistory, { data: req }); - } - //Position - if (item.bodyPosition && item.bodyPosition != null) { - console.log("[ExecuteOfficerProfileService] Processing position assignment"); - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) - console.log( - "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", - item.bodyPosition.posmasterId, - ); - let posMaster = await this.posMasterRepository.findOne({ - where: { - id: item.bodyPosition.posmasterId, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - console.log("[ExecuteOfficerProfileService] posMaster found:", !!posMaster); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - console.log("[ExecuteOfficerProfileService] posMaster isCurrent:", isCurrent); - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - console.log( - "[ExecuteOfficerProfileService] Finding current posMaster from 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, - }, - }); - console.log( - "[ExecuteOfficerProfileService] Current posMaster from ancestorDNA found:", - !!posMaster, - ); - } - - if (posMaster == null) { - console.error( - `[ExecuteOfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน - console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); - const posMasterOld = await this.posMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - // เคลียร์คนครองเก่าออกจากตำแหน่งเดิม - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - - // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก - const positionOld = await this.positionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.positionRepository.save(positionOld); - } - - // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ - console.log( - "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new 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); - } - - // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster - console.log( - "[ExecuteOfficerProfileService] STEP 4: Assigning new holder to posMaster", - ); - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - if (posMasterOld != null) { - await this.posMasterRepository.save(posMasterOld); - await CreatePosMasterHistoryOfficer(posMasterOld.id, req); - } - await this.posMasterRepository.save(posMaster); - console.log("[ExecuteOfficerProfileService] posMaster saved with new holder"); - - // STEP 5: กำหนด position ใหม่ - console.log( - "[ExecuteOfficerProfileService] STEP 5: Determining position to assign", - ); - // 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 ตรง - // ═══════════════════════════════════════════════════════════ - console.log( - "[ExecuteOfficerProfileService] CONDITION 1: Checking by positionId:", - item.bodyPosition?.positionId, - ); - if (item.bodyPosition?.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.bodyPosition.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - console.log( - "[ExecuteOfficerProfileService] CONDITION 1 matched, positionId:", - positionById.id, - ); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log( - "[ExecuteOfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", - ); - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, - }; - - if (item.bodyPosition.positionField) { - whereCondition.positionField = item.bodyPosition.positionField; - } - if (item.bodyPosition.posExecutiveId) { - whereCondition.posExecutiveId = item.bodyPosition.posExecutiveId; - } - if (item.bodyPosition.positionExecutiveField) { - whereCondition.positionExecutiveField = item.bodyPosition.positionExecutiveField; - } - if (item.bodyPosition.positionArea) { - whereCondition.positionArea = item.bodyPosition.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - console.log( - "[ExecuteOfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", - positionBy7Fields.id, - ); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log( - "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", - ); - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.bodyPosition.positionName, - posTypeId: item.bodyPosition.posTypeId, - posLevelId: item.bodyPosition.posLevelId, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - console.log( - "[ExecuteOfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", - positionBy3Fields.id, - ); - } else { - console.log( - "[ExecuteOfficerProfileService] No position matched for profileId:", - profile.id, - ); - } - } - - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // 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]; - // } - // } - - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (positionNew != null) { - console.log( - "[ExecuteOfficerProfileService] Final position assignment, isSit:", - posMaster.isSit, - "positionId:", - positionNew.id, - ); - positionNew.positionIsSelected = true; - if (!posMaster.isSit) { - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.positionField = positionNew.positionField ?? null; - profile.posExecutive = positionNew.posExecutive?.posExecutiveName ?? null; - profile.positionArea = positionNew.positionArea ?? null; - profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; - // profile.dateStart = new Date(); - } - await this.positionRepository.save(positionNew, { data: req }); - } else if (!posMaster.isSit) { - // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา - console.log( - "[ExecuteOfficerProfileService] positionNew is null, using bodyPosition data as fallback", - ); - profile.position = item.bodyPosition.positionName ?? null; - profile.posTypeId = item.bodyPosition.posTypeId ?? null; - profile.posLevelId = item.bodyPosition.posLevelId ?? null; - profile.positionField = item.bodyPosition.positionField ?? null; - profile.positionArea = item.bodyPosition.positionArea ?? null; - profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? null; - } - await this.profileRepository.save(profile, { data: req }); - setLogDataDiff(req, { before, after: profile }); - // await CreatePosMasterHistoryOfficer(posMaster.id, req); - await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { - positionId: positionNew?.id, - }); - } - // Insignia - if (_oldInsigniaIds.length > 0) { - console.log( - "[ExecuteOfficerProfileService] Processing old insignias, count:", - _oldInsigniaIds.length, - ); - const _insignias = await this.insigniaRepo.find({ - where: { id: In(_oldInsigniaIds), isDeleted: false }, - order: { createdAt: "ASC" }, - }); - for (const oldInsignia of _insignias) { - const newInsigniaData: CreateProfileInsignia = { - profileId: profile.id, - year: oldInsignia.year, - no: oldInsignia.no, - volume: oldInsignia.volume, - section: oldInsignia.section, - page: oldInsignia.page, - receiveDate: oldInsignia.receiveDate, - insigniaId: oldInsignia.insigniaId, - dateAnnounce: oldInsignia.dateAnnounce, - issue: oldInsignia.issue, - volumeNo: oldInsignia.volumeNo, - refCommandDate: oldInsignia.refCommandDate, - refCommandNo: oldInsignia.refCommandNo, - note: oldInsignia.note, - isUpload: oldInsignia.isUpload, - }; - const insignia = new ProfileInsignia(); - Object.assign(insignia, { ...newInsigniaData, ...meta }); - const history = new ProfileInsigniaHistory(); - Object.assign(history, { ...insignia, id: undefined }); - await this.insigniaRepo.save(insignia, { data: req }); - setLogDataDiff(req, { before, after: insignia }); - history.profileInsigniaId = insignia.id; - await this.insigniaHistoryRepo.save(history, { data: req }); - } - } - // เพิ่มรูปภาพโปรไฟล์ - if (item.bodyProfile.objectRefId) { - console.log( - "[ExecuteOfficerProfileService] Processing profile avatar image, objectRefId:", - item.bodyProfile.objectRefId, - ); - const _profileAvatar = new ProfileAvatar(); - Object.assign(_profileAvatar, { - ...meta, - profileId: profile.id, - profileEmployeeId: undefined, - }); - if (profile.profileAvatars && profile.profileAvatars.length > 0) { - await Promise.all( - profile.profileAvatars.map(async (item: any) => { - item.isActive = false; - await this.avatarRepository.save(item); - }), - ); - } - await this.avatarRepository.save(_profileAvatar); - let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`; - let fileName = `profile-${_profileAvatar.id}`; - _profileAvatar.isActive = true; - _profileAvatar.avatar = avatar; - _profileAvatar.avatarName = fileName; - await this.avatarRepository.save(_profileAvatar, { data: req }); - profile.avatar = avatar; - profile.avatarName = fileName; - await this.profileRepository.save(profile, { data: req }); - const checkAvatar = await this.avatarRepository.findOne({ - where: { avatar: avatar, avatarName: fileName }, - }); - if (checkAvatar && checkAvatar.profileId == null) { - checkAvatar.profileId = profile.id; - await this.avatarRepository.save(checkAvatar); - } - //duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม - await new CallAPI() - .PostData(req, `/salary/file/avatar/${item.bodyProfile.objectRefId}`, { - prefix: avatar, - fileName: fileName, - }) - .then(() => {}) - .catch(() => {}); + await positionRepository.save(positionNew, { data: req }); + } else if (!posMaster.isSit) { + // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา + console.log( + "[ExecuteOfficerProfileService] positionNew is null, using bodyPosition data as fallback", + ); + profile.position = item.bodyPosition.positionName ?? null; + profile.posTypeId = item.bodyPosition.posTypeId ?? null; + profile.posLevelId = item.bodyPosition.posLevelId ?? null; + profile.positionField = item.bodyPosition.positionField ?? null; + profile.positionArea = item.bodyPosition.positionArea ?? null; + profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? null; + } + await profileRepository.save(profile, { data: req }); + setLogDataDiff(req, { before, after: profile }); + // await CreatePosMasterHistoryOfficer(posMaster.id, req); + console.log( + `[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, citizenId: ${item.bodyProfile?.citizenId}`, + ); + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { + positionId: positionNew?.id, + }, manager); + } + // Insignia + if (_oldInsigniaIds.length > 0) { + console.log( + "[ExecuteOfficerProfileService] Processing old insignias, count:", + _oldInsigniaIds.length, + ); + const _insignias = await insigniaRepo.find({ + where: { id: In(_oldInsigniaIds), isDeleted: false }, + order: { createdAt: "ASC" }, + }); + for (const oldInsignia of _insignias) { + const newInsigniaData: CreateProfileInsignia = { + profileId: profile.id, + year: oldInsignia.year, + no: oldInsignia.no, + volume: oldInsignia.volume, + section: oldInsignia.section, + page: oldInsignia.page, + receiveDate: oldInsignia.receiveDate, + insigniaId: oldInsignia.insigniaId, + dateAnnounce: oldInsignia.dateAnnounce, + issue: oldInsignia.issue, + volumeNo: oldInsignia.volumeNo, + refCommandDate: oldInsignia.refCommandDate, + refCommandNo: oldInsignia.refCommandNo, + note: oldInsignia.note, + isUpload: oldInsignia.isUpload, + }; + const insignia = new ProfileInsignia(); + Object.assign(insignia, { ...newInsigniaData, ...meta }); + const history = new ProfileInsigniaHistory(); + Object.assign(history, { ...insignia, id: undefined }); + await insigniaRepo.save(insignia, { data: req }); + setLogDataDiff(req, { before, after: insignia }); + history.profileInsigniaId = insignia.id; + await insigniaHistoryRepo.save(history, { data: req }); + } + } + // เพิ่มรูปภาพโปรไฟล์ + if (item.bodyProfile.objectRefId) { + console.log( + "[ExecuteOfficerProfileService] Processing profile avatar image, objectRefId:", + item.bodyProfile.objectRefId, + ); + const _profileAvatar = new ProfileAvatar(); + Object.assign(_profileAvatar, { + ...meta, + profileId: profile.id, + profileEmployeeId: undefined, + }); + if (profile.profileAvatars && profile.profileAvatars.length > 0) { + for (const avatarItem of profile.profileAvatars) { + avatarItem.isActive = false; + await avatarRepository.save(avatarItem); } } - }), + await avatarRepository.save(_profileAvatar); + let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`; + let fileName = `profile-${_profileAvatar.id}`; + _profileAvatar.isActive = true; + _profileAvatar.avatar = avatar; + _profileAvatar.avatarName = fileName; + await avatarRepository.save(_profileAvatar, { data: req }); + profile.avatar = avatar; + profile.avatarName = fileName; + await profileRepository.save(profile, { data: req }); + const checkAvatar = await avatarRepository.findOne({ + where: { avatar: avatar, avatarName: fileName }, + }); + if (checkAvatar && checkAvatar.profileId == null) { + checkAvatar.profileId = profile.id; + await avatarRepository.save(checkAvatar); + } + //duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม + await new CallAPI() + .PostData(req, `/salary/file/avatar/${item.bodyProfile.objectRefId}`, { + prefix: avatar, + fileName: fileName, + }) + .then(() => {}) + .catch(() => {}); + } + } + + console.log( + `[ExecuteOfficerProfileService] Completed processOne — citizenId: ${item.bodyProfile?.citizenId}`, ); - console.log("[ExecuteOfficerProfileService] executeCreateOfficerProfile completed successfully"); } } diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index 2f0871dc..d9039b30 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -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 { + ): Promise { 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( diff --git a/src/services/ExecuteSalaryEmployeeCurrentService.ts b/src/services/ExecuteSalaryEmployeeCurrentService.ts index 919144c1..93b6eaed 100644 --- a/src/services/ExecuteSalaryEmployeeCurrentService.ts +++ b/src/services/ExecuteSalaryEmployeeCurrentService.ts @@ -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 { - 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 { + 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}`, + ); } } diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts index bfb1af4c..46952847 100644 --- a/src/services/ExecuteSalaryEmployeeLeaveService.ts +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -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 { - 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 { + 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"); } } diff --git a/src/services/ExecuteSalaryLeaveService.ts b/src/services/ExecuteSalaryLeaveService.ts index 04d6c32c..98a1da41 100644 --- a/src/services/ExecuteSalaryLeaveService.ts +++ b/src/services/ExecuteSalaryLeaveService.ts @@ -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 { - 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 { + 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}`, + ); } } diff --git a/src/services/ExecuteSalaryService.ts b/src/services/ExecuteSalaryService.ts index bbdaa3a4..d375d9db 100644 --- a/src/services/ExecuteSalaryService.ts +++ b/src/services/ExecuteSalaryService.ts @@ -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 { - 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 { + 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"); } } diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 144a5621..940991d1 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -217,92 +217,105 @@ export async function CreatePosMasterHistoryEmployee( posMasterId: string, request: RequestWithUser | null, type?: string | null, + manager?: EntityManager, ): Promise { - 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; }