From 7b37cc37db274351b84a57f22ebf29340e3e548c Mon Sep 17 00:00:00 2001 From: harid Date: Mon, 29 Jun 2026 14:28:50 +0700 Subject: [PATCH] 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";