Linear Flow Probation+Salary #224

This commit is contained in:
harid 2026-06-29 14:28:50 +07:00
parent 3d2fc5128a
commit 7b37cc37db
6 changed files with 976 additions and 572 deletions

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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<void> {
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<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
profileRepository: Repository<Profile>,
orgRevisionRepo: Repository<OrgRevision>,
): Promise<void> {
// 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<void> {
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<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
profileRepository: Repository<Profile>,
orgRevisionRepo: Repository<OrgRevision>,
): Promise<void> {
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}`,
);
}
}

View file

@ -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<void> {
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<Profile>,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
): Promise<void> {
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<void> {
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<ProfileEmployee>,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
): Promise<void> {
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);
}
}

View file

@ -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<boolean> {
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<boolean> {
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";