From d84ec4f55d0adc20a69f6de45851ab9797535892 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 12 Jun 2026 15:58:31 +0700 Subject: [PATCH 01/39] test --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index a09d47eb..ab3b372c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,7 +67,7 @@ async function main() { // Cron job for updating org revision - every day at 01:00:00 // const cronTime = "0 0 1 * * *"; - const cronTime = "0 45 15 * * *"; // test by dev + const cronTime = "0 10 16 * * *"; // test by dev cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); From aeae391034af6847b086eb316f0b9d739d84caa1 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 12 Jun 2026 17:53:23 +0700 Subject: [PATCH 02/39] no message --- src/app.ts | 3 +-- src/controllers/ProfileChangeNameController.ts | 10 ++++++++-- src/controllers/ProfileController.ts | 4 ++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index ab3b372c..c2cbe5f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -66,8 +66,7 @@ async function main() { }); // Cron job for updating org revision - every day at 01:00:00 - // const cronTime = "0 0 1 * * *"; - const cronTime = "0 10 16 * * *"; // test by dev + const cronTime = "0 0 1 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 0261666f..18bd4922 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -111,9 +111,15 @@ export class ProfileChangeNameController extends Controller { setLogDataDiff(req, { before, after: history }); profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + //old + // profile.prefix = body.prefix ?? profile.prefix; + // profile.rank = body.rank ?? profile.rank; + // profile.prefixMain = profile.rank ?? profile.prefix; + //new + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 42b322d3..c2bb6420 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5800,8 +5800,12 @@ export class ProfileController extends Controller { Object.assign(record, body); record.dateRetireLaw = calculateRetireLaw(record.birthDate); + //old record.prefixMain = record.prefix; record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; + //new + record.prefixMain = record.prefix; + record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefix; record.createdUserId = request.user.sub; record.createdFullName = request.user.name; record.createdAt = new Date(); From 8232c6696ec4d57e398b469060aa61ab760528cf Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 12 Jun 2026 17:53:33 +0700 Subject: [PATCH 03/39] no message --- src/controllers/ProfileController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index c2bb6420..c2925193 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5801,8 +5801,8 @@ export class ProfileController extends Controller { Object.assign(record, body); record.dateRetireLaw = calculateRetireLaw(record.birthDate); //old - record.prefixMain = record.prefix; - record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; + // record.prefixMain = record.prefix; + // record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; //new record.prefixMain = record.prefix; record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefix; From 7920832c5c62a976b9766257bf47194cec5804a6 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 12 Jun 2026 18:02:54 +0700 Subject: [PATCH 04/39] revert --- src/controllers/ProfileChangeNameController.ts | 9 ++------- src/controllers/ProfileController.ts | 6 +----- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 18bd4922..3f61301f 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -111,14 +111,9 @@ export class ProfileChangeNameController extends Controller { setLogDataDiff(req, { before, after: history }); profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - //old - // profile.prefix = body.prefix ?? profile.prefix; - // profile.rank = body.rank ?? profile.rank; - // profile.prefixMain = profile.rank ?? profile.prefix; - //new - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefix = body.prefix ?? profile.prefix; profile.rank = body.rank ?? profile.rank; + profile.prefixMain = profile.rank ?? profile.prefix; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index c2925193..42b322d3 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5800,12 +5800,8 @@ export class ProfileController extends Controller { Object.assign(record, body); record.dateRetireLaw = calculateRetireLaw(record.birthDate); - //old - // record.prefixMain = record.prefix; - // record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; - //new record.prefixMain = record.prefix; - record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefix; + record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; record.createdUserId = request.user.sub; record.createdFullName = request.user.name; record.createdAt = new Date(); From 5c5fc08269005629eedc52904e4d9c5f36b5d79a Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 10:58:56 +0700 Subject: [PATCH 05/39] =?UTF-8?q?test=20=E0=B9=80=E0=B8=9C=E0=B8=A2?= =?UTF-8?q?=E0=B9=81=E0=B8=9E=E0=B8=A3=E0=B9=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 3 +- .../PermissionProfileController.ts | 3 +- src/services/PositionService.ts | 8 ++--- src/services/rabbitmq.ts | 30 +++++++++---------- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app.ts b/src/app.ts index c2cbe5f7..0b9ce148 100644 --- a/src/app.ts +++ b/src/app.ts @@ -66,7 +66,8 @@ async function main() { }); // Cron job for updating org revision - every day at 01:00:00 - const cronTime = "0 0 1 * * *"; + // const cronTime = "0 0 1 * * *"; + const cronTime = "0 03 11 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); diff --git a/src/controllers/PermissionProfileController.ts b/src/controllers/PermissionProfileController.ts index 99e4cb47..0df114af 100644 --- a/src/controllers/PermissionProfileController.ts +++ b/src/controllers/PermissionProfileController.ts @@ -65,8 +65,7 @@ export class PermissionProfileController extends Controller { if (!request.user.role.includes("SUPER_ADMIN")) { rootId = - orgRevisionActive?.posMasters?.filter((x) => x.next_holderId == profile.id)[0] - // orgRevisionActive?.posMasters?.filter((x) => x.current_holderId == profile.id)[0] + orgRevisionActive?.posMasters?.filter((x) => x.current_holderId == profile.id)[0] ?.orgRootId || null; if (!rootId) return new HttpSuccess([]); } diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 45b3c1b0..c242c184 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -477,10 +477,10 @@ export async function BatchSavePosMasterHistoryOfficer( const profileChanged = existing && existing.profileId !== op.profileId; const positionChanged = existing && - existing.position !== op.pm?.position && - existing.posType !== op.pm?.posType && - existing.posLevel !== op.pm?.posLevel && - existing.posExecutive !== op.pm?.posExecutive; + (existing.position !== op.pm?.position || + existing.posType !== op.pm?.posType || + existing.posLevel !== op.pm?.posLevel || + existing.posExecutive !== op.pm?.posExecutive); // ถ้าไม่มี record เดิม หรือ profile เปลี่ยน หรือ position เปลี่ยน ให้สร้าง record ใหม่ if (shouldInsert || profileChanged || positionChanged) { diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index e06474b9..8ab9d22f 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1732,23 +1732,23 @@ async function clearMenuAndRoleCache(): Promise { console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`); } - // const posMasterKeys = await keysAsync("posMaster_*"); - // if (posMasterKeys.length > 0) { - // await delAsync(...posMasterKeys); - // console.log(`[AMQ] Cleared ${posMasterKeys.length} posMaster cache keys`); - // } + const posMasterKeys = await keysAsync("posMaster_*"); + if (posMasterKeys.length > 0) { + await delAsync(...posMasterKeys); + console.log(`[AMQ] Cleared ${posMasterKeys.length} posMaster cache keys`); + } - // const userKeys = await keysAsync("user_*"); - // if (userKeys.length > 0) { - // await delAsync(...userKeys); - // console.log(`[AMQ] Cleared ${userKeys.length} user cache keys`); - // } + const userKeys = await keysAsync("user_*"); + if (userKeys.length > 0) { + await delAsync(...userKeys); + console.log(`[AMQ] Cleared ${userKeys.length} user cache keys`); + } - // const orgKeys = await keysAsync("org_*"); - // if (orgKeys.length > 0) { - // await delAsync(...orgKeys); - // console.log(`[AMQ] Cleared ${orgKeys.length} org cache keys`); - // } + const orgKeys = await keysAsync("org_*"); + if (orgKeys.length > 0) { + await delAsync(...orgKeys); + console.log(`[AMQ] Cleared ${orgKeys.length} org cache keys`); + } } finally { redisClient.quit(); } From bc8fbf4cf867f85bfd9b488b8bfd3d63dee339f7 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 11:04:22 +0700 Subject: [PATCH 06/39] =?UTF-8?q?test=20=E0=B9=80=E0=B8=9C=E0=B8=A2?= =?UTF-8?q?=E0=B9=81=E0=B8=9E=E0=B8=A3=E0=B9=88=20(1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 0b9ce148..04c02abb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,7 +67,7 @@ async function main() { // Cron job for updating org revision - every day at 01:00:00 // const cronTime = "0 0 1 * * *"; - const cronTime = "0 03 11 * * *"; + const cronTime = "0 10 11 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); From 82c94073ffb47a78ebd72c7cb0130a41d835e439 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 11:32:01 +0700 Subject: [PATCH 07/39] =?UTF-8?q?test=20=E0=B9=80=E0=B8=9C=E0=B8=A2?= =?UTF-8?q?=E0=B9=81=E0=B8=9E=E0=B8=A3=E0=B9=88=20(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/rabbitmq.ts | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index b941ad13..7705593b 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -695,7 +695,19 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { where: { orgRevisionId: orgRevisionPublish.id, }, - select: ["id", "current_holderId", "ancestorDNA"], + select: [ + "id", + "current_holderId", + "ancestorDNA", + "posMasterNo", + "posMasterNoPrefix", + "posMasterNoSuffix", + "orgRootId", + "orgChild1Id", + "orgChild2Id", + "orgChild3Id", + "orgChild4Id", + ], }); // Task #2160 ดึง posMasterAssign ของ revision เดิม @@ -861,7 +873,21 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { const newHolderId = item?.next_holderId; const isHolderChanged = oldHolderId !== newHolderId; - if (isHolderChanged) { + // เช็คว่า holder เดิม แต่ตำแหน่งเปลี่ยน + const isSameHolder = oldHolderId === newHolderId && oldHolderId != null && newHolderId != null; + const isPositionChanged = + isSameHolder && + oldPm && + (oldPm.posMasterNo !== item.posMasterNo || + oldPm.posMasterNoPrefix !== item.posMasterNoPrefix || + oldPm.posMasterNoSuffix !== item.posMasterNoSuffix || + oldPm.orgRootId !== item.orgRoot?.id || + oldPm.orgChild1Id !== item.orgChild1?.id || + oldPm.orgChild2Id !== item.orgChild2?.id || + oldPm.orgChild3Id !== item.orgChild3?.id || + oldPm.orgChild4Id !== item.orgChild4?.id); + + if (isHolderChanged || isPositionChanged) { const nextHolderProfile = item.next_holderId != null && item.next_holderId !== "" ? profilesMap.get(item.next_holderId) From ed6ab06b524b37cd56b6e44e7bf2626afd34628f Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 11:32:43 +0700 Subject: [PATCH 08/39] fix --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 04c02abb..99365458 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,7 +67,7 @@ async function main() { // Cron job for updating org revision - every day at 01:00:00 // const cronTime = "0 0 1 * * *"; - const cronTime = "0 10 11 * * *"; + const cronTime = "0 50 11 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); From 8236caf458f162cf51f00a6c997a7290f8ba8f82 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 15 Jun 2026 11:52:20 +0700 Subject: [PATCH 09/39] fix #1594 --- src/controllers/ProfileChangeNameController.ts | 14 +++++++++----- .../ProfileChangeNameEmployeeController.ts | 12 ++++++++---- .../ProfileChangeNameEmployeeTempController.ts | 12 ++++++++---- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 3f61301f..8eea3a9e 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -111,10 +111,12 @@ export class ProfileChangeNameController extends Controller { setLogDataDiff(req, { before, after: history }); profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; - + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -185,9 +187,11 @@ export class ProfileChangeNameController extends Controller { if (profile && chkLastRecord.id === record.id) { profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before: before_profile, after: profile }); } diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 91dd3d55..6e28c477 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -117,9 +117,11 @@ export class ProfileChangeNameEmployeeController extends Controller { profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -191,9 +193,11 @@ export class ProfileChangeNameEmployeeController extends Controller { if (profile && chkLastRecord.id === record.id) { profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; await this.profileEmployeeRepo.save(profile); } diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 7f7b39aa..05dd9312 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -108,9 +108,11 @@ export class ProfileChangeNameEmployeeTempController extends Controller { profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -179,9 +181,11 @@ export class ProfileChangeNameEmployeeTempController extends Controller { if (profile && chkLastRecord.id === record.id) { profile.firstName = body.firstName ?? profile.firstName; profile.lastName = body.lastName ?? profile.lastName; - profile.prefix = body.prefix ?? profile.prefix; + // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; - profile.prefixMain = profile.rank ?? profile.prefix; + // profile.prefixMain = profile.rank ?? profile.prefix; // old + profile.prefixMain = profile.prefix; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; await this.profileEmployeeRepo.save(profile); } From 5b17073eebb86e0612ae3d4cf123a7c37aec7e95 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 14:21:10 +0700 Subject: [PATCH 10/39] =?UTF-8?q?test=20=E0=B9=80=E0=B8=9C=E0=B8=A2?= =?UTF-8?q?=E0=B9=81=E0=B8=9E=E0=B8=A3=E0=B9=88=20(3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 99365458..1c81f120 100644 --- a/src/app.ts +++ b/src/app.ts @@ -67,7 +67,7 @@ async function main() { // Cron job for updating org revision - every day at 01:00:00 // const cronTime = "0 0 1 * * *"; - const cronTime = "0 50 11 * * *"; + const cronTime = "0 30 14 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); From c398354208366c8d28cc06fea0ab679e3fb13de0 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 15:21:15 +0700 Subject: [PATCH 11/39] #1596 and revert cronjob --- src/app.ts | 3 +- src/controllers/OrganizationController.ts | 36 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/app.ts b/src/app.ts index 1c81f120..c2cbe5f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -66,8 +66,7 @@ async function main() { }); // Cron job for updating org revision - every day at 01:00:00 - // const cronTime = "0 0 1 * * *"; - const cronTime = "0 30 14 * * *"; + const cronTime = "0 0 1 * * *"; cron.schedule(cronTime, async () => { try { const orgController = new OrganizationController(); diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 7edc5998..d1b0cb19 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8702,6 +8702,42 @@ export class OrganizationController extends Controller { }; await queryRunner.commitTransaction(); + + // Clear Redis cache after successful publish + const redis = require("redis"); + const { promisify } = require("util"); + const redisClient = redis.createClient({ + host: process.env.REDIS_HOST || "localhost", + port: parseInt(process.env.REDIS_PORT || "6379"), + }); + const keysAsync = promisify(redisClient.keys).bind(redisClient); + const delAsync = promisify(redisClient.del).bind(redisClient); + + try { + const [posMasterKeys, userKeys, orgKeys, menuKeys, roleKeys] = await Promise.all([ + keysAsync("posMaster_*"), + keysAsync("user_*"), + keysAsync("org_*"), + keysAsync("menu_*"), + keysAsync("role_*"), + ]); + + if (posMasterKeys.length > 0) await delAsync(...posMasterKeys); + if (userKeys.length > 0) await delAsync(...userKeys); + if (orgKeys.length > 0) await delAsync(...orgKeys); + if (menuKeys.length > 0) await delAsync(...menuKeys); + if (roleKeys.length > 0) await delAsync(...roleKeys); + + console.log(`[moveDraftToCurrent] Cleared cache: posMaster=${posMasterKeys.length}, user=${userKeys.length}, org=${orgKeys.length}, menu=${menuKeys.length}, role=${roleKeys.length}`); + } catch (err) { + console.error("[moveDraftToCurrent] Error clearing cache:", err); + } finally { + redisClient.quit(); + } + + // Invalidate memory cache + orgStructureCache.invalidate(currentRevisionId); + return new HttpSuccess(summary); } catch (error) { console.error("Error moving draft to current:", error); From b7220687586b455e26560b3120ac5b34306caa86 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 15 Jun 2026 17:17:36 +0700 Subject: [PATCH 12/39] =?UTF-8?q?migrate=20=E0=B9=80=E0=B8=9E=E0=B8=B4?= =?UTF-8?q?=E0=B9=88=E0=B8=A1=E0=B8=9F=E0=B8=B4=E0=B8=A5=E0=B8=94=E0=B9=8C?= =?UTF-8?q?=E0=B8=9A=E0=B8=B1=E0=B8=99=E0=B8=97=E0=B8=B6=E0=B8=81=E0=B8=97?= =?UTF-8?q?=E0=B8=B5=E0=B9=88=E0=B9=82=E0=B8=84=E0=B8=A3=E0=B8=87=E0=B8=AA?= =?UTF-8?q?=E0=B8=A3=E0=B9=89=E0=B8=B2=E0=B8=87=20orgRoot,=20orgChild1,=20?= =?UTF-8?q?...,=20orgChild4=20#252?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrgChild1Controller.ts | 2 ++ src/controllers/OrgChild2Controller.ts | 2 ++ src/controllers/OrgChild3Controller.ts | 2 ++ src/controllers/OrgChild4Controller.ts | 2 ++ src/controllers/OrgRootController.ts | 2 ++ src/entities/OrgChild1.ts | 14 ++++++++++++ src/entities/OrgChild2.ts | 14 ++++++++++++ src/entities/OrgChild3.ts | 14 ++++++++++++ src/entities/OrgChild4.ts | 14 ++++++++++++ src/entities/OrgRoot.ts | 14 ++++++++++++ ...929-update_root_c1_to_c4_add_field_code.ts | 22 +++++++++++++++++++ 11 files changed, 102 insertions(+) create mode 100644 src/migration/1781517610929-update_root_c1_to_c4_add_field_code.ts diff --git a/src/controllers/OrgChild1Controller.ts b/src/controllers/OrgChild1Controller.ts index 6e44af05..d59ce669 100644 --- a/src/controllers/OrgChild1Controller.ts +++ b/src/controllers/OrgChild1Controller.ts @@ -74,6 +74,7 @@ export class OrgChild1Controller { DIVISION_CODE: orgChild1.DIVISION_CODE, SECTION_CODE: orgChild1.SECTION_CODE, JOB_CODE: orgChild1.JOB_CODE, + CHILD1_CODE: orgChild1.CHILD1_CODE, orgCode: orgChild1.orgRoot.orgRootCode + orgChild1.orgChild1Code, }; return new HttpSuccess(getOrgChild1); @@ -346,6 +347,7 @@ export class OrgChild1Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, isOfficer: requestBody.isOfficer, isInformation: requestBody.isInformation, orgChild1PhoneEx: requestBody.orgChild1PhoneEx, diff --git a/src/controllers/OrgChild2Controller.ts b/src/controllers/OrgChild2Controller.ts index 28ce564f..1cc4186d 100644 --- a/src/controllers/OrgChild2Controller.ts +++ b/src/controllers/OrgChild2Controller.ts @@ -85,6 +85,7 @@ export class OrgChild2Controller extends Controller { DIVISION_CODE: orgChild2.DIVISION_CODE, SECTION_CODE: orgChild2.SECTION_CODE, JOB_CODE: orgChild2.JOB_CODE, + CHILD2_CODE: orgChild2.CHILD2_CODE, orgCode: orgChild2.orgRoot.orgRootCode + orgChild2.orgChild2Code, }; return new HttpSuccess(getOrgChild2); @@ -252,6 +253,7 @@ export class OrgChild2Controller extends Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, orgChild2PhoneEx: requestBody.orgChild2PhoneEx, orgChild2PhoneIn: requestBody.orgChild2PhoneIn, orgChild2Fax: requestBody.orgChild2Fax, diff --git a/src/controllers/OrgChild3Controller.ts b/src/controllers/OrgChild3Controller.ts index 4ed10804..a3861d55 100644 --- a/src/controllers/OrgChild3Controller.ts +++ b/src/controllers/OrgChild3Controller.ts @@ -69,6 +69,7 @@ export class OrgChild3Controller { DIVISION_CODE: orgChild3.DIVISION_CODE, SECTION_CODE: orgChild3.SECTION_CODE, JOB_CODE: orgChild3.JOB_CODE, + CHILD3_CODE: orgChild3.CHILD3_CODE, orgCode: orgChild3.orgRoot.orgRootCode + orgChild3.orgChild3Code, }; return new HttpSuccess(getOrgChild3); @@ -207,6 +208,7 @@ export class OrgChild3Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, orgChild3PhoneEx: requestBody.orgChild3PhoneEx, orgChild3PhoneIn: requestBody.orgChild3PhoneIn, orgChild3Fax: requestBody.orgChild3Fax, diff --git a/src/controllers/OrgChild4Controller.ts b/src/controllers/OrgChild4Controller.ts index e18c15f9..eb044bbc 100644 --- a/src/controllers/OrgChild4Controller.ts +++ b/src/controllers/OrgChild4Controller.ts @@ -82,6 +82,7 @@ export class OrgChild4Controller extends Controller { DIVISION_CODE: orgChild4.DIVISION_CODE, SECTION_CODE: orgChild4.SECTION_CODE, JOB_CODE: orgChild4.JOB_CODE, + CHILD4_CODE: orgChild4.CHILD4_CODE, orgCode: orgChild4.orgRoot.orgRootCode + orgChild4.orgChild4Code, }; return new HttpSuccess(getOrgChild4); @@ -254,6 +255,7 @@ export class OrgChild4Controller extends Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, orgChild4PhoneEx: requestBody.orgChild4PhoneEx, orgChild4PhoneIn: requestBody.orgChild4PhoneIn, orgChild4Fax: requestBody.orgChild4Fax, diff --git a/src/controllers/OrgRootController.ts b/src/controllers/OrgRootController.ts index 45bf1436..69c5bba8 100644 --- a/src/controllers/OrgRootController.ts +++ b/src/controllers/OrgRootController.ts @@ -83,6 +83,7 @@ export class OrgRootController extends Controller { DIVISION_CODE: orgRoot.DIVISION_CODE, SECTION_CODE: orgRoot.SECTION_CODE, JOB_CODE: orgRoot.JOB_CODE, + ROOT_CODE: orgRoot.ROOT_CODE, orgCode: orgRoot.orgRootCode + "00", }; return new HttpSuccess(getOrgRoot); @@ -350,6 +351,7 @@ export class OrgRootController extends Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, }); await this.orgRootRepository.save(orgRoot, { data: request }); setLogDataDiff(request, { before, after: orgRoot }); diff --git a/src/entities/OrgChild1.ts b/src/entities/OrgChild1.ts index e322df8b..b211acf8 100644 --- a/src/entities/OrgChild1.ts +++ b/src/entities/OrgChild1.ts @@ -174,6 +174,14 @@ export class OrgChild1 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD1_CODE", + default: null, + }) + CHILD1_CODE: string; + @ManyToOne(() => OrgRoot, (orgRoot) => orgRoot.orgChild1s) @JoinColumn({ name: "orgRootId" }) orgRoot: OrgRoot; @@ -228,6 +236,9 @@ export class CreateOrgChild1 { @Column() JOB_CODE?: string | null; + @Column() + CHILD1_CODE?: string | null; + @Column() orgChild1PhoneEx?: string; @@ -283,6 +294,9 @@ export class UpdateOrgChild1 { @Column() JOB_CODE?: string | null; + @Column() + CHILD1_CODE?: string | null; + @Column() orgChild1PhoneEx?: string; diff --git a/src/entities/OrgChild2.ts b/src/entities/OrgChild2.ts index b2ed6a0c..1abf8b61 100644 --- a/src/entities/OrgChild2.ts +++ b/src/entities/OrgChild2.ts @@ -146,6 +146,14 @@ export class OrgChild2 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD2_CODE", + default: null, + }) + CHILD2_CODE: string; + @Column({ nullable: true, length: 40, @@ -222,6 +230,9 @@ export class CreateOrgChild2 { @Column() JOB_CODE?: string; + @Column() + CHILD2_CODE?: string; + @Column() orgChild2PhoneEx?: string; @@ -269,6 +280,9 @@ export class UpdateOrgChild2 { @Column() JOB_CODE?: string | null; + @Column() + CHILD2_CODE?: string | null; + @Column() orgChild2PhoneEx?: string; diff --git a/src/entities/OrgChild3.ts b/src/entities/OrgChild3.ts index 4c376fb6..6fbc1e72 100644 --- a/src/entities/OrgChild3.ts +++ b/src/entities/OrgChild3.ts @@ -153,6 +153,14 @@ export class OrgChild3 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD3_CODE", + default: null, + }) + CHILD3_CODE: string; + @Column({ nullable: true, length: 40, @@ -230,6 +238,9 @@ export class CreateOrgChild3 { @Column() JOB_CODE?: string; + @Column() + CHILD3_CODE?: string; + @Column() orgChild3PhoneEx?: string; @@ -279,6 +290,9 @@ export class UpdateOrgChild3 { @Column() JOB_CODE?: string | null; + @Column() + CHILD3_CODE?: string | null; + @Column() orgChild3PhoneEx?: string; diff --git a/src/entities/OrgChild4.ts b/src/entities/OrgChild4.ts index 314c9393..d0a28a27 100644 --- a/src/entities/OrgChild4.ts +++ b/src/entities/OrgChild4.ts @@ -158,6 +158,14 @@ export class OrgChild4 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD4_CODE", + default: null, + }) + CHILD4_CODE: string; + @Column({ nullable: true, length: 40, @@ -236,6 +244,9 @@ export class CreateOrgChild4 { @Column() JOB_CODE?: string; + @Column() + CHILD4_CODE?: string; + @Column() orgChild4PhoneEx?: string; @@ -283,6 +294,9 @@ export class UpdateOrgChild4 { @Column() JOB_CODE?: string | null; + @Column() + CHILD4_CODE?: string | null; + @Column() orgChild4PhoneEx?: string; diff --git a/src/entities/OrgRoot.ts b/src/entities/OrgRoot.ts index 3aefad6e..d474a1fe 100644 --- a/src/entities/OrgRoot.ts +++ b/src/entities/OrgRoot.ts @@ -166,6 +166,14 @@ export class OrgRoot extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "ROOT_CODE", + default: null, + }) + ROOT_CODE: string; + @ManyToOne(() => OrgRevision, (orgRevision) => orgRevision.orgRoots) @JoinColumn({ name: "orgRevisionId" }) orgRevision: OrgRevision; @@ -226,6 +234,9 @@ export class CreateOrgRoot { @Column() JOB_CODE?: string; + @Column() + ROOT_CODE?: string; + @Column() orgRootPhoneEx?: string; @@ -281,6 +292,9 @@ export class UpdateOrgRoot { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + @Column() orgRootPhoneEx?: string; diff --git a/src/migration/1781517610929-update_root_c1_to_c4_add_field_code.ts b/src/migration/1781517610929-update_root_c1_to_c4_add_field_code.ts new file mode 100644 index 00000000..157ab487 --- /dev/null +++ b/src/migration/1781517610929-update_root_c1_to_c4_add_field_code.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateRootC1ToC4AddFieldCode1781517610929 implements MigrationInterface { + name = 'UpdateRootC1ToC4AddFieldCode1781517610929' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`orgChild4\` ADD \`CHILD4_CODE\` varchar(3) NULL COMMENT 'CHILD4_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild3\` ADD \`CHILD3_CODE\` varchar(3) NULL COMMENT 'CHILD3_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild2\` ADD \`CHILD2_CODE\` varchar(3) NULL COMMENT 'CHILD2_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild1\` ADD \`CHILD1_CODE\` varchar(3) NULL COMMENT 'CHILD1_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgRoot\` ADD \`ROOT_CODE\` varchar(3) NULL COMMENT 'ROOT_CODE'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`orgRoot\` DROP COLUMN \`ROOT_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild1\` DROP COLUMN \`CHILD1_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild2\` DROP COLUMN \`CHILD2_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild3\` DROP COLUMN \`CHILD3_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild4\` DROP COLUMN \`CHILD4_CODE\``); + } + +} From 84fd3fb9e5943e282f70f249ded03d3b13699f74 Mon Sep 17 00:00:00 2001 From: Adisak Date: Mon, 15 Jun 2026 18:26:34 +0700 Subject: [PATCH 13/39] =?UTF-8?q?comment=20=E0=B9=80=E0=B8=84=E0=B8=A5?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=A3=E0=B9=8C=20redis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrganizationController.ts | 1285 +++++++++++---------- src/services/rabbitmq.ts | 48 +- 2 files changed, 665 insertions(+), 668 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index d1b0cb19..096c7111 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -468,157 +468,157 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - // .andWhere( - // _data.child1 != undefined && _data.child1 != null - // ? _data.child1[0] != null - // ? `orgChild1.id IN (:...node)` - // : `orgChild1.id is null` - // : "1=1", - // { - // node: _data.child1, - // }, - // ) - .select([ - "orgChild1.id", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + // .andWhere( + // _data.child1 != undefined && _data.child1 != null + // ? _data.child1[0] != null + // ? `orgChild1.id IN (:...node)` + // : `orgChild1.id is null` + // : "1=1", + // { + // node: _data.child1, + // }, + // ) + .select([ + "orgChild1.id", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - // .andWhere( - // _data.child2 != undefined && _data.child2 != null - // ? _data.child2[0] != null - // ? `orgChild2.id IN (:...node)` - // : `orgChild2.id is null` - // : "1=1", - // { - // node: _data.child2, - // }, - // ) - .select([ - "orgChild2.id", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + // .andWhere( + // _data.child2 != undefined && _data.child2 != null + // ? _data.child2[0] != null + // ? `orgChild2.id IN (:...node)` + // : `orgChild2.id is null` + // : "1=1", + // { + // node: _data.child2, + // }, + // ) + .select([ + "orgChild2.id", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - // .andWhere( - // _data.child3 != undefined && _data.child3 != null - // ? _data.child3[0] != null - // ? `orgChild3.id IN (:...node)` - // : `orgChild3.id is null` - // : "1=1", - // { - // node: _data.child3, - // }, - // ) - .select([ - "orgChild3.id", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + // .andWhere( + // _data.child3 != undefined && _data.child3 != null + // ? _data.child3[0] != null + // ? `orgChild3.id IN (:...node)` + // : `orgChild3.id is null` + // : "1=1", + // { + // node: _data.child3, + // }, + // ) + .select([ + "orgChild3.id", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - // .andWhere( - // _data.child4 != undefined && _data.child4 != null - // ? _data.child4[0] != null - // ? `orgChild4.id IN (:...node)` - // : `orgChild4.id is null` - // : "1=1", - // { - // node: _data.child4, - // }, - // ) - .select([ - "orgChild4.id", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + // .andWhere( + // _data.child4 != undefined && _data.child4 != null + // ? _data.child4[0] != null + // ? `orgChild4.id IN (:...node)` + // : `orgChild4.id is null` + // : "1=1", + // { + // node: _data.child4, + // }, + // ) + .select([ + "orgChild4.id", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -1271,13 +1271,13 @@ export class OrganizationController extends Controller { where: orgRevision.orgRevisionIsCurrent && !orgRevision.orgRevisionIsDraft ? { - orgRevisionId: id, - current_holderId: profile.id, - } + orgRevisionId: id, + current_holderId: profile.id, + } : { - orgRevisionId: id, - next_holderId: profile.id, - }, + orgRevisionId: id, + next_holderId: profile.id, + }, }); if (!posMaster) return new HttpSuccess([]); @@ -1755,161 +1755,161 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 !== undefined && _data.child1 !== null - ? _data.child1[0] !== null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is null` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.misId", - "orgChild1.isOfficer", - "orgChild1.isInformation", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 !== undefined && _data.child1 !== null + ? _data.child1[0] !== null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.misId", + "orgChild1.isOfficer", + "orgChild1.isInformation", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 !== undefined && _data.child2 !== null - ? _data.child2[0] !== null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.misId", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 !== undefined && _data.child2 !== null + ? _data.child2[0] !== null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.misId", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 !== undefined && _data.child3 !== null - ? _data.child3[0] !== null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.misId", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 !== undefined && _data.child3 !== null + ? _data.child3[0] !== null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.misId", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 !== undefined && _data.child4 !== null - ? _data.child4[0] !== null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.misId", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 !== undefined && _data.child4 !== null + ? _data.child4[0] !== null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.misId", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; const formattedData = await Promise.all( @@ -3479,18 +3479,18 @@ export class OrganizationController extends Controller { const formattedData_ = rootId === "root" ? [ - { - personID: "", - name: "", - avatar: "", - positionName: "", - positionNum: "", - positionNumInt: null, - departmentName: data.orgRevisionName, - organizationId: data.id, - children: formattedData, - }, - ] + { + personID: "", + name: "", + avatar: "", + positionName: "", + positionNum: "", + positionNumInt: null, + departmentName: data.orgRevisionName, + organizationId: data.id, + children: formattedData, + }, + ] : formattedData; return new HttpSuccess(formattedData_); } @@ -5816,163 +5816,163 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .select([ - "orgChild1.id", - "orgChild1.orgRootId", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - ]) - .addSelect([ - "posMasters.id", - "posMasters.posMasterNo", - "posMasters.orgChild2Id", - "posMasters.isDirector", - ]) - .addSelect([ - "current_holder.prefix", - "current_holder.firstName", - "current_holder.lastName", - ]) - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is null` - : "1=1", - { - node: _data.child1, - }, - ) - .leftJoin("orgChild1.posMasters", "posMasters") - .leftJoin("posMasters.current_holder", "current_holder") - .orderBy("orgChild1.orgChild1Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .select([ + "orgChild1.id", + "orgChild1.orgRootId", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild2Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is null` + : "1=1", + { + node: _data.child1, + }, + ) + .leftJoin("orgChild1.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") + .orderBy("orgChild1.orgChild1Order", "ASC") + .addOrderBy("posMasters.posMasterOrder", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .select([ - "orgChild2.id", - "orgChild2.orgChild1Id", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - ]) - .addSelect([ - "posMasters.id", - "posMasters.posMasterNo", - "posMasters.orgChild3Id", - "posMasters.isDirector", - ]) - .addSelect([ - "current_holder.prefix", - "current_holder.firstName", - "current_holder.lastName", - ]) - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .leftJoin("orgChild2.posMasters", "posMasters") - .leftJoin("posMasters.current_holder", "current_holder") - .orderBy("orgChild2.orgChild2Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .select([ + "orgChild2.id", + "orgChild2.orgChild1Id", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild3Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .leftJoin("orgChild2.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") + .orderBy("orgChild2.orgChild2Order", "ASC") + .addOrderBy("posMasters.posMasterOrder", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .select([ - "orgChild3.id", - "orgChild3.orgChild2Id", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - ]) - .addSelect([ - "posMasters.id", - "posMasters.posMasterNo", - "posMasters.orgChild4Id", - "posMasters.isDirector", - ]) - .addSelect([ - "current_holder.prefix", - "current_holder.firstName", - "current_holder.lastName", - ]) - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .leftJoin("orgChild3.posMasters", "posMasters") - .leftJoin("posMasters.current_holder", "current_holder") - .orderBy("orgChild3.orgChild3Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .select([ + "orgChild3.id", + "orgChild3.orgChild2Id", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + ]) + .addSelect([ + "posMasters.id", + "posMasters.posMasterNo", + "posMasters.orgChild4Id", + "posMasters.isDirector", + ]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .leftJoin("orgChild3.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") + .orderBy("orgChild3.orgChild3Order", "ASC") + .addOrderBy("posMasters.posMasterOrder", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .select([ - "orgChild4.id", - "orgChild4.orgChild3Id", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - ]) - .addSelect(["posMasters.id", "posMasters.posMasterNo", "posMasters.isDirector"]) - .addSelect([ - "current_holder.prefix", - "current_holder.firstName", - "current_holder.lastName", - ]) - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .leftJoin("orgChild4.posMasters", "posMasters") - .leftJoin("posMasters.current_holder", "current_holder") - .orderBy("orgChild4.orgChild4Order", "ASC") - .addOrderBy("posMasters.posMasterOrder", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .select([ + "orgChild4.id", + "orgChild4.orgChild3Id", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + ]) + .addSelect(["posMasters.id", "posMasters.posMasterNo", "posMasters.isDirector"]) + .addSelect([ + "current_holder.prefix", + "current_holder.firstName", + "current_holder.lastName", + ]) + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .leftJoin("orgChild4.posMasters", "posMasters") + .leftJoin("posMasters.current_holder", "current_holder") + .orderBy("orgChild4.orgChild4Order", "ASC") + .addOrderBy("posMasters.posMasterOrder", "ASC") + .getMany() : []; const cannotViewRootPosMaster = @@ -6024,8 +6024,8 @@ export class OrganizationController extends Controller { posMaster: cannotViewRootPosMaster ? [] : filterPosMasters(orgRoot.posMasters, "orgChild1Id").map((x) => - formatPosMaster(x, orgRoot.orgRootShortName, orgRoot.id, 0), - ), + formatPosMaster(x, orgRoot.orgRootShortName, orgRoot.id, 0), + ), children: orgChild1Data .filter((orgChild1) => orgChild1.orgRootId === orgRoot.id) .map((orgChild1) => ({ @@ -6049,8 +6049,8 @@ export class OrganizationController extends Controller { posMaster: cannotViewChild1PosMaster ? [] : filterPosMasters(orgChild1.posMasters, "orgChild2Id").map((x) => - formatPosMaster(x, orgChild1.orgChild1ShortName, orgChild1.id, 1), - ), + formatPosMaster(x, orgChild1.orgChild1ShortName, orgChild1.id, 1), + ), children: orgChild2Data .filter((orgChild2) => orgChild2.orgChild1Id === orgChild1.id) .map((orgChild2) => ({ @@ -6077,8 +6077,8 @@ export class OrganizationController extends Controller { posMaster: cannotViewChild2PosMaster ? [] : filterPosMasters(orgChild2.posMasters, "orgChild3Id").map((x) => - formatPosMaster(x, orgChild2.orgChild2ShortName, orgChild2.id, 2), - ), + formatPosMaster(x, orgChild2.orgChild2ShortName, orgChild2.id, 2), + ), children: orgChild3Data .filter((orgChild3) => orgChild3.orgChild2Id === orgChild2.id) .map((orgChild3) => ({ @@ -6105,8 +6105,8 @@ export class OrganizationController extends Controller { posMaster: cannotViewChild3PosMaster ? [] : filterPosMasters(orgChild3.posMasters, "orgChild4Id").map((x) => - formatPosMaster(x, orgChild3.orgChild3ShortName, orgChild3.id, 3), - ), + formatPosMaster(x, orgChild3.orgChild3ShortName, orgChild3.id, 3), + ), children: orgChild4Data .filter((orgChild4) => orgChild4.orgChild3Id === orgChild3.id) .map((orgChild4) => ({ @@ -6137,10 +6137,10 @@ export class OrganizationController extends Controller { posMaster: cannotViewChild4PosMaster ? [] : orgChild4.posMasters - .filter((x) => x.isDirector === true) - .map((x) => - formatPosMaster(x, orgChild4.orgChild4ShortName, orgChild4.id, 4), - ), + .filter((x) => x.isDirector === true) + .map((x) => + formatPosMaster(x, orgChild4.orgChild4ShortName, orgChild4.id, 4), + ), })), })), })), @@ -6311,159 +6311,159 @@ export class OrganizationController extends Controller { const orgChild1Data = orgRootIds && orgRootIds.length > 0 ? await AppDataSource.getRepository(OrgChild1) - .createQueryBuilder("orgChild1") - .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) - .andWhere( - _data.child1 != undefined && _data.child1 != null - ? _data.child1[0] != null - ? `orgChild1.id IN (:...node)` - : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` - : "1=1", - { - node: _data.child1, - }, - ) - .select([ - "orgChild1.id", - "orgChild1.ancestorDNA", - "orgChild1.orgChild1Name", - "orgChild1.orgChild1ShortName", - "orgChild1.orgChild1Code", - "orgChild1.orgChild1Order", - "orgChild1.orgChild1PhoneEx", - "orgChild1.orgChild1PhoneIn", - "orgChild1.orgChild1Fax", - "orgChild1.orgRootId", - "orgChild1.orgChild1Rank", - "orgChild1.orgChild1RankSub", - "orgChild1.DEPARTMENT_CODE", - "orgChild1.DIVISION_CODE", - "orgChild1.SECTION_CODE", - "orgChild1.JOB_CODE", - "orgChild1.responsibility", - ]) - .orderBy("orgChild1.orgChild1Order", "ASC") - .getMany() + .createQueryBuilder("orgChild1") + .where("orgChild1.orgRootId IN (:...ids)", { ids: orgRootIds }) + .andWhere( + _data.child1 != undefined && _data.child1 != null + ? _data.child1[0] != null + ? `orgChild1.id IN (:...node)` + : `orgChild1.id is ${_data.privilege == "PARENT" ? "not null" : "null"}` + : "1=1", + { + node: _data.child1, + }, + ) + .select([ + "orgChild1.id", + "orgChild1.ancestorDNA", + "orgChild1.orgChild1Name", + "orgChild1.orgChild1ShortName", + "orgChild1.orgChild1Code", + "orgChild1.orgChild1Order", + "orgChild1.orgChild1PhoneEx", + "orgChild1.orgChild1PhoneIn", + "orgChild1.orgChild1Fax", + "orgChild1.orgRootId", + "orgChild1.orgChild1Rank", + "orgChild1.orgChild1RankSub", + "orgChild1.DEPARTMENT_CODE", + "orgChild1.DIVISION_CODE", + "orgChild1.SECTION_CODE", + "orgChild1.JOB_CODE", + "orgChild1.responsibility", + ]) + .orderBy("orgChild1.orgChild1Order", "ASC") + .getMany() : []; const orgChild1Ids = orgChild1Data.map((orgChild1) => orgChild1.id) || null; const orgChild2Data = orgChild1Ids && orgChild1Ids.length > 0 ? await AppDataSource.getRepository(OrgChild2) - .createQueryBuilder("orgChild2") - .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) - .andWhere( - _data.child2 != undefined && _data.child2 != null - ? _data.child2[0] != null - ? `orgChild2.id IN (:...node)` - : `orgChild2.id is null` - : "1=1", - { - node: _data.child2, - }, - ) - .select([ - "orgChild2.id", - "orgChild2.ancestorDNA", - "orgChild2.orgChild2Name", - "orgChild2.orgChild2ShortName", - "orgChild2.orgChild2Code", - "orgChild2.orgChild2Order", - "orgChild2.orgChild2PhoneEx", - "orgChild2.orgChild2PhoneIn", - "orgChild2.orgChild2Fax", - "orgChild2.orgRootId", - "orgChild2.orgChild2Rank", - "orgChild2.orgChild2RankSub", - "orgChild2.DEPARTMENT_CODE", - "orgChild2.DIVISION_CODE", - "orgChild2.SECTION_CODE", - "orgChild2.JOB_CODE", - "orgChild2.orgChild1Id", - "orgChild2.responsibility", - ]) - .orderBy("orgChild2.orgChild2Order", "ASC") - .getMany() + .createQueryBuilder("orgChild2") + .where("orgChild2.orgChild1Id IN (:...ids)", { ids: orgChild1Ids }) + .andWhere( + _data.child2 != undefined && _data.child2 != null + ? _data.child2[0] != null + ? `orgChild2.id IN (:...node)` + : `orgChild2.id is null` + : "1=1", + { + node: _data.child2, + }, + ) + .select([ + "orgChild2.id", + "orgChild2.ancestorDNA", + "orgChild2.orgChild2Name", + "orgChild2.orgChild2ShortName", + "orgChild2.orgChild2Code", + "orgChild2.orgChild2Order", + "orgChild2.orgChild2PhoneEx", + "orgChild2.orgChild2PhoneIn", + "orgChild2.orgChild2Fax", + "orgChild2.orgRootId", + "orgChild2.orgChild2Rank", + "orgChild2.orgChild2RankSub", + "orgChild2.DEPARTMENT_CODE", + "orgChild2.DIVISION_CODE", + "orgChild2.SECTION_CODE", + "orgChild2.JOB_CODE", + "orgChild2.orgChild1Id", + "orgChild2.responsibility", + ]) + .orderBy("orgChild2.orgChild2Order", "ASC") + .getMany() : []; const orgChild2Ids = orgChild2Data.map((orgChild2) => orgChild2.id) || null; const orgChild3Data = orgChild2Ids && orgChild2Ids.length > 0 ? await AppDataSource.getRepository(OrgChild3) - .createQueryBuilder("orgChild3") - .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) - .andWhere( - _data.child3 != undefined && _data.child3 != null - ? _data.child3[0] != null - ? `orgChild3.id IN (:...node)` - : `orgChild3.id is null` - : "1=1", - { - node: _data.child3, - }, - ) - .select([ - "orgChild3.id", - "orgChild3.ancestorDNA", - "orgChild3.orgChild3Name", - "orgChild3.orgChild3ShortName", - "orgChild3.orgChild3Code", - "orgChild3.orgChild3Order", - "orgChild3.orgChild3PhoneEx", - "orgChild3.orgChild3PhoneIn", - "orgChild3.orgChild3Fax", - "orgChild3.orgRootId", - "orgChild3.orgChild3Rank", - "orgChild3.orgChild3RankSub", - "orgChild3.DEPARTMENT_CODE", - "orgChild3.DIVISION_CODE", - "orgChild3.SECTION_CODE", - "orgChild3.JOB_CODE", - "orgChild3.orgChild2Id", - "orgChild3.responsibility", - ]) - .orderBy("orgChild3.orgChild3Order", "ASC") - .getMany() + .createQueryBuilder("orgChild3") + .where("orgChild3.orgChild2Id IN (:...ids)", { ids: orgChild2Ids }) + .andWhere( + _data.child3 != undefined && _data.child3 != null + ? _data.child3[0] != null + ? `orgChild3.id IN (:...node)` + : `orgChild3.id is null` + : "1=1", + { + node: _data.child3, + }, + ) + .select([ + "orgChild3.id", + "orgChild3.ancestorDNA", + "orgChild3.orgChild3Name", + "orgChild3.orgChild3ShortName", + "orgChild3.orgChild3Code", + "orgChild3.orgChild3Order", + "orgChild3.orgChild3PhoneEx", + "orgChild3.orgChild3PhoneIn", + "orgChild3.orgChild3Fax", + "orgChild3.orgRootId", + "orgChild3.orgChild3Rank", + "orgChild3.orgChild3RankSub", + "orgChild3.DEPARTMENT_CODE", + "orgChild3.DIVISION_CODE", + "orgChild3.SECTION_CODE", + "orgChild3.JOB_CODE", + "orgChild3.orgChild2Id", + "orgChild3.responsibility", + ]) + .orderBy("orgChild3.orgChild3Order", "ASC") + .getMany() : []; const orgChild3Ids = orgChild3Data.map((orgChild3) => orgChild3.id) || null; const orgChild4Data = orgChild3Ids && orgChild3Ids.length > 0 ? await AppDataSource.getRepository(OrgChild4) - .createQueryBuilder("orgChild4") - .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) - .andWhere( - _data.child4 != undefined && _data.child4 != null - ? _data.child4[0] != null - ? `orgChild4.id IN (:...node)` - : `orgChild4.id is null` - : "1=1", - { - node: _data.child4, - }, - ) - .select([ - "orgChild4.id", - "orgChild4.ancestorDNA", - "orgChild4.orgChild4Name", - "orgChild4.orgChild4ShortName", - "orgChild4.orgChild4Code", - "orgChild4.orgChild4Order", - "orgChild4.orgChild4PhoneEx", - "orgChild4.orgChild4PhoneIn", - "orgChild4.orgChild4Fax", - "orgChild4.orgRootId", - "orgChild4.orgChild4Rank", - "orgChild4.orgChild4RankSub", - "orgChild4.DEPARTMENT_CODE", - "orgChild4.DIVISION_CODE", - "orgChild4.SECTION_CODE", - "orgChild4.JOB_CODE", - "orgChild4.orgChild3Id", - "orgChild4.responsibility", - ]) - .orderBy("orgChild4.orgChild4Order", "ASC") - .getMany() + .createQueryBuilder("orgChild4") + .where("orgChild4.orgChild3Id IN (:...ids)", { ids: orgChild3Ids }) + .andWhere( + _data.child4 != undefined && _data.child4 != null + ? _data.child4[0] != null + ? `orgChild4.id IN (:...node)` + : `orgChild4.id is null` + : "1=1", + { + node: _data.child4, + }, + ) + .select([ + "orgChild4.id", + "orgChild4.ancestorDNA", + "orgChild4.orgChild4Name", + "orgChild4.orgChild4ShortName", + "orgChild4.orgChild4Code", + "orgChild4.orgChild4Order", + "orgChild4.orgChild4PhoneEx", + "orgChild4.orgChild4PhoneIn", + "orgChild4.orgChild4Fax", + "orgChild4.orgRootId", + "orgChild4.orgChild4Rank", + "orgChild4.orgChild4RankSub", + "orgChild4.DEPARTMENT_CODE", + "orgChild4.DIVISION_CODE", + "orgChild4.SECTION_CODE", + "orgChild4.JOB_CODE", + "orgChild4.orgChild3Id", + "orgChild4.responsibility", + ]) + .orderBy("orgChild4.orgChild4Order", "ASC") + .getMany() : []; // const formattedData = orgRootData.map((orgRoot) => { @@ -8350,13 +8350,13 @@ export class OrganizationController extends Controller { profileId: null, shortName: pos ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null + pos.orgChild4?.orgChild4ShortName, + pos.orgChild3?.orgChild3ShortName, + pos.orgChild2?.orgChild2ShortName, + pos.orgChild1?.orgChild1ShortName, + pos.orgRoot?.orgRootShortName, + ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? + null : null, posMasterNoPrefix: pos.posMasterNoPrefix ?? null, posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, @@ -8425,13 +8425,13 @@ export class OrganizationController extends Controller { profileId: null, shortName: pos ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null + pos.orgChild4?.orgChild4ShortName, + pos.orgChild3?.orgChild3ShortName, + pos.orgChild2?.orgChild2ShortName, + pos.orgChild1?.orgChild1ShortName, + pos.orgRoot?.orgRootShortName, + ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? + null : null, posMasterNoPrefix: pos.posMasterNoPrefix ?? null, posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, @@ -8499,13 +8499,13 @@ export class OrganizationController extends Controller { profileId: null, shortName: pos ? [ - pos.orgChild4?.orgChild4ShortName, - pos.orgChild3?.orgChild3ShortName, - pos.orgChild2?.orgChild2ShortName, - pos.orgChild1?.orgChild1ShortName, - pos.orgRoot?.orgRootShortName, - ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? - null + pos.orgChild4?.orgChild4ShortName, + pos.orgChild3?.orgChild3ShortName, + pos.orgChild2?.orgChild2ShortName, + pos.orgChild1?.orgChild1ShortName, + pos.orgRoot?.orgRootShortName, + ].find((s: string | undefined) => typeof s === "string" && s.trim().length > 0) ?? + null : null, posMasterNoPrefix: pos.posMasterNoPrefix ?? null, posMasterNo: pos.posMasterNo != null ? String(pos.posMasterNo) : null, @@ -8651,14 +8651,14 @@ export class OrganizationController extends Controller { profileId: null, shortName: pmWithRelations ? [ - pmWithRelations.orgChild4?.orgChild4ShortName, - pmWithRelations.orgChild3?.orgChild3ShortName, - pmWithRelations.orgChild2?.orgChild2ShortName, - pmWithRelations.orgChild1?.orgChild1ShortName, - pmWithRelations.orgRoot?.orgRootShortName, - ].find( - (s: string | undefined) => typeof s === "string" && s.trim().length > 0, - ) ?? null + pmWithRelations.orgChild4?.orgChild4ShortName, + pmWithRelations.orgChild3?.orgChild3ShortName, + pmWithRelations.orgChild2?.orgChild2ShortName, + pmWithRelations.orgChild1?.orgChild1ShortName, + pmWithRelations.orgRoot?.orgRootShortName, + ].find( + (s: string | undefined) => typeof s === "string" && s.trim().length > 0, + ) ?? null : null, posMasterNoPrefix: draftPos.posMasterNoPrefix ?? null, posMasterNo: draftPos.posMasterNo != null ? String(draftPos.posMasterNo) : null, @@ -8703,40 +8703,43 @@ export class OrganizationController extends Controller { await queryRunner.commitTransaction(); - // Clear Redis cache after successful publish - const redis = require("redis"); - const { promisify } = require("util"); - const redisClient = redis.createClient({ - host: process.env.REDIS_HOST || "localhost", - port: parseInt(process.env.REDIS_PORT || "6379"), - }); - const keysAsync = promisify(redisClient.keys).bind(redisClient); - const delAsync = promisify(redisClient.del).bind(redisClient); + // Clear Redis cache after successful publish (only menu and role for menu display) + // const redis = require("redis"); + // const { promisify } = require("util"); + // const redisClient = redis.createClient({ + // host: process.env.REDIS_HOST || "localhost", + // port: parseInt(process.env.REDIS_PORT || "6379"), + // }); + // const keysAsync = promisify(redisClient.keys).bind(redisClient); + // const delAsync = promisify(redisClient.del).bind(redisClient); - try { - const [posMasterKeys, userKeys, orgKeys, menuKeys, roleKeys] = await Promise.all([ - keysAsync("posMaster_*"), - keysAsync("user_*"), - keysAsync("org_*"), - keysAsync("menu_*"), - keysAsync("role_*"), - ]); + // try { + // // Clear only menu and role cache (affects menu display) + // const menuRolePatterns = ["menu_*", "role_*"]; + // let totalCleared = 0; - if (posMasterKeys.length > 0) await delAsync(...posMasterKeys); - if (userKeys.length > 0) await delAsync(...userKeys); - if (orgKeys.length > 0) await delAsync(...orgKeys); - if (menuKeys.length > 0) await delAsync(...menuKeys); - if (roleKeys.length > 0) await delAsync(...roleKeys); + // for (const pattern of menuRolePatterns) { + // const keys = await keysAsync(pattern); + // if (keys.length > 0) { + // // Delete in chunks of 1000 to avoid argument limit + // const chunkSize = 1000; + // for (let i = 0; i < keys.length; i += chunkSize) { + // const chunk = keys.slice(i, i + chunkSize); + // await delAsync(...chunk); + // } + // totalCleared += keys.length; + // console.log(`[moveDraftToCurrent] Cleared ${keys.length} cache keys for pattern: ${pattern}`); + // } + // } + // console.log(`[moveDraftToCurrent] Total cache cleared: ${totalCleared} keys`); + // } catch (err) { + // console.error("[moveDraftToCurrent] Error clearing cache:", err); + // } finally { + // redisClient.quit(); + // } - console.log(`[moveDraftToCurrent] Cleared cache: posMaster=${posMasterKeys.length}, user=${userKeys.length}, org=${orgKeys.length}, menu=${menuKeys.length}, role=${roleKeys.length}`); - } catch (err) { - console.error("[moveDraftToCurrent] Error clearing cache:", err); - } finally { - redisClient.quit(); - } - - // Invalidate memory cache - orgStructureCache.invalidate(currentRevisionId); + // // Invalidate memory cache + // orgStructureCache.invalidate(currentRevisionId); return new HttpSuccess(summary); } catch (error) { @@ -9168,7 +9171,7 @@ export class OrganizationController extends Controller { org: draftPosMaster ? getOrgFullName(draftPosMaster as PosMaster) ?? _null : _null, }); } - + if (nextHolderId != null && draftPos.positionIsSelected) { // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (!draftPosMaster?.isSit) { @@ -9251,12 +9254,12 @@ export class OrganizationController extends Controller { child4DnaId: posMaster?.orgChild4?.ancestorDNA ?? null, shortName: posMaster ? [ - posMaster.orgChild4?.orgChild4ShortName, - posMaster.orgChild3?.orgChild3ShortName, - posMaster.orgChild2?.orgChild2ShortName, - posMaster.orgChild1?.orgChild1ShortName, - posMaster.orgRoot?.orgRootShortName, - ].find((s) => typeof s === "string" && s.trim().length > 0) ?? null + posMaster.orgChild4?.orgChild4ShortName, + posMaster.orgChild3?.orgChild3ShortName, + posMaster.orgChild2?.orgChild2ShortName, + posMaster.orgChild1?.orgChild1ShortName, + posMaster.orgRoot?.orgRootShortName, + ].find((s) => typeof s === "string" && s.trim().length > 0) ?? null : null, posMasterNoPrefix: posMaster?.posMasterNoPrefix ?? null, posMasterNo: posMaster?.posMasterNo != null ? String(posMaster.posMasterNo) : null, diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 7705593b..b9668904 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1737,6 +1737,7 @@ async function handler_org(msg: amqp.ConsumeMessage): Promise { } async function clearMenuAndRoleCache(): Promise { + console.log("[AMQ] clearMenuAndRoleCache: Starting..."); const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, @@ -1746,35 +1747,28 @@ async function clearMenuAndRoleCache(): Promise { const delAsync = promisify(redisClient.del).bind(redisClient); try { - const menuKeys = await keysAsync("menu_*"); - if (menuKeys.length > 0) { - await delAsync(...menuKeys); - console.log(`[AMQ] Cleared ${menuKeys.length} menu cache keys`); - } + // Clear menu and role cache (patterns that affect menu display) + const menuRolePatterns = ["menu_*", "role_*"]; - const roleKeys = await keysAsync("role_*"); - if (roleKeys.length > 0) { - await delAsync(...roleKeys); - console.log(`[AMQ] Cleared ${roleKeys.length} role cache keys`); - } - - const posMasterKeys = await keysAsync("posMaster_*"); - if (posMasterKeys.length > 0) { - await delAsync(...posMasterKeys); - console.log(`[AMQ] Cleared ${posMasterKeys.length} posMaster cache keys`); - } - - const userKeys = await keysAsync("user_*"); - if (userKeys.length > 0) { - await delAsync(...userKeys); - console.log(`[AMQ] Cleared ${userKeys.length} user cache keys`); - } - - const orgKeys = await keysAsync("org_*"); - if (orgKeys.length > 0) { - await delAsync(...orgKeys); - console.log(`[AMQ] Cleared ${orgKeys.length} org cache keys`); + for (const pattern of menuRolePatterns) { + console.log(`[AMQ] Checking pattern: ${pattern}`); + const keys = await keysAsync(pattern); + console.log(`[AMQ] Found ${keys.length} keys for pattern: ${pattern}`); + if (keys.length > 0) { + // Delete in chunks of 1000 to avoid argument limit + const chunkSize = 1000; + for (let i = 0; i < keys.length; i += chunkSize) { + const chunk = keys.slice(i, i + chunkSize); + await delAsync(...chunk); + } + console.log(`[AMQ] Cleared ${keys.length} cache keys for pattern: ${pattern}`); + } else { + console.log(`[AMQ] No keys found for pattern: ${pattern}`); + } } + console.log("[AMQ] clearMenuAndRoleCache: Completed successfully"); + } catch (error) { + console.error("[AMQ] clearMenuAndRoleCache ERROR:", error); } finally { redisClient.quit(); } From 7c77745d46a705bdc858e05d5672a782867bd69b Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 16 Jun 2026 09:53:23 +0700 Subject: [PATCH 14/39] migrate add field all tables #252 --- src/controllers/OrgChild1Controller.ts | 8 +++ src/controllers/OrgChild2Controller.ts | 8 +++ src/controllers/OrgChild3Controller.ts | 8 +++ src/controllers/OrgChild4Controller.ts | 8 +++ src/controllers/OrgRootController.ts | 8 +++ src/entities/OrgChild1.ts | 56 +++++++++++++++++++ src/entities/OrgChild2.ts | 56 +++++++++++++++++++ src/entities/OrgChild3.ts | 56 +++++++++++++++++++ src/entities/OrgChild4.ts | 56 +++++++++++++++++++ src/entities/OrgRoot.ts | 56 +++++++++++++++++++ ...597453-update_add_field_code_all_tables.ts | 51 +++++++++++++++++ 11 files changed, 371 insertions(+) create mode 100644 src/migration/1781577597453-update_add_field_code_all_tables.ts diff --git a/src/controllers/OrgChild1Controller.ts b/src/controllers/OrgChild1Controller.ts index d59ce669..6817d6fd 100644 --- a/src/controllers/OrgChild1Controller.ts +++ b/src/controllers/OrgChild1Controller.ts @@ -74,7 +74,11 @@ export class OrgChild1Controller { DIVISION_CODE: orgChild1.DIVISION_CODE, SECTION_CODE: orgChild1.SECTION_CODE, JOB_CODE: orgChild1.JOB_CODE, + ROOT_CODE: orgChild1.ROOT_CODE, CHILD1_CODE: orgChild1.CHILD1_CODE, + CHILD2_CODE: orgChild1.CHILD2_CODE, + CHILD3_CODE: orgChild1.CHILD3_CODE, + CHILD4_CODE: orgChild1.CHILD4_CODE, orgCode: orgChild1.orgRoot.orgRootCode + orgChild1.orgChild1Code, }; return new HttpSuccess(getOrgChild1); @@ -347,7 +351,11 @@ export class OrgChild1Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, + CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, + CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, + CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, isOfficer: requestBody.isOfficer, isInformation: requestBody.isInformation, orgChild1PhoneEx: requestBody.orgChild1PhoneEx, diff --git a/src/controllers/OrgChild2Controller.ts b/src/controllers/OrgChild2Controller.ts index 1cc4186d..650cc96e 100644 --- a/src/controllers/OrgChild2Controller.ts +++ b/src/controllers/OrgChild2Controller.ts @@ -85,7 +85,11 @@ export class OrgChild2Controller extends Controller { DIVISION_CODE: orgChild2.DIVISION_CODE, SECTION_CODE: orgChild2.SECTION_CODE, JOB_CODE: orgChild2.JOB_CODE, + ROOT_CODE: orgChild2.ROOT_CODE, + CHILD1_CODE: orgChild2.CHILD1_CODE, CHILD2_CODE: orgChild2.CHILD2_CODE, + CHILD3_CODE: orgChild2.CHILD3_CODE, + CHILD4_CODE: orgChild2.CHILD4_CODE, orgCode: orgChild2.orgRoot.orgRootCode + orgChild2.orgChild2Code, }; return new HttpSuccess(getOrgChild2); @@ -253,7 +257,11 @@ export class OrgChild2Controller extends Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, + CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, + CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, + CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, orgChild2PhoneEx: requestBody.orgChild2PhoneEx, orgChild2PhoneIn: requestBody.orgChild2PhoneIn, orgChild2Fax: requestBody.orgChild2Fax, diff --git a/src/controllers/OrgChild3Controller.ts b/src/controllers/OrgChild3Controller.ts index a3861d55..d8cc5939 100644 --- a/src/controllers/OrgChild3Controller.ts +++ b/src/controllers/OrgChild3Controller.ts @@ -69,7 +69,11 @@ export class OrgChild3Controller { DIVISION_CODE: orgChild3.DIVISION_CODE, SECTION_CODE: orgChild3.SECTION_CODE, JOB_CODE: orgChild3.JOB_CODE, + ROOT_CODE: orgChild3.ROOT_CODE, + CHILD1_CODE: orgChild3.CHILD1_CODE, + CHILD2_CODE: orgChild3.CHILD2_CODE, CHILD3_CODE: orgChild3.CHILD3_CODE, + CHILD4_CODE: orgChild3.CHILD4_CODE, orgCode: orgChild3.orgRoot.orgRootCode + orgChild3.orgChild3Code, }; return new HttpSuccess(getOrgChild3); @@ -208,7 +212,11 @@ export class OrgChild3Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, + CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, + CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, + CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, orgChild3PhoneEx: requestBody.orgChild3PhoneEx, orgChild3PhoneIn: requestBody.orgChild3PhoneIn, orgChild3Fax: requestBody.orgChild3Fax, diff --git a/src/controllers/OrgChild4Controller.ts b/src/controllers/OrgChild4Controller.ts index eb044bbc..4dac9e82 100644 --- a/src/controllers/OrgChild4Controller.ts +++ b/src/controllers/OrgChild4Controller.ts @@ -82,6 +82,10 @@ export class OrgChild4Controller extends Controller { DIVISION_CODE: orgChild4.DIVISION_CODE, SECTION_CODE: orgChild4.SECTION_CODE, JOB_CODE: orgChild4.JOB_CODE, + ROOT_CODE: orgChild4.ROOT_CODE, + CHILD1_CODE: orgChild4.CHILD1_CODE, + CHILD2_CODE: orgChild4.CHILD2_CODE, + CHILD3_CODE: orgChild4.CHILD3_CODE, CHILD4_CODE: orgChild4.CHILD4_CODE, orgCode: orgChild4.orgRoot.orgRootCode + orgChild4.orgChild4Code, }; @@ -255,6 +259,10 @@ export class OrgChild4Controller extends Controller { DIVISION_CODE: requestBody.DIVISION_CODE != null ? requestBody.DIVISION_CODE : _null, SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, + ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, + CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, + CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, + CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, orgChild4PhoneEx: requestBody.orgChild4PhoneEx, orgChild4PhoneIn: requestBody.orgChild4PhoneIn, diff --git a/src/controllers/OrgRootController.ts b/src/controllers/OrgRootController.ts index 69c5bba8..97f3f6a3 100644 --- a/src/controllers/OrgRootController.ts +++ b/src/controllers/OrgRootController.ts @@ -84,6 +84,10 @@ export class OrgRootController extends Controller { SECTION_CODE: orgRoot.SECTION_CODE, JOB_CODE: orgRoot.JOB_CODE, ROOT_CODE: orgRoot.ROOT_CODE, + CHILD1_CODE: orgRoot.CHILD1_CODE, + CHILD2_CODE: orgRoot.CHILD2_CODE, + CHILD3_CODE: orgRoot.CHILD3_CODE, + CHILD4_CODE: orgRoot.CHILD4_CODE, orgCode: orgRoot.orgRootCode + "00", }; return new HttpSuccess(getOrgRoot); @@ -352,6 +356,10 @@ export class OrgRootController extends Controller { SECTION_CODE: requestBody.SECTION_CODE != null ? requestBody.SECTION_CODE : _null, JOB_CODE: requestBody.JOB_CODE != null ? requestBody.JOB_CODE : _null, ROOT_CODE: requestBody.ROOT_CODE != null ? requestBody.ROOT_CODE : _null, + CHILD1_CODE: requestBody.CHILD1_CODE != null ? requestBody.CHILD1_CODE : _null, + CHILD2_CODE: requestBody.CHILD2_CODE != null ? requestBody.CHILD2_CODE : _null, + CHILD3_CODE: requestBody.CHILD3_CODE != null ? requestBody.CHILD3_CODE : _null, + CHILD4_CODE: requestBody.CHILD4_CODE != null ? requestBody.CHILD4_CODE : _null, }); await this.orgRootRepository.save(orgRoot, { data: request }); setLogDataDiff(request, { before, after: orgRoot }); diff --git a/src/entities/OrgChild1.ts b/src/entities/OrgChild1.ts index b211acf8..ef2a9be6 100644 --- a/src/entities/OrgChild1.ts +++ b/src/entities/OrgChild1.ts @@ -174,6 +174,14 @@ export class OrgChild1 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "ROOT_CODE", + default: null, + }) + ROOT_CODE: string; + @Column({ nullable: true, length: 3, @@ -182,6 +190,30 @@ export class OrgChild1 extends EntityBase { }) CHILD1_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD2_CODE", + default: null, + }) + CHILD2_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD3_CODE", + default: null, + }) + CHILD3_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD4_CODE", + default: null, + }) + CHILD4_CODE: string; + @ManyToOne(() => OrgRoot, (orgRoot) => orgRoot.orgChild1s) @JoinColumn({ name: "orgRootId" }) orgRoot: OrgRoot; @@ -236,9 +268,21 @@ export class CreateOrgChild1 { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + @Column() CHILD1_CODE?: string | null; + @Column() + CHILD2_CODE?: string | null; + + @Column() + CHILD3_CODE?: string | null; + + @Column() + CHILD4_CODE?: string | null; + @Column() orgChild1PhoneEx?: string; @@ -294,9 +338,21 @@ export class UpdateOrgChild1 { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + @Column() CHILD1_CODE?: string | null; + @Column() + CHILD2_CODE?: string | null; + + @Column() + CHILD3_CODE?: string | null; + + @Column() + CHILD4_CODE?: string | null; + @Column() orgChild1PhoneEx?: string; diff --git a/src/entities/OrgChild2.ts b/src/entities/OrgChild2.ts index 1abf8b61..50f40769 100644 --- a/src/entities/OrgChild2.ts +++ b/src/entities/OrgChild2.ts @@ -146,6 +146,22 @@ export class OrgChild2 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "ROOT_CODE", + default: null, + }) + ROOT_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD1_CODE", + default: null, + }) + CHILD1_CODE: string; + @Column({ nullable: true, length: 3, @@ -154,6 +170,22 @@ export class OrgChild2 extends EntityBase { }) CHILD2_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD3_CODE", + default: null, + }) + CHILD3_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD4_CODE", + default: null, + }) + CHILD4_CODE: string; + @Column({ nullable: true, length: 40, @@ -230,9 +262,21 @@ export class CreateOrgChild2 { @Column() JOB_CODE?: string; + @Column() + ROOT_CODE?: string; + + @Column() + CHILD1_CODE?: string; + @Column() CHILD2_CODE?: string; + @Column() + CHILD3_CODE?: string; + + @Column() + CHILD4_CODE?: string; + @Column() orgChild2PhoneEx?: string; @@ -280,9 +324,21 @@ export class UpdateOrgChild2 { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + + @Column() + CHILD1_CODE?: string | null; + @Column() CHILD2_CODE?: string | null; + @Column() + CHILD3_CODE?: string | null; + + @Column() + CHILD4_CODE?: string | null; + @Column() orgChild2PhoneEx?: string; diff --git a/src/entities/OrgChild3.ts b/src/entities/OrgChild3.ts index 6fbc1e72..72a29336 100644 --- a/src/entities/OrgChild3.ts +++ b/src/entities/OrgChild3.ts @@ -153,6 +153,30 @@ export class OrgChild3 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "ROOT_CODE", + default: null, + }) + ROOT_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD1_CODE", + default: null, + }) + CHILD1_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD2_CODE", + default: null, + }) + CHILD2_CODE: string; + @Column({ nullable: true, length: 3, @@ -161,6 +185,14 @@ export class OrgChild3 extends EntityBase { }) CHILD3_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD4_CODE", + default: null, + }) + CHILD4_CODE: string; + @Column({ nullable: true, length: 40, @@ -238,9 +270,21 @@ export class CreateOrgChild3 { @Column() JOB_CODE?: string; + @Column() + ROOT_CODE?: string; + + @Column() + CHILD1_CODE?: string; + + @Column() + CHILD2_CODE?: string; + @Column() CHILD3_CODE?: string; + @Column() + CHILD4_CODE?: string; + @Column() orgChild3PhoneEx?: string; @@ -290,9 +334,21 @@ export class UpdateOrgChild3 { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + + @Column() + CHILD1_CODE?: string | null; + + @Column() + CHILD2_CODE?: string | null; + @Column() CHILD3_CODE?: string | null; + @Column() + CHILD4_CODE?: string | null; + @Column() orgChild3PhoneEx?: string; diff --git a/src/entities/OrgChild4.ts b/src/entities/OrgChild4.ts index d0a28a27..4f053b00 100644 --- a/src/entities/OrgChild4.ts +++ b/src/entities/OrgChild4.ts @@ -158,6 +158,38 @@ export class OrgChild4 extends EntityBase { }) JOB_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "ROOT_CODE", + default: null, + }) + ROOT_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD1_CODE", + default: null, + }) + CHILD1_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD2_CODE", + default: null, + }) + CHILD2_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD3_CODE", + default: null, + }) + CHILD3_CODE: string; + @Column({ nullable: true, length: 3, @@ -244,6 +276,18 @@ export class CreateOrgChild4 { @Column() JOB_CODE?: string; + @Column() + ROOT_CODE?: string; + + @Column() + CHILD1_CODE?: string; + + @Column() + CHILD2_CODE?: string; + + @Column() + CHILD3_CODE?: string; + @Column() CHILD4_CODE?: string; @@ -294,6 +338,18 @@ export class UpdateOrgChild4 { @Column() JOB_CODE?: string | null; + @Column() + ROOT_CODE?: string | null; + + @Column() + CHILD1_CODE?: string | null; + + @Column() + CHILD2_CODE?: string | null; + + @Column() + CHILD3_CODE?: string | null; + @Column() CHILD4_CODE?: string | null; diff --git a/src/entities/OrgRoot.ts b/src/entities/OrgRoot.ts index d474a1fe..02e22bb8 100644 --- a/src/entities/OrgRoot.ts +++ b/src/entities/OrgRoot.ts @@ -174,6 +174,38 @@ export class OrgRoot extends EntityBase { }) ROOT_CODE: string; + @Column({ + nullable: true, + length: 3, + comment: "CHILD1_CODE", + default: null, + }) + CHILD1_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD2_CODE", + default: null, + }) + CHILD2_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD3_CODE", + default: null, + }) + CHILD3_CODE: string; + + @Column({ + nullable: true, + length: 3, + comment: "CHILD4_CODE", + default: null, + }) + CHILD4_CODE: string; + @ManyToOne(() => OrgRevision, (orgRevision) => orgRevision.orgRoots) @JoinColumn({ name: "orgRevisionId" }) orgRevision: OrgRevision; @@ -237,6 +269,18 @@ export class CreateOrgRoot { @Column() ROOT_CODE?: string; + @Column() + CHILD1_CODE?: string; + + @Column() + CHILD2_CODE?: string; + + @Column() + CHILD3_CODE?: string; + + @Column() + CHILD4_CODE?: string; + @Column() orgRootPhoneEx?: string; @@ -295,6 +339,18 @@ export class UpdateOrgRoot { @Column() ROOT_CODE?: string | null; + @Column() + CHILD1_CODE?: string | null; + + @Column() + CHILD2_CODE?: string | null; + + @Column() + CHILD3_CODE?: string | null; + + @Column() + CHILD4_CODE?: string | null; + @Column() orgRootPhoneEx?: string; diff --git a/src/migration/1781577597453-update_add_field_code_all_tables.ts b/src/migration/1781577597453-update_add_field_code_all_tables.ts new file mode 100644 index 00000000..39ea6466 --- /dev/null +++ b/src/migration/1781577597453-update_add_field_code_all_tables.ts @@ -0,0 +1,51 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UpdateAddFieldCodeAllTables1781577597453 implements MigrationInterface { + name = 'UpdateAddFieldCodeAllTables1781577597453' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`orgChild4\` ADD \`ROOT_CODE\` varchar(3) NULL COMMENT 'ROOT_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild4\` ADD \`CHILD1_CODE\` varchar(3) NULL COMMENT 'CHILD1_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild4\` ADD \`CHILD2_CODE\` varchar(3) NULL COMMENT 'CHILD2_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild4\` ADD \`CHILD3_CODE\` varchar(3) NULL COMMENT 'CHILD3_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild3\` ADD \`ROOT_CODE\` varchar(3) NULL COMMENT 'ROOT_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild3\` ADD \`CHILD1_CODE\` varchar(3) NULL COMMENT 'CHILD1_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild3\` ADD \`CHILD2_CODE\` varchar(3) NULL COMMENT 'CHILD2_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild3\` ADD \`CHILD4_CODE\` varchar(3) NULL COMMENT 'CHILD4_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild2\` ADD \`ROOT_CODE\` varchar(3) NULL COMMENT 'ROOT_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild2\` ADD \`CHILD1_CODE\` varchar(3) NULL COMMENT 'CHILD1_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild2\` ADD \`CHILD3_CODE\` varchar(3) NULL COMMENT 'CHILD3_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild2\` ADD \`CHILD4_CODE\` varchar(3) NULL COMMENT 'CHILD4_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild1\` ADD \`ROOT_CODE\` varchar(3) NULL COMMENT 'ROOT_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild1\` ADD \`CHILD2_CODE\` varchar(3) NULL COMMENT 'CHILD2_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild1\` ADD \`CHILD3_CODE\` varchar(3) NULL COMMENT 'CHILD3_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgChild1\` ADD \`CHILD4_CODE\` varchar(3) NULL COMMENT 'CHILD4_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgRoot\` ADD \`CHILD1_CODE\` varchar(3) NULL COMMENT 'CHILD1_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgRoot\` ADD \`CHILD2_CODE\` varchar(3) NULL COMMENT 'CHILD2_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgRoot\` ADD \`CHILD3_CODE\` varchar(3) NULL COMMENT 'CHILD3_CODE'`); + await queryRunner.query(`ALTER TABLE \`orgRoot\` ADD \`CHILD4_CODE\` varchar(3) NULL COMMENT 'CHILD4_CODE'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE \`orgRoot\` DROP COLUMN \`CHILD4_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgRoot\` DROP COLUMN \`CHILD3_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgRoot\` DROP COLUMN \`CHILD2_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgRoot\` DROP COLUMN \`CHILD1_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild1\` DROP COLUMN \`CHILD4_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild1\` DROP COLUMN \`CHILD3_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild1\` DROP COLUMN \`CHILD2_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild1\` DROP COLUMN \`ROOT_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild2\` DROP COLUMN \`CHILD4_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild2\` DROP COLUMN \`CHILD3_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild2\` DROP COLUMN \`CHILD1_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild2\` DROP COLUMN \`ROOT_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild3\` DROP COLUMN \`CHILD4_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild3\` DROP COLUMN \`CHILD2_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild3\` DROP COLUMN \`CHILD1_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild3\` DROP COLUMN \`ROOT_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild4\` DROP COLUMN \`CHILD3_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild4\` DROP COLUMN \`CHILD2_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild4\` DROP COLUMN \`CHILD1_CODE\``); + await queryRunner.query(`ALTER TABLE \`orgChild4\` DROP COLUMN \`ROOT_CODE\``); + } +} From e84f93f6db076a432d9082942824d95531d81b85 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 16 Jun 2026 10:25:45 +0700 Subject: [PATCH 15/39] fix #252 --- src/controllers/OrganizationController.ts | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 096c7111..7063964a 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1747,6 +1747,11 @@ export class OrganizationController extends Controller { "orgRoot.DIVISION_CODE", "orgRoot.SECTION_CODE", "orgRoot.JOB_CODE", + "orgRoot.ROOT_CODE", + "orgRoot.CHILD1_CODE", + "orgRoot.CHILD2_CODE", + "orgRoot.CHILD3_CODE", + "orgRoot.CHILD4_CODE", "orgRoot.responsibility", ]) .orderBy("orgRoot.orgRootOrder", "ASC") @@ -1786,6 +1791,11 @@ export class OrganizationController extends Controller { "orgChild1.DIVISION_CODE", "orgChild1.SECTION_CODE", "orgChild1.JOB_CODE", + "orgChild1.ROOT_CODE", + "orgChild1.CHILD1_CODE", + "orgChild1.CHILD2_CODE", + "orgChild1.CHILD3_CODE", + "orgChild1.CHILD4_CODE", "orgChild1.responsibility", ]) .orderBy("orgChild1.orgChild1Order", "ASC") @@ -1825,6 +1835,11 @@ export class OrganizationController extends Controller { "orgChild2.DIVISION_CODE", "orgChild2.SECTION_CODE", "orgChild2.JOB_CODE", + "orgChild2.ROOT_CODE", + "orgChild2.CHILD1_CODE", + "orgChild2.CHILD2_CODE", + "orgChild2.CHILD3_CODE", + "orgChild2.CHILD4_CODE", "orgChild2.orgChild1Id", "orgChild2.responsibility", ]) @@ -1865,6 +1880,11 @@ export class OrganizationController extends Controller { "orgChild3.DIVISION_CODE", "orgChild3.SECTION_CODE", "orgChild3.JOB_CODE", + "orgChild3.ROOT_CODE", + "orgChild3.CHILD1_CODE", + "orgChild3.CHILD2_CODE", + "orgChild3.CHILD3_CODE", + "orgChild3.CHILD4_CODE", "orgChild3.orgChild2Id", "orgChild3.responsibility", ]) @@ -1905,6 +1925,11 @@ export class OrganizationController extends Controller { "orgChild4.DIVISION_CODE", "orgChild4.SECTION_CODE", "orgChild4.JOB_CODE", + "orgChild4.ROOT_CODE", + "orgChild4.CHILD1_CODE", + "orgChild4.CHILD2_CODE", + "orgChild4.CHILD3_CODE", + "orgChild4.CHILD4_CODE", "orgChild4.orgChild3Id", "orgChild4.responsibility", ]) @@ -1930,6 +1955,11 @@ export class OrganizationController extends Controller { DIVISION_CODE: orgRoot.DIVISION_CODE, SECTION_CODE: orgRoot.SECTION_CODE, JOB_CODE: orgRoot.JOB_CODE, + ROOT_CODE: orgRoot.ROOT_CODE, + CHILD1_CODE: orgRoot.CHILD1_CODE, + CHILD2_CODE: orgRoot.CHILD2_CODE, + CHILD3_CODE: orgRoot.CHILD3_CODE, + CHILD4_CODE: orgRoot.CHILD4_CODE, orgTreeOrder: orgRoot.orgRootOrder, orgTreePhoneEx: orgRoot.orgRootPhoneEx, orgTreePhoneIn: orgRoot.orgRootPhoneIn, @@ -1961,6 +1991,11 @@ export class OrganizationController extends Controller { DIVISION_CODE: orgChild1.DIVISION_CODE, SECTION_CODE: orgChild1.SECTION_CODE, JOB_CODE: orgChild1.JOB_CODE, + ROOT_CODE: orgChild1.ROOT_CODE, + CHILD1_CODE: orgChild1.CHILD1_CODE, + CHILD2_CODE: orgChild1.CHILD2_CODE, + CHILD3_CODE: orgChild1.CHILD3_CODE, + CHILD4_CODE: orgChild1.CHILD4_CODE, orgTreeOrder: orgChild1.orgChild1Order, orgRootCode: orgRoot.orgRootCode, orgTreePhoneEx: orgChild1.orgChild1PhoneEx, @@ -2005,6 +2040,11 @@ export class OrganizationController extends Controller { DIVISION_CODE: orgChild2.DIVISION_CODE, SECTION_CODE: orgChild2.SECTION_CODE, JOB_CODE: orgChild2.JOB_CODE, + ROOT_CODE: orgChild2.ROOT_CODE, + CHILD1_CODE: orgChild2.CHILD1_CODE, + CHILD2_CODE: orgChild2.CHILD2_CODE, + CHILD3_CODE: orgChild2.CHILD3_CODE, + CHILD4_CODE: orgChild2.CHILD4_CODE, orgTreeOrder: orgChild2.orgChild2Order, orgRootCode: orgRoot.orgRootCode, orgTreePhoneEx: orgChild2.orgChild2PhoneEx, @@ -2054,6 +2094,11 @@ export class OrganizationController extends Controller { DIVISION_CODE: orgChild3.DIVISION_CODE, SECTION_CODE: orgChild3.SECTION_CODE, JOB_CODE: orgChild3.JOB_CODE, + ROOT_CODE: orgChild3.ROOT_CODE, + CHILD1_CODE: orgChild3.CHILD1_CODE, + CHILD2_CODE: orgChild3.CHILD2_CODE, + CHILD3_CODE: orgChild3.CHILD3_CODE, + CHILD4_CODE: orgChild3.CHILD4_CODE, orgTreeOrder: orgChild3.orgChild3Order, orgRootCode: orgRoot.orgRootCode, orgTreePhoneEx: orgChild3.orgChild3PhoneEx, @@ -2110,6 +2155,11 @@ export class OrganizationController extends Controller { DIVISION_CODE: orgChild4.DIVISION_CODE, SECTION_CODE: orgChild4.SECTION_CODE, JOB_CODE: orgChild4.JOB_CODE, + ROOT_CODE: orgChild4.ROOT_CODE, + CHILD1_CODE: orgChild4.CHILD1_CODE, + CHILD2_CODE: orgChild4.CHILD2_CODE, + CHILD3_CODE: orgChild4.CHILD3_CODE, + CHILD4_CODE: orgChild4.CHILD4_CODE, orgTreeOrder: orgChild4.orgChild4Order, orgRootCode: orgRoot.orgRootCode, orgTreePhoneEx: orgChild4.orgChild4PhoneEx, From 61e4bcdf03808d09a9f1eec2ed0c85a9de7facf3 Mon Sep 17 00:00:00 2001 From: Adisak Date: Tue, 16 Jun 2026 13:58:16 +0700 Subject: [PATCH 16/39] =?UTF-8?q?test=20clear=20redis=20=E0=B9=80=E0=B8=9C?= =?UTF-8?q?=E0=B8=A2=E0=B9=81=E0=B8=9E=E0=B8=A3=E0=B9=88=E0=B8=A2=E0=B9=88?= =?UTF-8?q?=E0=B8=AD=E0=B8=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrganizationController.ts | 66 +++++++++++------------ 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 096c7111..860ef2c6 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8704,42 +8704,42 @@ export class OrganizationController extends Controller { await queryRunner.commitTransaction(); // Clear Redis cache after successful publish (only menu and role for menu display) - // const redis = require("redis"); - // const { promisify } = require("util"); - // const redisClient = redis.createClient({ - // host: process.env.REDIS_HOST || "localhost", - // port: parseInt(process.env.REDIS_PORT || "6379"), - // }); - // const keysAsync = promisify(redisClient.keys).bind(redisClient); - // const delAsync = promisify(redisClient.del).bind(redisClient); + const redis = require("redis"); + const { promisify } = require("util"); + const redisClient = redis.createClient({ + host: process.env.REDIS_HOST || "localhost", + port: parseInt(process.env.REDIS_PORT || "6379"), + }); + const keysAsync = promisify(redisClient.keys).bind(redisClient); + const delAsync = promisify(redisClient.del).bind(redisClient); - // try { - // // Clear only menu and role cache (affects menu display) - // const menuRolePatterns = ["menu_*", "role_*"]; - // let totalCleared = 0; + try { + // Clear only menu and role cache (affects menu display) + const menuRolePatterns = ["menu_*", "role_*"]; + let totalCleared = 0; - // for (const pattern of menuRolePatterns) { - // const keys = await keysAsync(pattern); - // if (keys.length > 0) { - // // Delete in chunks of 1000 to avoid argument limit - // const chunkSize = 1000; - // for (let i = 0; i < keys.length; i += chunkSize) { - // const chunk = keys.slice(i, i + chunkSize); - // await delAsync(...chunk); - // } - // totalCleared += keys.length; - // console.log(`[moveDraftToCurrent] Cleared ${keys.length} cache keys for pattern: ${pattern}`); - // } - // } - // console.log(`[moveDraftToCurrent] Total cache cleared: ${totalCleared} keys`); - // } catch (err) { - // console.error("[moveDraftToCurrent] Error clearing cache:", err); - // } finally { - // redisClient.quit(); - // } + for (const pattern of menuRolePatterns) { + const keys = await keysAsync(pattern); + if (keys.length > 0) { + // Delete in chunks of 1000 to avoid argument limit + const chunkSize = 1000; + for (let i = 0; i < keys.length; i += chunkSize) { + const chunk = keys.slice(i, i + chunkSize); + await delAsync(...chunk); + } + totalCleared += keys.length; + console.log(`[moveDraftToCurrent] Cleared ${keys.length} cache keys for pattern: ${pattern}`); + } + } + console.log(`[moveDraftToCurrent] Total cache cleared: ${totalCleared} keys`); + } catch (err) { + console.error("[moveDraftToCurrent] Error clearing cache:", err); + } finally { + redisClient.quit(); + } - // // Invalidate memory cache - // orgStructureCache.invalidate(currentRevisionId); + // Invalidate memory cache + orgStructureCache.invalidate(currentRevisionId); return new HttpSuccess(summary); } catch (error) { From ca5b11e36b073d6729287244cd79a7aa31d8196d Mon Sep 17 00:00:00 2001 From: Adisak Date: Tue, 16 Jun 2026 15:24:35 +0700 Subject: [PATCH 17/39] fix #1584 --- src/controllers/OrganizationController.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index f976b5bb..55f5ac93 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8612,6 +8612,7 @@ export class OrganizationController extends Controller { orgChild2Id, orgChild3Id, orgChild4Id, + authRoleId: draftPos.authRoleId, current_holderId: draftPos.next_holderId, next_holderId: null, isSit: draftPos.isSit, From db36b250e3d798519ca939314d90b0097de5151b Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 16 Jun 2026 15:50:15 +0700 Subject: [PATCH 18/39] =?UTF-8?q?face=20=E0=B9=80=E0=B8=A5=E0=B8=B7?= =?UTF-8?q?=E0=B8=AD=E0=B8=81=E0=B8=AB=E0=B8=B1=E0=B8=A7=E0=B8=AB=E0=B8=99?= =?UTF-8?q?=E0=B9=89=E0=B8=B2=E0=B8=97=E0=B8=B1=E0=B9=89=E0=B8=87=E0=B8=AB?= =?UTF-8?q?=E0=B8=99=E0=B9=88=E0=B8=A7=E0=B8=A2=E0=B8=87=E0=B8=B2=E0=B8=99?= =?UTF-8?q?=20#1597?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/WorkflowController.ts | 73 +++++++++++++++------------ 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/src/controllers/WorkflowController.ts b/src/controllers/WorkflowController.ts index ae5c8859..d8aa0b77 100644 --- a/src/controllers/WorkflowController.ts +++ b/src/controllers/WorkflowController.ts @@ -865,6 +865,7 @@ export class WorkflowController extends Controller { type?: string | null; sortBy?: string | null; descending?: boolean; + isAllDirector?: boolean; }, ) { const userKeycloak = body.keycloakId ?? request.user.sub; @@ -953,38 +954,46 @@ export class WorkflowController extends Controller { let mainConditions: any[] = []; if (type.trim().toUpperCase() === "OPERATE" || body.type === "employee") { - mainConditions = [ - { ...baseCondition, orgRootId: In(roodIds), orgChild1Id: IsNull() }, - { - ...baseCondition, - orgRootId: In(roodIds), - orgChild1Id: posMasterUser.orgChild1Id, - orgChild2Id: IsNull(), - }, - { - ...baseCondition, - orgRootId: In(roodIds), - orgChild1Id: posMasterUser.orgChild1Id, - orgChild2Id: posMasterUser.orgChild2Id, - orgChild3Id: IsNull(), - }, - { - ...baseCondition, - orgRootId: In(roodIds), - orgChild1Id: posMasterUser.orgChild1Id, - orgChild2Id: posMasterUser.orgChild2Id, - orgChild3Id: posMasterUser.orgChild3Id, - orgChild4Id: IsNull(), - }, - { - ...baseCondition, - orgRootId: In(roodIds), - orgChild1Id: posMasterUser.orgChild1Id, - orgChild2Id: posMasterUser.orgChild2Id, - orgChild3Id: posMasterUser.orgChild3Id, - orgChild4Id: posMasterUser.orgChild4Id, - }, - ]; + + if (body.isAllDirector === true) { + mainConditions = [ + { ...baseCondition, orgRootId: In(roodIds) } + ]; + } + else { + mainConditions = [ + { ...baseCondition, orgRootId: In(roodIds), orgChild1Id: IsNull() }, + { + ...baseCondition, + orgRootId: In(roodIds), + orgChild1Id: posMasterUser.orgChild1Id, + orgChild2Id: IsNull(), + }, + { + ...baseCondition, + orgRootId: In(roodIds), + orgChild1Id: posMasterUser.orgChild1Id, + orgChild2Id: posMasterUser.orgChild2Id, + orgChild3Id: IsNull(), + }, + { + ...baseCondition, + orgRootId: In(roodIds), + orgChild1Id: posMasterUser.orgChild1Id, + orgChild2Id: posMasterUser.orgChild2Id, + orgChild3Id: posMasterUser.orgChild3Id, + orgChild4Id: IsNull(), + }, + { + ...baseCondition, + orgRootId: In(roodIds), + orgChild1Id: posMasterUser.orgChild1Id, + orgChild2Id: posMasterUser.orgChild2Id, + orgChild3Id: posMasterUser.orgChild3Id, + orgChild4Id: posMasterUser.orgChild4Id, + }, + ]; + } } else if (isLowLevel) { mainConditions = [ { From e6bdea2b206013f80506a2e03aa243e5e052ff7b Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 17 Jun 2026 15:42:48 +0700 Subject: [PATCH 19/39] =?UTF-8?q?=E0=B8=AD=E0=B8=B1=E0=B8=95=E0=B8=A3?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=E0=B8=B3=E0=B8=A5=E0=B8=B1=E0=B8=87=E0=B8=A5?= =?UTF-8?q?=E0=B8=B9=E0=B8=81=E0=B8=88=E0=B9=89=E0=B8=B2=E0=B8=87=E0=B8=9B?= =?UTF-8?q?=E0=B8=A3=E0=B8=B0=E0=B8=88=E0=B8=B3=20=E0=B8=81=E0=B8=A3?= =?UTF-8?q?=E0=B8=93=E0=B8=B5=E0=B8=99=E0=B8=B1=E0=B9=88=E0=B8=87=E0=B8=97?= =?UTF-8?q?=E0=B8=B1=E0=B8=9A=E0=B8=97=E0=B8=B5=E0=B9=88=E0=B8=95=E0=B9=89?= =?UTF-8?q?=E0=B8=AD=E0=B8=87=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=E0=B8=95?= =?UTF-8?q?=E0=B8=B3=E0=B9=81=E0=B8=AB=E0=B8=99=E0=B9=88=E0=B8=87=E0=B8=88?= =?UTF-8?q?=E0=B8=A3=E0=B8=B4=E0=B8=87=20#2563?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/PositionService.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index c242c184..2b0cc324 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -12,6 +12,7 @@ import { Position } from "../entities/Position"; import { ProfileEducation } from "../entities/ProfileEducation"; import { RequestWithUser } from "../middlewares/user"; import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; /** * function สำหรับดึงตำแหน่งที่รักษาการแทน @@ -221,6 +222,7 @@ export async function CreatePosMasterHistoryEmployee( await AppDataSource.transaction(async (manager) => { const repoPosmaster = manager.getRepository(EmployeePosMaster); const repoHistory = manager.getRepository(PosMasterEmployeeHistory); + const repoProfileEmployee = manager.getRepository(ProfileEmployee); const pm = await repoPosmaster.findOne({ where: { id: posMasterId }, @@ -245,15 +247,28 @@ export async function CreatePosMasterHistoryEmployee( 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?.posLevel?.posLevelName ?? _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?.posLevel?.posLevelName ?? _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 = selectedPosition?.positionName ?? _null; - h.posType = selectedPosition?.posType?.posTypeName ?? _null; - h.posLevel = selectedPosition?.posLevel?.posLevelName ?? _null; + h.position = position; + h.posType = posTypeName; + h.posLevel = posLevelName; } h.rootDnaId = pm.orgRoot?.ancestorDNA || _null; h.child1DnaId = pm.orgChild1?.ancestorDNA || _null; From 12da1677946322fcc0054a36ffffd0322caaa8f5 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 17 Jun 2026 16:53:26 +0700 Subject: [PATCH 20/39] fix #2570 --- src/controllers/ProfileChangeNameController.ts | 4 ++-- src/controllers/ProfileChangeNameEmployeeController.ts | 4 ++-- src/controllers/ProfileChangeNameEmployeeTempController.ts | 4 ++-- src/controllers/ProfileController.ts | 3 ++- src/controllers/ProfileEmployeeController.ts | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 8eea3a9e..69de3557 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -115,7 +115,7 @@ export class ProfileChangeNameController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -191,7 +191,7 @@ export class ProfileChangeNameController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before: before_profile, after: profile }); } diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 6e28c477..35e14022 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -121,7 +121,7 @@ export class ProfileChangeNameEmployeeController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -197,7 +197,7 @@ export class ProfileChangeNameEmployeeController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileEmployeeRepo.save(profile); } diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 05dd9312..465a7354 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -112,7 +112,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -185,7 +185,7 @@ export class ProfileChangeNameEmployeeTempController extends Controller { profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefixMain; + profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; await this.profileEmployeeRepo.save(profile); } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 42b322d3..c6f1b68c 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -5801,7 +5801,8 @@ export class ProfileController extends Controller { Object.assign(record, body); record.dateRetireLaw = calculateRetireLaw(record.birthDate); record.prefixMain = record.prefix; - record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; + // record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; + record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefix; record.createdUserId = request.user.sub; record.createdFullName = request.user.name; record.createdAt = new Date(); diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 59f9e91d..61efbdb7 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -2415,7 +2415,7 @@ export class ProfileEmployeeController extends Controller { Object.assign(record, body); record.dateRetireLaw = calculateRetireLaw(record.birthDate); record.prefixMain = record.prefix; - record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefixMain; + record.prefix = record.rank && record.rank.length > 0 ? record.rank : record.prefix; record.createdUserId = request.user.sub; record.createdFullName = request.user.name; record.createdAt = new Date(); From e7eeb3e34da25a3033f4361f3c5c5a0e8f753c5c Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 17 Jun 2026 17:14:52 +0700 Subject: [PATCH 21/39] fix #2570 --- src/controllers/ProfileChangeNameController.ts | 8 ++++---- src/controllers/ProfileChangeNameEmployeeController.ts | 8 ++++---- .../ProfileChangeNameEmployeeTempController.ts | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/controllers/ProfileChangeNameController.ts b/src/controllers/ProfileChangeNameController.ts index 69de3557..22f147b8 100644 --- a/src/controllers/ProfileChangeNameController.ts +++ b/src/controllers/ProfileChangeNameController.ts @@ -114,8 +114,8 @@ export class ProfileChangeNameController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -190,8 +190,8 @@ export class ProfileChangeNameController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileRepository.save(profile, { data: req }); setLogDataDiff(req, { before: before_profile, after: profile }); } diff --git a/src/controllers/ProfileChangeNameEmployeeController.ts b/src/controllers/ProfileChangeNameEmployeeController.ts index 35e14022..a632e718 100644 --- a/src/controllers/ProfileChangeNameEmployeeController.ts +++ b/src/controllers/ProfileChangeNameEmployeeController.ts @@ -120,8 +120,8 @@ export class ProfileChangeNameEmployeeController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -196,8 +196,8 @@ export class ProfileChangeNameEmployeeController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileEmployeeRepo.save(profile); } diff --git a/src/controllers/ProfileChangeNameEmployeeTempController.ts b/src/controllers/ProfileChangeNameEmployeeTempController.ts index 465a7354..86c6b352 100644 --- a/src/controllers/ProfileChangeNameEmployeeTempController.ts +++ b/src/controllers/ProfileChangeNameEmployeeTempController.ts @@ -111,8 +111,8 @@ export class ProfileChangeNameEmployeeTempController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileEmployeeRepo.save(profile, { data: req }); setLogDataDiff(req, { before, after: profile }); @@ -184,8 +184,8 @@ export class ProfileChangeNameEmployeeTempController extends Controller { // profile.prefix = body.prefix ?? profile.prefix; //old profile.rank = body.rank ?? profile.rank; // profile.prefixMain = profile.rank ?? profile.prefix; // old - profile.prefixMain = profile.prefix; - profile.prefix = profile.rank && profile.rank.length > 0 ? profile.rank : profile.prefix; + profile.prefixMain = body.prefix ?? profile.prefix; + profile.prefix = body.rank && body.rank.length > 0 ? body.rank : body.prefix ?? profile.prefix; await this.profileEmployeeRepo.save(profile); } From bdd31ce72c7d19c12587a4d7855945f7823e1f30 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 17 Jun 2026 17:57:37 +0700 Subject: [PATCH 22/39] fix #2563 --- src/controllers/EmployeePositionController.ts | 50 +++++++++++++------ src/entities/ProfileEmployee.ts | 2 +- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/controllers/EmployeePositionController.ts b/src/controllers/EmployeePositionController.ts index ecf11619..5809841c 100644 --- a/src/controllers/EmployeePositionController.ts +++ b/src/controllers/EmployeePositionController.ts @@ -39,8 +39,7 @@ import { RequestWithUser } from "../middlewares/user"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; import { - CreatePosMasterHistoryEmployee, - CreatePosMasterHistoryOfficer, + CreatePosMasterHistoryEmployee } from "../services/PositionService"; import { PosMasterEmployeeHistory } from "../entities/PosMasterEmployeeHistory"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; @@ -2377,26 +2376,47 @@ export class EmployeePositionController extends Controller { dataMaster.positions.forEach(async (position) => { if (position.id === requestBody.position) { position.positionIsSelected = true; - const profile = await this.profileRepository.findOne({ - where: { id: requestBody.profileId }, - }); - if (profile != null) { - const _null: any = null; - profile.posLevelId = position?.posLevelId ?? _null; - profile.posTypeId = position?.posTypeId ?? _null; - profile.position = position?.positionName ?? _null; - await this.profileRepository.save(profile); - } } else { position.positionIsSelected = false; } await this.employeePositionRepository.save(position); }); - + const _null: any = null; + const before = null; dataMaster.isSit = requestBody.isSit; - dataMaster.current_holderId = requestBody.profileId; dataMaster.lastUpdatedAt = new Date(); - // dataMaster.next_holderId = requestBody.profileId; + + //เช็คถ้า revision ปัจจุบันให้ปั๊มที่ profile + const chkRevision = await this.orgRevisionRepository.findOne({ + where: { id: dataMaster.orgRevisionId }, + }); + if (chkRevision?.orgRevisionIsCurrent) { + const _profile = await this.profileRepository.findOne({ + where: { id: requestBody.profileId }, + }); + if (_profile) { + let _position = await this.employeePositionRepository.findOne({ + where: { id: requestBody.position, posMasterId: requestBody.posMaster }, + }); + if (_position) { + + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (!dataMaster.isSit) { + _profile.position = _position.positionName; + _profile.posTypeId = _position.posTypeId; + _profile.posLevelId = _position.posLevelId; + } + await this.profileRepository.save(_profile); + setLogDataDiff(request, { before, after: _profile }); + } + } + dataMaster.current_holderId = requestBody.profileId; + dataMaster.next_holderId = _null; + } else { + dataMaster.next_holderId = requestBody.profileId; + dataMaster.current_holderId = _null; + } + await this.employeePosMasterRepository.save(dataMaster); await CreatePosMasterHistoryEmployee(dataMaster.id, request); diff --git a/src/entities/ProfileEmployee.ts b/src/entities/ProfileEmployee.ts index 11c2159b..45f5e159 100644 --- a/src/entities/ProfileEmployee.ts +++ b/src/entities/ProfileEmployee.ts @@ -1014,7 +1014,7 @@ export class UpdateInformationProfileEmployee { } export type UpdateProfileEmployee = { - prefix: string; + prefix?: string | null; rank?: string | null; firstName: string; lastName: string; From 9f7803cc7427f479a4d0e5e6530ba018acd0b2a1 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 18 Jun 2026 10:21:04 +0700 Subject: [PATCH 23/39] fix #2563 --- src/services/PositionService.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/services/PositionService.ts b/src/services/PositionService.ts index 2b0cc324..144a5621 100644 --- a/src/services/PositionService.ts +++ b/src/services/PositionService.ts @@ -250,7 +250,9 @@ export async function CreatePosMasterHistoryEmployee( let position = selectedPosition?.positionName ?? _null; let posTypeName = selectedPosition?.posType?.posTypeName ?? _null; - let posLevelName = selectedPosition?.posLevel?.posLevelName ?? _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 }, @@ -258,7 +260,9 @@ export async function CreatePosMasterHistoryEmployee( }); position = profile?.position ?? _null; posTypeName = profile?.posType?.posTypeName ?? _null; - posLevelName = profile?.posLevel?.posLevelName ?? _null; + posLevelName = profile?.posType && profile?.posLevel + ? `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim() + : _null; } h.ancestorDNA = pm.ancestorDNA; if (!type || type != "DELETE") { From c26fb19c1c60c5f406fd31d24bec880cb067e1ae Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 18 Jun 2026 11:48:46 +0700 Subject: [PATCH 24/39] =?UTF-8?q?Linear=20Flow=20(=E0=B8=97=E0=B8=94?= =?UTF-8?q?=E0=B8=AA=E0=B8=AD=E0=B8=9A=E0=B9=80=E0=B8=89=E0=B8=9E=E0=B8=B2?= =?UTF-8?q?=E0=B8=B0=E0=B8=84=E0=B8=B3=E0=B8=AA=E0=B8=B1=E0=B9=88=E0=B8=87?= =?UTF-8?q?=20C-PM-01)=20#224?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 954 +----------------- src/services/OfficerProfileService.ts | 1310 +++++++++++++++++++++++++ src/services/rabbitmq.ts | 57 +- 3 files changed, 1368 insertions(+), 953 deletions(-) create mode 100644 src/services/OfficerProfileService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 4b7c66e7..8ae7b5b7 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -104,6 +104,7 @@ import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; import { RetirementService } from "../services/RetirementService"; +import { OfficerProfileService } from "../services/OfficerProfileService"; import { promisify } from "util"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -6661,6 +6662,10 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้างทะเบียนประวัติข้าราชการ หลังออกคำสั่งบรรจุสอบ หรือ รับโอน + * @summary API สร้างทะเบียนประวัติข้าราชการ หลังออกคำสั่งบรรจุสอบ หรือ รับโอน + */ @Post("excexute/create-officer-profile") public async CreateOfficeProfileExcecute( @Request() req: RequestWithUser, @@ -6707,953 +6712,10 @@ export class CommandController extends Controller { }[]; }, ) { - console.log("[Excexute/CreateOfficerProfile] Starting CreateOfficeProfileExcecute"); - console.log("[Excexute/CreateOfficerProfile] Request body count:", body.data?.length); - const roleKeycloak = await this.roleKeycloakRepo.findOne({ - where: { name: Like("USER") }, + await new OfficerProfileService().executeCreateOfficerProfile(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - console.log("[Excexute/CreateOfficerProfile] roleKeycloak found:", !!roleKeycloak); - const list = await getRoles(); - console.log("[Excexute/CreateOfficerProfile] Roles list retrieved, length:", Array.isArray(list) ? list.length : "not array"); - if (!Array.isArray(list)) { - console.error("[Excexute/CreateOfficerProfile] Failed - Cannot get role(s) data from the server"); - throw new Error("Failed. Cannot get role(s) data from the server."); - } - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - console.log("[Excexute/CreateOfficerProfile] Getting command data"); - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "" }, - }); - console.log("[Excexute/CreateOfficerProfile] Command found:", !!_command, "isBangkok:", _command?.isBangkok); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - console.log("[Excexute/CreateOfficerProfile] Setting position codes for OFFICE"); - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - console.log("[Excexute/CreateOfficerProfile] OFFICE position codes set:", _posNumCodeSit, _posNumCodeSitAbb); - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - console.log("[Excexute/CreateOfficerProfile] Setting position codes for BANGKOK"); - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - console.log("[Excexute/CreateOfficerProfile] BANGKOK position codes set:", _posNumCodeSit, _posNumCodeSitAbb); - } else { - console.log("[Excexute/CreateOfficerProfile] Setting position codes from admin profile"); - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - console.log("[Excexute/CreateOfficerProfile] Admin profile position codes set:", _posNumCodeSit, _posNumCodeSitAbb); - } - } - const before = null; - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - console.log("[Excexute/CreateOfficerProfile] Starting to process", body.data.length, "profile(s)"); - await Promise.all( - body.data.map(async (item, index) => { - console.log("[Excexute/CreateOfficerProfile] Processing item", index + 1, "of", body.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 })) - ) { - console.error("[Excexute/CreateOfficerProfile] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", item.bodyProfile.posLevelId); - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); - } - if ( - item.bodyProfile.posTypeId && - !(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) - ) { - console.error("[Excexute/CreateOfficerProfile] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", item.bodyProfile.posTypeId); - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); - } - - console.log("[Excexute/CreateOfficerProfile] Processing citizenId:", item.bodyProfile.citizenId); - let registrationProvinceId = await this.provinceRepo.findOneBy({ - id: item.bodyProfile.registrationProvinceId ?? "", - }); - 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] Checking Keycloak user for citizenId:", item.bodyProfile.citizenId); - const checkUser = await getUserByUsername(item.bodyProfile.citizenId); - console.log("[Excexute/CreateOfficerProfile] Keycloak user exists:", checkUser.length > 0); - if (checkUser.length == 0) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] Calling createUser for:", item.bodyProfile.citizenId); - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] createUser FAILED - field:", userKeycloakId.field, "errorMessage:", userKeycloakId.errorMessage, "params:", userKeycloakId.params); - throw new HttpError(HttpStatus.BAD_REQUEST, `Keycloak validation failed: ${userKeycloakId.field} - ${userKeycloakId.errorMessage}`); - } - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] USER role assigned to new user, result:", result); - } else { - console.log("[Excexute/CreateOfficerProfile] Updating existing Keycloak user"); - userKeycloakId = checkUser[0].id; - console.log("[Excexute/CreateOfficerProfile] Existing userKeycloakId:", userKeycloakId); - const rolesData = await getRoleMappings(userKeycloakId); - if (rolesData) { - const _delRole = rolesData.map((x: any) => ({ - id: x.id, - name: x.name, - })); - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] USER role assigned to existing user"); - } - - let profile: any = await this.profileRepository.findOne({ - where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, - relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], - }); - console.log("[Excexute/CreateOfficerProfile] Profile found:", !!profile, "for citizenId:", item.bodyProfile.citizenId); - let _oldInsigniaIds: string[] = []; - let _oldSalaries: any[] = []; - //ลูกจ้างประจำ หรือ บุคคลภายนอก - if (!profile) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] Employee profile found:", !!profileEmployee); - if (profileEmployee) { - console.log("[Excexute/CreateOfficerProfile] 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 = req.user.sub; - profileEmployee.lastUpdateFullName = req.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("[Excexute/CreateOfficerProfile] Saving new profile"); - await this.profileRepository.save(profile); - console.log("[Excexute/CreateOfficerProfile] New profile saved, profileId:", profile.id); - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - console.log("[Excexute/CreateOfficerProfile] Keycloak attributes updated"); - setLogDataDiff(req, { before, after: profile }); - } - //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ - else { - console.log("[Excexute/CreateOfficerProfile] Existing profile found, isLeave:", profile.isLeave, "leaveType:", profile.leaveType); - //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก - if ( - profile.isLeave && - ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) - ) { - console.log("[Excexute/CreateOfficerProfile] Profile is leaving with eligible leave type, creating new profile record"); - //ดึง 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); - console.log("[Excexute/CreateOfficerProfile] New profile record saved for leaving officer, profileId:", profile.id); - setLogDataDiff(req, { before, after: profile }); - } else { - console.log("[Excexute/CreateOfficerProfile] 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 = req.user.sub; - profile.lastUpdateFullName = req.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("[Excexute/CreateOfficerProfile] Existing active profile updated, profileId:", profile.id); - setLogDataDiff(req, { before, after: profile }); - } - } - - if (profile && profile.id) { - console.log("[Excexute/CreateOfficerProfile] Processing additional data for profileId:", profile.id); - //Educations - if (item.bodyEducations && item.bodyEducations.length > 0) { - console.log("[Excexute/CreateOfficerProfile] 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 }); - }), - ); - } - //Certificates - if (item.bodyCertificates && item.bodyCertificates.length > 0) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] Processing position assignment"); - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] posMaster found:", !!posMaster); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - console.log("[Excexute/CreateOfficerProfile] posMaster isCurrent:", isCurrent); - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] Current posMaster from ancestorDNA found:", !!posMaster); - } - - if (posMaster == null) { - console.error( - `[Excexute/CreateOfficerProfile] not found posMasterId: ${item.bodyPosition.posmasterId}` - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] posMaster saved with new holder"); - - // STEP 5: กำหนด position ใหม่ - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] CONDITION 1 matched, positionId:", positionById.id); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] CONDITION 2 matched with 7 fields, positionId:", positionBy7Fields.id); - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.bodyPosition) { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] CONDITION 3 matched with 3 fields, positionId:", positionBy3Fields.id); - } else { - console.log("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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("[Excexute/CreateOfficerProfile] 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(() => {}); - } - } - }), - ); - console.log("[Excexute/CreateOfficerProfile] CreateOfficeProfileExcecute completed successfully"); return new HttpSuccess(); } diff --git a/src/services/OfficerProfileService.ts b/src/services/OfficerProfileService.ts new file mode 100644 index 00000000..fae024d3 --- /dev/null +++ b/src/services/OfficerProfileService.ts @@ -0,0 +1,1310 @@ +import { In, Like } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { CreateProfileAllFields, Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { CreateProfileSalary, ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { CreateProfileEducation, ProfileEducation } from "../entities/ProfileEducation"; +import { ProfileEducationHistory } from "../entities/ProfileEducationHistory"; +import { + CreateProfileCertificate, + ProfileCertificate, +} from "../entities/ProfileCertificate"; +import { ProfileCertificateHistory } from "../entities/ProfileCertificateHistory"; +import { ProfileFamilyCouple } from "../entities/ProfileFamilyCouple"; +import { ProfileFamilyCoupleHistory } from "../entities/ProfileFamilyCoupleHistory"; +import { ProfileFamilyFather } from "../entities/ProfileFamilyFather"; +import { ProfileFamilyFatherHistory } from "../entities/ProfileFamilyFatherHistory"; +import { ProfileFamilyMother } from "../entities/ProfileFamilyMother"; +import { ProfileFamilyMotherHistory } from "../entities/ProfileFamilyMotherHistory"; +import { CreateProfileInsignia, ProfileInsignia } from "../entities/ProfileInsignia"; +import { ProfileInsigniaHistory } from "../entities/ProfileInsigniaHistory"; +import { ProfileAvatar } from "../entities/ProfileAvatar"; +import { PosLevel } from "../entities/PosLevel"; +import { PosType } from "../entities/PosType"; +import { Province } from "../entities/Province"; +import { District } from "../entities/District"; +import { SubDistrict } from "../entities/SubDistrict"; +import { OrgRoot } from "../entities/OrgRoot"; +import { RoleKeycloak } from "../entities/RoleKeycloak"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { Command } from "../entities/Command"; +import { + calculateRetireDate, + calculateRetireLaw, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { + addUserRoles, + createUser, + getRoleMappings, + getRoles, + getUserByUsername, + removeUserRoles, + updateUserAttributes, +} from "../keycloak"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; +import CallAPI from "../interfaces/call-api"; + +/** + * Input: ตำแหน่งที่จะกำหนดให้กับ profile ใหม่ (ส่งมาจากฝั่งบรรจุ) + */ +export interface OfficerPositionInput { + posmasterId: string; + positionId: string; + positionName: string; + posTypeId: string; + posLevelId: string; + posExecutiveId: string | null; + positionField: string | null; + positionExecutiveField: string | null; + positionArea: string | null; +} + +/** + * Input: ข้อมูลคู่สมรส + */ +export interface OfficerMarryInput { + marry?: boolean | null; + marryPrefix?: string | null; + marryFirstName?: string | null; + marryLastName?: string | null; + marryOccupation?: string | null; + marryNationality?: string | null; +} + +/** + * Input: ข้อมูลบิดา + */ +export interface OfficerFatherInput { + fatherPrefix?: string | null; + fatherFirstName?: string | null; + fatherLastName?: string | null; + fatherOccupation?: string | null; + fatherNationality?: string | null; +} + +/** + * Input: ข้อมูลมารดา + */ +export interface OfficerMotherInput { + motherPrefix?: string | null; + motherFirstName?: string | null; + motherLastName?: string | null; + motherOccupation?: string | null; + motherNationality?: string | null; +} + +/** + * Input: ข้อมูล 1 คนที่จะบรรจุ/แต่งตั้ง (ตรงกับ body.data[i] ของ endpoint เดิม) + */ +export interface OfficerProfileItem { + bodyProfile: CreateProfileAllFields; + bodyEducations?: CreateProfileEducation[]; + bodyCertificates?: CreateProfileCertificate[]; + bodySalarys?: CreateProfileSalary | null; + bodyPosition?: OfficerPositionInput | null; + bodyMarry?: OfficerMarryInput | null; + bodyFather?: OfficerFatherInput | null; + bodyMother?: OfficerMotherInput | null; +} + +/** + * Context สำหรับ audit/log — แยกขาดจาก HTTP request เพื่อให้ service + * สามารถถูกเรียกได้ทั้งจาก endpoint (ผ่าน Express req) และจาก message consumer + * (ผ่าน pseudo-req ที่สร้างขึ้น) โดยไม่ผูกติดกับ HTTP layer + */ +export interface ExecutionContext { + /** user ที่ทริกเกอร์งานนี้ (sub = keycloak id, name = ชื่อเต็ม) */ + user: { sub: string; name: string }; + /** Express request (สำหรับ subscriber pattern: setLogDataDiff, save({data: req})) */ + req?: any; +} + +/** + * Service สำหรับสร้าง/อัปเดตทะเบียนประวัติข้าราชการ (Profile) หลังออกคำสั่งบรรจุ + * + * ถูกออกแบบมาเพื่อแก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ + * โดยให้ฝั่งบรรจุส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เอง + * ที่ต้นทาง (Linear Flow) แทนการเรียกซ้อนกันกลับไปมา + * + * - endpoint /org/command/excexute/create-officer-profile เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (no HTTP loopback) + * + * Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ + */ +export class OfficerProfileService { + 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 posLevelRepo = AppDataSource.getRepository(PosLevel); + private posTypeRepo = AppDataSource.getRepository(PosType); + private provinceRepo = AppDataSource.getRepository(Province); + private districtRepo = AppDataSource.getRepository(District); + private subDistrictRepo = AppDataSource.getRepository(SubDistrict); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private positionRepository = AppDataSource.getRepository(Position); + private profileEducationRepo = AppDataSource.getRepository(ProfileEducation); + private profileEducationHistoryRepo = AppDataSource.getRepository(ProfileEducationHistory); + private certificateRepo = AppDataSource.getRepository(ProfileCertificate); + private certificateHistoryRepo = AppDataSource.getRepository(ProfileCertificateHistory); + private profileFamilyCoupleRepo = AppDataSource.getRepository(ProfileFamilyCouple); + private profileFamilyCoupleHistoryRepo = AppDataSource.getRepository(ProfileFamilyCoupleHistory); + private profileFamilyFatherRepo = AppDataSource.getRepository(ProfileFamilyFather); + private profileFamilyFatherHistoryRepo = AppDataSource.getRepository(ProfileFamilyFatherHistory); + private profileFamilyMotherRepo = AppDataSource.getRepository(ProfileFamilyMother); + private profileFamilyMotherHistoryRepo = AppDataSource.getRepository(ProfileFamilyMotherHistory); + private insigniaRepo = AppDataSource.getRepository(ProfileInsignia); + private insigniaHistoryRepo = AppDataSource.getRepository(ProfileInsigniaHistory); + private avatarRepository = AppDataSource.getRepository(ProfileAvatar); + + /** + * ประมวลผลสร้าง/อัปเดท profile ข้าราชการ ตามข้อมูลที่ฝั่งบรรจุส่งมา + * + * @param data รายการข้อมูลที่จะประมวลผล (1 คำสั่งอาจมีหลายคน) + * @param ctx context สำหรับ audit/log + */ + async executeCreateOfficerProfile( + data: OfficerProfileItem[], + ctx: ExecutionContext, + ): Promise { + console.log("[OfficerProfileService] Starting executeCreateOfficerProfile"); + console.log("[OfficerProfileService] Request body count:", data?.length); + + const req = ctx.req; + const roleKeycloak = await this.roleKeycloakRepo.findOne({ + where: { name: Like("USER") }, + }); + console.log("[OfficerProfileService] roleKeycloak found:", !!roleKeycloak); + const list = await getRoles(); + console.log( + "[OfficerProfileService] Roles list retrieved, length:", + Array.isArray(list) ? list.length : "not array", + ); + if (!Array.isArray(list)) { + console.error( + "[OfficerProfileService] Failed - Cannot get role(s) data from the server", + ); + throw new Error("Failed. Cannot get role(s) data from the server."); + } + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + console.log("[OfficerProfileService] Getting command data"); + const _command = await this.commandRepository.findOne({ + where: { + id: data.find((x) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "", + }, + }); + console.log( + "[OfficerProfileService] Command found:", + !!_command, + "isBangkok:", + _command?.isBangkok, + ); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + console.log("[OfficerProfileService] Setting position codes for OFFICE"); + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy + ? orgRootDeputy?.orgRootName + : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + console.log( + "[OfficerProfileService] OFFICE position codes set:", + _posNumCodeSit, + _posNumCodeSitAbb, + ); + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + console.log("[OfficerProfileService] Setting position codes for BANGKOK"); + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + console.log( + "[OfficerProfileService] BANGKOK position codes set:", + _posNumCodeSit, + _posNumCodeSitAbb, + ); + } else { + console.log("[OfficerProfileService] Setting position codes from admin profile"); + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: [ + "current_holders", + "current_holders.orgRevision", + "current_holders.orgRoot", + ], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot + .orgRootName ?? ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + console.log( + "[OfficerProfileService] Admin profile position codes set:", + _posNumCodeSit, + _posNumCodeSitAbb, + ); + } + } + const before = null; + const meta = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + console.log( + "[OfficerProfileService] Starting to process", + data.length, + "profile(s)", + ); + await Promise.all( + data.map(async (item, index) => { + console.log( + "[OfficerProfileService] 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 })) + ) { + console.error( + "[OfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", + item.bodyProfile.posLevelId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); + } + if ( + item.bodyProfile.posTypeId && + !(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) + ) { + console.error( + "[OfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", + item.bodyProfile.posTypeId, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); + } + + console.log( + "[OfficerProfileService] Processing citizenId:", + item.bodyProfile.citizenId, + ); + let registrationProvinceId = await this.provinceRepo.findOneBy({ + id: item.bodyProfile.registrationProvinceId ?? "", + }); + 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("[OfficerProfileService] 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( + "[OfficerProfileService] Checking Keycloak user for citizenId:", + item.bodyProfile.citizenId, + ); + const checkUser = await getUserByUsername(item.bodyProfile.citizenId); + console.log( + "[OfficerProfileService] Keycloak user exists:", + checkUser.length > 0, + ); + if (checkUser.length == 0) { + console.log("[OfficerProfileService] 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( + "[OfficerProfileService] Calling createUser for:", + item.bodyProfile.citizenId, + ); + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] USER role assigned to new user, result:", + result, + ); + } else { + console.log("[OfficerProfileService] Updating existing Keycloak user"); + userKeycloakId = checkUser[0].id; + console.log( + "[OfficerProfileService] Existing userKeycloakId:", + userKeycloakId, + ); + const rolesData = await getRoleMappings(userKeycloakId); + if (rolesData) { + const _delRole = rolesData.map((x: any) => ({ + id: x.id, + name: x.name, + })); + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] USER role assigned to existing user", + ); + } + + let profile: any = await this.profileRepository.findOne({ + where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ }, + relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], + }); + console.log( + "[OfficerProfileService] Profile found:", + !!profile, + "for citizenId:", + item.bodyProfile.citizenId, + ); + let _oldInsigniaIds: string[] = []; + let _oldSalaries: any[] = []; + //ลูกจ้างประจำ หรือ บุคคลภายนอก + if (!profile) { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] Employee profile found:", + !!profileEmployee, + ); + if (profileEmployee) { + console.log( + "[OfficerProfileService] 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("[OfficerProfileService] Saving new profile"); + await this.profileRepository.save(profile); + console.log( + "[OfficerProfileService] New profile saved, profileId:", + profile.id, + ); + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + console.log("[OfficerProfileService] Keycloak attributes updated"); + setLogDataDiff(req, { before, after: profile }); + } + //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ + else { + console.log( + "[OfficerProfileService] Existing profile found, isLeave:", + profile.isLeave, + "leaveType:", + profile.leaveType, + ); + //สร้างโปรไฟล์ใหม่ ถ้าสถานะพ้นราชการ คำสั่งโอนออกหรือคำสั่งขอลาออก + if ( + profile.isLeave && + ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) + ) { + console.log( + "[OfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + ); + //ดึง 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); + console.log( + "[OfficerProfileService] New profile record saved for leaving officer, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } else { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] Existing active profile updated, profileId:", + profile.id, + ); + setLogDataDiff(req, { before, after: profile }); + } + } + + if (profile && profile.id) { + console.log( + "[OfficerProfileService] Processing additional data for profileId:", + profile.id, + ); + //Educations + if (item.bodyEducations && item.bodyEducations.length > 0) { + console.log( + "[OfficerProfileService] 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 }); + }), + ); + } + //Certificates + if (item.bodyCertificates && item.bodyCertificates.length > 0) { + console.log( + "[OfficerProfileService] 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("[OfficerProfileService] 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("[OfficerProfileService] 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("[OfficerProfileService] 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( + "[OfficerProfileService] 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("[OfficerProfileService] 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("[OfficerProfileService] Processing position assignment"); + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) + console.log( + "[OfficerProfileService] 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("[OfficerProfileService] posMaster found:", !!posMaster); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + console.log("[OfficerProfileService] posMaster isCurrent:", isCurrent); + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] Current posMaster from ancestorDNA found:", + !!posMaster, + ); + } + + if (posMaster == null) { + console.error( + `[OfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + } + + // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน + console.log("[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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("[OfficerProfileService] posMaster saved with new holder"); + + // STEP 5: กำหนด position ใหม่ + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] CONDITION 1 matched, positionId:", + positionById.id, + ); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", + positionBy7Fields.id, + ); + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.bodyPosition) { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", + positionBy3Fields.id, + ); + } else { + console.log( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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( + "[OfficerProfileService] 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(() => {}); + } + } + }), + ); + console.log("[OfficerProfileService] executeCreateOfficerProfile completed successfully"); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index b9668904..b9c53537 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -29,6 +29,7 @@ import { sendWebSocket } from "./webSocket"; import { PayloadSendNoti } from "../interfaces/utils"; import { PermissionProfile } from "../entities/PermissionProfile"; import { PosMasterHistory } from "../entities/PosMasterHistory"; +import { OfficerProfileService } from "./OfficerProfileService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -320,13 +321,55 @@ async function handler(msg: amqp.ConsumeMessage): Promise { 20, ); - for (const chunk of chunks) { - await new CallAPI().PostData( - { headers: { authorization: token } }, - path + "/excecute", - { refIds: chunk }, - false, - ); + // ───────────────────────────────────────────────────────────── + // Linear Flow + // ทดสอบเฉพาะ C-PM-01 รับ resultData จาก .NET แล้วเรียก OfficerProfileService ตรงๆ ไม่ผ่าน HTTP loopback + // ───────────────────────────────────────────────────────────── + const isLinearFlow = command.commandType?.code === "C-PM-01"; + + if (isLinearFlow) { + let resultData: any[] = []; + + for (const chunk of chunks) { + const res = await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + // CallAPI.PostData คืน response.data.result (ตาม call-api.ts) + if (res?.result && Array.isArray(res.result)) { + resultData.push(...res.result); + } + } + + console.log(`[AMQ] Received ${resultData.length} profiles from .NET (C-PM-01)`); + + // เรียก OfficerProfileService + if (resultData.length > 0) { + // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) + const pseudoReq = { + headers: { authorization: token }, + user, + }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + + await new OfficerProfileService().executeCreateOfficerProfile(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via OfficerProfileService`); + } + } else { + // Flow เดิม + for (const chunk of chunks) { + await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + } } Object.assign(command, { status, lastUpdateUserId, lastUpdateFullName, lastUpdatedAt }); From 9dddaf40dbc3b7e8dc7791c67b81b534cdc23467 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 18 Jun 2026 15:48:52 +0700 Subject: [PATCH 25/39] Linear Flow (C-PM-01, C-PM-02, C-PM-14) #224 --- src/services/rabbitmq.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index b9c53537..d2fd4def 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -323,11 +323,13 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // ───────────────────────────────────────────────────────────── // Linear Flow - // ทดสอบเฉพาะ C-PM-01 รับ resultData จาก .NET แล้วเรียก OfficerProfileService ตรงๆ ไม่ผ่าน HTTP loopback + // ทดสอบคำสั่ง "C-PM-01", "C-PM-02", "C-PM-14" + // รับ resultData จาก .NET แล้วเรียก OfficerProfileService ตรงๆ ไม่ผ่าน HTTP loopback // ───────────────────────────────────────────────────────────── - const isLinearFlow = command.commandType?.code === "C-PM-01"; + const isLinearFlow = ["C-PM-01", "C-PM-02", "C-PM-14"].includes(command.commandType?.code); if (isLinearFlow) { + console.log(`[AMQ] Linear Flow`); let resultData: any[] = []; for (const chunk of chunks) { @@ -337,13 +339,14 @@ async function handler(msg: amqp.ConsumeMessage): Promise { { refIds: chunk }, false, ); - // CallAPI.PostData คืน response.data.result (ตาม call-api.ts) - if (res?.result && Array.isArray(res.result)) { - resultData.push(...res.result); + // response (resultData) จาก .NET + if (Array.isArray(res)) { + console.log(`[AMQ] Push result data`); + resultData.push(...res); } } - console.log(`[AMQ] Received ${resultData.length} profiles from .NET (C-PM-01)`); + console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${command.commandType?.code})`); // เรียก OfficerProfileService if (resultData.length > 0) { @@ -361,7 +364,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { console.log(`[AMQ] Processed ${resultData.length} profiles via OfficerProfileService`); } } else { - // Flow เดิม + console.log(`[AMQ] Circular Flow`); for (const chunk of chunks) { await new CallAPI().PostData( { headers: { authorization: token } }, From 2f17c100506b0c028507b4121ee9c8c0a746a703 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 18 Jun 2026 16:26:27 +0700 Subject: [PATCH 26/39] fix error #224 --- src/services/OfficerProfileService.ts | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/services/OfficerProfileService.ts b/src/services/OfficerProfileService.ts index fae024d3..2225e7c4 100644 --- a/src/services/OfficerProfileService.ts +++ b/src/services/OfficerProfileService.ts @@ -180,6 +180,46 @@ export class OfficerProfileService { console.log("[OfficerProfileService] Starting executeCreateOfficerProfile"); console.log("[OfficerProfileService] Request body count:", data?.length); + // ───────────────────────────────────────────────────────────── + // Normalize date fields + // ผ่าน HTTP endpoint → tsoa แปลง ISO string → Date ให้อัตโนมัติ + // แต่ผ่าน RabbitMQ handler (axios) → จะได้ string → ต้องแปลงเอง + // ไม่งั้น calculateRetireDate/getFullYear ฯลฯ จะพัง + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const bp = item.bodyProfile as any; + if (bp) { + bp.birthDate = toDate(bp.birthDate); + bp.dateStart = toDate(bp.dateStart); + bp.dateAppoint = toDate(bp.dateAppoint); + bp.dateRetire = toDate(bp.dateRetire); + } + const bs = item.bodySalarys as any; + if (bs) { + bs.commandDateAffect = toDate(bs.commandDateAffect); + bs.commandDateSign = toDate(bs.commandDateSign); + } + if (item.bodyEducations) { + for (const edu of item.bodyEducations as any[]) { + edu.startDate = toDate(edu.startDate); + edu.endDate = toDate(edu.endDate); + edu.finishDate = toDate(edu.finishDate); + } + } + if (item.bodyCertificates) { + for (const cer of item.bodyCertificates as any[]) { + cer.expireDate = toDate(cer.expireDate); + cer.issueDate = toDate(cer.issueDate); + } + } + } + const req = ctx.req; const roleKeycloak = await this.roleKeycloakRepo.findOne({ where: { name: Like("USER") }, From 616ccf9e6452e25ef8eb22c523fb8b9191e8b703 Mon Sep 17 00:00:00 2001 From: harid Date: Thu, 18 Jun 2026 18:29:03 +0700 Subject: [PATCH 27/39] Linear Flow (PlacementService) #224 --- src/controllers/CommandController.ts | 722 +----------------- ...ice.ts => ExecuteOfficerProfileService.ts} | 152 ++-- src/services/ExecuteSalaryCurrentService.ts | 431 +++++++++++ .../ExecuteSalaryEmployeeCurrentService.ts | 262 +++++++ src/services/ExecuteSalaryService.ts | 327 ++++++++ src/services/rabbitmq.ts | 43 +- 6 files changed, 1160 insertions(+), 777 deletions(-) rename src/services/{OfficerProfileService.ts => ExecuteOfficerProfileService.ts} (90%) create mode 100644 src/services/ExecuteSalaryCurrentService.ts create mode 100644 src/services/ExecuteSalaryEmployeeCurrentService.ts create mode 100644 src/services/ExecuteSalaryService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 8ae7b5b7..5b324311 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -104,7 +104,10 @@ import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; import { RetirementService } from "../services/RetirementService"; -import { OfficerProfileService } from "../services/OfficerProfileService"; +import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileService"; +import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; +import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService"; +import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { promisify } from "util"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -3648,6 +3651,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary ข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) + * + * Thin wrapper — เรียก ExecuteSalaryCurrentService (C-PM-03, 04, 05, 06, 07, 39, 47) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API สร้าง ProfileSalary ข้าราชการ + เปลี่ยนตำแหน่ง + */ @Post("excexute/salary-current") public async newSalaryAndUpdateCurrent( @Request() req: RequestWithUser, @@ -3689,326 +3700,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryCurrentService().executeSalaryCurrent(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - const profile: any = await this.profileRepository.findOneBy({ id: item.profileId }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - let _null: any = null; - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - Object.assign(data, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.commandId = item.commandId ?? _null; - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster == null) { - console.error( - `[CommandController] PosMaster not found - posMasterId: ${item.posmasterId}, ` - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - const posMasterOld = await this.posMasterRepository.findOne({ - where: { - current_holderId: item.profileId, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - - const positionOld = await this.positionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { - posMasterId: posMasterOld?.id, - userId: req.user.sub, - endpoint: "updateMaster", - action: "command_change_reset_old_position", - }); - - positionOld.positionIsSelected = false; - await this.positionRepository.save(positionOld); - } - - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - console.log( - `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${req.user.sub}, endpoint: updateMaster)` - ); - - const clearPosition = checkPosition.map((positions) => { - logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { - posMasterId: posMaster!.id, - userId: req.user.sub, - endpoint: "updateMaster", - action: "command_change_clear_positions", - }); - - return { - ...positions, - positionIsSelected: false, - }; - }); - await this.positionRepository.save(clearPosition); - } - - posMaster.current_holderId = item.profileId; - 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); - - // STEP 2: กำหนด position ใหม่ - // 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; - - // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel - const posTypeId = item.positionTypeId || item.positionType; - const posLevelId = item.positionLevelId || item.positionLevel; - - // ═══════════════════════════════════════════════════════════ - // 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.positionName && posTypeId && posLevelId) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }; - - // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) - 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.positionName && posTypeId && posLevelId) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" } - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // ═══════════════════════════════════════════════════════════ - // // 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]; - // } - // } - - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (positionNew != null) { - positionNew.positionIsSelected = true; - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - 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.amount = item.amount ?? null; - profile.amountSpecial = item.amountSpecial ?? null; - await this.profileRepository.save(profile); - await this.positionRepository.save(positionNew); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - }), - ); - return new HttpSuccess(); } @@ -4016,7 +3711,7 @@ export class CommandController extends Controller { public async newSalaryEmployeeAndUpdateCurrent( @Request() req: RequestWithUser, @Body() - body: { + body: { data: { profileId: string; amount?: Double | null; @@ -4046,162 +3741,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - const profile: any = await this.profileEmployeeRepository.findOneBy({ id: item.profileId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { - ...item, - ...meta, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - const posMaster = await this.employeePosMasterRepository.findOne({ - where: { id: item.posmasterId }, - relations: ["orgRoot"], - }); - if (posMaster == null) - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - - 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; - - const positionOld = await this.employeePositionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.employeePositionRepository.save(positionOld); - } - - 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); - } - - 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); - }), - ); - return new HttpSuccess(); } @@ -5003,6 +4546,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary ข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ + * + * Thin wrapper — เรียก ExecuteSalaryService (C-PM-13, 15, 16) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API สร้าง ProfileSalary ข้าราชการ (leave/โอน/ช่วยราชการ) + */ @Post("excexute/salary") public async newSalaryAndUpdate( @Request() req: RequestWithUser, @@ -5045,221 +4596,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - relations: ["commandType"], - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryService().executeSalary(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - const profile: any = await this.profileRepository.findOne({ - where: { id: item.profileId }, - // relations: ["roleKeycloaks"], - 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); - } - - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - if (item.isLeave != undefined && item.isLeave == true) { - await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE"); - await removeProfileInOrganize(profile.id, "OFFICER"); - } - 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(data, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - if (_command) { - /* - const command = await this.commandRepository.findOne({ - where: { id: item.commandId }, - relations: ["commandType"], - }); - */ - 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: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.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(" "); - } - } - } - }), - ); - return new HttpSuccess(); } @@ -6712,7 +6052,7 @@ export class CommandController extends Controller { }[]; }, ) { - await new OfficerProfileService().executeCreateOfficerProfile(body.data, { + await new ExecuteOfficerProfileService().executeCreateOfficerProfile(body.data, { user: { sub: req.user.sub, name: req.user.name }, req, }); diff --git a/src/services/OfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts similarity index 90% rename from src/services/OfficerProfileService.ts rename to src/services/ExecuteOfficerProfileService.ts index 2225e7c4..0d77719f 100644 --- a/src/services/OfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -138,7 +138,7 @@ export interface ExecutionContext { * * Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ */ -export class OfficerProfileService { +export class ExecuteOfficerProfileService { private commandRepository = AppDataSource.getRepository(Command); private profileRepository = AppDataSource.getRepository(Profile); private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); @@ -177,8 +177,8 @@ export class OfficerProfileService { data: OfficerProfileItem[], ctx: ExecutionContext, ): Promise { - console.log("[OfficerProfileService] Starting executeCreateOfficerProfile"); - console.log("[OfficerProfileService] Request body count:", data?.length); + console.log("[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile"); + console.log("[ExecuteOfficerProfileService] Request body count:", data?.length); // ───────────────────────────────────────────────────────────── // Normalize date fields @@ -224,35 +224,35 @@ export class OfficerProfileService { const roleKeycloak = await this.roleKeycloakRepo.findOne({ where: { name: Like("USER") }, }); - console.log("[OfficerProfileService] roleKeycloak found:", !!roleKeycloak); + console.log("[ExecuteOfficerProfileService] roleKeycloak found:", !!roleKeycloak); const list = await getRoles(); console.log( - "[OfficerProfileService] Roles list retrieved, length:", + "[ExecuteOfficerProfileService] Roles list retrieved, length:", Array.isArray(list) ? list.length : "not array", ); if (!Array.isArray(list)) { console.error( - "[OfficerProfileService] Failed - Cannot get role(s) data from the server", + "[ExecuteOfficerProfileService] Failed - Cannot get role(s) data from the server", ); throw new Error("Failed. Cannot get role(s) data from the server."); } let _posNumCodeSit: string = ""; let _posNumCodeSitAbb: string = ""; - console.log("[OfficerProfileService] Getting command data"); + console.log("[ExecuteOfficerProfileService] Getting command data"); const _command = await this.commandRepository.findOne({ where: { id: data.find((x) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "", }, }); console.log( - "[OfficerProfileService] Command found:", + "[ExecuteOfficerProfileService] Command found:", !!_command, "isBangkok:", _command?.isBangkok, ); if (_command) { if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - console.log("[OfficerProfileService] Setting position codes for OFFICE"); + console.log("[ExecuteOfficerProfileService] Setting position codes for OFFICE"); const orgRootDeputy = await this.orgRootRepository.findOne({ where: { isDeputy: true, @@ -268,21 +268,21 @@ export class OfficerProfileService { : "สำนักปลัดกรุงเทพมหานคร"; _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; console.log( - "[OfficerProfileService] OFFICE position codes set:", + "[ExecuteOfficerProfileService] OFFICE position codes set:", _posNumCodeSit, _posNumCodeSitAbb, ); } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - console.log("[OfficerProfileService] Setting position codes for BANGKOK"); + console.log("[ExecuteOfficerProfileService] Setting position codes for BANGKOK"); _posNumCodeSit = "กรุงเทพมหานคร"; _posNumCodeSitAbb = "กทม."; console.log( - "[OfficerProfileService] BANGKOK position codes set:", + "[ExecuteOfficerProfileService] BANGKOK position codes set:", _posNumCodeSit, _posNumCodeSitAbb, ); } else { - console.log("[OfficerProfileService] Setting position codes from admin profile"); + console.log("[ExecuteOfficerProfileService] Setting position codes from admin profile"); let _profileAdmin = await this.profileRepository.findOne({ where: { keycloak: _command?.createdUserId.toString(), @@ -306,7 +306,7 @@ export class OfficerProfileService { _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot .orgRootShortName ?? ""; console.log( - "[OfficerProfileService] Admin profile position codes set:", + "[ExecuteOfficerProfileService] Admin profile position codes set:", _posNumCodeSit, _posNumCodeSitAbb, ); @@ -322,14 +322,14 @@ export class OfficerProfileService { lastUpdatedAt: new Date(), }; console.log( - "[OfficerProfileService] Starting to process", + "[ExecuteOfficerProfileService] Starting to process", data.length, "profile(s)", ); await Promise.all( data.map(async (item, index) => { console.log( - "[OfficerProfileService] Processing item", + "[ExecuteOfficerProfileService] Processing item", index + 1, "of", data.length, @@ -342,7 +342,7 @@ export class OfficerProfileService { !(await this.posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) ) { console.error( - "[OfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", + "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", item.bodyProfile.posLevelId, ); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลระดับตำแหน่งนี้"); @@ -352,14 +352,14 @@ export class OfficerProfileService { !(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) ) { console.error( - "[OfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", + "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", item.bodyProfile.posTypeId, ); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลประเภทตำแหน่งนี้"); } console.log( - "[OfficerProfileService] Processing citizenId:", + "[ExecuteOfficerProfileService] Processing citizenId:", item.bodyProfile.citizenId, ); let registrationProvinceId = await this.provinceRepo.findOneBy({ @@ -380,7 +380,7 @@ export class OfficerProfileService { let currentSubDistrictId = await this.subDistrictRepo.findOneBy({ id: item.bodyProfile.currentSubDistrictId ?? "", }); - console.log("[OfficerProfileService] Address validation completed"); + console.log("[ExecuteOfficerProfileService] Address validation completed"); let _dateRetire = item.bodyProfile.birthDate == null @@ -394,16 +394,16 @@ export class OfficerProfileService { let userKeycloakId: any; let result: any; console.log( - "[OfficerProfileService] Checking Keycloak user for citizenId:", + "[ExecuteOfficerProfileService] Checking Keycloak user for citizenId:", item.bodyProfile.citizenId, ); const checkUser = await getUserByUsername(item.bodyProfile.citizenId); console.log( - "[OfficerProfileService] Keycloak user exists:", + "[ExecuteOfficerProfileService] Keycloak user exists:", checkUser.length > 0, ); if (checkUser.length == 0) { - console.log("[OfficerProfileService] Creating new Keycloak user"); + 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()) @@ -419,11 +419,11 @@ export class OfficerProfileService { password = `${_date}${_month}${_year}`; } console.log( - "[OfficerProfileService] Calling createUser for:", + "[ExecuteOfficerProfileService] Calling createUser for:", item.bodyProfile.citizenId, ); console.log( - "[OfficerProfileService] createUser data - firstName:", + "[ExecuteOfficerProfileService] createUser data - firstName:", item.bodyProfile.firstName, "lastName:", item.bodyProfile.lastName, @@ -440,7 +440,7 @@ export class OfficerProfileService { userKeycloakId.errorMessage ) { console.error( - "[OfficerProfileService] createUser FAILED - field:", + "[ExecuteOfficerProfileService] createUser FAILED - field:", userKeycloakId.field, "errorMessage:", userKeycloakId.errorMessage, @@ -453,7 +453,7 @@ export class OfficerProfileService { ); } console.log( - "[OfficerProfileService] User created in Keycloak, userKeycloakId:", + "[ExecuteOfficerProfileService] User created in Keycloak, userKeycloakId:", userKeycloakId, ); result = await addUserRoles( @@ -466,14 +466,14 @@ export class OfficerProfileService { })), ); console.log( - "[OfficerProfileService] USER role assigned to new user, result:", + "[ExecuteOfficerProfileService] USER role assigned to new user, result:", result, ); } else { - console.log("[OfficerProfileService] Updating existing Keycloak user"); + console.log("[ExecuteOfficerProfileService] Updating existing Keycloak user"); userKeycloakId = checkUser[0].id; console.log( - "[OfficerProfileService] Existing userKeycloakId:", + "[ExecuteOfficerProfileService] Existing userKeycloakId:", userKeycloakId, ); const rolesData = await getRoleMappings(userKeycloakId); @@ -483,7 +483,7 @@ export class OfficerProfileService { name: x.name, })); console.log( - "[OfficerProfileService] Removing old roles:", + "[ExecuteOfficerProfileService] Removing old roles:", _delRole.length, ); await removeUserRoles(userKeycloakId, _delRole); @@ -498,7 +498,7 @@ export class OfficerProfileService { })), ); console.log( - "[OfficerProfileService] USER role assigned to existing user", + "[ExecuteOfficerProfileService] USER role assigned to existing user", ); } @@ -507,7 +507,7 @@ export class OfficerProfileService { relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], }); console.log( - "[OfficerProfileService] Profile found:", + "[ExecuteOfficerProfileService] Profile found:", !!profile, "for citizenId:", item.bodyProfile.citizenId, @@ -517,7 +517,7 @@ export class OfficerProfileService { //ลูกจ้างประจำ หรือ บุคคลภายนอก if (!profile) { console.log( - "[OfficerProfileService] No existing profile found, creating new profile", + "[ExecuteOfficerProfileService] No existing profile found, creating new profile", ); //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม let profileEmployee: any = await this.profileEmployeeRepository.findOne({ @@ -525,12 +525,12 @@ export class OfficerProfileService { relations: ["profileInsignias", "roleKeycloaks"], }); console.log( - "[OfficerProfileService] Employee profile found:", + "[ExecuteOfficerProfileService] Employee profile found:", !!profileEmployee, ); if (profileEmployee) { console.log( - "[OfficerProfileService] Converting employee profile to officer profile", + "[ExecuteOfficerProfileService] Converting employee profile to officer profile", ); const _order = await this.salaryRepo.findOne({ where: { profileEmployeeId: profileEmployee.id }, @@ -618,10 +618,10 @@ export class OfficerProfileService { profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; profile.phone = item.bodyProfile.phone ?? null; - console.log("[OfficerProfileService] Saving new profile"); + console.log("[ExecuteOfficerProfileService] Saving new profile"); await this.profileRepository.save(profile); console.log( - "[OfficerProfileService] New profile saved, profileId:", + "[ExecuteOfficerProfileService] New profile saved, profileId:", profile.id, ); // update user attribute in keycloak @@ -629,13 +629,13 @@ export class OfficerProfileService { profileId: [profile.id], prefix: [profile.prefix || ""], }); - console.log("[OfficerProfileService] Keycloak attributes updated"); + console.log("[ExecuteOfficerProfileService] Keycloak attributes updated"); setLogDataDiff(req, { before, after: profile }); } //ขรก.ในระบบ หรือ ขรก.ในระบบที่สถานะพ้นจากราชการ else { console.log( - "[OfficerProfileService] Existing profile found, isLeave:", + "[ExecuteOfficerProfileService] Existing profile found, isLeave:", profile.isLeave, "leaveType:", profile.leaveType, @@ -646,7 +646,7 @@ export class OfficerProfileService { ["PLACEMENT_TRANSFER", "RETIRE_RESIGN"].includes(profile.leaveType) ) { console.log( - "[OfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", + "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", ); //ดึง profileSalary เดิม _oldSalaries = await this.salaryRepo.find({ @@ -698,13 +698,13 @@ export class OfficerProfileService { profile.phone = item.bodyProfile.phone ?? null; await this.profileRepository.save(profile); console.log( - "[OfficerProfileService] New profile record saved for leaving officer, profileId:", + "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", profile.id, ); setLogDataDiff(req, { before, after: profile }); } else { console.log( - "[OfficerProfileService] Updating existing active profile", + "[ExecuteOfficerProfileService] Updating existing active profile", ); profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; profile.keycloak = @@ -793,7 +793,7 @@ export class OfficerProfileService { : profile.phone; await this.profileRepository.save(profile); console.log( - "[OfficerProfileService] Existing active profile updated, profileId:", + "[ExecuteOfficerProfileService] Existing active profile updated, profileId:", profile.id, ); setLogDataDiff(req, { before, after: profile }); @@ -802,13 +802,13 @@ export class OfficerProfileService { if (profile && profile.id) { console.log( - "[OfficerProfileService] Processing additional data for profileId:", + "[ExecuteOfficerProfileService] Processing additional data for profileId:", profile.id, ); //Educations if (item.bodyEducations && item.bodyEducations.length > 0) { console.log( - "[OfficerProfileService] Processing educations, count:", + "[ExecuteOfficerProfileService] Processing educations, count:", item.bodyEducations.length, ); await Promise.all( @@ -834,7 +834,7 @@ export class OfficerProfileService { //Certificates if (item.bodyCertificates && item.bodyCertificates.length > 0) { console.log( - "[OfficerProfileService] Processing certificates, count:", + "[ExecuteOfficerProfileService] Processing certificates, count:", item.bodyCertificates.length, ); await Promise.all( @@ -853,7 +853,7 @@ export class OfficerProfileService { } //FamilyCouple if (item.bodyMarry != null) { - console.log("[OfficerProfileService] Processing couple/marry data"); + console.log("[ExecuteOfficerProfileService] Processing couple/marry data"); const profileCouple = new ProfileFamilyCouple(); const data = { profileId: profile.id, @@ -875,7 +875,7 @@ export class OfficerProfileService { } //FamilyFather if (item.bodyFather != null) { - console.log("[OfficerProfileService] Processing father data"); + console.log("[ExecuteOfficerProfileService] Processing father data"); const profileFather = new ProfileFamilyFather(); const data = { profileId: profile.id, @@ -896,7 +896,7 @@ export class OfficerProfileService { } //FamilyMother if (item.bodyMother != null) { - console.log("[OfficerProfileService] Processing mother data"); + console.log("[ExecuteOfficerProfileService] Processing mother data"); const profileMother = new ProfileFamilyMother(); const data = { profileId: profile.id, @@ -919,7 +919,7 @@ export class OfficerProfileService { //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ if (_oldSalaries.length > 0) { console.log( - "[OfficerProfileService] Restoring old salaries, count:", + "[ExecuteOfficerProfileService] Restoring old salaries, count:", _oldSalaries.length, ); await Promise.all( @@ -938,7 +938,7 @@ export class OfficerProfileService { } //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว if (item.bodySalarys && item.bodySalarys != null) { - console.log("[OfficerProfileService] Processing new salary data"); + console.log("[ExecuteOfficerProfileService] Processing new salary data"); const dest_item = await this.salaryRepo.findOne({ where: { profileId: profile.id }, order: { order: "DESC" }, @@ -963,10 +963,10 @@ export class OfficerProfileService { } //Position if (item.bodyPosition && item.bodyPosition != null) { - console.log("[OfficerProfileService] Processing position assignment"); + console.log("[ExecuteOfficerProfileService] Processing position assignment"); // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา (อาจเป็นตำแหน่งเก่าหรือใหม่ก็ได้) console.log( - "[OfficerProfileService] STEP 1: Finding posMaster, posmasterId:", + "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", item.bodyPosition.posmasterId, ); let posMaster = await this.posMasterRepository.findOne({ @@ -982,18 +982,18 @@ export class OfficerProfileService { orgChild4: true, }, }); - console.log("[OfficerProfileService] posMaster found:", !!posMaster); + console.log("[ExecuteOfficerProfileService] posMaster found:", !!posMaster); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ const isCurrent = posMaster?.orgRevision?.orgRevisionIsCurrent === true && posMaster?.orgRevision?.orgRevisionIsDraft === false; - console.log("[OfficerProfileService] posMaster isCurrent:", isCurrent); + console.log("[ExecuteOfficerProfileService] posMaster isCurrent:", isCurrent); // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA if (!isCurrent && posMaster?.ancestorDNA) { console.log( - "[OfficerProfileService] Finding current posMaster from ancestorDNA", + "[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA", ); posMaster = await this.posMasterRepository.findOne({ where: { @@ -1013,20 +1013,20 @@ export class OfficerProfileService { }, }); console.log( - "[OfficerProfileService] Current posMaster from ancestorDNA found:", + "[ExecuteOfficerProfileService] Current posMaster from ancestorDNA found:", !!posMaster, ); } if (posMaster == null) { console.error( - `[OfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, + `[ExecuteOfficerProfileService] not found posMasterId: ${item.bodyPosition.posmasterId}`, ); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); } // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน - console.log("[OfficerProfileService] STEP 2: Clearing old position data"); + console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); const posMasterOld = await this.posMasterRepository.findOne({ where: { current_holderId: profile.id, @@ -1053,7 +1053,7 @@ export class OfficerProfileService { // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ console.log( - "[OfficerProfileService] STEP 3: Clearing other selected positions in new posMaster", + "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster", ); const checkPosition = await this.positionRepository.find({ where: { @@ -1071,7 +1071,7 @@ export class OfficerProfileService { // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster console.log( - "[OfficerProfileService] STEP 4: Assigning new holder to posMaster", + "[ExecuteOfficerProfileService] STEP 4: Assigning new holder to posMaster", ); posMaster.current_holderId = profile.id; posMaster.lastUpdatedAt = new Date(); @@ -1082,11 +1082,11 @@ export class OfficerProfileService { await CreatePosMasterHistoryOfficer(posMasterOld.id, req); } await this.posMasterRepository.save(posMaster); - console.log("[OfficerProfileService] posMaster saved with new holder"); + console.log("[ExecuteOfficerProfileService] posMaster saved with new holder"); // STEP 5: กำหนด position ใหม่ console.log( - "[OfficerProfileService] STEP 5: Determining position to assign", + "[ExecuteOfficerProfileService] STEP 5: Determining position to assign", ); // Match position ตามลำดับ priority: // Condition 1: match จาก positionId @@ -1100,7 +1100,7 @@ export class OfficerProfileService { // CONDITION 1: เช็คจาก positionId ตรง // ═══════════════════════════════════════════════════════════ console.log( - "[OfficerProfileService] CONDITION 1: Checking by positionId:", + "[ExecuteOfficerProfileService] CONDITION 1: Checking by positionId:", item.bodyPosition?.positionId, ); if (item.bodyPosition?.positionId) { @@ -1115,7 +1115,7 @@ export class OfficerProfileService { if (positionById) { positionNew = positionById; console.log( - "[OfficerProfileService] CONDITION 1 matched, positionId:", + "[ExecuteOfficerProfileService] CONDITION 1 matched, positionId:", positionById.id, ); } @@ -1126,7 +1126,7 @@ export class OfficerProfileService { // ═══════════════════════════════════════════════════════════ if (!positionNew && item.bodyPosition) { console.log( - "[OfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", + "[ExecuteOfficerProfileService] CONDITION 1 not matched, trying CONDITION 2: Match 7 fields", ); // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่ไม่ใช่ null const whereCondition: any = { @@ -1158,7 +1158,7 @@ export class OfficerProfileService { if (positionBy7Fields) { positionNew = positionBy7Fields; console.log( - "[OfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", + "[ExecuteOfficerProfileService] CONDITION 2 matched with 7 fields, positionId:", positionBy7Fields.id, ); } @@ -1169,7 +1169,7 @@ export class OfficerProfileService { // ═══════════════════════════════════════════════════════════ if (!positionNew && item.bodyPosition) { console.log( - "[OfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", + "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", ); const positionBy3Fields = await this.positionRepository.findOne({ where: { @@ -1185,12 +1185,12 @@ export class OfficerProfileService { if (positionBy3Fields) { positionNew = positionBy3Fields; console.log( - "[OfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", + "[ExecuteOfficerProfileService] CONDITION 3 matched with 3 fields, positionId:", positionBy3Fields.id, ); } else { console.log( - "[OfficerProfileService] No position matched for profileId:", + "[ExecuteOfficerProfileService] No position matched for profileId:", profile.id, ); } @@ -1222,7 +1222,7 @@ export class OfficerProfileService { // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { console.log( - "[OfficerProfileService] Final position assignment, isSit:", + "[ExecuteOfficerProfileService] Final position assignment, isSit:", posMaster.isSit, "positionId:", positionNew.id, @@ -1242,7 +1242,7 @@ export class OfficerProfileService { } else if (!posMaster.isSit) { // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา console.log( - "[OfficerProfileService] positionNew is null, using bodyPosition data as fallback", + "[ExecuteOfficerProfileService] positionNew is null, using bodyPosition data as fallback", ); profile.position = item.bodyPosition.positionName ?? null; profile.posTypeId = item.bodyPosition.posTypeId ?? null; @@ -1261,7 +1261,7 @@ export class OfficerProfileService { // Insignia if (_oldInsigniaIds.length > 0) { console.log( - "[OfficerProfileService] Processing old insignias, count:", + "[ExecuteOfficerProfileService] Processing old insignias, count:", _oldInsigniaIds.length, ); const _insignias = await this.insigniaRepo.find({ @@ -1299,7 +1299,7 @@ export class OfficerProfileService { // เพิ่มรูปภาพโปรไฟล์ if (item.bodyProfile.objectRefId) { console.log( - "[OfficerProfileService] Processing profile avatar image, objectRefId:", + "[ExecuteOfficerProfileService] Processing profile avatar image, objectRefId:", item.bodyProfile.objectRefId, ); const _profileAvatar = new ProfileAvatar(); @@ -1345,6 +1345,6 @@ export class OfficerProfileService { } }), ); - console.log("[OfficerProfileService] executeCreateOfficerProfile completed successfully"); + console.log("[ExecuteOfficerProfileService] executeCreateOfficerProfile completed successfully"); } } diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts new file mode 100644 index 00000000..1fd5a54a --- /dev/null +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -0,0 +1,431 @@ +import { Double } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { Command } from "../entities/Command"; +import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; +import { logPositionIsSelectedChange, setLogDataDiff } from "../interfaces/utils"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-current + * (C-PM-03, 04, 05, 06, 07, 39, 47 — เปลี่ยนตำแหน่งปัจจุบันของข้าราชการ + salary ใหม่) + */ +export interface SalaryCurrentItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionExecutive: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + positionType: string | null; + positionLevel: string | null; + positionTypeId?: string | null; + positionLevelId?: string | null; + posmasterId: string; + positionId: string; + posExecutiveId?: string | null; + positionField?: string | null; + commandId?: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryCurrentExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) + * + * ใช้กับ commandType: C-PM-03, 04, 05, 06, 07, 39, 47 + * + * - endpoint /org/command/excexute/salary-current เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateCurrent ต้นฉบับ + */ +export class ExecuteSalaryCurrentService { + 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 positionRepository = AppDataSource.getRepository(Position); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + /** + * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของข้าราชการ + */ + async executeSalaryCurrent( + data: SalaryCurrentItem[], + ctx: SalaryCurrentExecutionContext, + ): Promise { + console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); + console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + await Promise.all( + data.map(async (item) => { + const profile: any = await this.profileRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + let _null: any = null; + const dest_item = await this.salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + + 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(), + }; + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + 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.commandId = item.commandId ?? _null; + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history, { data: req }); + + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา + let posMaster = await this.posMasterRepository.findOne({ + where: { id: item.posmasterId }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await this.posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + } + + if (posMaster == null) { + console.error( + `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + } + + const posMasterOld = await this.posMasterRepository.findOne({ + where: { + current_holderId: item.profileId, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + + const positionOld = await this.positionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { + posMasterId: posMasterOld?.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_reset_old_position", + }); + + positionOld.positionIsSelected = false; + await this.positionRepository.save(positionOld); + } + + const checkPosition = await this.positionRepository.find({ + where: { + posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + console.log( + `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${ctx.user.sub}, endpoint: updateMaster)`, + ); + + const clearPosition = checkPosition.map((positions) => { + logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { + posMasterId: posMaster!.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_clear_positions", + }); + + return { + ...positions, + positionIsSelected: false, + }; + }); + await this.positionRepository.save(clearPosition); + } + + posMaster.current_holderId = item.profileId; + 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); + + // STEP 2: กำหนด position ใหม่ + // 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; + + // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel + const posTypeId = item.positionTypeId || item.positionType; + const posLevelId = item.positionLevelId || item.positionLevel; + + // ═══════════════════════════════════════════════════════════ + // 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.positionName && posTypeId && posLevelId) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }; + + // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) + 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.positionName && posTypeId && posLevelId) { + const positionBy3Fields = await this.positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + } + } + + // // ═══════════════════════════════════════════════════════════ + // // 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]; + // } + // } + + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (positionNew != null) { + positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + 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.amount = item.amount ?? null; + profile.amountSpecial = item.amountSpecial ?? null; + await this.profileRepository.save(profile); + await this.positionRepository.save(positionNew); + } + await CreatePosMasterHistoryOfficer(posMaster.id, req); + }), + ); + + console.log("[ExecuteSalaryCurrentService] executeSalaryCurrent completed successfully"); + } +} diff --git a/src/services/ExecuteSalaryEmployeeCurrentService.ts b/src/services/ExecuteSalaryEmployeeCurrentService.ts new file mode 100644 index 00000000..919144c1 --- /dev/null +++ b/src/services/ExecuteSalaryEmployeeCurrentService.ts @@ -0,0 +1,262 @@ +import { Double } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import HttpStatus from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { EmployeePosition } from "../entities/EmployeePosition"; +import { Command } from "../entities/Command"; +import { setLogDataDiff } from "../interfaces/utils"; +import { CreatePosMasterHistoryEmployee } from "./PositionService"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-employee-current + * (C-PM-22, 24 — เปลี่ยนตำแหน่งปัจจุบันของลูกจ้าง + salary ใหม่) + */ +export interface SalaryEmployeeCurrentItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionType: string | null; + positionLevel: string | null; + posmasterId: string; + positionId: string; + commandId?: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryEmployeeCurrentExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ของลูกจ้าง + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) + * + * ใช้กับ commandType: C-PM-22, 24 + * + * - endpoint /org/command/excexute/salary-employee-current เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateCurrent ต้นฉบับ + */ +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 + อัปเดตตำแหน่งปัจจุบันของลูกจ้าง + */ + async executeSalaryEmployeeCurrent( + data: SalaryEmployeeCurrentItem[], + ctx: SalaryEmployeeCurrentExecutionContext, + ): Promise { + console.log("[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent"); + console.log("[ExecuteSalaryEmployeeCurrentService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + 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 ดังกล่าว"); + } + + 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 }); + + await this.salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history, { data: req }); + + const posMaster = await this.employeePosMasterRepository.findOne({ + where: { id: item.posmasterId }, + relations: ["orgRoot"], + }); + if (posMaster == null) + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + + 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; + + const positionOld = await this.employeePositionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await this.employeePositionRepository.save(positionOld); + } + + 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); + } + + 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); + }), + ); + + console.log("[ExecuteSalaryEmployeeCurrentService] executeSalaryEmployeeCurrent completed successfully"); + } +} diff --git a/src/services/ExecuteSalaryService.ts b/src/services/ExecuteSalaryService.ts new file mode 100644 index 00000000..bbdaa3a4 --- /dev/null +++ b/src/services/ExecuteSalaryService.ts @@ -0,0 +1,327 @@ +import { Double } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { ProfileAssistance } from "../entities/ProfileAssistance"; +import { ProfileAssistanceHistory } from "../entities/ProfileAssistanceHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { PosMaster } from "../entities/PosMaster"; +import { Command } from "../entities/Command"; +import { + checkCommandType, + removePostMasterAct, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary + * (C-PM-13 โอน, C-PM-15 ช่วยราชการ, C-PM-16 เกษียณ/ปลดเกษียณ) + */ +export interface SalaryItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionExecutive: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + positionType: string | null; + positionLevel: string | null; + commandId?: string | null; + leaveReason?: string | null; + dateLeave?: Date | string | null; + isLeave?: boolean; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + officerOrg?: string | null; + dateStart?: Date | string | null; + dateEnd?: Date | string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + refId?: string | null; +} + +/** + * Context สำหรับ audit/log (เหมือน ExecuteOfficerProfileService) + */ +export interface SalaryExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ + * + * ใช้กับ commandType: C-PM-13 (โอน), C-PM-15 (ช่วยราชการ), C-PM-16 (เกษียณ) + * + * - endpoint /org/command/excexute/salary เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdate ต้นฉบับ + */ +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 + */ + async executeSalary(data: SalaryItem[], ctx: SalaryExecutionContext): Promise { + console.log("[ExecuteSalaryService] Starting executeSalary"); + console.log("[ExecuteSalaryService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.dateStart = toDate(it.dateStart); + it.dateEnd = toDate(it.dateEnd); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + relations: ["commandType"], + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + 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); + } + + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + 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, + 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) { + await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE"); + await removeProfileInOrganize(profile.id, "OFFICER"); + } + 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] executeSalary completed successfully"); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index d2fd4def..bf684c77 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -29,7 +29,10 @@ import { sendWebSocket } from "./webSocket"; import { PayloadSendNoti } from "../interfaces/utils"; import { PermissionProfile } from "../entities/PermissionProfile"; import { PosMasterHistory } from "../entities/PosMasterHistory"; -import { OfficerProfileService } from "./OfficerProfileService"; +import { ExecuteOfficerProfileService } from "./ExecuteOfficerProfileService"; +import { ExecuteSalaryService } from "./ExecuteSalaryService"; +import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService"; +import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -323,13 +326,22 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // ───────────────────────────────────────────────────────────── // Linear Flow - // ทดสอบคำสั่ง "C-PM-01", "C-PM-02", "C-PM-14" - // รับ resultData จาก .NET แล้วเรียก OfficerProfileService ตรงๆ ไม่ผ่าน HTTP loopback + // รับ resultData จาก .NET แล้วเรียก Service ตรงๆ ตาม commandType (ไม่ผ่าน HTTP loopback) + // - ExecuteOfficerProfileService : C-PM-01, 02, 14 (บรรจุ/รับโอน) + // - ExecuteSalaryCurrentService : C-PM-03, 04, 05, 06, 07, 39, 47 (แต่งตั้ง-เลื่อน-ย้าย) + // - ExecuteSalaryEmployeeCurrent : C-PM-22, 24 (ลูกจ้าง ปรับระดับชั้นงาน-ย้าย) + // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) + // - คำสั่งอื่น ยังใช้ Circular Flow เดิม // ───────────────────────────────────────────────────────────── - const isLinearFlow = ["C-PM-01", "C-PM-02", "C-PM-14"].includes(command.commandType?.code); + const code = command.commandType?.code; + const isOfficerProfile = ["C-PM-01", "C-PM-02", "C-PM-14"].includes(code); + const isSalaryCurrent = ["C-PM-03", "C-PM-04", "C-PM-05", "C-PM-06", "C-PM-07", "C-PM-39", "C-PM-47"].includes(code); + const isSalaryEmployeeCurrent = ["C-PM-22", "C-PM-24"].includes(code); + const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code); + const isLinearFlow = isOfficerProfile || isSalaryCurrent || isSalaryEmployeeCurrent || isSalary; if (isLinearFlow) { - console.log(`[AMQ] Linear Flow`); + console.log(`[AMQ] Linear Flow (${code})`); let resultData: any[] = []; for (const chunk of chunks) { @@ -346,9 +358,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } } - console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${command.commandType?.code})`); + console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`); - // เรียก OfficerProfileService + // Route ไป service ที่ถูกต้องตาม commandType if (resultData.length > 0) { // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) const pseudoReq = { @@ -360,11 +372,22 @@ async function handler(msg: amqp.ConsumeMessage): Promise { req: pseudoReq, }; - await new OfficerProfileService().executeCreateOfficerProfile(resultData, ctx); - console.log(`[AMQ] Processed ${resultData.length} profiles via OfficerProfileService`); + if (isOfficerProfile) { + await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); + } else if (isSalaryCurrent) { + await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`); + } else if (isSalaryEmployeeCurrent) { + await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); + } else if (isSalary) { + await new ExecuteSalaryService().executeSalary(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryService`); + } } } else { - console.log(`[AMQ] Circular Flow`); + console.log(`[AMQ] Circular Flow (${code})`); for (const chunk of chunks) { await new CallAPI().PostData( { headers: { authorization: token } }, From d33e818ce80b1603a53e658d1e46b60285faef2f Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 19 Jun 2026 09:25:18 +0700 Subject: [PATCH 28/39] Linear Flow (RetirementService) #224 --- src/controllers/CommandController.ts | 718 +----------------- .../ExecuteSalaryEmployeeLeaveService.ts | 321 ++++++++ src/services/ExecuteSalaryLeaveService.ts | 628 +++++++++++++++ src/services/rabbitmq.ts | 30 +- 4 files changed, 989 insertions(+), 708 deletions(-) create mode 100644 src/services/ExecuteSalaryEmployeeLeaveService.ts create mode 100644 src/services/ExecuteSalaryLeaveService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 5b324311..1ea05633 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -108,6 +108,8 @@ import { ExecuteOfficerProfileService } from "../services/ExecuteOfficerProfileS import { ExecuteSalaryService } from "../services/ExecuteSalaryService"; import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; +import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; +import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; import { promisify } from "util"; const REDIS_HOST = process.env.REDIS_HOST; const REDIS_PORT = process.env.REDIS_PORT; @@ -3748,6 +3750,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary ข้าราชการ + handle leave/กลับเข้าราชการ + * + * Thin wrapper — เรียก ExecuteSalaryLeaveService (C-PM-08, 09, 17, 18, 41, 48) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API สร้าง ProfileSalary ข้าราชการ + leave/กลับเข้าราชการ + */ @Post("excexute/salary-leave") public async newSalaryAndUpdateLeave( @Request() req: RequestWithUser, @@ -3804,496 +3814,10 @@ export class CommandController extends Controller { }[]; }, ) { - const roleKeycloak = await this.roleKeycloakRepo.findOne({ - where: { name: Like("USER") }, + await new ExecuteSalaryLeaveService().executeSalaryLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - relations: ["commandType"], - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: { - roleKeycloaks: true - }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - removePostMasterAct(profile.id); - } - //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก - else if (item.resignId && code && ["C-PM-41"].includes(code)) { - const commandResign = await this.commandReciveRepository.findOne({ - where: { refId: item.resignId }, - relations: { command: true }, - }); - const executeDate = commandResign - ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) - : today; - if ( - commandResign && - _command.status !== "REPORTED" && - (_command.status !== "WAITING" || today < executeDate) - ) { - await reOrderCommandRecivesAndDelete(commandResign!.id); - } - } - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const returnWork = await checkReturnCommandType(String(item.commandId)); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - data.dateGovernment = item.commandDateAffect ?? new Date(); - data.order = dest_item == null ? 1 : dest_item.order + 1; - const meta = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - if (!returnWork) { - Object.assign(data, { ...item, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.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 = req.user.sub; - profile.lastUpdateFullName = req.user.name; - profile.lastUpdatedAt = new Date(); - const clearProfile = await checkCommandType(String(item.commandId)); - - //ปั๊มประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - if (curRevision) { - const curPosMaster = await this.posMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: curRevision.id, - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - orgRootRef = curPosMaster?.orgRoot ?? null; - orgChild1Ref = curPosMaster?.orgChild1 ?? null; - orgChild2Ref = curPosMaster?.orgChild2 ?? null; - orgChild3Ref = curPosMaster?.orgChild3 ?? null; - orgChild4Ref = curPosMaster?.orgChild4 ?? null; - if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { - await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); - } - } - - //ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "OFFICER"); - } - if (clearProfile.status) { - if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { - const delUserKeycloak = await deleteUser(profile.keycloak); - if (delUserKeycloak) { - // Task #228 - // profile.keycloak = _null; - profile.roleKeycloaks = []; - profile.isActive = false; - profile.isDelete = true; - } - } - profile.leaveCommandId = item.commandId ?? _null; - profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; - profile.leaveRemark = clearProfile.leaveRemark ?? _null; - profile.leaveDate = item.commandDateAffect ?? _null; - profile.leaveType = clearProfile.LeaveType ?? _null; - //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) - // profile.position = _null; - // profile.posTypeId = _null; - // profile.posLevelId = _null; - } - - if (item.isGovernment == true) { - if (returnWork) { - //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ - await removeProfileInOrganize(profile.id, "OFFICER"); - //ปั๊มตำแหน่งใหม่ - // หา posMaster และเช็ค orgRevisionIsCurrent - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId?.toString() }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster) { - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.positionRepository.save(clearPosition); - } - posMaster.current_holderId = profile.id; - posMaster.lastUpdatedAt = new Date(); - // posMaster.conditionReason = _null; - // posMaster.isCondition = false; - await this.posMasterRepository.save(posMaster); - - // Match position ตามลำดับ priority: - // Condition 1: match จาก positionId - // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) - // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) - // Fallback: เลือก position แรกใน posMaster - - let positionNew: Position | null = null; - - // ═══════════════════════════════════════════════════════════ - // CONDITION 1: เช็คจาก positionId ตรง - // ═══════════════════════════════════════════════════════════ - if (item.positionId) { - const positionById = await this.positionRepository.findOne({ - where: { - id: item.positionId, - posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง - }, - relations: ["posExecutive"], - }); - - if (positionById) { - positionNew = positionById; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }; - - if (item.positionField) { - whereCondition.positionField = item.positionField; - } - if (item.posExecutiveId) { - whereCondition.posExecutiveId = item.posExecutiveId; - } - if (item.positionExecutiveField) { - whereCondition.positionExecutiveField = item.positionExecutiveField; - } - if (item.positionArea) { - whereCondition.positionArea = item.positionArea; - } - - const positionBy7Fields = await this.positionRepository.findOne({ - where: whereCondition, - relations: ["posExecutive"], - order: { orderNo: "ASC" } - }); - - if (positionBy7Fields) { - positionNew = positionBy7Fields; - } - } - - // ═══════════════════════════════════════════════════════════ - // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) - // ═══════════════════════════════════════════════════════════ - if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionNameNew, - posTypeId: item.positionTypeNew, - posLevelId: item.positionLevelNew, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" } - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) - // if (!positionNew) { - // const fallbackPositions = await this.positionRepository.find({ - // where: { - // posMasterId: posMaster.id, - // }, - // relations: ["posExecutive"], - // order: { - // orderNo: "ASC", - // }, - // take: 1, - // }); - - // if (fallbackPositions.length > 0) { - // positionNew = fallbackPositions[0]; - // } - // } - - if (positionNew) { - positionNew.positionIsSelected = true; - await this.positionRepository.save(positionNew, { data: req }); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - } - const newMapProfileSalary = { - profileId: profile.id, - commandId: item.commandId, - positionName: item.positionNameNew ?? null, - positionType: item.posTypeNameNew ?? null, - positionLevel: item.posLevelNameNew ?? null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - posNo: item.posNoNew, - posNoAbb: item.posNoAbbNew, - orgRoot: item.orgRootNew, - orgChild1: item.orgChild1New, - orgChild2: item.orgChild2New, - orgChild3: item.orgChild3New, - orgChild4: item.orgChild4New, - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, { ...newMapProfileSalary, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data); - history.profileSalaryId = data.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 gregorianYear = profile.birthDate.getFullYear() + 543; - - // const formattedDate = - // profile.birthDate.toISOString().slice(8, 10) + - // profile.birthDate.toISOString().slice(5, 7) + - // gregorianYear; - // password = formattedDate; - const _date = new Date(profile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak - const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; - userKeycloakId = await createUser(profile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: profile.lastName, - }); - const list = await getRoles(); - let result = false; - if (Array.isArray(list) && userKeycloakId) { - result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - } - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - profile.keycloak = - userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; - } - //ถ้ามีอยู่แล้วให้ใช้อันเดิม - else { - const rolesData = await getRoleMappings(checkUser[0].id); - if (rolesData) { - const _roleKeycloak = await this.roleKeycloakRepo.find({ - where: { name: In(rolesData.map((x: any) => x.name)) }, - }); - profile.roleKeycloaks = - _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; - } - profile.keycloak = checkUser[0].id; - } - profile.amount = item.amount ?? _null; - profile.amountSpecial = item.amountSpecial ?? _null; - profile.isActive = true; - profile.isDelete = false; - } - await this.profileRepository.save(profile); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - - // update user attribute in keycloak - await updateUserAttributes(profile.keycloak ?? "", { - profileId: [profile.id], - prefix: [profile.prefix || ""], - }); - - // Task #2190 - if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - - } - }), - ); - return new HttpSuccess(); } @@ -4337,223 +3861,13 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, - relations: { commandType: true }, + await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const today = new Date().setHours(0, 0, 0, 0); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.profileId }, - // relations: ["roleKeycloaks"], - 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 data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { - ...item, - ...meta, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - data.dateGovernment = item.commandDateAffect ?? meta.createdAt; - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.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 = req.user.sub; - profile.lastUpdateFullName = req.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"); - } - } - - // ลบตำแหน่ง - if (item.isLeave == true) { - await removeProfileInOrganize(profile.id, "EMPLOYEE"); - } - - 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", - // ); - // } - - // 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(" "); - } - } - }), - ); - return new HttpSuccess(); } - /** - * API สร้าง ProfileSalary ข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ - * - * Thin wrapper — เรียก ExecuteSalaryService (C-PM-13, 15, 16) - * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) - * - * @summary API สร้าง ProfileSalary ข้าราชการ (leave/โอน/ช่วยราชการ) - */ @Post("excexute/salary") public async newSalaryAndUpdate( @Request() req: RequestWithUser, diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts new file mode 100644 index 00000000..bfb1af4c --- /dev/null +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -0,0 +1,321 @@ +import { Double } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatus from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { CommandRecive } from "../entities/CommandRecive"; +import { Command } from "../entities/Command"; +import { checkCommandType, removeProfileInOrganize, setLogDataDiff } from "../interfaces/utils"; +import { reOrderCommandRecivesAndDelete } from "./CommandService"; +import { CreatePosMasterHistoryEmployee } from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-employee-leave + * (C-PM-23, 42, 43 — ลาออก/ยกเลิกลาออก/กลับเข้าราชการ ของลูกจ้าง) + */ +export interface SalaryEmployeeLeaveItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionType: string | null; + positionLevel: string | null; + isLeave: boolean; + leaveReason?: string | null; + dateLeave?: Date | string | null; + isGovernment?: boolean | null; + commandId?: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + positionExecutive?: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + resignId: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryEmployeeLeaveExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ลูกจ้าง + handle leave/กลับเข้าราชการ + * + * ใช้กับ commandType: C-PM-23, 42, 43 + * + * - endpoint /org/command/excexute/salary-employee-leave เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateLeave ต้นฉบับ + */ +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 ของลูกจ้าง + */ + async executeSalaryEmployeeLeave( + data: SalaryEmployeeLeaveItem[], + ctx: SalaryEmployeeLeaveExecutionContext, + ): Promise { + console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave"); + console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + relations: { commandType: true }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + const today = new Date().setHours(0, 0, 0, 0); + await Promise.all( + data.map(async (item) => { + const profile = await this.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"); + } + } + + // ลบตำแหน่ง + if (item.isLeave == true) { + await removeProfileInOrganize(profile.id, "EMPLOYEE"); + } + + 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", + // ); + // } + + // 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] executeSalaryEmployeeLeave completed successfully"); + } +} diff --git a/src/services/ExecuteSalaryLeaveService.ts b/src/services/ExecuteSalaryLeaveService.ts new file mode 100644 index 00000000..04d6c32c --- /dev/null +++ b/src/services/ExecuteSalaryLeaveService.ts @@ -0,0 +1,628 @@ +import { Double, In, Like } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { RoleKeycloak } from "../entities/RoleKeycloak"; +import { CommandRecive } from "../entities/CommandRecive"; +import { Command } from "../entities/Command"; +import { + checkCommandType, + checkReturnCommandType, + removePostMasterAct, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { reOrderCommandRecivesAndDelete } from "./CommandService"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { getOrgFullName, getPosMasterNo } from "../utils/org-formatting"; +import { + addUserRoles, + createUser, + deleteUser, + getRoleMappings, + getRoles, + getUserByUsername, + updateUserAttributes, +} from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave + * (C-PM-08, 09, 17, 18, 41, 48 — ลาออก/พักราชการ/กลับเข้าราชการ ของข้าราชการ) + */ +export interface SalaryLeaveItem { + profileId: string; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + positionExecutive: string | null; + positionExecutiveField?: string | null; + positionArea?: string | null; + positionType: string | null; + positionLevel: string | null; + isLeave: boolean; + leaveReason?: string | null; + dateLeave?: Date | string | null; + posExecutiveId?: string | null; + positionField?: string | null; + commandId?: string | null; + isGovernment?: boolean | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + commandNo: string | null; + commandYear: number | null; + posNo: string | null; + posNoAbb: string | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + positionId?: string | null; + positionTypeNew?: string | null; + positionLevelNew?: string | null; + positionNameNew?: string | null; + posmasterId?: string | null; + posTypeNameNew?: string | null; + posLevelNameNew?: string | null; + posNoNew?: string | null; + posNoAbbNew?: string | null; + orgRootNew?: string | null; + orgChild1New?: string | null; + orgChild2New?: string | null; + orgChild3New?: string | null; + orgChild4New?: string | null; + resignId?: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryLeaveExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary ข้าราชการ + handle leave/กลับเข้าราชการ + * + * ใช้กับ commandType: C-PM-08, 09, 17, 18, 41, 48 + * + * - endpoint /org/command/excexute/salary-leave เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeave ต้นฉบับ + */ +export class ExecuteSalaryLeaveService { + private commandRepository = AppDataSource.getRepository(Command); + private commandReciveRepository = AppDataSource.getRepository(CommandRecive); + private profileRepository = AppDataSource.getRepository(Profile); + private salaryRepo = AppDataSource.getRepository(ProfileSalary); + private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory); + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private positionRepository = AppDataSource.getRepository(Position); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepo = AppDataSource.getRepository(OrgRevision); + private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + + /** + * ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการ + */ + async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise { + console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave"); + console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length); + + const req = ctx.req; + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + const roleKeycloak = await this.roleKeycloakRepo.findOne({ + where: { name: Like("USER") }, + }); + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + relations: ["commandType"], + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + const today = new Date().setHours(0, 0, 0, 0); + await Promise.all( + data.map(async (item) => { + const profile = await this.profileRepository.findOne({ + where: { id: item.profileId }, + relations: { + roleKeycloaks: true, + }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + //ลบตำแหน่งที่รักษาการแทน + const code = _command?.commandType?.code; + if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + removePostMasterAct(profile.id); + } + //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก + else if (item.resignId && code && ["C-PM-41"].includes(code)) { + const commandResign = await this.commandReciveRepository.findOne({ + where: { refId: item.resignId }, + relations: { command: true }, + }); + const executeDate = commandResign + ? new Date(commandResign.command.commandExcecuteDate).setHours(0, 0, 0, 0) + : today; + if ( + commandResign && + _command.status !== "REPORTED" && + (_command.status !== "WAITING" || today < executeDate) + ) { + await reOrderCommandRecivesAndDelete(commandResign!.id); + } + } + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + const returnWork = await checkReturnCommandType(String(item.commandId)); + const dest_item = await this.salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? new Date(); + dataSalary.order = dest_item == null ? 1 : dest_item.order + 1; + const meta = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + if (!returnWork) { + Object.assign(dataSalary, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await this.salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history, { data: req }); + } + const _null: any = null; + profile.isLeave = item.isLeave; + profile.leaveReason = item.leaveReason ?? _null; + profile.dateLeave = item.dateLeave ?? _null; + profile.lastUpdateUserId = ctx.user.sub; + profile.lastUpdateFullName = ctx.user.name; + profile.lastUpdatedAt = new Date(); + const clearProfile = await checkCommandType(String(item.commandId)); + + //ปั๊มประวัติก่อนลบตำแหน่ง + const curRevision = await this.orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + let orgRootRef = null; + let orgChild1Ref = null; + let orgChild2Ref = null; + let orgChild3Ref = null; + let orgChild4Ref = null; + if (curRevision) { + const curPosMaster = await this.posMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: curRevision.id, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + orgRootRef = curPosMaster?.orgRoot ?? null; + orgChild1Ref = curPosMaster?.orgChild1 ?? null; + orgChild2Ref = curPosMaster?.orgChild2 ?? null; + orgChild3Ref = curPosMaster?.orgChild3 ?? null; + orgChild4Ref = curPosMaster?.orgChild4 ?? null; + if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { + await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE"); + } + } + + //ลบตำแหน่ง + if (item.isLeave == true) { + await removeProfileInOrganize(profile.id, "OFFICER"); + } + if (clearProfile.status) { + if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { + const delUserKeycloak = await deleteUser(profile.keycloak); + if (delUserKeycloak) { + // Task #228 + // profile.keycloak = _null; + profile.roleKeycloaks = []; + profile.isActive = false; + profile.isDelete = true; + } + } + profile.leaveCommandId = item.commandId ?? _null; + profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`; + profile.leaveRemark = clearProfile.leaveRemark ?? _null; + profile.leaveDate = item.commandDateAffect ?? _null; + profile.leaveType = clearProfile.LeaveType ?? _null; + //ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516) + // profile.position = _null; + // profile.posTypeId = _null; + // profile.posLevelId = _null; + } + + if (item.isGovernment == true) { + if (returnWork) { + //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ + await removeProfileInOrganize(profile.id, "OFFICER"); + //ปั๊มตำแหน่งใหม่ + // หา posMaster และเช็ค orgRevisionIsCurrent + let posMaster = await this.posMasterRepository.findOne({ + where: { id: item.posmasterId?.toString() }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await this.posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + } + + if (posMaster) { + const checkPosition = await this.positionRepository.find({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await this.positionRepository.save(clearPosition); + } + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + await this.posMasterRepository.save(posMaster); + + // Match position ตามลำดับ priority: + // Condition 1: match จาก positionId + // Condition 2: match 7 ฟิลด์ (positionName, posTypeId, posLevelId, positionField, positionArea, positionExecutiveField, posExecutiveId) + // Condition 3: match 3 ฟิลด์ (positionName, posTypeId, posLevelId) + // Fallback: เลือก position แรกใน posMaster + + let positionNew: Position | null = null; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + if (item.positionId) { + const positionById = await this.positionRepository.findOne({ + where: { + id: item.positionId, + posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง + }, + relations: ["posExecutive"], + }); + + if (positionById) { + positionNew = positionById; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 2: Match 7 ฟิลด์ (ถ้า Condition 1 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, + }; + + if (item.positionField) { + whereCondition.positionField = item.positionField; + } + if (item.posExecutiveId) { + whereCondition.posExecutiveId = item.posExecutiveId; + } + if (item.positionExecutiveField) { + whereCondition.positionExecutiveField = item.positionExecutiveField; + } + if (item.positionArea) { + whereCondition.positionArea = item.positionArea; + } + + const positionBy7Fields = await this.positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { + const positionBy3Fields = await this.positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionNameNew, + posTypeId: item.positionTypeNew, + posLevelId: item.positionLevelNew, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + } + } + + // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) + // if (!positionNew) { + // const fallbackPositions = await this.positionRepository.find({ + // where: { + // posMasterId: posMaster.id, + // }, + // relations: ["posExecutive"], + // order: { + // orderNo: "ASC", + // }, + // take: 1, + // }); + + // if (fallbackPositions.length > 0) { + // positionNew = fallbackPositions[0]; + // } + // } + + if (positionNew) { + positionNew.positionIsSelected = true; + await this.positionRepository.save(positionNew, { data: req }); + } + await CreatePosMasterHistoryOfficer(posMaster.id, req); + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + } + const newMapProfileSalary = { + profileId: profile.id, + commandId: item.commandId, + positionName: item.positionNameNew ?? null, + positionType: item.posTypeNameNew ?? null, + positionLevel: item.posLevelNameNew ?? null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + posNo: item.posNoNew, + posNoAbb: item.posNoAbbNew, + orgRoot: item.orgRootNew, + orgChild1: item.orgChild1New, + orgChild2: item.orgChild2New, + orgChild3: item.orgChild3New, + orgChild4: item.orgChild4New, + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + await this.salaryRepo.save(dataSalary); + history.profileSalaryId = dataSalary.id; + await this.salaryHistoryRepo.save(history); + profile.leaveReason = _null; + profile.leaveCommandId = _null; + profile.leaveCommandNo = _null; + profile.leaveRemark = _null; + profile.leaveDate = _null; + profile.leaveType = _null; + profile.position = item.positionNameNew ?? _null; + profile.posTypeId = item.positionTypeNew ?? _null; + profile.posLevelId = item.positionLevelNew ?? _null; + } + let userKeycloakId; + const checkUser = await getUserByUsername(profile.citizenId); + //ถ้ายังไม่มี user keycloak ให้สร้างใหม่ + if (checkUser.length == 0) { + let password = profile.citizenId; + if (profile.birthDate != null) { + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + userKeycloakId = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName, + }); + const list = await getRoles(); + let result = false; + if (Array.isArray(list) && userKeycloakId) { + result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + } + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + profile.keycloak = + userKeycloakId && typeof userKeycloakId === "string" ? userKeycloakId : ""; + } + //ถ้ามีอยู่แล้วให้ใช้อันเดิม + else { + const rolesData = await getRoleMappings(checkUser[0].id); + if (rolesData) { + const _roleKeycloak = await this.roleKeycloakRepo.find({ + where: { name: In(rolesData.map((x: any) => x.name)) }, + }); + profile.roleKeycloaks = + _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + } + profile.keycloak = checkUser[0].id; + } + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.isActive = true; + profile.isDelete = false; + } + await this.profileRepository.save(profile); + + // if (profile.id) { + // await this.keycloakAttributeService.clearOrgDnaAttributes( + // [profile.id], + // "PROFILE", + // ); + // } + + // update user attribute in keycloak + await updateUserAttributes(profile.keycloak ?? "", { + profileId: [profile.id], + prefix: [profile.prefix || ""], + }); + + // Task #2190 + if (code && ["C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + }), + ); + + console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully"); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index bf684c77..d84a3eed 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -33,6 +33,8 @@ import { ExecuteOfficerProfileService } from "./ExecuteOfficerProfileService"; import { ExecuteSalaryService } from "./ExecuteSalaryService"; import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; +import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; +import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -326,11 +328,13 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // ───────────────────────────────────────────────────────────── // Linear Flow - // รับ resultData จาก .NET แล้วเรียก Service ตรงๆ ตาม commandType (ไม่ผ่าน HTTP loopback) - // - ExecuteOfficerProfileService : C-PM-01, 02, 14 (บรรจุ/รับโอน) - // - ExecuteSalaryCurrentService : C-PM-03, 04, 05, 06, 07, 39, 47 (แต่งตั้ง-เลื่อน-ย้าย) - // - ExecuteSalaryEmployeeCurrent : C-PM-22, 24 (ลูกจ้าง ปรับระดับชั้นงาน-ย้าย) - // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) + // รับ resultData จาก .NET แล้วเรียก Service ตรงๆ ตาม commandType (ไม่ผ่าน HTTP loopback) + // - ExecuteOfficerProfileService : C-PM-01, 02, 14 (บรรจุ/รับโอน) + // - ExecuteSalaryCurrentService : C-PM-03, 04, 05, 06, 07, 39, 47 (แต่งตั้ง-เลื่อน-ย้าย) + // - ExecuteSalaryEmployeeCurrentService : C-PM-22, 24 (ลูกจ้าง ปรับระดับชั้นงาน-ย้าย) + // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) + // - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ) + // - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave) // - คำสั่งอื่น ยังใช้ Circular Flow เดิม // ───────────────────────────────────────────────────────────── const code = command.commandType?.code; @@ -338,7 +342,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isSalaryCurrent = ["C-PM-03", "C-PM-04", "C-PM-05", "C-PM-06", "C-PM-07", "C-PM-39", "C-PM-47"].includes(code); const isSalaryEmployeeCurrent = ["C-PM-22", "C-PM-24"].includes(code); const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code); - const isLinearFlow = isOfficerProfile || isSalaryCurrent || isSalaryEmployeeCurrent || isSalary; + const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code); + const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code); + const isLinearFlow = + isOfficerProfile || + isSalaryCurrent || + isSalaryEmployeeCurrent || + isSalary || + isSalaryLeave || + isSalaryEmployeeLeave; if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); @@ -384,6 +396,12 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } else if (isSalary) { await new ExecuteSalaryService().executeSalary(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryService`); + } else if (isSalaryLeave) { + await new ExecuteSalaryLeaveService().executeSalaryLeave(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveService`); + } else if (isSalaryEmployeeLeave) { + await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`); } } } else { From 41862b8dca0db9c6fef45ffe5c856583f7c47720 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 19 Jun 2026 18:13:03 +0700 Subject: [PATCH 29/39] fix api web services #62 --- src/controllers/ApiWebServiceController.ts | 24 +++++++++++++------- src/controllers/ProfileController.ts | 1 + src/controllers/ProfileEmployeeController.ts | 1 + 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/controllers/ApiWebServiceController.ts b/src/controllers/ApiWebServiceController.ts index fbf0ab78..52fcb3a3 100644 --- a/src/controllers/ApiWebServiceController.ts +++ b/src/controllers/ApiWebServiceController.ts @@ -161,39 +161,47 @@ export class ApiWebServiceController extends Controller { ); } else if (dnaIds.dnaChild3Id) { conditions.push( - `${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, + accessType === "NORMAL" + ? `(${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}") AND ${tableAlias}.orgChild4Id IS NULL)` + : `${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild3Id}")`, ); // For CHILD type, include all descendants if (accessType === "CHILD") { conditions.push( - `(${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR ${tableAlias}.orgChild4Id IS NOT NULL)`, + `(${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%") OR ${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild3Id}%"))`, ); } } else if (dnaIds.dnaChild2Id) { conditions.push( - `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, + accessType === "NORMAL" + ? `(${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}") AND ${tableAlias}.orgChild3Id IS NULL AND ${tableAlias}.orgChild4Id IS NULL)` + : `${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild2Id}")`, ); if (accessType === "CHILD") { conditions.push( - `(${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR ${tableAlias}.orgChild3Id IS NOT NULL)`, + `(${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR ${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%") OR ${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild2Id}%"))`, ); } } else if (dnaIds.dnaChild1Id) { conditions.push( - `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, + accessType === "NORMAL" + ? `(${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}") AND ${tableAlias}.orgChild2Id IS NULL AND ${tableAlias}.orgChild3Id IS NULL AND ${tableAlias}.orgChild4Id IS NULL)` + : `${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaChild1Id}")`, ); if (accessType === "CHILD") { conditions.push( - `(${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild2Id IS NOT NULL)`, + `(${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%") OR ${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaChild1Id}%"))`, ); } } else if (dnaIds.dnaRootId) { conditions.push( - `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, + accessType === "NORMAL" + ? `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}") AND ${tableAlias}.orgChild1Id IS NULL AND ${tableAlias}.orgChild2Id IS NULL AND ${tableAlias}.orgChild3Id IS NULL AND ${tableAlias}.orgChild4Id IS NULL)` + : `${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA = "${dnaIds.dnaRootId}")`, ); if (accessType === "CHILD") { conditions.push( - `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IS NOT NULL)`, + `(${tableAlias}.orgRootId IN (SELECT id FROM orgRoot WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild1Id IN (SELECT id FROM orgChild1 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild2Id IN (SELECT id FROM orgChild2 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild3Id IN (SELECT id FROM orgChild3 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%") OR ${tableAlias}.orgChild4Id IN (SELECT id FROM orgChild4 WHERE orgRevisionId = "${this.currentRevisionId}" AND ancestorDNA LIKE "${dnaIds.dnaRootId}%"))`, ); } } diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index c6f1b68c..3b6e7689 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -7162,6 +7162,7 @@ export class ProfileController extends Controller { : `profile.isLeave IS TRUE` : "1=1", ) + .andWhere("profile.isActive IS TRUE AND profile.isDelete IS FALSE") .andWhere(nodeCondition, { nodeId: nodeId }) .andWhere( new Brackets((qb) => { diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 61efbdb7..104528ac 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -3338,6 +3338,7 @@ export class ProfileEmployeeController extends Controller { : `profileEmployee.isLeave IS TRUE` : "1=1", ) + .andWhere("profileEmployee.isActive IS TRUE AND profileEmployee.isDelete IS FALSE") .andWhere("profileEmployee.employeeClass LIKE :type", { type: "PERM", }) From bbc6a5e6a94407136f28ec5b8d52e9e75c75a942 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 22 Jun 2026 10:06:59 +0700 Subject: [PATCH 30/39] =?UTF-8?q?fix=20=E0=B8=81=E0=B8=B2=E0=B8=A3?= =?UTF-8?q?=E0=B8=84=E0=B9=89=E0=B8=99=E0=B8=AB=E0=B8=B2=20labelName=20#25?= =?UTF-8?q?73?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/OrganizationController.ts | 78 +++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 55f5ac93..8358313b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -1441,7 +1441,14 @@ export class OrganizationController extends Controller { orgRoot.orgRootCode + orgChild1.orgChild1Code + " " + - orgChild1.orgChild1ShortName, + orgChild1.orgChild1ShortName + + "/" + + orgRoot.orgRootName + + " " + + orgRoot.orgRootCode + + "00" + + " " + + orgRoot.orgRootShortName, // totalPosition: child1Counts.totalPosition, // totalPositionCurrentUse: child1Counts.totalPositionCurrentUse, // totalPositionCurrentVacant: child1Counts.totalPositionCurrentVacant, @@ -1491,7 +1498,21 @@ export class OrganizationController extends Controller { orgRoot.orgRootCode + orgChild2.orgChild2Code + " " + - orgChild2.orgChild2ShortName, + orgChild2.orgChild2ShortName + + "/" + + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName + + "/" + + orgRoot.orgRootName + + " " + + orgRoot.orgRootCode + + "00" + + " " + + orgRoot.orgRootShortName, // totalPosition: child2Counts.totalPosition, // totalPositionCurrentUse: child2Counts.totalPositionCurrentUse, // totalPositionCurrentVacant: child2Counts.totalPositionCurrentVacant, @@ -1542,7 +1563,28 @@ export class OrganizationController extends Controller { orgRoot.orgRootCode + orgChild3.orgChild3Code + " " + - orgChild3.orgChild3ShortName, + orgChild3.orgChild3ShortName + + "/" + + orgChild2.orgChild2Name + + " " + + orgRoot.orgRootCode + + orgChild2.orgChild2Code + + " " + + orgChild2.orgChild2ShortName + + "/" + + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName + + "/" + + orgRoot.orgRootName + + " " + + orgRoot.orgRootCode + + "00" + + " " + + orgRoot.orgRootShortName, // totalPosition: child3Counts.totalPosition, // totalPositionCurrentUse: child3Counts.totalPositionCurrentUse, // totalPositionCurrentVacant: child3Counts.totalPositionCurrentVacant, @@ -1595,7 +1637,35 @@ export class OrganizationController extends Controller { orgRoot.orgRootCode + orgChild4.orgChild4Code + " " + - orgChild4.orgChild4ShortName, + orgChild4.orgChild4ShortName + + "/" + + orgChild3.orgChild3Name + + " " + + orgRoot.orgRootCode + + orgChild3.orgChild3Code + + " " + + orgChild3.orgChild3ShortName + + "/" + + orgChild2.orgChild2Name + + " " + + orgRoot.orgRootCode + + orgChild2.orgChild2Code + + " " + + orgChild2.orgChild2ShortName + + "/" + + orgChild1.orgChild1Name + + " " + + orgRoot.orgRootCode + + orgChild1.orgChild1Code + + " " + + orgChild1.orgChild1ShortName + + "/" + + orgRoot.orgRootName + + " " + + orgRoot.orgRootCode + + "00" + + " " + + orgRoot.orgRootShortName, // totalPosition: child4Counts.totalPosition, // totalPositionCurrentUse: child4Counts.totalPositionCurrentUse, // totalPositionCurrentVacant: child4Counts.totalPositionCurrentVacant, From 00c35e8974ce0688af83e226221afc567888f63c Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 22 Jun 2026 13:32:26 +0700 Subject: [PATCH 31/39] =?UTF-8?q?fix=20=E0=B8=AA=E0=B9=88=E0=B8=87?= =?UTF-8?q?=E0=B8=A3=E0=B8=B2=E0=B8=A2=E0=B8=8A=E0=B8=B7=E0=B9=88=E0=B8=AD?= =?UTF-8?q?=E0=B9=84=E0=B8=9B=E0=B8=A2=E0=B8=B1=E0=B8=87=E0=B8=A3=E0=B8=B2?= =?UTF-8?q?=E0=B8=A2=E0=B8=81=E0=B8=B2=E0=B8=A3=E0=B8=AD=E0=B8=B7=E0=B9=88?= =?UTF-8?q?=E0=B8=99=20=E0=B9=86=20#2571?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/ProfileController.ts | 163 +++++++++++-------- src/controllers/ProfileEmployeeController.ts | 96 ++++++++++- 2 files changed, 181 insertions(+), 78 deletions(-) diff --git a/src/controllers/ProfileController.ts b/src/controllers/ProfileController.ts index 3b6e7689..05ab62f8 100644 --- a/src/controllers/ProfileController.ts +++ b/src/controllers/ProfileController.ts @@ -8726,14 +8726,10 @@ export class ProfileController extends Controller { "current_holders.orgChild2", "current_holders.orgChild3", "current_holders.orgChild4", - // "profileSalary", "profileEducations", "profileActpositions", ], order: { - // profileSalary: { - // order: "DESC", - // }, profileEducations: { level: "ASC", }, @@ -8798,72 +8794,6 @@ export class ProfileController extends Controller { }); const holder = profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id); const numPart = holder ? [holder.posMasterNoPrefix, holder.posMasterNo, holder.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; - const shortName = - holder == null - ? null - : holder.orgChild4 != null - ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` - : holder.orgChild3 != null - ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` - : holder.orgChild2 != null - ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` - : holder.orgChild1 != null - ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` - : holder.orgRoot != null - ? `${holder.orgRoot.orgRootShortName} ${numPart}` - : null; - // const posMasterActs = await this.posMasterActRepository.find({ - // relations: [ - // "posMaster", - // "posMaster.orgRoot", - // "posMaster.orgChild1", - // "posMaster.orgChild2", - // "posMaster.orgChild3", - // "posMaster.orgChild4", - // "posMaster.current_holder", - // "posMaster.current_holder.posLevel", - // "posMaster.current_holder.posType", - // ], - // where: { - // posMaster: { - // orgRevisionId: orgRevisionPublish.id, - // }, - // posMasterChild: { - // current_holderId: profile.id, - // }, - // }, - // }); - // const data = await Promise.all( - // posMasterActs - // .sort((a, b) => a.posMaster.posMasterOrder - b.posMaster.posMasterOrder) - // .map((item) => { - // const shortName = - // item.posMaster != null && item.posMaster.orgChild4 != null - // ? `${item.posMaster.orgChild4.orgChild4ShortName} ${item.posMaster.posMasterNo}` - // : item.posMaster != null && item.posMaster?.orgChild3 != null - // ? `${item.posMaster.orgChild3.orgChild3ShortName} ${item.posMaster.posMasterNo}` - // : item.posMaster != null && item.posMaster?.orgChild2 != null - // ? `${item.posMaster.orgChild2.orgChild2ShortName} ${item.posMaster.posMasterNo}` - // : item.posMaster != null && item.posMaster?.orgChild1 != null - // ? `${item.posMaster.orgChild1.orgChild1ShortName} ${item.posMaster.posMasterNo}` - // : item.posMaster != null && item.posMaster?.orgRoot != null - // ? `${item.posMaster.orgRoot.orgRootShortName} ${item.posMaster.posMasterNo}` - // : null; - // return { - // id: item.id, - // posMasterOrder: item.posMasterOrder, - // profileId: item.posMaster?.current_holder?.id ?? null, - // citizenId: item.posMaster?.current_holder?.citizenId ?? null, - // prefix: item.posMaster?.current_holder?.prefix ?? null, - // firstName: item.posMaster?.current_holder?.firstName ?? null, - // lastName: item.posMaster?.current_holder?.lastName ?? null, - // posLevel: item.posMaster?.current_holder?.posLevel?.posLevelName ?? null, - // posType: item.posMaster?.current_holder?.posType?.posTypeName ?? null, - // position: item.posMaster?.current_holder?.position ?? null, - // posNo: shortName, - // }; - // }), - // ); const data = await Promise.all( profile.profileActpositions .filter((x) => x.status) @@ -8910,6 +8840,96 @@ export class ProfileController extends Controller { }, }); + let _Org = null; + let _PosNo = null; + if (!profile.org && !profile.posMasterNo) { + if (profile.isLeave) { + const profileWithSalary = await this.profileRepo.findOne({ + where: { + id: id, + profileSalary: { + commandCode: In([ + "0", + "9", + "1", + "2", + "3", + "4", + "8", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "20", + ]), + }, + }, + relations: { profileSalary: true }, + order: { + profileSalary: { + order: "DESC", + createdAt: "DESC", + }, + }, + }); + const profileSalaryList = profileWithSalary?.profileSalary || []; + + if (profileSalaryList.length > 0) { + const _profileSalary = + profile.leaveType == "RETIRE" + ? profileSalaryList.length > 1 + ? profileSalaryList[1] + : profileSalaryList[0] + : profileSalaryList[0]; + + if (_profileSalary) { + const orgLeaveParts = [ + _profileSalary.orgChild4 ?? null, + _profileSalary.orgChild3 ?? null, + _profileSalary.orgChild2 ?? null, + _profileSalary.orgChild1 ?? null, + _profileSalary.orgRoot ?? null, + ]; + _Org = orgLeaveParts + .filter((x: any) => x !== undefined && x !== null) + .join("\n"); + _PosNo = `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`; + } + } + } else { + _Org = [ + child4?.orgChild4Name, + child3?.orgChild3Name, + child2?.orgChild2Name, + child1?.orgChild1Name, + root?.orgRootName, + ] + .filter((x) => x != null && x !== "") + .join("\n"); + + _PosNo = + holder == null + ? null + : holder.orgChild4 != null + ? `${holder.orgChild4.orgChild4ShortName} ${numPart}` + : holder.orgChild3 != null + ? `${holder.orgChild3.orgChild3ShortName} ${numPart}` + : holder.orgChild2 != null + ? `${holder.orgChild2.orgChild2ShortName} ${numPart}` + : holder.orgChild1 != null + ? `${holder.orgChild1.orgChild1ShortName} ${numPart}` + : holder.orgRoot != null + ? `${holder.orgRoot.orgRootShortName} ${numPart}` + : null; + } + } else { + _Org = profile.org; + _PosNo = profile.posMasterNo; + } + const _profile: any = { profileId: profile.id, prefix: profile.prefix, @@ -8961,7 +8981,8 @@ export class ProfileController extends Controller { node: null, nodeId: null, nodeDnaId: null, - posNo: shortName, + posNo: _PosNo, + org: _Org, isPosmasterAct: data.length > 0, posmasterAct: data, salary: profile ? profile.amount : null, diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 104528ac..660a36d6 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -6462,6 +6462,92 @@ export class ProfileEmployeeController extends Controller { ? null : profile.current_holders.find((x) => x.orgRevisionId == orgRevisionPublish.id)?.orgChild4; const _numPart = posMaster ? [posMaster.posMasterNoPrefix, posMaster.posMasterNo, posMaster.posMasterNoSuffix].filter((p) => p !== null && p !== undefined && p !== '').join(' ') : ''; + + // org / posNo — ล้อ fallback ของ officer (ProfileController.getProfileByProfileid) + // employee ไม่มี profile.org / profile.posMasterNo จึงแยกตาม isLeave โดยตรง + let _Org: string | null = null; + let _PosNo: string | null = null; + if (profile.isLeave) { + const profileWithSalary = await this.profileRepo.findOne({ + where: { + id: id, + profileSalary: { + commandCode: In([ + "0", + "9", + "1", + "2", + "3", + "4", + "8", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "20", + ]), + }, + }, + relations: { profileSalary: true }, + order: { + profileSalary: { + order: "DESC", + createdAt: "DESC", + }, + }, + }); + const profileSalaryList = profileWithSalary?.profileSalary || []; + + if (profileSalaryList.length > 0) { + const _profileSalary = + profile.leaveType == "RETIRE" + ? profileSalaryList.length > 1 + ? profileSalaryList[1] + : profileSalaryList[0] + : profileSalaryList[0]; + + if (_profileSalary) { + _Org = [ + _profileSalary.orgChild4 ?? null, + _profileSalary.orgChild3 ?? null, + _profileSalary.orgChild2 ?? null, + _profileSalary.orgChild1 ?? null, + _profileSalary.orgRoot ?? null, + ] + .filter((x: any) => x !== undefined && x !== null) + .join("\n"); + _PosNo = `${_profileSalary.posNoAbb} ${_profileSalary.posNo}`; + } + } + } else { + _Org = [ + child4?.orgChild4Name, + child3?.orgChild3Name, + child2?.orgChild2Name, + child1?.orgChild1Name, + root?.orgRootName, + ] + .filter((x) => x != null && x !== "") + .join("\n"); + + _PosNo = + posMaster == null + ? null + : child4 != null + ? `${child4.orgChild4ShortName} ${_numPart}` + : child3 != null + ? `${child3.orgChild3ShortName} ${_numPart}` + : child2 != null + ? `${child2.orgChild2ShortName} ${_numPart}` + : child1 != null + ? `${child1.orgChild1ShortName} ${_numPart}` + : root != null + ? `${root.orgRootShortName} ${_numPart}` + : null; + } const _profile: any = { profileId: profile.id, prefix: profile.prefix, @@ -6474,7 +6560,7 @@ export class ProfileEmployeeController extends Controller { position: profile.position, leaveDate: profile.dateLeave, posMasterNo: posMaster == null ? null : posMaster.posMasterNo, - posLevelName: `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`, + posLevelName: `${profile?.posType?.posTypeShortName ?? ""} ${profile?.posLevel?.posLevelName ?? ""}`.trim(), posLevelRank: profile.posLevel == null ? null : profile.posLevel.posLevelRank, posLevelId: profile.posLevel == null ? null : profile.posLevel.id, posTypeName: profile.posType == null ? null : profile.posType.posTypeName, @@ -6503,7 +6589,8 @@ export class ProfileEmployeeController extends Controller { child4ShortName: child4 == null ? null : child4.orgChild4ShortName, node: null, nodeId: null, - posNo: null, + posNo: _PosNo, + org: _Org, salary: profile.amount, education: profile && profile.profileEducations.length > 0 @@ -6518,27 +6605,22 @@ export class ProfileEmployeeController extends Controller { _profile.node = 4; _profile.nodeId = _profile.child4Id; _profile.nodeShortName = _profile.child4ShortName; - _profile.posNo = `${_profile.child4ShortName} ${_numPart}`; } else if (_profile.child3Id != null) { _profile.node = 3; _profile.nodeId = _profile.child3Id; _profile.nodeShortName = _profile.child3ShortName; - _profile.posNo = `${_profile.child3ShortName} ${_numPart}`; } else if (_profile.child2Id != null) { _profile.node = 2; _profile.nodeId = _profile.child2Id; _profile.nodeShortName = _profile.child2ShortName; - _profile.posNo = `${_profile.child2ShortName} ${_numPart}`; } else if (_profile.child1Id != null) { _profile.node = 1; _profile.nodeId = _profile.child1Id; _profile.nodeShortName = _profile.child1ShortName; - _profile.posNo = `${_profile.child1ShortName} ${_numPart}`; } else if (_profile.rootId != null) { _profile.node = 0; _profile.nodeId = _profile.rootId; _profile.nodeShortName = _profile.rootShortName; - _profile.posNo = `${_profile.rootShortName} ${_numPart}`; } return new HttpSuccess(_profile); } From 64be68d0a3a06e8ecbad95af1c4a1132ef15e4a7 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 13:34:06 +0700 Subject: [PATCH 32/39] =?UTF-8?q?[ExecuteSalaryCurrentService]=20=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=AD=E0=B8=9A=20transaction=20+=20respone=20succe?= =?UTF-8?q?ss/fail=20count?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/controllers/CommandController.ts | 15 +- src/services/ExecuteSalaryCurrentService.ts | 630 +++++++++++--------- src/services/rabbitmq.ts | 12 +- 3 files changed, 366 insertions(+), 291 deletions(-) diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 1ea05633..d0dbbe51 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 } from "../services/ExecuteSalaryCurrentService"; +import { ExecuteSalaryCurrentService, ExecuteSalaryResult } from "../services/ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; @@ -3702,11 +3702,14 @@ export class CommandController extends Controller { }[]; }, ) { - await new ExecuteSalaryCurrentService().executeSalaryCurrent(body.data, { - user: { sub: req.user.sub, name: req.user.name }, - req, - }); - return new HttpSuccess(); + const result: ExecuteSalaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( + body.data, + { + user: { sub: req.user.sub, name: req.user.name }, + req, + }, + ); + return new HttpSuccess(result); } @Post("excexute/salary-employee-current") diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index 1fd5a54a..fdc2cf1d 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.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"; @@ -60,6 +60,16 @@ export interface SalaryCurrentExecutionContext { req?: any; } +/** + * ผลลัพธ์การประมวลผล batch — แต่ละคนทำงานแบบ independent + * คนที่ fail จะ rollback เฉพาะตัว (per-item transaction) ไม่กระทบคนอื่น + */ +export interface ExecuteSalaryResult { + successCount: number; + failureCount: number; + failures: { profileId: string; reason: string }[]; +} + /** * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง) * @@ -69,28 +79,29 @@ export interface SalaryCurrentExecutionContext { * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateCurrent ต้นฉบับ + * + * Batch semantics: ประมวลผลทุกคนแบบ sequential (ทีละคน) แต่ละคนครอบด้วย + * transaction ของตัวเอง เพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง + * posMaster/position ตัวเดียวกัน — คนที่ throw จะ rollback เฉพาะตัว ไม่กระทบคนอื่น + * ผลลัพธ์รายงานเป็น success/failure count + รายชื่อคนที่ fail */ export class ExecuteSalaryCurrentService { 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 positionRepository = AppDataSource.getRepository(Position); private orgRootRepository = AppDataSource.getRepository(OrgRoot); /** - * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของข้าราชการ + * ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของข้าราชการทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน */ async executeSalaryCurrent( data: SalaryCurrentItem[], ctx: SalaryCurrentExecutionContext, - ): Promise { + ): Promise { console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); - const req = ctx.req; - // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // ───────────────────────────────────────────────────────────── @@ -149,283 +160,336 @@ export class ExecuteSalaryCurrentService { .orgRootShortName ?? ""; } } - await Promise.all( - data.map(async (item) => { - const profile: any = await this.profileRepository.findOneBy({ id: item.profileId }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - let _null: any = null; - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - order: { order: "DESC" }, + + // ───────────────────────────────────────────────────────────── + // Per-item transaction: แต่ละคนมี transaction ของตัวเอง (sequential) + // ประมวลทีละคนเพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง + // posMaster/position ตัวเดียวกัน คนที่ throw จะ rollback เฉพาะตัว (manager) + // และไม่กระทบคนอื่นใน batch + // ───────────────────────────────────────────────────────────── + const failures: ExecuteSalaryResult["failures"] = []; + let successCount = 0; + for (const item of data ?? []) { + try { + await AppDataSource.transaction(async (manager) => { + await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); }); - const before = null; - const dataSalary = new ProfileSalary(); + successCount++; + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryCurrentService] Failed profileId=${item.profileId}: ${reason}`, + err, + ); + failures.push({ profileId: item.profileId ?? "unknown", reason }); + } + } - 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(), - }; - dataSalary.posNumCodeSit = _posNumCodeSit; - dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; - 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.commandId = item.commandId ?? _null; - history.profileSalaryId = dataSalary.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา - let posMaster = await this.posMasterRepository.findOne({ - where: { id: item.posmasterId }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - - // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ - const isCurrent = - posMaster?.orgRevision?.orgRevisionIsCurrent === true && - posMaster?.orgRevision?.orgRevisionIsDraft === false; - - // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA - if (!isCurrent && posMaster?.ancestorDNA) { - posMaster = await this.posMasterRepository.findOne({ - where: { - ancestorDNA: posMaster.ancestorDNA, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: { - orgRevision: true, - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - } - - if (posMaster == null) { - console.error( - `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, - ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - } - - const posMasterOld = await this.posMasterRepository.findOne({ - where: { - current_holderId: item.profileId, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - - const positionOld = await this.positionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { - posMasterId: posMasterOld?.id, - userId: ctx.user.sub, - endpoint: "updateMaster", - action: "command_change_reset_old_position", - }); - - positionOld.positionIsSelected = false; - await this.positionRepository.save(positionOld); - } - - const checkPosition = await this.positionRepository.find({ - where: { - posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - console.log( - `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${ctx.user.sub}, endpoint: updateMaster)`, - ); - - const clearPosition = checkPosition.map((positions) => { - logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { - posMasterId: posMaster!.id, - userId: ctx.user.sub, - endpoint: "updateMaster", - action: "command_change_clear_positions", - }); - - return { - ...positions, - positionIsSelected: false, - }; - }); - await this.positionRepository.save(clearPosition); - } - - posMaster.current_holderId = item.profileId; - 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); - - // STEP 2: กำหนด position ใหม่ - // 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; - - // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel - const posTypeId = item.positionTypeId || item.positionType; - const posLevelId = item.positionLevelId || item.positionLevel; - - // ═══════════════════════════════════════════════════════════ - // 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.positionName && posTypeId && posLevelId) { - // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า - const whereCondition: any = { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }; - - // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) - 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.positionName && posTypeId && posLevelId) { - const positionBy3Fields = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionName: item.positionName, - posTypeId: posTypeId, - posLevelId: posLevelId, - }, - relations: ["posExecutive"], - order: { orderNo: "ASC" }, - }); - - if (positionBy3Fields) { - positionNew = positionBy3Fields; - } - } - - // // ═══════════════════════════════════════════════════════════ - // // 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]; - // } - // } - - // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ - if (positionNew != null) { - positionNew.positionIsSelected = true; - // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit - profile.posMasterNo = getPosMasterNo(posMaster); - profile.org = getOrgFullName(posMaster); - 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.amount = item.amount ?? null; - profile.amountSpecial = item.amountSpecial ?? null; - await this.profileRepository.save(profile); - await this.positionRepository.save(positionNew); - } - await CreatePosMasterHistoryOfficer(posMaster.id, req); - }), + console.log( + `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: ${failures.length}`, ); - console.log("[ExecuteSalaryCurrentService] executeSalaryCurrent completed successfully"); + return { successCount, failureCount: failures.length, failures }; + } + + /** + * ประมวลผล 1 คน ภายใน transaction เดียว (manager) + * ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน + * ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ (กัน partial commit) + */ + private async processOne( + item: SalaryCurrentItem, + ctx: SalaryCurrentExecutionContext, + manager: EntityManager, + _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 positionRepository = manager.getRepository(Position); + + const profile: any = await profileRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + let _null: any = null; + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + + 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(), + }; + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + 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.commandId = item.commandId ?? _null; + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา + let posMaster = await posMasterRepository.findOne({ + where: { id: item.posmasterId }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + + // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ + const isCurrent = + posMaster?.orgRevision?.orgRevisionIsCurrent === true && + posMaster?.orgRevision?.orgRevisionIsDraft === false; + + // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA + if (!isCurrent && posMaster?.ancestorDNA) { + posMaster = await posMasterRepository.findOne({ + where: { + ancestorDNA: posMaster.ancestorDNA, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: { + orgRevision: true, + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + } + + if (posMaster == null) { + console.error( + `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, + ); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + } + + const posMasterOld = await posMasterRepository.findOne({ + where: { + current_holderId: item.profileId, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + + const positionOld = await positionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + logPositionIsSelectedChange(positionOld.id, positionOld.positionIsSelected, false, { + posMasterId: posMasterOld?.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_reset_old_position", + }); + + positionOld.positionIsSelected = false; + await positionRepository.save(positionOld); + } + + const checkPosition = await positionRepository.find({ + where: { + posMasterId: posMaster!.id, // ใช้ posMaster ตัวใหม่ (ที่อาจจะเปลี่ยนจาก ancestorDNA) + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + console.log( + `[positionIsSelected-DEBUG] Command change: clearing ${checkPosition.length} positions (posMasterId: ${posMaster!.id}, userId: ${ctx.user.sub}, endpoint: updateMaster)`, + ); + + const clearPosition = checkPosition.map((positions) => { + logPositionIsSelectedChange(positions.id, positions.positionIsSelected, false, { + posMasterId: posMaster!.id, + userId: ctx.user.sub, + endpoint: "updateMaster", + action: "command_change_clear_positions", + }); + + return { + ...positions, + positionIsSelected: false, + }; + }); + await positionRepository.save(clearPosition); + } + + posMaster.current_holderId = item.profileId; + posMaster.lastUpdatedAt = new Date(); + // posMaster.conditionReason = _null; + // posMaster.isCondition = false; + if (posMasterOld != null) { + await posMasterRepository.save(posMasterOld); + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + } + await posMasterRepository.save(posMaster); + + // STEP 2: กำหนด position ใหม่ + // 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; + + // Resolve ID: ใช้ positionTypeId/positionLevelId ก่อน ถ้าไม่มี fallback เป็น positionType/positionLevel + const posTypeId = item.positionTypeId || item.positionType; + const posLevelId = item.positionLevelId || item.positionLevel; + + // ═══════════════════════════════════════════════════════════ + // CONDITION 1: เช็คจาก positionId ตรง + // ═══════════════════════════════════════════════════════════ + if (item.positionId) { + const positionById = await 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.positionName && posTypeId && posLevelId) { + // สร้าง where clause แบบ dynamic - ใส่เฉพาะฟิลด์ที่มีค่า + const whereCondition: any = { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }; + + // เพิ่มเฉพาะฟิลด์ที่มีค่า (ไม่ใช่ null, undefined, หรือ string ว่าง) + 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 positionRepository.findOne({ + where: whereCondition, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy7Fields) { + positionNew = positionBy7Fields; + } + } + + // ═══════════════════════════════════════════════════════════ + // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) + // ═══════════════════════════════════════════════════════════ + if (!positionNew && item.positionName && posTypeId && posLevelId) { + const positionBy3Fields = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionName: item.positionName, + posTypeId: posTypeId, + posLevelId: posLevelId, + }, + relations: ["posExecutive"], + order: { orderNo: "ASC" }, + }); + + if (positionBy3Fields) { + positionNew = positionBy3Fields; + } + } + + // // ═══════════════════════════════════════════════════════════ + // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster + // // ═══════════════════════════════════════════════════════════ + // 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]; + // } + // } + + // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ + if (positionNew != null) { + positionNew.positionIsSelected = true; + // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit + profile.posMasterNo = getPosMasterNo(posMaster); + profile.org = getOrgFullName(posMaster); + 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.amount = item.amount ?? null; + profile.amountSpecial = item.amountSpecial ?? null; + await profileRepository.save(profile); + await positionRepository.save(positionNew); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); } } diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index d84a3eed..c1d6e778 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -388,8 +388,16 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); } else if (isSalaryCurrent) { - await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx); - console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`); + const salaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( + resultData, + ctx, + ); + console.log( + `[AMQ] Processed via ExecuteSalaryCurrentService — success: ${salaryResult.successCount}, failure: ${salaryResult.failureCount}`, + ); + for (const f of salaryResult.failures) { + console.error(`[AMQ] ExecuteSalaryCurrentService failed profileId=${f.profileId}: ${f.reason}`); + } } else if (isSalaryEmployeeCurrent) { await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); From 5acd4853681e633ad4d622aca9eb6f9ebeec0668 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 16:35:30 +0700 Subject: [PATCH 33/39] fix #62 --- src/controllers/ProfileEmployeeController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/ProfileEmployeeController.ts b/src/controllers/ProfileEmployeeController.ts index 660a36d6..21eb9a5d 100644 --- a/src/controllers/ProfileEmployeeController.ts +++ b/src/controllers/ProfileEmployeeController.ts @@ -3254,8 +3254,8 @@ export class ProfileEmployeeController extends Controller { .leftJoinAndSelect("current_holders.orgChild2", "orgChild2") .leftJoinAndSelect("current_holders.orgChild3", "orgChild3") .leftJoinAndSelect("current_holders.orgChild4", "orgChild4") - .where(node && nodeId ? "current_holders.orgRevisionId = :orgRevisionId" : "1=1", { - orgRevisionId: node && nodeId ? findRevision.id : undefined, + .where("current_holders.orgRevisionId = :orgRevisionId", { + orgRevisionId: findRevision.id, }) .andWhere( _data.root != undefined && _data.root != null From ecd0388eb0329114f481ff3c9966254263920db6 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 23 Jun 2026 18:33:27 +0700 Subject: [PATCH 34/39] =?UTF-8?q?=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/ExecuteSalaryCurrentService.ts | 131 ++++++++++++-------- src/services/rabbitmq.ts | 12 +- 2 files changed, 79 insertions(+), 64 deletions(-) diff --git a/src/services/ExecuteSalaryCurrentService.ts b/src/services/ExecuteSalaryCurrentService.ts index fdc2cf1d..2f0871dc 100644 --- a/src/services/ExecuteSalaryCurrentService.ts +++ b/src/services/ExecuteSalaryCurrentService.ts @@ -61,8 +61,9 @@ export interface SalaryCurrentExecutionContext { } /** - * ผลลัพธ์การประมวลผล batch — แต่ละคนทำงานแบบ independent - * คนที่ fail จะ rollback เฉพาะตัว (per-item transaction) ไม่กระทบคนอื่น + * ผลลัพธ์การประมวลผล batch — all-or-nothing (single transaction ครอบทั้ง batch) + * ถ้าทุกคนสำเร็จจะ return result; ถ้ามีคนใด throw จะ rollback ทั้ง batch + * และ propagate error ออกไป (caller เห็นเป็น failure ทั้งหมด) */ export interface ExecuteSalaryResult { successCount: number; @@ -80,10 +81,9 @@ export interface ExecuteSalaryResult { * * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateCurrent ต้นฉบับ * - * Batch semantics: ประมวลผลทุกคนแบบ sequential (ทีละคน) แต่ละคนครอบด้วย - * transaction ของตัวเอง เพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง - * posMaster/position ตัวเดียวกัน — คนที่ throw จะ rollback เฉพาะตัว ไม่กระทบคนอื่น - * ผลลัพธ์รายงานเป็น success/failure count + รายชื่อคนที่ fail + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count */ export class ExecuteSalaryCurrentService { private commandRepository = AppDataSource.getRepository(Command); @@ -99,8 +99,12 @@ export class ExecuteSalaryCurrentService { data: SalaryCurrentItem[], ctx: SalaryCurrentExecutionContext, ): Promise { - console.log("[ExecuteSalaryCurrentService] Starting executeSalaryCurrent"); - console.log("[ExecuteSalaryCurrentService] Request body count:", data?.length); + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryCurrentService] Starting executeSalaryCurrent — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryCurrentService] Request body count: ${data?.length ?? 0}`); // ───────────────────────────────────────────────────────────── // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) @@ -162,39 +166,37 @@ export class ExecuteSalaryCurrentService { } // ───────────────────────────────────────────────────────────── - // Per-item transaction: แต่ละคนมี transaction ของตัวเอง (sequential) - // ประมวลทีละคนเพื่อกัน race condition เมื่อหลายคนใน batch อ้างอิง - // posMaster/position ตัวเดียวกัน คนที่ throw จะ rollback เฉพาะตัว (manager) - // และไม่กระทบคนอื่นใน batch + // Single transaction ครอบทั้ง batch (all-or-nothing) + // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch + // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow // ───────────────────────────────────────────────────────────── - const failures: ExecuteSalaryResult["failures"] = []; let successCount = 0; - for (const item of data ?? []) { - try { - await AppDataSource.transaction(async (manager) => { + 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 - ? err.message - : err instanceof Error + successCount++; + } catch (err) { + const reason = + err instanceof HttpError ? err.message - : "unexpected error"; - console.error( - `[ExecuteSalaryCurrentService] Failed profileId=${item.profileId}: ${reason}`, - err, - ); - failures.push({ profileId: item.profileId ?? "unknown", reason }); + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryCurrentService] Failed — commandCode: ${commandCode}, commandId: ${commandId}, profileId: ${item.profileId}, reason: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } } - } + }); console.log( - `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: ${failures.length}`, + `[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: 0`, ); - return { successCount, failureCount: failures.length, failures }; + return { successCount, failureCount: 0, failures: [] }; } /** @@ -219,7 +221,7 @@ export class ExecuteSalaryCurrentService { const profile: any = await profileRepository.findOneBy({ id: item.profileId }); if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, `ไม่พบข้อมูลทะเบียนประวัตินี้ profileId: ${item.profileId}`); } let _null: any = null; const dest_item = await salaryRepo.findOne({ @@ -250,6 +252,9 @@ export class ExecuteSalaryCurrentService { await salaryHistoryRepo.save(history, { data: req }); // STEP 1: หา posMaster ที่จะใช้งานตาม id ที่ส่งมา + console.log( + `[ExecuteSalaryCurrentService] STEP 1: Finding posMaster — posmasterId: ${item.posmasterId}, profileId: ${item.profileId}`, + ); let posMaster = await posMasterRepository.findOne({ where: { id: item.posmasterId }, relations: { @@ -261,14 +266,21 @@ export class ExecuteSalaryCurrentService { orgChild4: true, }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 1: posMaster found: ${!!posMaster}, ancestorDNA: ${posMaster?.ancestorDNA ?? "null"}, orgRevisionId: ${posMaster?.orgRevisionId ?? "null"}`, + ); // เช็คว่า posMaster ที่หามาอยู่ในโครงสร้างปัจจุบันหรือไม่ const isCurrent = posMaster?.orgRevision?.orgRevisionIsCurrent === true && posMaster?.orgRevision?.orgRevisionIsDraft === false; + console.log(`[ExecuteSalaryCurrentService] STEP 1: isCurrent: ${isCurrent}`); // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA if (!isCurrent && posMaster?.ancestorDNA) { + console.log( + `[ExecuteSalaryCurrentService] STEP 1: Not current — re-resolving via ancestorDNA: ${posMaster.ancestorDNA}`, + ); posMaster = await posMasterRepository.findOne({ where: { ancestorDNA: posMaster.ancestorDNA, @@ -286,13 +298,16 @@ export class ExecuteSalaryCurrentService { orgChild4: true, }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 1: ancestorDNA re-resolve — found: ${!!posMaster}`, + ); } if (posMaster == null) { console.error( - `[ExecuteSalaryCurrentService] PosMaster not found - posMasterId: ${item.posmasterId}, `, + `[ExecuteSalaryCurrentService] STEP 1: PosMaster not found — posmasterId: ${item.posmasterId}`, ); - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + throw new HttpError(HttpStatusCode.NOT_FOUND, `ไม่พบข้อมูลตำแหน่งนี้ posMasterId: ${item.posmasterId}`); } const posMasterOld = await posMasterRepository.findOne({ @@ -359,6 +374,9 @@ export class ExecuteSalaryCurrentService { await posMasterRepository.save(posMasterOld); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager); + console.log( + `[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`, + ); } await posMasterRepository.save(posMaster); @@ -375,6 +393,10 @@ export class ExecuteSalaryCurrentService { const posTypeId = item.positionTypeId || item.positionType; const posLevelId = item.positionLevelId || item.positionLevel; + console.log( + `[ExecuteSalaryCurrentService] STEP 2: Resolving position — posMasterId: ${posMaster.id}, positionId: ${item.positionId ?? "null"}, positionName: ${item.positionName ?? "null"}, posTypeId: ${posTypeId ?? "null"}, posLevelId: ${posLevelId ?? "null"}`, + ); + // ═══════════════════════════════════════════════════════════ // CONDITION 1: เช็คจาก positionId ตรง // ═══════════════════════════════════════════════════════════ @@ -386,6 +408,9 @@ export class ExecuteSalaryCurrentService { }, relations: ["posExecutive"], }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 1: match: ${!!positionById}`, + ); if (positionById) { positionNew = positionById; @@ -423,6 +448,10 @@ export class ExecuteSalaryCurrentService { relations: ["posExecutive"], order: { orderNo: "ASC" }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 2: match: ${!!positionBy7Fields}`, + whereCondition, + ); if (positionBy7Fields) { positionNew = positionBy7Fields; @@ -443,31 +472,18 @@ export class ExecuteSalaryCurrentService { relations: ["posExecutive"], order: { orderNo: "ASC" }, }); + console.log( + `[ExecuteSalaryCurrentService] STEP 2 / Condition 3: match: ${!!positionBy3Fields}`, + ); if (positionBy3Fields) { positionNew = positionBy3Fields; } } - // // ═══════════════════════════════════════════════════════════ - // // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster - // // ═══════════════════════════════════════════════════════════ - // 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]; - // } - // } + console.log( + `[ExecuteSalaryCurrentService] STEP 2: Resolved positionNew: ${positionNew ? positionNew.id : "null (no match — profile position not updated)"}`, + ); // ถ้าไม่ใช่ตำแหน่งนั่งทับ (isSit = false) ถึงจะอัพเดทตำแหน่งในทะเบียนประวัติ if (positionNew != null) { @@ -488,8 +504,15 @@ 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 เดียวกัน await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); + + console.log( + `[ExecuteSalaryCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`, + ); } } diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index c1d6e778..d84a3eed 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -388,16 +388,8 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOfficerProfileService().executeCreateOfficerProfile(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteOfficerProfileService`); } else if (isSalaryCurrent) { - const salaryResult = await new ExecuteSalaryCurrentService().executeSalaryCurrent( - resultData, - ctx, - ); - console.log( - `[AMQ] Processed via ExecuteSalaryCurrentService — success: ${salaryResult.successCount}, failure: ${salaryResult.failureCount}`, - ); - for (const f of salaryResult.failures) { - console.error(`[AMQ] ExecuteSalaryCurrentService failed profileId=${f.profileId}: ${f.reason}`); - } + await new ExecuteSalaryCurrentService().executeSalaryCurrent(resultData, ctx); + console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryCurrentService`); } else if (isSalaryEmployeeCurrent) { await new ExecuteSalaryEmployeeCurrentService().executeSalaryEmployeeCurrent(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeCurrentService`); From 832c5d2cb36f2d46ef9c33f54e486624b2b9e485 Mon Sep 17 00:00:00 2001 From: harid Date: Wed, 24 Jun 2026 18:05:54 +0700 Subject: [PATCH 35/39] 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; } From 3d2fc5128ab70ceca97d01985c9e261840172a73 Mon Sep 17 00:00:00 2001 From: harid Date: Fri, 26 Jun 2026 18:09:45 +0700 Subject: [PATCH 36/39] Linear Flow discipline + organization #224 --- src/controllers/CommandController.ts | 1170 +---------------- src/services/ExecuteOfficerProfileService.ts | 17 +- src/services/ExecuteOrgCommandService.ts | 860 ++++++++++++ .../ExecuteSalaryEmployeeLeaveService.ts | 4 + .../ExecuteSalaryLeaveDisciplineService.ts | 615 +++++++++ src/services/ExecuteSalaryService.ts | 6 +- src/services/rabbitmq.ts | 67 +- 7 files changed, 1591 insertions(+), 1148 deletions(-) create mode 100644 src/services/ExecuteOrgCommandService.ts create mode 100644 src/services/ExecuteSalaryLeaveDisciplineService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 1ea05633..7ca097fd 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -81,8 +81,6 @@ import { District } from "../entities/District"; import { Province } from "../entities/Province"; import { ProfileAssistance } from "../entities/ProfileAssistance"; import { ProfileAssistanceHistory } from "../entities/ProfileAssistanceHistory"; -import { ProfileActposition } from "../entities/ProfileActposition"; -import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory"; import { ProfileFamilyFather } from "../entities/ProfileFamilyFather"; import { ProfileFamilyFatherHistory } from "../entities/ProfileFamilyFatherHistory"; import { ProfileFamilyCouple } from "../entities/ProfileFamilyCouple"; @@ -110,9 +108,8 @@ import { ExecuteSalaryCurrentService } from "../services/ExecuteSalaryCurrentSer import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; -import { promisify } from "util"; -const REDIS_HOST = process.env.REDIS_HOST; -const REDIS_PORT = process.env.REDIS_PORT; +import { ExecuteSalaryLeaveDisciplineService } from "../services/ExecuteSalaryLeaveDisciplineService"; +import { ExecuteOrgCommandService } from "../services/ExecuteOrgCommandService"; @Route("api/v1/org/command") @Tags("Command") @@ -122,7 +119,6 @@ const REDIS_PORT = process.env.REDIS_PORT; "เกิดข้อผิดพลาด ไม่สามารถแสดงรายการได้ กรุณาลองใหม่ในภายหลัง", ) export class CommandController extends Controller { - private redis = require("redis"); private commandRepository = AppDataSource.getRepository(Command); private commandTypeRepository = AppDataSource.getRepository(CommandType); private commandSendRepository = AppDataSource.getRepository(CommandSend); @@ -157,8 +153,6 @@ export class CommandController extends Controller { private subDistrictRepo = AppDataSource.getRepository(SubDistrict); private assistanceRepository = AppDataSource.getRepository(ProfileAssistance); private assistanceHistoryRepository = AppDataSource.getRepository(ProfileAssistanceHistory); - private actpositionRepository = AppDataSource.getRepository(ProfileActposition); - private actpositionHistoryRepository = AppDataSource.getRepository(ProfileActpositionHistory); private profileFamilyCoupleRepo = AppDataSource.getRepository(ProfileFamilyCouple); private profileFamilyCoupleHistoryRepo = AppDataSource.getRepository(ProfileFamilyCoupleHistory); private profileFamilyMotherRepo = AppDataSource.getRepository(ProfileFamilyMother); @@ -4035,6 +4029,14 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API สร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย + * + * Thin wrapper — เรียก ExecuteSalaryLeaveDisciplineService (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + * + * @summary API คำสั่งวินัย (ข้าราชการ/ลูกจ้าง) + leave + */ @Post("excexute/salary-leave-discipline") public async newSalaryAndUpdateLeaveDiscipline( @Request() req: RequestWithUser, @@ -4072,472 +4074,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - relations: ["commandType"], - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - let orgRootRef = null; - let orgChild1Ref = null; - let orgChild2Ref = null; - let orgChild3Ref = null; - let orgChild4Ref = null; - let profile; - let isEmployee: boolean = false; - let retireTypeName: string = ""; - // ขรก. - if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") { - profile = await this.profileRepository.findOne({ - relations: [ - // "profileSalary", - "posLevel", - "posType", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "current_holders.positions", - "current_holders.positions.posExecutive", - "roleKeycloaks", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - //ลบตำแหน่งที่รักษาการแทน - const code = _command?.commandType?.code; - if (code && ["C-PM-19", "C-PM-20"].includes(code)) { - removePostMasterAct(profile.id); - } - - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: profile.id, - commandId: item.commandId, - position: profile.position, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? 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, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: item.orgRoot, - orgChild1: item.orgChild1, - orgChild2: item.orgChild2, - orgChild3: item.orgChild3, - orgChild4: item.orgChild4, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: item.posNo, - posNoAbb: item.posNoAbb, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - - // ประวัติวินัย - const dataDis = new ProfileDiscipline(); - - const metaDis = { - date: item.commandDateAffect, - refCommandDate: item.commandDateSign, - refCommandNo: `${item.commandNo}/${item.commandYear}`, - refCommandId: item.commandId, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(dataDis, { ...item, ...metaDis }); - const historyDis = new ProfileDisciplineHistory(); - Object.assign(historyDis, { ...dataDis, id: undefined }); - - await this.disciplineRepository.save(dataDis); - historyDis.profileDisciplineId = dataDis.id; - await this.disciplineHistoryRepository.save(historyDis); - - // ทะเบียนประวัติ - if (item.isLeave != null) { - const _profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const _null: any = null; - _profile.isLeave = item.isLeave; - _profile.leaveReason = item.leaveReason ?? _null; - _profile.dateLeave = item.dateLeave ?? _null; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - if (item.isLeave == true) { - if (orgRevisionRef) { - await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE"); - } - await removeProfileInOrganize(_profile.id, "OFFICER"); - } - const clearProfile = await checkCommandType(String(item.commandId)); - if (clearProfile.status) { - retireTypeName = clearProfile.retireTypeName ?? ""; - 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.profileRepository.save(_profile); - // if (_profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [_profile.id], - // "PROFILE", - // ); - // } - } - } - // ลูกจ้าง - else { - isEmployee = true; - profile = await this.profileEmployeeRepository.findOne({ - relations: [ - // "profileSalary", - "posLevel", - "posType", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "roleKeycloaks", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - orgRootRef = orgRevisionRef?.orgRoot ?? null; - orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; - orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; - orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; - orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; - - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileEmployeeId: profile.id, - commandId: item.commandId, - position: profile.position, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: - profile?.posType && profile?.posLevel - ? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}` - : null, - amount: item.amount ? item.amount : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: item.orgRoot, - orgChild1: item.orgChild1, - orgChild2: item.orgChild2, - orgChild3: item.orgChild3, - orgChild4: item.orgChild4, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: item.posNo, - posNoAbb: item.posNoAbb, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - - // ประวัติวินัย - const dataDis = new ProfileDiscipline(); - - const metaDis = { - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(dataDis, { - ...item, - ...metaDis, - date: item.commandDateAffect, - refCommandDate: item.commandDateSign, - refCommandNo: item.commandNo, - profileEmployeeId: item.profileId, - profileId: undefined, - }); - const historyDis = new ProfileDisciplineHistory(); - Object.assign(historyDis, { ...dataDis, id: undefined }); - - await this.disciplineRepository.save(dataDis); - historyDis.profileDisciplineId = dataDis.id; - await this.disciplineHistoryRepository.save(historyDis); - - // ทะเบียนประวัติ - if (item.isLeave != null) { - const _profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const _null: any = null; - _profile.isLeave = item.isLeave; - _profile.leaveReason = item.leaveReason ?? _null; - _profile.dateLeave = item.dateLeave ?? _null; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - if (item.isLeave == true) { - // บันทึกประวัติก่อนลบตำแหน่ง - const curRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - if (curRevision) { - const curPosMaster = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: _profile.id, - orgRevisionId: curRevision.id, - }, - }); - if (curPosMaster) { - await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE"); - } - } - await removeProfileInOrganize(_profile.id, "EMPLOYEE"); - } - const clearProfile = await checkCommandType(String(item.commandId)); - if (clearProfile.status) { - retireTypeName = clearProfile.retireTypeName ?? ""; - 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", - // ); - // } - } - } - // Task #2190 - if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) { - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - let _posLevelName: string = !isEmployee - ? `${profile.posLevel?.posLevelName}` - : `${profile.posType?.posTypeName} ${profile.posLevel?.posLevelName}`; - } - }), - ); - return new HttpSuccess(); } @@ -5459,365 +4999,14 @@ export class CommandController extends Controller { }[]; }, ) { - let _reqBody: any[] = []; - const roleKeycloak = await this.roleKeycloakRepo.findOne({ - where: { name: Like("USER") }, + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand21Employee (C-PM-21) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand21Employee(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.refIds.find((x) => x.commandId)?.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.refIds.map(async (item) => { - const profile = await this.profileEmployeeRepository.findOne({ - where: { id: item.refId }, - relations: ["roleKeycloaks"], - }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - const orgRevision = await this.orgRevisionRepository.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - const _posMaster = await this.employeePosMasterRepository.findOne({ - where: { - orgRevisionId: orgRevision?.id, - id: profile.posmasterIdTemp, - // current_holderId: profile.id - }, - relations: { - orgRoot: true, - orgChild1: true, - orgChild2: true, - orgChild3: true, - orgChild4: true, - }, - }); - const orgRootRef = _posMaster?.orgRoot ?? null; - const orgChild1Ref = _posMaster?.orgChild1 ?? null; - const orgChild2Ref = _posMaster?.orgChild2 ?? null; - const orgChild3Ref = _posMaster?.orgChild3 ?? null; - const orgChild4Ref = _posMaster?.orgChild4 ?? null; - let orgShortName = ""; - if (_posMaster != null) { - if (_posMaster.orgChild1Id === null) { - orgShortName = _posMaster.orgRoot?.orgRootShortName; - } else if (_posMaster.orgChild2Id === null) { - orgShortName = _posMaster.orgChild1?.orgChild1ShortName; - } else if (_posMaster.orgChild3Id === null) { - orgShortName = _posMaster.orgChild2?.orgChild2ShortName; - } else if (_posMaster.orgChild4Id === null) { - orgShortName = _posMaster.orgChild3?.orgChild3ShortName; - } else { - orgShortName = _posMaster.orgChild4?.orgChild4ShortName; - } - } - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: item.refId }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileEmployeeId: profile.id, - amount: item.amount, - amountSpecial: item.amountSpecial, - commandId: item.commandId, - positionSalaryAmount: item.positionSalaryAmount, - mouthSalaryAmount: item.mouthSalaryAmount, - position: profile.positionTemp, - positionName: profile.positionTemp, - positionType: profile.posTypeNameTemp, - positionLevel: profile.posLevelNameTemp, - order: dest_item == null ? 1 : dest_item.order + 1, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: profile.posMasterNoTemp ?? "", - posNoAbb: orgShortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - const posMaster = await this.employeePosMasterRepository.findOne({ - where: { id: profile.posmasterIdTemp }, - relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], - }); - if (posMaster == null) - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); - - const posMasterOld = await this.employeePosMasterRepository.findOne({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - if (posMasterOld != null) { - posMasterOld.current_holderId = null; - posMasterOld.lastUpdatedAt = new Date(); - } - // if (posMasterOld != null) posMasterOld.next_holderId = null; - - const positionOld = await this.employeePositionRepository.findOne({ - where: { - posMasterId: posMasterOld?.id, - positionIsSelected: true, - }, - }); - if (positionOld != null) { - positionOld.positionIsSelected = false; - await this.employeePositionRepository.save(positionOld); - } - - const checkPosition = await this.employeePositionRepository.find({ - where: { - posMasterId: profile.posmasterIdTemp, - positionIsSelected: true, - }, - }); - if (checkPosition.length > 0) { - const clearPosition = checkPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.employeePositionRepository.save(clearPosition); - } - - posMaster.current_holderId = profile.id; - 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); - await CreatePosMasterHistoryEmployee(posMaster.id, req); - - const clsTempPosmaster = await this.employeeTempPosMasterRepository.find({ - where: { - current_holderId: profile.id, - orgRevisionId: posMaster.orgRevisionId, - }, - }); - - if (clsTempPosmaster.length > 0) { - const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({ - ...posMasterTemp, - current_holderId: null, - next_holderId: null, - })); - await this.employeeTempPosMasterRepository.save(clearTempPosmaster); - - const checkTempPosition = await this.employeePositionRepository.find({ - where: { - posMasterTempId: In(clearTempPosmaster.map((x) => x.id)), - positionIsSelected: true, - }, - }); - if (checkTempPosition.length > 0) { - const clearTempPosition = checkTempPosition.map((positions) => ({ - ...positions, - positionIsSelected: false, - })); - await this.employeePositionRepository.save(clearTempPosition); - } - await Promise.all( - clsTempPosmaster.map( - async (posMasterTemp) => - await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req), - ), - ); - } - - const positionNew = await this.employeePositionRepository.findOne({ - where: { - id: profile.positionIdTemp, - posMasterId: profile.posmasterIdTemp, - }, - }); - - if (positionNew != null) { - // Create Keycloak - const checkUser = await getUserByUsername(profile.citizenId); - if (checkUser.length == 0) { - let password = profile.citizenId; - if (profile.birthDate != null) { - // const gregorianYear = profile.birthDate.getFullYear() + 543; - - // const formattedDate = - // profile.birthDate.toISOString().slice(8, 10) + - // profile.birthDate.toISOString().slice(5, 7) + - // gregorianYear; - // password = formattedDate; - const _date = new Date(profile.birthDate.toDateString()) - .getDate() - .toString() - .padStart(2, "0"); - const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) - .toString() - .padStart(2, "0"); - const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; - password = `${_date}${_month}${_year}`; - } - // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak - const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; - const userKeycloakId = await createUser(profile.citizenId, password, { - firstName: sanitizedFirstName, - lastName: profile.lastName, - // email: profile.email, - }); - // if (typeof userKeycloakId !== "string") { - // throw new Error(userKeycloakId.errorMessage); - // } - const list = await getRoles(); - if (!Array.isArray(list)) - throw new Error("Failed. Cannot get role(s) data from the server."); - const result = await addUserRoles( - userKeycloakId, - list - .filter((v) => v.name === "USER") - .map((x) => ({ - id: x.id, - name: x.name, - })), - ); - // if (!result) throw new Error("Failed. Cannot set user's role."); - profile.keycloak = - userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : ""; - profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; - // End Create Keycloak - } else { - const rolesData = await getRoleMappings(checkUser[0].id); - if (rolesData) { - const _roleKeycloak = await this.roleKeycloakRepo.find({ - where: { name: In(rolesData.map((x: any) => x.name)) }, - }); - profile.roleKeycloaks = - _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; - } - profile.keycloak = checkUser[0].id; - } - positionNew.positionIsSelected = true; - profile.posLevelId = positionNew.posLevelId; - profile.posTypeId = positionNew.posTypeId; - profile.position = positionNew.positionName; - profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; - profile.positionEmployeePositionId = positionNew.positionName; - profile.statusTemp = "DONE"; - profile.employeeClass = "PERM"; - const _null: any = null; - profile.employeeWage = item.amount == null ? _null : item.amount.toString(); - profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); - profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date(); - profile.amount = item.amount == null ? _null : item.amount; - profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; - _reqBody.push({ - profileId: profile.id, - prefix: profile.prefix, - firstName: profile.firstName, - lastName: profile.lastName, - citizenId: profile.citizenId, - root: posMaster.orgRoot.orgRootName, - rootId: posMaster.orgRootId, - rootShortName: posMaster.orgRoot.orgRootShortName, - rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null, - child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null, - child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null, - child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null, - child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null, - }); - await this.profileEmployeeRepository.save(profile); - await this.employeePositionRepository.save(positionNew); - await CreatePosMasterHistoryEmployee(posMaster.id, req); - //ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว - const posMasterTemp = await this.employeeTempPosMasterRepository.findOne({ - where: { - orgRevisionId: orgRevision?.id, - current_holderId: profile.id, - }, - }); - if (posMasterTemp) { - await this.employeeTempPosMasterRepository.update(posMasterTemp.id, { - current_holderId: _null, - }); - await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req); - } - } - }), - ); - await new CallAPI() - .PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", { - profileEmps: _reqBody, - }) - .catch((error) => { - throw new Error("Failed. Cannot update status. ", error); - }); return new HttpSuccess(); } @@ -5860,153 +5049,14 @@ export class CommandController extends Controller { }[]; }, ) { - // 1. Bulk update status - await this.posMasterActRepository.update( - { id: In(body.refIds.map((x) => x.refId)) }, - { statusReport: "DONE" }, - ); - - // 2. ดึงข้อมูลครบทุก relation ที่จำเป็น - const posMasters = await this.posMasterActRepository.find({ - where: { id: In(body.refIds.map((x) => x.refId)) }, - relations: [ - "posMasterChild", - "posMasterChild.current_holder", - "posMaster", - "posMaster.current_holder", - "posMaster.positions", - "posMaster.orgRoot", - "posMaster.orgChild1", - "posMaster.orgChild2", - "posMaster.orgChild3", - "posMaster.orgChild4", - ], + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand40Officer (C-PM-40) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand40Officer(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - - // 3. ตรวจสอบว่ามี body.refIds[0] หรือไม่ - const firstRef = body.refIds[0]; - if (!firstRef) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบข้อมูล refIds"); - } - - const profileIdsToClearCache = new Set(); - - await Promise.all( - posMasters.map(async (item) => { - // 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด - if (!item.posMasterChild?.current_holderId || !item.posMaster) { - console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`); - return; - } - - if (item.posMasterChild.current_holderId) { - profileIdsToClearCache.add(item.posMasterChild.current_holderId); - } - - // 5. สร้าง orgShortName แบบปลอดภัย - const orgShortName = - [ - item.posMaster?.orgChild4?.orgChild4ShortName, - item.posMaster?.orgChild3?.orgChild3ShortName, - item.posMaster?.orgChild2?.orgChild2ShortName, - item.posMaster?.orgChild1?.orgChild1ShortName, - item.posMaster?.orgRoot?.orgRootShortName, - ].find(Boolean) ?? ""; - - // 6. หา position ที่ถูกเลือกแบบปลอดภัย - const selectedPosition = item.posMaster?.positions; - const positionName = - selectedPosition - ?.map((pos) => pos.positionName) - .filter(Boolean) - .join(", ") ?? "-"; - - // 7. สร้าง metaAct แบบปลอดภัย - const metaAct = { - profileId: item.posMasterChild.current_holderId, - dateStart: firstRef.commandDateAffect ?? null, - dateEnd: null, - position: positionName, - status: true, - commandId: firstRef.commandId ?? null, - createdUserId: req.user?.sub ?? null, - createdFullName: req.user?.name ?? null, - lastUpdateUserId: req.user?.sub ?? null, - lastUpdateFullName: req.user?.name ?? null, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: firstRef.commandNo ?? null, - refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`, - commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null, - posNo: - orgShortName && item.posMaster?.posMasterNo - ? `${orgShortName} ${item.posMaster.posMasterNo}` - : item.posMaster?.posMasterNo ?? "-", - posNoAbb: orgShortName, - commandDateAffect: firstRef.commandDateAffect ?? null, - commandDateSign: firstRef.commandDateSign ?? null, - commandCode: firstRef.commandCode ?? null, - commandName: firstRef.commandName ?? null, - remark: firstRef.remark ?? null, - }; - - try { - // 8. ปิดสถานะรักษาการ - const existingActPositions = await this.actpositionRepository.find({ - where: { - profileId: item.posMasterChild.current_holderId, - status: true, - isDeleted: false, - }, - }); - - if (existingActPositions.length > 0) { - const updatedActPositions = existingActPositions.map((_data) => ({ - ..._data, - status: false, - dateEnd: new Date(), - })); - - await this.actpositionRepository.save(updatedActPositions); - } - - // 9. บันทึกข้อมูลใหม่ - const dataAct = new ProfileActposition(); - Object.assign(dataAct, metaAct); - - const historyAct = new ProfileActpositionHistory(); - Object.assign(historyAct, { ...dataAct, id: undefined }); - - await this.actpositionRepository.save(dataAct); - historyAct.profileActpositionId = dataAct.id; - await this.actpositionHistoryRepository.save(historyAct); - } catch (error) { - console.error(`Error processing item ${item.id}:`, error); - throw new HttpError( - HttpStatus.INTERNAL_SERVER_ERROR, - `เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`, - ); - } - }), - ); - - if (profileIdsToClearCache.size > 0) { - await Promise.all( - Array.from(profileIdsToClearCache).map(async (profileId) => { - const redisClient = await this.redis.createClient({ - host: REDIS_HOST, - port: REDIS_PORT, - }); - - const delAsync = promisify(redisClient.del).bind(redisClient); - await delAsync("role_" + profileId); - await delAsync("menu_" + profileId); - - redisClient.quit(); - }), - ); - } - return new HttpSuccess(); } @@ -6157,166 +5207,14 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.refIds.find((x) => x.commandId)?.commandId ?? "" }, + /** + * Thin wrapper — เรียก ExecuteOrgCommandService.executeCommand38Officer (C-PM-38) + * ทั้ง endpoint นี้และ consumer ใน rabbitmq ใช้ service ตัวเดียวกัน (Linear Flow) + */ + await new ExecuteOrgCommandService().executeCommand38Officer(body.refIds, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.refIds.map(async (item) => { - const posMaster = await this.posMasterRepository.findOne({ - where: { id: item.refId }, - relations: [ - "orgRoot", - "orgChild1", - "orgChild2", - "orgChild3", - "orgChild4", - "current_holder", - "current_holder.posLevel", - "current_holder.posType", - ], - }); - if (!posMaster) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว"); - } - if (posMaster.next_holderId != null) { - const orgRootRef = posMaster?.orgRoot ?? null; - const orgChild1Ref = posMaster?.orgChild1 ?? null; - const orgChild2Ref = posMaster?.orgChild2 ?? null; - const orgChild3Ref = posMaster?.orgChild3 ?? null; - const orgChild4Ref = posMaster?.orgChild4 ?? null; - const shortName = - posMaster != null && posMaster.orgChild4 != null - ? `${posMaster.orgChild4.orgChild4ShortName}` - : posMaster != null && posMaster.orgChild3 != null - ? `${posMaster.orgChild3.orgChild3ShortName}` - : posMaster != null && posMaster.orgChild2 != null - ? `${posMaster.orgChild2.orgChild2ShortName}` - : posMaster != null && posMaster.orgChild1 != null - ? `${posMaster.orgChild1.orgChild1ShortName}` - : posMaster != null && posMaster?.orgRoot != null - ? `${posMaster.orgRoot.orgRootShortName}` - : null; - const profile = await this.profileRepository.findOne({ - where: { id: posMaster.next_holderId }, - }); - const position = await this.positionRepository.findOne({ - where: { - posMasterId: posMaster.id, - positionIsSelected: true, - }, - relations: ["posType", "posLevel"], - }); - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: profile?.id }, - order: { order: "DESC" }, - }); - const before = null; - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: profile?.id, - date: new Date(), - amount: item.amount, - commandId: item.commandId, - positionSalaryAmount: item.positionSalaryAmount, - mouthSalaryAmount: item.mouthSalaryAmount, - position: position?.positionName ?? null, - positionType: position?.posType?.posTypeName ?? null, - positionLevel: position?.posLevel?.posLevelName ?? null, - order: dest_item == null ? 1 : dest_item.order + 1, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posMaster.posMasterNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - // if (profile != null) { - // profile.position = position?.positionName ?? ""; - // profile.posTypeId = position?.posTypeId ?? ""; - // profile.posLevelId = position?.posLevelId ?? ""; - // profile.lastUpdateUserId = req.user.sub; - // profile.lastUpdateFullName = req.user.name; - // profile.lastUpdatedAt = new Date(); - // await this.profileRepository.save(profile); - // } - } - }), - ); - // const posMasters = await this.posMasterRepository.find({ - // where: { id: In(body.refIds.map((x) => x.refId)) }, - // }); - // const data = posMasters.map((_data) => ({ - // ..._data, - // current_holderId: _data.next_holderId, - // next_holderId: null, - // statusReport: "PENDING", - // })); - // await this.posMasterRepository.save(data); return new HttpSuccess(); } diff --git a/src/services/ExecuteOfficerProfileService.ts b/src/services/ExecuteOfficerProfileService.ts index c9e0bacb..f06159dc 100644 --- a/src/services/ExecuteOfficerProfileService.ts +++ b/src/services/ExecuteOfficerProfileService.ts @@ -129,14 +129,23 @@ export interface ExecutionContext { /** * Service สำหรับสร้าง/อัปเดตทะเบียนประวัติข้าราชการ (Profile) หลังออกคำสั่งบรรจุ * - * ถูกออกแบบมาเพื่อแก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ - * โดยให้ฝั่งบรรจุส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เอง - * ที่ต้นทาง (Linear Flow) แทนการเรียกซ้อนกันกลับไปมา + * ใช้กับ commandType: C-PM-01, 02, 14 * * - endpoint /org/command/excexute/create-officer-profile เรียกผ่าน service นี้ (thin wrapper) - * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (no HTTP loopback) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) * * Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes) + * ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว + * + * Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ + * ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน */ export class ExecuteOfficerProfileService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts new file mode 100644 index 00000000..62ed231f --- /dev/null +++ b/src/services/ExecuteOrgCommandService.ts @@ -0,0 +1,860 @@ +import { Double, EntityManager, In, Like } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import Extension from "../interfaces/extension"; +import CallAPI from "../interfaces/call-api"; +import { setLogDataDiff } from "../interfaces/utils"; +import { + CreatePosMasterHistoryEmployee, + CreatePosMasterHistoryEmployeeTemp, +} from "./PositionService"; +import { + addUserRoles, + createUser, + getRoles, + getUserByUsername, + getRoleMappings, +} from "../keycloak"; +import { Command } from "../entities/Command"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { RoleKeycloak } from "../entities/RoleKeycloak"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster"; +import { EmployeePosition } from "../entities/EmployeePosition"; +import { PosMaster } from "../entities/PosMaster"; +import { Position } from "../entities/Position"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { PosMasterAct } from "../entities/PosMasterAct"; +import { ProfileActposition } from "../entities/ProfileActposition"; +import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory"; +import { promisify } from "util"; + +const redis = require("redis"); +const REDIS_HOST = process.env.REDIS_HOST; +const REDIS_PORT = process.env.REDIS_PORT; + +/** + * Input: refIds ที่ consumer ใน rabbitmq build ขึ้น (เดิมคือ body.refIds ของ endpoint /excecute) + * ใช้กับ C-PM-21, C-PM-38, C-PM-40 + */ +export interface CommandRefItem { + refId: string; + commandId?: string | null; + amount: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount: Double | null; + mouthSalaryAmount: Double | null; + commandNo: string | null; + commandYear: number; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; +} + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface OrgCommandExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งที่เดิม "ยิงเข้าตัว" (HTTP loopback เข้า org เอง) + * + * ใช้กับ commandType: + * - C-PM-21 : command21/employee/report/excecute (ลูกจ้าง → พนักงานประจำ) + * - C-PM-38 : command38/officer/report/excecute (เงินเดือน next_holder ข้าราชการ) + * - C-PM-40 : command40/officer/report/excecute (รักษาการ) + * + * - endpoint commandXX/.../excecute ทั้ง 3 เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow / ทำต่อ) + * แทนการ PostData(path + "/excecute") ที่เป็น HTTP loopback เข้า org ตัวเอง + * + * Behavior ทั้งหมด preserve จาก CommandController ต้นฉบับ + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * + * ⚠️ หมายเหตุ side-effect ที่อยู่นอก DB transaction: + * - Keycloak operations (createUser/addUserRoles/getRoleMappings) ใน C-PM-21 ทำภายใน transaction + * เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจากนี้ + * Keycloak จะถูกเปลี่ยนไปแล้ว + * - .NET call (C-PM-21) ทำหลัง transaction commit แล้ว เพราะ .NET ไม่สามารถ rollback ได้ + * - Redis cache clear (C-PM-40) ทำหลัง transaction commit (เป็นการ del cache key — idempotent) + * - CreatePosMasterHistoryEmployeeTemp สร้าง nested transaction ของตัวเอง (ไม่รับ manager) + */ +export class ExecuteOrgCommandService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + private orgRevisionRepository = AppDataSource.getRepository(OrgRevision); + private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); + private posMasterRepository = AppDataSource.getRepository(PosMaster); + private posMasterActRepository = AppDataSource.getRepository(PosMasterAct); + + // ───────────────────────────────────────────────────────────── + // แก้ปัญหา _posNumCodeSit/_command resolution ที่ซ้ำกันในทุก endpoint + // (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ command: Command | null; posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + let profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { command, posNumCodeSit, posNumCodeSitAbb }; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-21 : command21/employee/report/excecute + // ลูกจ้างชั่วคราว → พนักงานประจำ (บรรจุ) + // ═══════════════════════════════════════════════════════════════ + /** + * @returns profileEmps ที่จะส่งต่อให้ .NET (สำหรับ consumer rabbitmq เรียก .NET เอง) + * ถ้าเรียกจาก thin-wrapper endpoint จะเรียก .NET ภายใน method นี้เอง + */ + async executeCommand21Employee( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + options?: { callDotNet?: boolean }, + ): Promise<{ profileEmps: any[] }> { + const req = ctx.req; + const callDotNet = options?.callDotNet ?? true; + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand21Employee — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const roleKeycloak = await this.roleKeycloakRepo.findOne({ + where: { name: Like("USER") }, + }); + const { command: _command, posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + const profileEmps: any[] = []; + + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOneCommand21( + item, + ctx, + manager, + roleKeycloak, + _command, + _posNumCodeSit, + _posNumCodeSitAbb, + profileEmps, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteOrgCommandService] Failed C-PM-21, commandId=${commandId}, refId=${item.refId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + // .NET call ทำหลัง commit (เหมือน endpoint เดิมที่เรียกหลัง Promise.all) — .NET ไม่ rollback ได้ + if (callDotNet && profileEmps.length > 0) { + await new CallAPI() + .PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", { + profileEmps, + }) + .catch((error) => { + throw new Error(`Failed. Cannot update status. ${error?.message ?? ""}`); + }); + } + + console.log( + `[ExecuteOrgCommandService] Completed C-PM-21 — ${profileEmps.length} profiles sent to .NET`, + ); + return { profileEmps }; + } + + private async processOneCommand21( + item: CommandRefItem, + ctx: OrgCommandExecutionContext, + manager: EntityManager, + roleKeycloak: RoleKeycloak | null, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileEmps: any[], + ): Promise { + const req = ctx.req; + + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + const employeeTempPosMasterRepository = manager.getRepository(EmployeeTempPosMaster); + const employeePositionRepository = manager.getRepository(EmployeePosition); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + const profile = await profileEmployeeRepository.findOne({ + where: { id: item.refId }, + relations: ["roleKeycloaks"], + }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + const orgRevision = await this.orgRevisionRepository.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + const _posMaster = await employeePosMasterRepository.findOne({ + where: { + orgRevisionId: orgRevision?.id, + id: profile.posmasterIdTemp, + }, + relations: { + orgRoot: true, + orgChild1: true, + orgChild2: true, + orgChild3: true, + orgChild4: true, + }, + }); + const orgRootRef = _posMaster?.orgRoot ?? null; + const orgChild1Ref = _posMaster?.orgChild1 ?? null; + const orgChild2Ref = _posMaster?.orgChild2 ?? null; + const orgChild3Ref = _posMaster?.orgChild3 ?? null; + const orgChild4Ref = _posMaster?.orgChild4 ?? null; + let orgShortName = ""; + if (_posMaster != null) { + if (_posMaster.orgChild1Id === null) { + orgShortName = _posMaster.orgRoot?.orgRootShortName; + } else if (_posMaster.orgChild2Id === null) { + orgShortName = _posMaster.orgChild1?.orgChild1ShortName; + } else if (_posMaster.orgChild3Id === null) { + orgShortName = _posMaster.orgChild2?.orgChild2ShortName; + } else if (_posMaster.orgChild4Id === null) { + orgShortName = _posMaster.orgChild3?.orgChild3ShortName; + } else { + orgShortName = _posMaster.orgChild4?.orgChild4ShortName; + } + } + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.refId }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileEmployeeId: profile.id, + amount: item.amount, + amountSpecial: item.amountSpecial, + positionSalaryAmount: item.positionSalaryAmount, + mouthSalaryAmount: item.mouthSalaryAmount, + position: profile.positionTemp, + positionName: profile.positionTemp, + positionType: profile.posTypeNameTemp, + positionLevel: profile.posLevelNameTemp, + order: dest_item == null ? 1 : dest_item.order + 1, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: profile.posMasterNoTemp ?? "", + posNoAbb: orgShortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + + const posMaster = await employeePosMasterRepository.findOne({ + where: { id: profile.posmasterIdTemp }, + relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"], + }); + if (posMaster == null) + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); + + const posMasterOld = await employeePosMasterRepository.findOne({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + if (posMasterOld != null) { + posMasterOld.current_holderId = null; + posMasterOld.lastUpdatedAt = new Date(); + } + + const positionOld = await employeePositionRepository.findOne({ + where: { + posMasterId: posMasterOld?.id, + positionIsSelected: true, + }, + }); + if (positionOld != null) { + positionOld.positionIsSelected = false; + await employeePositionRepository.save(positionOld); + } + + const checkPosition = await employeePositionRepository.find({ + where: { + posMasterId: profile.posmasterIdTemp, + positionIsSelected: true, + }, + }); + if (checkPosition.length > 0) { + const clearPosition = checkPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await employeePositionRepository.save(clearPosition); + } + + posMaster.current_holderId = profile.id; + posMaster.lastUpdatedAt = new Date(); + posMaster.next_holderId = null; + if (posMasterOld != null) { + await employeePosMasterRepository.save(posMasterOld); + await CreatePosMasterHistoryEmployee(posMasterOld.id, req, undefined, manager); + } + await employeePosMasterRepository.save(posMaster); + await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager); + + const clsTempPosmaster = await employeeTempPosMasterRepository.find({ + where: { + current_holderId: profile.id, + orgRevisionId: posMaster.orgRevisionId, + }, + }); + + if (clsTempPosmaster.length > 0) { + const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({ + ...posMasterTemp, + current_holderId: null, + next_holderId: null, + })); + await employeeTempPosMasterRepository.save(clearTempPosmaster); + + const checkTempPosition = await employeePositionRepository.find({ + where: { + posMasterTempId: In(clearTempPosmaster.map((x) => x.id)), + positionIsSelected: true, + }, + }); + if (checkTempPosition.length > 0) { + const clearTempPosition = checkTempPosition.map((positions) => ({ + ...positions, + positionIsSelected: false, + })); + await employeePositionRepository.save(clearTempPosition); + } + await Promise.all( + clsTempPosmaster.map( + async (posMasterTemp) => + await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req), + ), + ); + } + + const positionNew = await employeePositionRepository.findOne({ + where: { + id: profile.positionIdTemp, + posMasterId: profile.posmasterIdTemp, + }, + }); + + if (positionNew != null) { + // Create Keycloak + const checkUser = await getUserByUsername(profile.citizenId); + if (checkUser.length == 0) { + let password = profile.citizenId; + if (profile.birthDate != null) { + const _date = new Date(profile.birthDate.toDateString()) + .getDate() + .toString() + .padStart(2, "0"); + const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1) + .toString() + .padStart(2, "0"); + const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543; + password = `${_date}${_month}${_year}`; + } + // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak + const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; + const userKeycloakId = await createUser(profile.citizenId, password, { + firstName: sanitizedFirstName, + lastName: profile.lastName, + }); + const list = await getRoles(); + if (!Array.isArray(list)) + throw new Error("Failed. Cannot get role(s) data from the server."); + const result = await addUserRoles( + userKeycloakId, + list + .filter((v) => v.name === "USER") + .map((x) => ({ + id: x.id, + name: x.name, + })), + ); + profile.keycloak = + userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : ""; + profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : []; + // End Create Keycloak + } else { + const rolesData = await getRoleMappings(checkUser[0].id); + if (rolesData) { + const _roleKeycloak = await this.roleKeycloakRepo.find({ + where: { name: In(rolesData.map((x: any) => x.name)) }, + }); + profile.roleKeycloaks = + _roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : []; + } + profile.keycloak = checkUser[0].id; + } + positionNew.positionIsSelected = true; + profile.posLevelId = positionNew.posLevelId; + profile.posTypeId = positionNew.posTypeId; + profile.position = positionNew.positionName; + profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null; + profile.positionEmployeePositionId = positionNew.positionName; + profile.statusTemp = "DONE"; + profile.employeeClass = "PERM"; + const _null: any = null; + profile.employeeWage = item.amount == null ? _null : item.amount.toString(); + profile.dateStart = _command ? _command.commandExcecuteDate : new Date(); + profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date(); + profile.amount = item.amount == null ? _null : item.amount; + profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial; + profileEmps.push({ + profileId: profile.id, + prefix: profile.prefix, + firstName: profile.firstName, + lastName: profile.lastName, + citizenId: profile.citizenId, + root: posMaster.orgRoot.orgRootName, + rootId: posMaster.orgRootId, + rootShortName: posMaster.orgRoot.orgRootShortName, + rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null, + child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null, + child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null, + child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null, + child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null, + }); + await profileEmployeeRepository.save(profile); + await employeePositionRepository.save(positionNew); + await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager); + //ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว + const posMasterTemp = await employeeTempPosMasterRepository.findOne({ + where: { + orgRevisionId: orgRevision?.id, + current_holderId: profile.id, + }, + }); + if (posMasterTemp) { + await employeeTempPosMasterRepository.update(posMasterTemp.id, { + current_holderId: _null, + }); + await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req); + } + } + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-38 : command38/officer/report/excecute + // เงินเดือน next_holder ของข้าราชการ + // ═══════════════════════════════════════════════════════════════ + async executeCommand38Officer( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + ): Promise { + const req = ctx.req; + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand38Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + for (const item of data ?? []) { + try { + await this.processOneCommand38( + item, + ctx, + manager, + _posNumCodeSit, + _posNumCodeSitAbb, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteOrgCommandService] Failed C-PM-38, commandId=${commandId}, refId=${item.refId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteOrgCommandService] Completed C-PM-38 — ${data?.length ?? 0} items`); + } + + private async processOneCommand38( + item: CommandRefItem, + ctx: OrgCommandExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const posMasterRepository = manager.getRepository(PosMaster); + const profileRepository = manager.getRepository(Profile); + const positionRepository = manager.getRepository(Position); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + const posMaster = await posMasterRepository.findOne({ + where: { id: item.refId }, + relations: [ + "orgRoot", + "orgChild1", + "orgChild2", + "orgChild3", + "orgChild4", + "current_holder", + "current_holder.posLevel", + "current_holder.posType", + ], + }); + if (!posMaster) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว"); + } + if (posMaster.next_holderId != null) { + const orgRootRef = posMaster?.orgRoot ?? null; + const orgChild1Ref = posMaster?.orgChild1 ?? null; + const orgChild2Ref = posMaster?.orgChild2 ?? null; + const orgChild3Ref = posMaster?.orgChild3 ?? null; + const orgChild4Ref = posMaster?.orgChild4 ?? null; + const shortName = + posMaster != null && posMaster.orgChild4 != null + ? `${posMaster.orgChild4.orgChild4ShortName}` + : posMaster != null && posMaster.orgChild3 != null + ? `${posMaster.orgChild3.orgChild3ShortName}` + : posMaster != null && posMaster.orgChild2 != null + ? `${posMaster.orgChild2.orgChild2ShortName}` + : posMaster != null && posMaster.orgChild1 != null + ? `${posMaster.orgChild1.orgChild1ShortName}` + : posMaster != null && posMaster?.orgRoot != null + ? `${posMaster.orgRoot.orgRootShortName}` + : null; + const profile = await profileRepository.findOne({ + where: { id: posMaster.next_holderId }, + }); + const position = await positionRepository.findOne({ + where: { + posMasterId: posMaster.id, + positionIsSelected: true, + }, + relations: ["posType", "posLevel"], + }); + const dest_item = await salaryRepo.findOne({ + where: { profileId: profile?.id }, + order: { order: "DESC" }, + }); + const before = null; + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: profile?.id, + date: new Date(), + amount: item.amount, + commandId: item.commandId, + positionSalaryAmount: item.positionSalaryAmount, + mouthSalaryAmount: item.mouthSalaryAmount, + position: position?.positionName ?? null, + positionType: position?.posType?.posTypeName ?? null, + positionLevel: position?.posLevel?.posLevelName ?? null, + order: dest_item == null ? 1 : dest_item.order + 1, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posMaster.posMasterNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary, { data: req }); + setLogDataDiff(req, { before, after: dataSalary }); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history, { data: req }); + } + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-40 : command40/officer/report/excecute + // รักษาการ (ProfileActposition) + // ═══════════════════════════════════════════════════════════════ + async executeCommand40Officer( + data: CommandRefItem[], + ctx: OrgCommandExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteOrgCommandService] executeCommand40Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + // 3. ตรวจสอบว่ามี data[0] หรือไม่ + const firstRef = data[0]; + if (!firstRef) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบข้อมูล refIds"); + } + + const profileIdsToClearCache = new Set(); + + await AppDataSource.transaction(async (manager) => { + // 1. Bulk update status + await manager.getRepository(PosMasterAct).update( + { id: In(data.map((x) => x.refId)) }, + { statusReport: "DONE" }, + ); + + // 2. ดึงข้อมูลครบทุก relation ที่จำเป็น + const posMasters = await manager.getRepository(PosMasterAct).find({ + where: { id: In(data.map((x) => x.refId)) }, + relations: [ + "posMasterChild", + "posMasterChild.current_holder", + "posMaster", + "posMaster.current_holder", + "posMaster.positions", + "posMaster.orgRoot", + "posMaster.orgChild1", + "posMaster.orgChild2", + "posMaster.orgChild3", + "posMaster.orgChild4", + ], + }); + + for (const item of posMasters) { + try { + // 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด + if (!item.posMasterChild?.current_holderId || !item.posMaster) { + console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`); + continue; + } + + if (item.posMasterChild.current_holderId) { + profileIdsToClearCache.add(item.posMasterChild.current_holderId); + } + + // 5. สร้าง orgShortName แบบปลอดภัย + const orgShortName = + [ + item.posMaster?.orgChild4?.orgChild4ShortName, + item.posMaster?.orgChild3?.orgChild3ShortName, + item.posMaster?.orgChild2?.orgChild2ShortName, + item.posMaster?.orgChild1?.orgChild1ShortName, + item.posMaster?.orgRoot?.orgRootShortName, + ].find(Boolean) ?? ""; + + // 6. หา position ที่ถูกเลือกแบบปลอดภัย + const selectedPosition = item.posMaster?.positions; + const positionName = + selectedPosition + ?.map((pos) => pos.positionName) + .filter(Boolean) + .join(", ") ?? "-"; + + // 7. สร้าง metaAct แบบปลอดภัย + const metaAct = { + profileId: item.posMasterChild.current_holderId, + dateStart: firstRef.commandDateAffect ?? null, + dateEnd: null, + position: positionName, + status: true, + commandId: firstRef.commandId ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: firstRef.commandNo ?? null, + refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`, + commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null, + posNo: + orgShortName && item.posMaster?.posMasterNo + ? `${orgShortName} ${item.posMaster.posMasterNo}` + : item.posMaster?.posMasterNo ?? "-", + posNoAbb: orgShortName, + commandDateAffect: firstRef.commandDateAffect ?? null, + commandDateSign: firstRef.commandDateSign ?? null, + commandCode: firstRef.commandCode ?? null, + commandName: firstRef.commandName ?? null, + remark: firstRef.remark ?? null, + }; + + // 8. ปิดสถานะรักษาการ + const actpositionRepository = manager.getRepository(ProfileActposition); + const actpositionHistoryRepository = manager.getRepository(ProfileActpositionHistory); + + const existingActPositions = await actpositionRepository.find({ + where: { + profileId: item.posMasterChild.current_holderId, + status: true, + isDeleted: false, + }, + }); + + if (existingActPositions.length > 0) { + const updatedActPositions = existingActPositions.map((_data) => ({ + ..._data, + status: false, + dateEnd: new Date(), + })); + + await actpositionRepository.save(updatedActPositions); + } + + // 9. บันทึกข้อมูลใหม่ + const dataAct = new ProfileActposition(); + Object.assign(dataAct, metaAct); + + const historyAct = new ProfileActpositionHistory(); + Object.assign(historyAct, { ...dataAct, id: undefined }); + + await actpositionRepository.save(dataAct); + historyAct.profileActpositionId = dataAct.id; + await actpositionHistoryRepository.save(historyAct); + } catch (error) { + console.error(`Error processing item ${item.id}:`, error); + throw new HttpError( + HttpStatusCode.INTERNAL_SERVER_ERROR, + `เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`, + ); + } + } + }); + + // Redis cache clear ทำหลัง commit (del cache key — idempotent) + if (profileIdsToClearCache.size > 0) { + await Promise.all( + Array.from(profileIdsToClearCache).map(async (profileId) => { + const redisClient = await redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + }); + + const delAsync = promisify(redisClient.del).bind(redisClient); + await delAsync("role_" + profileId); + await delAsync("menu_" + profileId); + + redisClient.quit(); + }), + ); + } + + console.log(`[ExecuteOrgCommandService] Completed C-PM-40 — ${data?.length ?? 0} items`); + } +} diff --git a/src/services/ExecuteSalaryEmployeeLeaveService.ts b/src/services/ExecuteSalaryEmployeeLeaveService.ts index 46952847..95bfa948 100644 --- a/src/services/ExecuteSalaryEmployeeLeaveService.ts +++ b/src/services/ExecuteSalaryEmployeeLeaveService.ts @@ -75,6 +75,10 @@ export interface SalaryEmployeeLeaveExecutionContext { * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว */ export class ExecuteSalaryEmployeeLeaveService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/ExecuteSalaryLeaveDisciplineService.ts b/src/services/ExecuteSalaryLeaveDisciplineService.ts new file mode 100644 index 00000000..99a1dffc --- /dev/null +++ b/src/services/ExecuteSalaryLeaveDisciplineService.ts @@ -0,0 +1,615 @@ +import { Double, EntityManager } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { ProfileDiscipline } from "../entities/ProfileDiscipline"; +import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { EmployeePosMaster } from "../entities/EmployeePosMaster"; +import { Command } from "../entities/Command"; +import { + checkCommandType, + removePostMasterAct, + removeProfileInOrganize, + setLogDataDiff, +} from "../interfaces/utils"; +import { + CreatePosMasterHistoryEmployee, + CreatePosMasterHistoryOfficer, +} from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave-discipline + * (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 — คำสั่งวินัย ข้าราชการ/ลูกจ้าง) + * + * profileType "OFFICER" → ข้าราชการ, ค่าอื่น/null → ลูกจ้าง + */ +export interface SalaryLeaveDisciplineItem { + profileId: string; + profileType?: string | null; + isLeave: boolean | null; + leaveReason?: string | null; + dateLeave?: Date | string | null; + detail?: string | null; + level?: string | null; + unStigma?: string | null; + commandId?: string | null; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + isGovernment?: boolean | null; + commandNo: string | null; + commandYear: number | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + orgRoot?: string | null; + orgChild1?: string | null; + orgChild2?: string | null; + orgChild3?: string | null; + orgChild4?: string | null; + posNo?: string | null; + posNoAbb?: string | null; +} + +/** + * Context สำหรับ audit/log + */ +export interface SalaryLeaveDisciplineExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับสร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย + * + * ใช้กับ commandType: C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 + * + * - endpoint /org/command/excexute/salary-leave-discipline เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeaveDiscipline ต้นฉบับ + * รวมถึงกรณี OFFICER ที่การ save ProfileSalary + ProfileDiscipline ถูก comment out ไว้ + * (เก็บไว้เพื่อ preserve behavior เดิม — มีเพียง EMPLOYEE เท่านั้นที่ save จริง) + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * ถ้าทุกคนสำเร็จจะ return result รายงาน success count + * + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว + */ +export class ExecuteSalaryLeaveDisciplineService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + /** + * ประมวลผลคำสั่งวินัยทั้ง batch + * + * @returns สรุปผล success/failure ต่อคน + */ + async executeSalaryLeaveDiscipline( + data: SalaryLeaveDisciplineItem[], + ctx: SalaryLeaveDisciplineExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; + const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; + console.log( + `[ExecuteSalaryLeaveDisciplineService] Starting executeSalaryLeaveDiscipline — commandCode: ${commandCode}, commandId: ${commandId}`, + ); + console.log(`[ExecuteSalaryLeaveDisciplineService] Request body count: ${data?.length ?? 0}`); + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + const toDate = (v: any): Date | null => { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + }; + for (const item of data ?? []) { + const it = item as any; + it.dateLeave = toDate(it.dateLeave); + it.commandDateAffect = toDate(it.commandDateAffect); + it.commandDateSign = toDate(it.commandDateSign); + } + + let _posNumCodeSit: string = ""; + let _posNumCodeSitAbb: string = ""; + const _command = await this.commandRepository.findOne({ + relations: ["commandType"], + where: { id: data.find((x) => x.commandId)?.commandId ?? "" }, + }); + if (_command) { + if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; + } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + _posNumCodeSit = "กรุงเทพมหานคร"; + _posNumCodeSitAbb = "กทม."; + } else { + let _profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: _command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + _posNumCodeSit = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + _posNumCodeSitAbb = + _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + + // ───────────────────────────────────────────────────────────── + // 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); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryLeaveDisciplineService] 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: SalaryLeaveDisciplineItem, + ctx: SalaryLeaveDisciplineExecutionContext, + manager: EntityManager, + _command: Command | null, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + ): Promise { + const req = ctx.req; + + const profileRepository = manager.getRepository(Profile); + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const disciplineRepository = manager.getRepository(ProfileDiscipline); + const disciplineHistoryRepository = manager.getRepository(ProfileDisciplineHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + const employeePosMasterRepository = manager.getRepository(EmployeePosMaster); + + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + + const orgRevision = await orgRevisionRepo.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + + let orgRootRef: any = null; + let orgChild1Ref: any = null; + let orgChild2Ref: any = null; + let orgChild3Ref: any = null; + let orgChild4Ref: any = null; + + const code = _command?.commandType?.code; + + // ═══════════════════════════════════════════════════════════ + // OFFICER (ข้าราชการ) + // ═══════════════════════════════════════════════════════════ + if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") { + const profile: any = await profileRepository.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "current_holders.positions", + "current_holders.positions.posExecutive", + "roleKeycloaks", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + + //ลบตำแหน่งที่รักษาการแทน (await + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน) + if (code && ["C-PM-19", "C-PM-20"].includes(code)) { + await removePostMasterAct(profile.id, manager); + } + + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + const position = + profile.current_holders + .filter((x: any) => x.orgRevisionId == orgRevision?.id)[0] + ?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + // ประวัติตำแหน่ง + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: profile.id, + commandId: item.commandId, + position: profile.position, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? 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, + order: nextOrder, + orgRoot: item.orgRoot, + orgChild1: item.orgChild1, + orgChild2: item.orgChild2, + orgChild3: item.orgChild3, + orgChild4: item.orgChild4, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: item.posNo, + posNoAbb: item.posNoAbb, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(data, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + // ── preserve: OFFICER branch ไม่ save ProfileSalary (comment ตามต้นฉบับ) ── + // await salaryRepo.save(data, { data: req }); + // history.profileSalaryId = data.id; + // await salaryHistoryRepo.save(history, { data: req }); + + // ประวัติวินัย + const dataDis = new ProfileDiscipline(); + const metaDis = { + date: item.commandDateAffect, + refCommandDate: item.commandDateSign, + refCommandNo: `${item.commandNo}/${item.commandYear}`, + refCommandId: item.commandId, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + Object.assign(dataDis, { ...item, ...metaDis }); + const historyDis = new ProfileDisciplineHistory(); + Object.assign(historyDis, { ...dataDis, id: undefined }); + // ── preserve: OFFICER branch ไม่ save ProfileDiscipline (comment ตามต้นฉบับ) ── + // await disciplineRepository.save(dataDis, { data: req }); + // historyDis.profileDisciplineId = dataDis.id; + // await disciplineHistoryRepository.save(historyDis, { data: req }); + + // ทะเบียนประวัติ + if (item.isLeave != null) { + const _profile: any = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + 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(); + if (item.isLeave == true) { + if (orgRevisionRef) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager); + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(_profile.id, "OFFICER", manager); + } + const clearProfile = await checkCommandType(String(item.commandId)); + 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; + } + await profileRepository.save(_profile, { data: req }); + setLogDataDiff(req, { before: null, after: _profile }); + } + } + // ═══════════════════════════════════════════════════════════ + // EMPLOYEE (ลูกจ้าง) + // ═══════════════════════════════════════════════════════════ + else { + const profile: any = await profileEmployeeRepository.findOne({ + relations: [ + "posLevel", + "posType", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "roleKeycloaks", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + const lastSalary = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + orgRootRef = orgRevisionRef?.orgRoot ?? null; + orgChild1Ref = orgRevisionRef?.orgChild1 ?? null; + orgChild2Ref = orgRevisionRef?.orgChild2 ?? null; + orgChild3Ref = orgRevisionRef?.orgChild3 ?? null; + orgChild4Ref = orgRevisionRef?.orgChild4 ?? null; + + // ประวัติตำแหน่ง + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileEmployeeId: profile.id, + commandId: item.commandId, + position: profile.position, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: + profile?.posType && profile?.posLevel + ? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}` + : null, + amount: item.amount ? item.amount : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: item.orgRoot, + orgChild1: item.orgChild1, + orgChild2: item.orgChild2, + orgChild3: item.orgChild3, + orgChild4: item.orgChild4, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: item.posNo, + posNoAbb: item.posNoAbb, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(data, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before: null, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + // ประวัติวินัย + const dataDis = new ProfileDiscipline(); + const metaDis = { + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + }; + Object.assign(dataDis, { + ...item, + ...metaDis, + date: item.commandDateAffect, + refCommandDate: item.commandDateSign, + refCommandNo: item.commandNo, + profileEmployeeId: item.profileId, + profileId: undefined, + }); + const historyDis = new ProfileDisciplineHistory(); + Object.assign(historyDis, { ...dataDis, id: undefined }); + await disciplineRepository.save(dataDis, { data: req }); + setLogDataDiff(req, { before: null, after: dataDis }); + historyDis.profileDisciplineId = dataDis.id; + await disciplineHistoryRepository.save(historyDis, { data: req }); + + // ทะเบียนประวัติ + if (item.isLeave != null) { + const _profile: any = await profileEmployeeRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + 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(); + if (item.isLeave == true) { + // บันทึกประวัติก่อนลบตำแหน่ง + const curRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + if (curRevision) { + const curPosMaster = await employeePosMasterRepository.findOne({ + where: { + current_holderId: _profile.id, + orgRevisionId: curRevision.id, + }, + }); + if (curPosMaster) { + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager); + } + } + // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน + await removeProfileInOrganize(_profile.id, "EMPLOYEE", manager); + } + const clearProfile = await checkCommandType(String(item.commandId)); + 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, { data: req }); + setLogDataDiff(req, { before: null, after: _profile }); + } + } + + // Task #2190 (preserve: organizeName computed แต่ยังไม่ได้ใช้ในต้นฉบับ — เก็บไว้ตาม behavior เดิม) + if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) { + let organizeName = ""; + if (orgRootRef) { + const names = [ + orgChild4Ref?.orgChild4Name, + orgChild3Ref?.orgChild3Name, + orgChild2Ref?.orgChild2Name, + orgChild1Ref?.orgChild1Name, + orgRootRef?.orgRootName, + ].filter(Boolean); + organizeName = names.join(" "); + } + } + + console.log( + `[ExecuteSalaryLeaveDisciplineService] Completed processOne — profileId: ${item.profileId}`, + ); + } +} diff --git a/src/services/ExecuteSalaryService.ts b/src/services/ExecuteSalaryService.ts index d375d9db..7bc07823 100644 --- a/src/services/ExecuteSalaryService.ts +++ b/src/services/ExecuteSalaryService.ts @@ -70,7 +70,7 @@ export interface SalaryExecutionContext { /** * Service สำหรับสร้าง ProfileSalary ของข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ * - * ใช้กับ commandType: C-PM-13 (โอน), C-PM-15 (ช่วยราชการ), C-PM-16 (เกษียณ) + * ใช้กับ commandType: C-PM-13, 15, 16 * * - endpoint /org/command/excexute/salary เรียกผ่าน service นี้ (thin wrapper) * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) @@ -81,7 +81,9 @@ export interface SalaryExecutionContext { * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) * ถ้าทุกคนสำเร็จจะ return result รายงาน success count * - * Keycloak operations (deleteUser) ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้ + * ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior + * เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ + * → Keycloak จะถูกเปลี่ยนไปแล้ว */ export class ExecuteSalaryService { private commandRepository = AppDataSource.getRepository(Command); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index d84a3eed..f662e247 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -35,6 +35,8 @@ import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; +import { ExecuteSalaryLeaveDisciplineService } from "./ExecuteSalaryLeaveDisciplineService"; +import { ExecuteOrgCommandService } from "./ExecuteOrgCommandService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -335,6 +337,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise { // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) // - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ) // - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave) + // - ExecuteSalaryLeaveDisciplineService : C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 (คำสั่งวินัย) + // - ExecuteOrgCommandService : C-PM-21, 38, 40 (org-self — path ชี้กลับ org เอง + // เรียก Service ตรงๆ ไม่ผ่าน HTTP loopback เพราะ PostData(path+"/excecute") = ยิงเข้าตัว) // - คำสั่งอื่น ยังใช้ Circular Flow เดิม // ───────────────────────────────────────────────────────────── const code = command.commandType?.code; @@ -344,17 +349,46 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code); const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code); const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code); + const isSalaryLeaveDiscipline = ["C-PM-19", "C-PM-20", "C-PM-25", "C-PM-26", "C-PM-27", "C-PM-28", + "C-PM-29", "C-PM-30", "C-PM-31", "C-PM-32", + ].includes(code); + // C-PM-21/38/40: path ชี้กลับ org เอง (ไม่ใช่ .NET) → ต้องเรียก Service ตรงๆ ไม่ผ่าน loopback + const isCommand21 = code === "C-PM-21"; + const isCommand38 = code === "C-PM-38"; + const isCommand40 = code === "C-PM-40"; + const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40; const isLinearFlow = isOfficerProfile || isSalaryCurrent || isSalaryEmployeeCurrent || isSalary || isSalaryLeave || - isSalaryEmployeeLeave; + isSalaryEmployeeLeave || + isSalaryLeaveDiscipline; - if (isLinearFlow) { + // Org-self (C-PM-21/38/40): เรียก Service ตรงๆ (Linear Flow / ทำต่อ) ไม่ผ่าน HTTP loopback + // เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว + if (isOrgSelfLinear) { + console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`); + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + const flatRefIds = chunks.flat(); + if (isCommand21) { + await new ExecuteOrgCommandService().executeCommand21Employee(flatRefIds, ctx); + } else if (isCommand38) { + await new ExecuteOrgCommandService().executeCommand38Officer(flatRefIds, ctx); + } else if (isCommand40) { + await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx); + } + console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`); + } else if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); + const isCpm32 = code === "C-PM-32"; let resultData: any[] = []; + let resultData1: any[] = []; //เฉพาะ C-PM-32 (ฝั่ง "การพิจารณาลงโทษ") for (const chunk of chunks) { const res = await new CallAPI().PostData( @@ -363,9 +397,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { { refIds: chunk }, false, ); - // response (resultData) จาก .NET - if (Array.isArray(res)) { - console.log(`[AMQ] Push result data`); + if (isCpm32 && res && !Array.isArray(res)) { + // C-PM-32: response เป็น object { data, data1 } → แยก 2 track + console.log( + `[AMQ] C-PM-32 split response — data: ${res.data?.length ?? 0}, data1: ${res.data1?.length ?? 0}`, + ); + if (Array.isArray(res.data)) resultData.push(...res.data); + if (Array.isArray(res.data1)) resultData1.push(...res.data1); + } else if (Array.isArray(res)) { + console.log(`[AMQ] Push result data (${res.length})`); resultData.push(...res); } } @@ -373,7 +413,7 @@ async function handler(msg: amqp.ConsumeMessage): Promise { console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`); // Route ไป service ที่ถูกต้องตาม commandType - if (resultData.length > 0) { + if (resultData.length > 0 || resultData1.length > 0) { // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) const pseudoReq = { headers: { authorization: token }, @@ -402,6 +442,21 @@ async function handler(msg: amqp.ConsumeMessage): Promise { } else if (isSalaryEmployeeLeave) { await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx); console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`); + } else if (isSalaryLeaveDiscipline) { + // C-PM-32 (คำสั่งยุติเรื่อง): response เป็น object { data, data1 } + // profileId เดียวกันอาจอยู่ในทั้ง 2 track → ต้องส่งให้ org แยก 2 ครั้ง ห้าม merge + if (resultData.length > 0) { + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveDisciplineService`, + ); + } + if (isCpm32 && resultData1.length > 0) { + await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData1, ctx); + console.log( + `[AMQ] Processed resultData1: ${resultData1.length} profiles via ExecuteSalaryLeaveDisciplineService`, + ); + } } } } else { From 7b37cc37db274351b84a57f22ebf29340e3e548c Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 29 Jun 2026 14:28:50 +0700 Subject: [PATCH 37/39] Linear Flow Probation+Salary #224 --- src/controllers/CommandController.ts | 408 +------------ src/controllers/ProfileSalaryController.ts | 88 +-- .../ProfileSalaryEmployeeController.ts | 91 +-- src/services/ExecuteSalaryProbationService.ts | 539 ++++++++++++++++++ src/services/ExecuteSalaryReportService.ts | 318 +++++++++++ src/services/rabbitmq.ts | 104 ++++ 6 files changed, 976 insertions(+), 572 deletions(-) create mode 100644 src/services/ExecuteSalaryProbationService.ts create mode 100644 src/services/ExecuteSalaryReportService.ts diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index 7ca097fd..19dd6a3f 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -109,6 +109,7 @@ import { ExecuteSalaryEmployeeCurrentService } from "../services/ExecuteSalaryEm import { ExecuteSalaryLeaveService } from "../services/ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "../services/ExecuteSalaryEmployeeLeaveService"; import { ExecuteSalaryLeaveDisciplineService } from "../services/ExecuteSalaryLeaveDisciplineService"; +import { ExecuteSalaryProbationService } from "../services/ExecuteSalaryProbationService"; import { ExecuteOrgCommandService } from "../services/ExecuteOrgCommandService"; @Route("api/v1/org/command") @@ -4424,170 +4425,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - let commandType: any = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryProbationService().executeProbationPass(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - commandType = await this.commandTypeRepository.findOne({ - select: { code: true }, - where: { id: _command.commandTypeId }, - }); - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - // const leaveType = await this.leaveType.findOne({ - // select: { id: true, limit: true, code: true }, - // where: { code: "LV-005" } - // }); - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - relations: [ - "posType", - "posLevel", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - ], - where: { id: item.profileId }, - }); - if (!profile) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, - }); - - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.id) ?? null; - const shortName = - orgRevisionRef?.orgChild4?.orgChild4ShortName ?? - orgRevisionRef?.orgChild3?.orgChild3ShortName ?? - orgRevisionRef?.orgChild2?.orgChild2ShortName ?? - orgRevisionRef?.orgChild1?.orgChild1ShortName ?? - orgRevisionRef?.orgRoot?.orgRootShortName ?? - null; - const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null; - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - // ประวัติตำแหน่ง - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - profileId: item.profileId, - commandId: item.commandId, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - amount: item.amount ? item.amount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - order: nextOrder, - orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null, - orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null, - orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null, - orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null, - orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - }; - Object.assign(data, meta); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history); - }), - ); - - if (commandType && String(commandType.code) == "C-PM-11") { - const profileIds = body.data.map((x) => x.profileId); - await this.profileRepository.update({ id: In(profileIds) }, { isProbation: false }); - // // Task #2304 อัปเดตจำนวนสิทธิ์การลา เมื่อผ่านทดลองงานฯ - // if (leaveType != null) { - // await Promise.all( - // body.data.map((item) => - // new CallAPI().PutData(req, `/leave-beginning/schedule`, { - // profileId: item.profileId, - // leaveTypeId: leaveType.id, - // leaveYear: item.commandYear, - // leaveDays: leaveType.limit, - // leaveDaysUsed: 0, - // leaveCount: 0, - // beginningLeaveDays: 0, - // beginningLeaveCount: 0, - // }) - // .then(() => {}) - // .catch(() => {}) - // ) - // ); - // } - } return new HttpSuccess(); } @@ -4614,245 +4455,10 @@ export class CommandController extends Controller { }[]; }, ) { - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.data.find((x) => x.commandId)?.commandId ?? "" }, + await new ExecuteSalaryProbationService().executeProbationLeave(body.data, { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepository.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - await Promise.all( - body.data.map(async (item) => { - const profile = await this.profileRepository.findOne({ - relations: [ - // "profileSalary", - "posType", - "posLevel", - "current_holders", - "current_holders.orgRoot", - "current_holders.orgChild1", - "current_holders.orgChild2", - "current_holders.orgChild3", - "current_holders.orgChild4", - "current_holders.positions", - "current_holders.positions.posExecutive", - ], - where: { id: item.profileId }, - // order: { - // profileSalary: { - // order: "DESC", - // }, - // }, - }); - if (!profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); - } - const lastSalary = await this.salaryRepo.findOne({ - where: { profileId: item.profileId }, - select: ["order"], - order: { order: "DESC" }, - }); - const nextOrder = lastSalary ? lastSalary.order + 1 : 1; - let _commandYear = item.commandYear; - if (item.commandYear) { - _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; - } - const _profile = await this.profileRepository.findOne({ - where: { id: item.profileId }, - relations: ["roleKeycloaks"], - }); - if (!_profile) { - throw new HttpError(HttpStatus.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); - } - let dateLeave_: any = item.commandDateAffect; - _profile.isLeave = true; - _profile.leaveReason = - "คำสั่งให้ข้าราชการออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐานที่กำหนด"; - _profile.dateLeave = dateLeave_; - _profile.lastUpdateUserId = req.user.sub; - _profile.lastUpdateFullName = req.user.name; - _profile.lastUpdatedAt = new Date(); - - const orgRevision = await this.orgRevisionRepo.findOne({ - where: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }); - const orgRevisionRef = - profile?.current_holders?.find((x) => x.orgRevisionId == orgRevision?.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 shortName = - !profile.current_holders || profile.current_holders.length == 0 - ? null - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild4 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild4.orgChild4ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild3 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild3.orgChild3ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild2 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild2.orgChild2ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgChild1 != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgChild1.orgChild1ShortName}` - : profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) != - null && - profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id) - ?.orgRoot != null - ? `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.orgRoot.orgRootShortName}` - : null; - const posNo = `${profile.current_holders.find((x) => x.orgRevisionId == orgRevision?.id)?.posMasterNo}`; - let position = - profile.current_holders - .filter((x) => x.orgRevisionId == orgRevision?.id)[0] - ?.positions?.filter((pos) => pos.positionIsSelected === true)[0] ?? null; - const profileSalary: ProfileSalary = Object.assign(new ProfileSalary(), { - profileId: item.profileId, - commandId: item.commandId, - positionName: profile.position, - positionType: profile?.posType?.posTypeName ?? null, - positionLevel: profile?.posLevel?.posLevelName ?? null, - positionExecutive: position?.posExecutive?.posExecutiveName ?? null, - amount: item.amount ? item.amount : null, - amountSpecial: item.amountSpecial ? item.amountSpecial : null, - positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, - mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, - // order: - // profile.profileSalary.length >= 0 - // ? profile.profileSalary.length > 0 - // ? profile.profileSalary[0].order + 1 - // : 1 - // : null, - order: nextOrder, - orgRoot: orgRootRef?.orgRootName ?? null, - orgChild1: orgChild1Ref?.orgChild1Name ?? null, - orgChild2: orgChild2Ref?.orgChild2Name ?? null, - orgChild3: orgChild3Ref?.orgChild3Name ?? null, - orgChild4: orgChild4Ref?.orgChild4Name ?? null, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - dateGovernment: item.commandDateAffect ?? new Date(), - isGovernment: item.isGovernment, - commandNo: item.commandNo, - commandYear: item.commandYear, - posNo: posNo, - posNoAbb: shortName, - commandDateAffect: item.commandDateAffect, - commandDateSign: item.commandDateSign, - commandCode: item.commandCode, - commandName: item.commandName, - remark: item.remark, - posNumCodeSit: _posNumCodeSit, - posNumCodeSitAbb: _posNumCodeSitAbb, - }); - if (orgRevisionRef) { - await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE"); - } - await removeProfileInOrganize(profile.id, "OFFICER"); - 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.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 Promise.all([ - this.profileRepository.save(_profile), - this.salaryRepo.save(profileSalary), - ]); - - // if (profile.id) { - // await this.keycloakAttributeService.clearOrgDnaAttributes( - // [profile.id], - // "PROFILE", - // ); - // } - - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...profileSalary, id: undefined }); - history.profileSalaryId = profileSalary.id; - await this.salaryHistoryRepo.save(history); - // Task #2190 - let organizeName = ""; - if (orgRootRef) { - const names = [ - orgChild4Ref?.orgChild4Name, - orgChild3Ref?.orgChild3Name, - orgChild2Ref?.orgChild2Name, - orgChild1Ref?.orgChild1Name, - orgRootRef?.orgRootName, - ].filter(Boolean); - organizeName = names.join(" "); - } - }), - ); return new HttpSuccess(); } diff --git a/src/controllers/ProfileSalaryController.ts b/src/controllers/ProfileSalaryController.ts index 8abe9aa2..c294cb25 100644 --- a/src/controllers/ProfileSalaryController.ts +++ b/src/controllers/ProfileSalaryController.ts @@ -23,6 +23,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService"; import { normalizeDurationSumSimple } from "../utils/tenure"; import { TenurePositionOfficer, @@ -1380,91 +1381,10 @@ export class ProfileSalaryController extends Controller { @Post("update") public async updateSalary(@Request() req: RequestWithUser, @Body() body: CreateProfileSalary) { - if (!body.profileId) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileId"); - } - - const profile = await this.profileRepo.findOneBy({ id: body.profileId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id); - - const dest_item = await this.salaryRepo.findOne({ - where: { profileId: body.profileId }, - order: { order: "DESC" }, + await new ExecuteSalaryReportService().executeOfficerSalaryUpdate([body], { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - const before = null; - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileRepo.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { ...body, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - let _null: any = null; - profile.amount = body.amount ?? _null; - profile.amountSpecial = body.amountSpecial ?? _null; - profile.positionSalaryAmount = body.positionSalaryAmount ?? _null; - profile.mouthSalaryAmount = body.mouthSalaryAmount ?? _null; - await this.profileRepo.save(profile); return new HttpSuccess(); } diff --git a/src/controllers/ProfileSalaryEmployeeController.ts b/src/controllers/ProfileSalaryEmployeeController.ts index 5f2ea997..6f6847f7 100644 --- a/src/controllers/ProfileSalaryEmployeeController.ts +++ b/src/controllers/ProfileSalaryEmployeeController.ts @@ -27,6 +27,7 @@ import { Profile } from "../entities/Profile"; import { In, LessThan, IsNull, MoreThan } from "typeorm"; import permission from "../interfaces/permission"; import { setLogDataDiff } from "../interfaces/utils"; +import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService"; import { normalizeDurationSumSimple } from "../utils/tenure"; import { Command } from "../entities/Command"; import { OrgRoot } from "../entities/OrgRoot"; @@ -507,94 +508,10 @@ export class ProfileSalaryEmployeeController extends Controller { @Request() req: RequestWithUser, @Body() body: CreateProfileSalaryEmployee, ) { - if (!body.profileEmployeeId) { - throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); - } - - const profile = await this.profileRepo.findOneBy({ id: body.profileEmployeeId }); - if (!profile) { - throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); - } - await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id); - - const dest_item = await this.salaryRepo.findOne({ - where: { profileEmployeeId: body.profileEmployeeId }, - order: { order: "DESC" }, + await new ExecuteSalaryReportService().executeEmployeeSalaryUpdate([body], { + user: { sub: req.user.sub, name: req.user.name }, + req, }); - const before = null; - let _posNumCodeSit: string = ""; - let _posNumCodeSitAbb: string = ""; - const _command = await this.commandRepository.findOne({ - where: { id: body.commandId ?? "" }, - }); - if (_command) { - if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { - const orgRootDeputy = await this.orgRootRepository.findOne({ - where: { - isDeputy: true, - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - relations: ["orgRevision"], - }); - _posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; - _posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป."; - } else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { - _posNumCodeSit = "กรุงเทพมหานคร"; - _posNumCodeSitAbb = "กทม."; - } else { - let _profileAdmin = await this.profileGovementRepo.findOne({ - where: { - keycloak: _command?.createdUserId.toString(), - current_holders: { - orgRevision: { - orgRevisionIsCurrent: true, - orgRevisionIsDraft: false, - }, - }, - }, - relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], - }); - _posNumCodeSit = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? - ""; - _posNumCodeSitAbb = - _profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot - .orgRootShortName ?? ""; - } - } - const data = new ProfileSalary(); - data.posNumCodeSit = _posNumCodeSit; - data.posNumCodeSitAbb = _posNumCodeSitAbb; - const meta = { - order: dest_item == null ? 1 : dest_item.order + 1, - createdUserId: req.user.sub, - createdFullName: req.user.name, - lastUpdateUserId: req.user.sub, - lastUpdateFullName: req.user.name, - createdAt: new Date(), - lastUpdatedAt: new Date(), - }; - - Object.assign(data, { ...body, ...meta }); - const history = new ProfileSalaryHistory(); - Object.assign(history, { ...data, id: undefined }); - - await this.salaryRepo.save(data, { data: req }); - setLogDataDiff(req, { before, after: data }); - history.profileSalaryId = data.id; - await this.salaryHistoryRepo.save(history, { data: req }); - - let _null: any = null; - profile.amount = body.amount ?? _null; - profile.amountSpecial = body.amountSpecial ?? _null; - profile.positionSalaryAmount = body.positionSalaryAmount ?? _null; - profile.mouthSalaryAmount = body.mouthSalaryAmount ?? _null; - profile.salaryLevel = body.salaryLevel ?? _null; - profile.group = body.group ?? _null; - await this.profileRepo.save(profile); return new HttpSuccess(); } diff --git a/src/services/ExecuteSalaryProbationService.ts b/src/services/ExecuteSalaryProbationService.ts new file mode 100644 index 00000000..41dfb7b6 --- /dev/null +++ b/src/services/ExecuteSalaryProbationService.ts @@ -0,0 +1,539 @@ +import { Double, EntityManager, In, Repository } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import { Profile } from "../entities/Profile"; +import { ProfileSalary } from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { Command } from "../entities/Command"; +import { OrgRoot } from "../entities/OrgRoot"; +import { OrgRevision } from "../entities/OrgRevision"; +import { checkCommandType, removeProfileInOrganize } from "../interfaces/utils"; +import { CreatePosMasterHistoryOfficer } from "./PositionService"; +import { deleteUser } from "../keycloak"; + +/** + * Input: ข้อมูล 1 คนที่ probation return กลับมา (หลัง Linear Flow refactor) + * - C-PM-11 : excexute/salary-probation (ผ่านทดลองงาน) + * - C-PM-12 : excexute/salary-probation-leave (ออกเพราะผลทดลองฯ ต่ำกว่ามาตรฐาน) + * + * shape เดียวกับ body.data ของ endpoint /org/command/excexute/salary-probation(-leave) เดิม + */ +export interface ProbationSalaryItem { + profileId: string; + commandId?: string | null; + amount?: Double | null; + amountSpecial?: Double | null; + positionSalaryAmount?: Double | null; + mouthSalaryAmount?: Double | null; + commandNo: string | null; + commandYear: number | null; + commandDateAffect?: Date | string | null; + commandDateSign?: Date | string | null; + positionName?: string | null; + commandCode?: string | null; + commandName?: string | null; + remark: string | null; + isGovernment?: boolean | null; // C-PM-12 เท่านั้น +} + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface ProbationExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งทดลองปฏิบัติหน้าที่ราชการ (probation) — C-PM-11, C-PM-12 + * + * เดิมเป็น circular callback: org AMQ → probation → PostData("/org/command/excexute/salary-probation(-leave)") + * หลัง Linear Flow: org AMQ → probation (return salary data) → เรียก service นี้โดยตรง (no callback) + * + * - C-PM-11 : executeProbationPass — สร้าง ProfileSalary + history, isProbation=false + * - C-PM-12 : executeProbationLeave — leave logic + deleteUser(Keycloak) + สร้าง ProfileSalary + history + * + * - endpoint /org/command/excexute/salary-probation(-leave) เรียกผ่าน service นี้ (thin wrapper) + * - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow) + * + * Behavior ทั้งหมด preserve จาก CommandController + * (newSalaryAndUpdateLeaveDisciplinefgh / ExecuteCommand12Async ต้นฉบับ) + * + * Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential) + * ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด) + * + * ⚠️ Keycloak: deleteUser (C-PM-12) ทำภายใน transaction เพื่อ preserve behavior เดิม + * (consistent กับ ExecuteSalaryService C-PM-13/15/16) — Keycloak ไม่สามารถ rollback ได้ + * ถ้า DB rollback หลังจาก deleteUser สำเร็จ → user จะถูกลบใน Keycloak ไปแล้ว + */ +export class ExecuteSalaryProbationService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + // ───────────────────────────────────────────────────────────── + // แก้ปัญหา _posNumCodeSit resolution ที่ซ้ำกันในทุก endpoint + // (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + const profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { posNumCodeSit, posNumCodeSitAbb }; + } + + // normalize date (AMQ path ส่ง string มา → แปลงเป็น Date / null ถ้า invalid) + private toDate(v: any): Date | null { + if (v == null || v === "") return null; + if (v instanceof Date) return isNaN(v.getTime()) ? null : v; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-11 : ผ่านทดลองปฏิบัติหน้าที่ราชการ + // สร้าง ProfileSalary + history แล้ว set isProbation=false + // ═══════════════════════════════════════════════════════════════ + async executeProbationPass( + data: ProbationSalaryItem[], + ctx: ProbationExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryProbationService] executeProbationPass (C-PM-11) — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + // ───────────────────────────────────────────────────────────── + // Normalize date fields (ผ่าน AMQ handler จะได้ string → ต้องแปลงเป็น Date) + // ───────────────────────────────────────────────────────────── + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = this.toDate(it.commandDateAffect); + it.commandDateSign = this.toDate(it.commandDateSign); + } + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + const profileIds = (data ?? []).map((x) => x.profileId).filter(Boolean); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + + for (const item of data ?? []) { + try { + await this.processOneProbationPass( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + salaryRepo, + salaryHistoryRepo, + profileRepository, + orgRevisionRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryProbationService] Failed C-PM-11, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + + // C-PM-11: ผ่านทดลองงาน → isProbation = false (bulk update ใน transaction เดียวกัน) + if (profileIds.length > 0) { + await profileRepository.update({ id: In(profileIds) }, { isProbation: false }); + } + }); + + console.log(`[ExecuteSalaryProbationService] Completed C-PM-11 — ${data?.length ?? 0} items`); + } + + private async processOneProbationPass( + item: ProbationSalaryItem, + ctx: ProbationExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + profileRepository: Repository, + orgRevisionRepo: Repository, + ): Promise { + // current orgRevision (อ่านครั้งเดียวต่อคน — preserve query pattern ของ endpoint เดิม) + const orgRevision = await orgRevisionRepo.findOne({ + where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, + }); + + const profile: any = await profileRepository.findOne({ + relations: [ + "posType", + "posLevel", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); + } + + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null; + const shortName = + orgRevisionRef?.orgChild4?.orgChild4ShortName ?? + orgRevisionRef?.orgChild3?.orgChild3ShortName ?? + orgRevisionRef?.orgChild2?.orgChild2ShortName ?? + orgRevisionRef?.orgChild1?.orgChild1ShortName ?? + orgRevisionRef?.orgRoot?.orgRootShortName ?? + null; + const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null; + // NOTE: endpoint เดิมไม่ได้ load relation "current_holders.positions" → position เป็น null (preserve) + const position = + profile.current_holders + ?.filter((x: any) => x.orgRevisionId == orgRevision?.id)[0] + ?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + const dataSalary = new ProfileSalary(); + dataSalary.posNumCodeSit = _posNumCodeSit; + dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb; + const meta = { + profileId: item.profileId, + commandId: item.commandId, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + amount: item.amount ? item.amount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null, + orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null, + orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null, + orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null, + orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + }; + Object.assign(dataSalary, meta); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...dataSalary, id: undefined }); + + await salaryRepo.save(dataSalary); + history.profileSalaryId = dataSalary.id; + await salaryHistoryRepo.save(history); + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-12 : ออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐาน + // leave logic (removeProfileInOrganize + deleteUser Keycloak) + สร้าง ProfileSalary + history + // ═══════════════════════════════════════════════════════════════ + async executeProbationLeave( + data: ProbationSalaryItem[], + ctx: ProbationExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryProbationService] executeProbationLeave (C-PM-12) — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + for (const item of data ?? []) { + const it = item as any; + it.commandDateAffect = this.toDate(it.commandDateAffect); + it.commandDateSign = this.toDate(it.commandDateSign); + } + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + const orgRevisionRepo = manager.getRepository(OrgRevision); + + for (const item of data ?? []) { + try { + await this.processOneProbationLeave( + item, + ctx, + manager, + _posNumCodeSit, + _posNumCodeSitAbb, + salaryRepo, + salaryHistoryRepo, + profileRepository, + orgRevisionRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryProbationService] Failed C-PM-12, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryProbationService] Completed C-PM-12 — ${data?.length ?? 0} items`); + } + + private async processOneProbationLeave( + item: ProbationSalaryItem, + ctx: ProbationExecutionContext, + manager: EntityManager, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + profileRepository: Repository, + orgRevisionRepo: Repository, + ): Promise { + const req = ctx.req; + + const profile: any = await profileRepository.findOne({ + relations: [ + "posType", + "posLevel", + "current_holders", + "current_holders.orgRoot", + "current_holders.orgChild1", + "current_holders.orgChild2", + "current_holders.orgChild3", + "current_holders.orgChild4", + "current_holders.positions", + "current_holders.positions.posExecutive", + ], + where: { id: item.profileId }, + }); + if (!profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); + } + + const lastSalary = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + select: ["order"], + order: { order: "DESC" }, + }); + const nextOrder = lastSalary ? lastSalary.order + 1 : 1; + let _commandYear = item.commandYear; + if (item.commandYear) { + _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; + } + + // _profile (load แยกสำหรับ mutation เกี่ยวกับ Keycloak/leave — preserve pattern เดิม) + const _profile: any = await profileRepository.findOne({ + where: { id: item.profileId }, + relations: ["roleKeycloaks"], + }); + if (!_profile) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์"); + } + + let dateLeave_: any = item.commandDateAffect; + _profile.isLeave = true; + _profile.leaveReason = + "คำสั่งให้ข้าราชการออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐานที่กำหนด"; + _profile.dateLeave = dateLeave_; + _profile.lastUpdateUserId = ctx.user.sub; + _profile.lastUpdateFullName = ctx.user.name; + _profile.lastUpdatedAt = new Date(); + + const orgRevision = await orgRevisionRepo.findOne({ + where: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }); + const orgRevisionRef = + profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.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 matchHolder = profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id); + const shortName = + !profile.current_holders || profile.current_holders.length == 0 + ? null + : matchHolder != null && matchHolder?.orgChild4 != null + ? `${matchHolder.orgChild4.orgChild4ShortName}` + : matchHolder != null && matchHolder?.orgChild3 != null + ? `${matchHolder.orgChild3.orgChild3ShortName}` + : matchHolder != null && matchHolder?.orgChild2 != null + ? `${matchHolder.orgChild2.orgChild2ShortName}` + : matchHolder != null && matchHolder?.orgChild1 != null + ? `${matchHolder.orgChild1.orgChild1ShortName}` + : matchHolder != null && matchHolder?.orgRoot != null + ? `${matchHolder.orgRoot.orgRootShortName}` + : null; + const posNo = `${matchHolder?.posMasterNo}`; + const position = + matchHolder?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null; + + const profileSalary: ProfileSalary = Object.assign(new ProfileSalary(), { + profileId: item.profileId, + commandId: item.commandId, + positionName: profile.position, + positionType: profile?.posType?.posTypeName ?? null, + positionLevel: profile?.posLevel?.posLevelName ?? null, + positionExecutive: position?.posExecutive?.posExecutiveName ?? null, + amount: item.amount ? item.amount : null, + amountSpecial: item.amountSpecial ? item.amountSpecial : null, + positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null, + mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null, + order: nextOrder, + orgRoot: orgRootRef?.orgRootName ?? null, + orgChild1: orgChild1Ref?.orgChild1Name ?? null, + orgChild2: orgChild2Ref?.orgChild2Name ?? null, + orgChild3: orgChild3Ref?.orgChild3Name ?? null, + orgChild4: orgChild4Ref?.orgChild4Name ?? null, + createdUserId: ctx.user.sub, + createdFullName: ctx.user.name, + lastUpdateUserId: ctx.user.sub, + lastUpdateFullName: ctx.user.name, + createdAt: new Date(), + lastUpdatedAt: new Date(), + dateGovernment: item.commandDateAffect ?? new Date(), + isGovernment: item.isGovernment, + commandNo: item.commandNo, + commandYear: item.commandYear, + posNo: posNo, + posNoAbb: shortName, + commandDateAffect: item.commandDateAffect, + commandDateSign: item.commandDateSign, + commandCode: item.commandCode, + commandName: item.commandName, + remark: item.remark, + posNumCodeSit: _posNumCodeSit, + posNumCodeSitAbb: _posNumCodeSitAbb, + }); + + if (orgRevisionRef) { + await CreatePosMasterHistoryOfficer(orgRevisionRef.id, 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 (preserve behavior เดิม — Keycloak ไม่ rollback ได้) + if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) { + const delUserKeycloak = await deleteUser(_profile.keycloak); + if (delUserKeycloak) { + // Task #228 + _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 Promise.all([ + profileRepository.save(_profile), + salaryRepo.save(profileSalary), + ]); + + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...profileSalary, id: undefined }); + history.profileSalaryId = profileSalary.id; + await salaryHistoryRepo.save(history); + + console.log( + `[ExecuteSalaryProbationService] processOneProbationLeave done — profileId: ${item.profileId}`, + ); + } +} diff --git a/src/services/ExecuteSalaryReportService.ts b/src/services/ExecuteSalaryReportService.ts new file mode 100644 index 00000000..7c72c367 --- /dev/null +++ b/src/services/ExecuteSalaryReportService.ts @@ -0,0 +1,318 @@ +import { EntityManager, Repository } from "typeorm"; +import { AppDataSource } from "../database/data-source"; +import HttpError from "../interfaces/http-error"; +import HttpStatusCode from "../interfaces/http-status"; +import permission from "../interfaces/permission"; +import { setLogDataDiff } from "../interfaces/utils"; +import { Profile } from "../entities/Profile"; +import { ProfileEmployee } from "../entities/ProfileEmployee"; +import { + CreateProfileSalary, + CreateProfileSalaryEmployee, + ProfileSalary, +} from "../entities/ProfileSalary"; +import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory"; +import { Command } from "../entities/Command"; +import { OrgRoot } from "../entities/OrgRoot"; + +/** + * Context สำหรับ audit/log (เหมือน ExecuteSalaryService) + */ +export interface SalaryReportExecutionContext { + user: { sub: string; name: string }; + req?: any; +} + +/** + * Service สำหรับคำสั่งเงินเดือนที่ยิงมาจาก salary service + * + * - C-PM-33, C-PM-34, C-PM-35, C-PM-45 : officer → /org/profile/salary/update + * - C-PM-36, C-PM-37, C-PM-46 : employee → /org/profile-employee/salary/update + * + * เดิมเป็น circular callback: org AMQ → salary service → PostData("/org/profile/(employee/)salary/update") + * หลัง Linear Flow: org AMQ → salary service (return salary data) → เรียก service นี้โดยตรง (no callback) + * + * - executeOfficerSalaryUpdate : สร้าง ProfileSalary + history + อัปเดต Profile (amount*) + * - executeEmployeeSalaryUpdate : สร้าง ProfileSalary + history + อัปเดต ProfileEmployee (amount* + salaryLevel/group) + * + * Behavior ทั้งหมด preserve จาก + * ProfileSalaryController.updateSalary / ProfileSalaryEmployeeController.updateSalary ต้นฉบับ + * (รวม permission check + setLogDataDiff + save({data: req})) + * + * Batch semantics: all-or-nothing — transaction เดียวครอบทั้ง batch + * ถ้าคนใด throw (validation/permission) จะ rollback ทั้ง batch และ propagate error + * + * ⚠️ หมายเหตุ permission check: ทำ per-item ภายใน transaction (preserve behavior เดิมที่เช็คทุกคน) + * ทำ HTTP loopback ไป /org/permission/user/... ด้วย token ใน ctx.req — หาก batch ใหญ่อาจช้า + * (เหมือนเดิม เพราะ salary เดิมก็ยิงเข้า endpoint ทีละคนพร้อม permission check) + */ +export class ExecuteSalaryReportService { + private commandRepository = AppDataSource.getRepository(Command); + private profileRepository = AppDataSource.getRepository(Profile); + private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee); + private orgRootRepository = AppDataSource.getRepository(OrgRoot); + + // ───────────────────────────────────────────────────────────── + // resolve _posNumCodeSit/Abb จาก command (ทำครั้งเดียวก่อนเข้า transaction) + // admin-lookup ใช้ Profile (officer: profileRepo / employee: profileGovementRepo — ทั้งคู่คือ Profile) + // ───────────────────────────────────────────────────────────── + private async resolvePosNumCodeSit( + commandId: string | null | undefined, + ): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> { + let posNumCodeSit = ""; + let posNumCodeSitAbb = ""; + const command = commandId + ? await this.commandRepository.findOne({ where: { id: commandId } }) + : null; + if (command) { + if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") { + const orgRootDeputy = await this.orgRootRepository.findOne({ + where: { + isDeputy: true, + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + relations: ["orgRevision"], + }); + posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร"; + posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป."; + } else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") { + posNumCodeSit = "กรุงเทพมหานคร"; + posNumCodeSitAbb = "กทม."; + } else { + const profileAdmin = await this.profileRepository.findOne({ + where: { + keycloak: command?.createdUserId.toString(), + current_holders: { + orgRevision: { + orgRevisionIsCurrent: true, + orgRevisionIsDraft: false, + }, + }, + }, + relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"], + }); + posNumCodeSit = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ?? + ""; + posNumCodeSitAbb = + profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot + .orgRootShortName ?? ""; + } + } + return { posNumCodeSit, posNumCodeSitAbb }; + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-33/34/35/45 : officer salary update + // ═══════════════════════════════════════════════════════════════ + async executeOfficerSalaryUpdate( + data: CreateProfileSalary[], + ctx: SalaryReportExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryReportService] executeOfficerSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileRepository = manager.getRepository(Profile); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + for (const item of data ?? []) { + try { + await this.processOneOfficer( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + profileRepository, + salaryRepo, + salaryHistoryRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryReportService] Failed officer, commandId=${commandId}, profileId=${item.profileId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryReportService] Completed officer — ${data?.length ?? 0} items`); + } + + private async processOneOfficer( + item: CreateProfileSalary, + ctx: SalaryReportExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileRepository: Repository, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + ): Promise { + const req = ctx.req; + + if (!item.profileId) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileId"); + } + const profile = await profileRepository.findOneBy({ id: item.profileId }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id); + + const dest_item = await salaryRepo.findOne({ + where: { profileId: item.profileId }, + order: { order: "DESC" }, + }); + const before = null; + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.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(data, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.positionSalaryAmount = item.positionSalaryAmount ?? _null; + profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null; + await profileRepository.save(profile); + } + + // ═══════════════════════════════════════════════════════════════ + // C-PM-36/37/46 : employee salary update + // ═══════════════════════════════════════════════════════════════ + async executeEmployeeSalaryUpdate( + data: CreateProfileSalaryEmployee[], + ctx: SalaryReportExecutionContext, + ): Promise { + const commandId = data?.find((x) => x.commandId)?.commandId ?? ""; + console.log( + `[ExecuteSalaryReportService] executeEmployeeSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`, + ); + + const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } = + await this.resolvePosNumCodeSit(commandId); + + await AppDataSource.transaction(async (manager) => { + const profileEmployeeRepository = manager.getRepository(ProfileEmployee); + const salaryRepo = manager.getRepository(ProfileSalary); + const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory); + + for (const item of data ?? []) { + try { + await this.processOneEmployee( + item, + ctx, + _posNumCodeSit, + _posNumCodeSitAbb, + profileEmployeeRepository, + salaryRepo, + salaryHistoryRepo, + ); + } catch (err) { + const reason = + err instanceof HttpError + ? err.message + : err instanceof Error + ? err.message + : "unexpected error"; + console.error( + `[ExecuteSalaryReportService] Failed employee, commandId=${commandId}, profileEmployeeId=${item.profileEmployeeId}: ${reason}`, + err, + ); + throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure + } + } + }); + + console.log(`[ExecuteSalaryReportService] Completed employee — ${data?.length ?? 0} items`); + } + + private async processOneEmployee( + item: CreateProfileSalaryEmployee, + ctx: SalaryReportExecutionContext, + _posNumCodeSit: string, + _posNumCodeSitAbb: string, + profileEmployeeRepository: Repository, + salaryRepo: Repository, + salaryHistoryRepo: Repository, + ): Promise { + const req = ctx.req; + + if (!item.profileEmployeeId) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); + } + const profile = await profileEmployeeRepository.findOneBy({ id: item.profileEmployeeId }); + if (!profile) { + throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว"); + } + await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id); + + const dest_item = await salaryRepo.findOne({ + where: { profileEmployeeId: item.profileEmployeeId }, + order: { order: "DESC" }, + }); + const before = null; + const data = new ProfileSalary(); + data.posNumCodeSit = _posNumCodeSit; + data.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(data, { ...item, ...meta }); + const history = new ProfileSalaryHistory(); + Object.assign(history, { ...data, id: undefined }); + + await salaryRepo.save(data, { data: req }); + setLogDataDiff(req, { before, after: data }); + history.profileSalaryId = data.id; + await salaryHistoryRepo.save(history, { data: req }); + + const _null: any = null; + profile.amount = item.amount ?? _null; + profile.amountSpecial = item.amountSpecial ?? _null; + profile.positionSalaryAmount = item.positionSalaryAmount ?? _null; + profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null; + profile.salaryLevel = item.salaryLevel ?? _null; + profile.group = item.group ?? _null; + await profileEmployeeRepository.save(profile); + } +} diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index f662e247..227c431c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -37,6 +37,8 @@ import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; import { ExecuteSalaryLeaveDisciplineService } from "./ExecuteSalaryLeaveDisciplineService"; import { ExecuteOrgCommandService } from "./ExecuteOrgCommandService"; +import { ExecuteSalaryProbationService } from "./ExecuteSalaryProbationService"; +import { ExecuteSalaryReportService } from "./ExecuteSalaryReportService"; const redis = require("redis"); const REDIS_HOST = process.env.REDIS_HOST; @@ -357,6 +359,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise { const isCommand38 = code === "C-PM-38"; const isCommand40 = code === "C-PM-40"; const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40; + // C-PM-10/11/12: ยิงไป probation service — เป็น branch แยก (ไม่ใช่ .NET linear flow) + // - C-PM-10: fire-only (probation update ในตัวเอง ไม่มี org-side action) + // - C-PM-11/12: probation return salary data → เรียก ExecuteSalaryProbationService ตรงๆ + const isProbation = ["C-PM-10", "C-PM-11", "C-PM-12"].includes(code); + // C-PM-33/34/35/45 (officer) + C-PM-36/37/46 (employee): ยิงไป salary service + // เป็น branch แยก — salary return salary data → เรียก ExecuteSalaryReportService ตรงๆ + const isSalaryServiceOfficer = ["C-PM-33", "C-PM-34", "C-PM-35", "C-PM-45"].includes(code); + const isSalaryServiceEmployee = ["C-PM-36", "C-PM-37", "C-PM-46"].includes(code); + const isSalaryService = isSalaryServiceOfficer || isSalaryServiceEmployee; const isLinearFlow = isOfficerProfile || isSalaryCurrent || @@ -384,6 +395,99 @@ async function handler(msg: amqp.ConsumeMessage): Promise { await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx); } console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`); + } else if (isProbation) { + // Probation Linear Flow (C-PM-10/11/12) + // - C-PM-10: fire-only — probation อัปเดต appoint ในตัวเอง ไม่มี org-side action + // - C-PM-11/12: fire → probation return salary data → route ไป ExecuteSalaryProbationService + // แทนการ callback เข้า org (Circular Flow เดิม) + console.log(`[AMQ] Probation Linear Flow (${code})`); + if (code === "C-PM-10") { + for (const chunk of chunks) { + await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + } + console.log(`[AMQ] C-PM-10 fire-only — no org-side action`); + } else { + let resultData: any[] = []; + for (const chunk of chunks) { + const res = await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + // รองรับทั้ง array และ { data: [...] } (contract ของ probation หลัง Linear Flow) + if (res && Array.isArray(res.data)) { + resultData.push(...res.data); + } else if (Array.isArray(res)) { + resultData.push(...res); + } + } + + if (resultData.length > 0) { + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + + if (code === "C-PM-11") { + await new ExecuteSalaryProbationService().executeProbationPass(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryProbationService (C-PM-11)`, + ); + } else if (code === "C-PM-12") { + await new ExecuteSalaryProbationService().executeProbationLeave(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryProbationService (C-PM-12)`, + ); + } + } + } + } else if (isSalaryService) { + // Salary Service Linear Flow (C-PM-33/34/35/45 officer, C-PM-36/37/46 employee) + // fire → salary service return salary data → route ไป ExecuteSalaryReportService + // แทนการ callback เข้า /org/profile(/-employee)/salary/update (Circular Flow เดิม) + console.log(`[AMQ] Salary Service Linear Flow (${code})`); + let resultData: any[] = []; + for (const chunk of chunks) { + const res = await new CallAPI().PostData( + { headers: { authorization: token } }, + path + "/excecute", + { refIds: chunk }, + false, + ); + // รองรับทั้ง array และ { data: [...] } (contract ของ salary service หลัง Linear Flow) + if (res && Array.isArray(res.data)) { + resultData.push(...res.data); + } else if (Array.isArray(res)) { + resultData.push(...res); + } + } + + if (resultData.length > 0) { + const pseudoReq = { headers: { authorization: token }, user }; + const ctx = { + user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, + req: pseudoReq, + }; + + if (isSalaryServiceOfficer) { + await new ExecuteSalaryReportService().executeOfficerSalaryUpdate(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryReportService (officer)`, + ); + } else { + await new ExecuteSalaryReportService().executeEmployeeSalaryUpdate(resultData, ctx); + console.log( + `[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryReportService (employee)`, + ); + } + } } else if (isLinearFlow) { console.log(`[AMQ] Linear Flow (${code})`); const isCpm32 = code === "C-PM-32"; From 2f7ec40e783b1b5172fded9bb49bfdca9f85a8b7 Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 29 Jun 2026 16:01:16 +0700 Subject: [PATCH 38/39] add pass redis test --- src/controllers/AuthRoleController.ts | 2 ++ src/controllers/OrganizationController.ts | 1 + src/controllers/PermissionController.ts | 7 +++++++ src/controllers/PosMasterActController.ts | 2 ++ src/interfaces/extension.ts | 1 + src/interfaces/permission.ts | 13 ++++++++----- src/services/ExecuteOrgCommandService.ts | 1 + src/services/rabbitmq.ts | 1 + 8 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index 13f5ce04..a549dfb5 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -128,6 +128,7 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { @@ -296,6 +297,7 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); await redisClient.flushdb(function (err: any, succeeded: any) { diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 8358313b..605b2fa5 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8830,6 +8830,7 @@ export class OrganizationController extends Controller { const redisClient = redis.createClient({ host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), + password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 44747b09..324a42b4 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -42,6 +42,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -293,6 +294,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -691,6 +693,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -807,6 +810,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -902,6 +906,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1155,6 +1160,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1413,6 +1419,7 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); // const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index fbc09201..9ccf1d9c 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -328,6 +328,7 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); @@ -865,6 +866,7 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index 405ed849..a2a55256 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -384,6 +384,7 @@ class Extension { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); const deleteKey = delAsync(type + id); diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index 5d22d274..aa1903ec 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -217,17 +217,20 @@ class CheckAuth { // Create Redis client const redisClient = this.redis.createClient({ - socket: { - host: REDIS_HOST, - port: REDIS_PORT, - }, + // socket: { + // host: REDIS_HOST, + // port: REDIS_PORT, + // }, + host: REDIS_HOST, + port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); redisClient.on("error", (err: any) => { console.error("[REDIS] Connection error:", err.message); }); - await redisClient.connect(); + // await redisClient.connect(); console.log("[REDIS] Connected successfully!"); const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts index 62ed231f..31a7d317 100644 --- a/src/services/ExecuteOrgCommandService.ts +++ b/src/services/ExecuteOrgCommandService.ts @@ -844,6 +844,7 @@ export class ExecuteOrgCommandService { const redisClient = await redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index 227c431c..b4e81c41 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1987,6 +1987,7 @@ async function clearMenuAndRoleCache(): Promise { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, + password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); From 8807ee32262231de2e00442de68abf6683994b96 Mon Sep 17 00:00:00 2001 From: harid Date: Tue, 30 Jun 2026 11:04:19 +0700 Subject: [PATCH 39/39] revert add password redis --- src/controllers/AuthRoleController.ts | 2 -- src/controllers/OrganizationController.ts | 1 - src/controllers/PermissionController.ts | 7 ------- src/controllers/PosMasterActController.ts | 2 -- src/interfaces/extension.ts | 1 - src/interfaces/permission.ts | 13 +++++-------- src/services/ExecuteOrgCommandService.ts | 1 - src/services/rabbitmq.ts | 1 - 8 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/controllers/AuthRoleController.ts b/src/controllers/AuthRoleController.ts index a549dfb5..13f5ce04 100644 --- a/src/controllers/AuthRoleController.ts +++ b/src/controllers/AuthRoleController.ts @@ -128,7 +128,6 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); redisClient.del("role_" + posMaster.current_holderId, (err: Error) => { @@ -297,7 +296,6 @@ export class AuthRoleController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); await redisClient.flushdb(function (err: any, succeeded: any) { diff --git a/src/controllers/OrganizationController.ts b/src/controllers/OrganizationController.ts index 605b2fa5..8358313b 100644 --- a/src/controllers/OrganizationController.ts +++ b/src/controllers/OrganizationController.ts @@ -8830,7 +8830,6 @@ export class OrganizationController extends Controller { const redisClient = redis.createClient({ host: process.env.REDIS_HOST || "localhost", port: parseInt(process.env.REDIS_PORT || "6379"), - password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/controllers/PermissionController.ts b/src/controllers/PermissionController.ts index 324a42b4..44747b09 100644 --- a/src/controllers/PermissionController.ts +++ b/src/controllers/PermissionController.ts @@ -42,7 +42,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -294,7 +293,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -693,7 +691,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -810,7 +807,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -906,7 +902,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1160,7 +1155,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const getAsync = promisify(redisClient.get).bind(redisClient); @@ -1419,7 +1413,6 @@ export class PermissionController extends Controller { redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); // const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/controllers/PosMasterActController.ts b/src/controllers/PosMasterActController.ts index 9ccf1d9c..fbc09201 100644 --- a/src/controllers/PosMasterActController.ts +++ b/src/controllers/PosMasterActController.ts @@ -328,7 +328,6 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); @@ -866,7 +865,6 @@ export class PosMasterActController extends Controller { const redisClient = await this.redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/interfaces/extension.ts b/src/interfaces/extension.ts index a2a55256..405ed849 100644 --- a/src/interfaces/extension.ts +++ b/src/interfaces/extension.ts @@ -384,7 +384,6 @@ class Extension { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); const deleteKey = delAsync(type + id); diff --git a/src/interfaces/permission.ts b/src/interfaces/permission.ts index aa1903ec..5d22d274 100644 --- a/src/interfaces/permission.ts +++ b/src/interfaces/permission.ts @@ -217,20 +217,17 @@ class CheckAuth { // Create Redis client const redisClient = this.redis.createClient({ - // socket: { - // host: REDIS_HOST, - // port: REDIS_PORT, - // }, - host: REDIS_HOST, - port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, + socket: { + host: REDIS_HOST, + port: REDIS_PORT, + }, }); redisClient.on("error", (err: any) => { console.error("[REDIS] Connection error:", err.message); }); - // await redisClient.connect(); + await redisClient.connect(); console.log("[REDIS] Connected successfully!"); const getAsync = promisify(redisClient.get).bind(redisClient); diff --git a/src/services/ExecuteOrgCommandService.ts b/src/services/ExecuteOrgCommandService.ts index 31a7d317..62ed231f 100644 --- a/src/services/ExecuteOrgCommandService.ts +++ b/src/services/ExecuteOrgCommandService.ts @@ -844,7 +844,6 @@ export class ExecuteOrgCommandService { const redisClient = await redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const delAsync = promisify(redisClient.del).bind(redisClient); diff --git a/src/services/rabbitmq.ts b/src/services/rabbitmq.ts index b4e81c41..227c431c 100644 --- a/src/services/rabbitmq.ts +++ b/src/services/rabbitmq.ts @@ -1987,7 +1987,6 @@ async function clearMenuAndRoleCache(): Promise { const redisClient = redis.createClient({ host: REDIS_HOST, port: REDIS_PORT, - password: process.env.REDIS_PASSWORD, }); const keysAsync = promisify(redisClient.keys).bind(redisClient);