Compare commits
9 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759e69d44d | ||
|
|
cf161f9809 | ||
|
|
30ddb4e3f3 | ||
|
|
005f3f1334 | ||
|
|
8807ee3226 | ||
|
|
2f7ec40e78 | ||
|
|
7b37cc37db | ||
|
|
3d2fc5128a | ||
|
|
832c5d2cb3 |
19 changed files with 5051 additions and 3863 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -7227,7 +7227,9 @@ export class OrganizationDotnetController extends Controller {
|
|||
dateStart: profileEmp?.dateStart ?? null,
|
||||
dateAppoint: profileEmp?.dateAppoint ?? null,
|
||||
keycloak: profileEmp?.keycloak ?? null,
|
||||
posNo: `${item.shortName} ${item.posMasterNo}`,
|
||||
posNo: `${item.shortName} ${[item.posMasterNoPrefix, item.posMasterNo, item.posMasterNoSuffix]
|
||||
.filter((p) => p !== null && p !== undefined && p !== "")
|
||||
.join(" ")}`,
|
||||
position: item.position,
|
||||
positionLevel: item.posLevel,
|
||||
positionType: item.posType,
|
||||
|
|
@ -8900,7 +8902,7 @@ export class OrganizationDotnetController extends Controller {
|
|||
|
||||
const profiles = await this.profileRepo.find({
|
||||
where: { id: In(profileIds) },
|
||||
select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak"],
|
||||
select: ["id", "citizenId", "dateStart", "dateAppoint", "keycloak", "posMasterNo"],
|
||||
});
|
||||
|
||||
const profileMap = new Map(profiles.map((p) => [p.id, p]));
|
||||
|
|
@ -8918,7 +8920,11 @@ export class OrganizationDotnetController extends Controller {
|
|||
dateStart: profile?.dateStart ?? null,
|
||||
dateAppoint: profile?.dateAppoint ?? null,
|
||||
keycloak: profile?.keycloak ?? null,
|
||||
posNo: `${item.shortName} ${item.posMasterNo}`,
|
||||
posNo:
|
||||
profile?.posMasterNo ??
|
||||
`${item.shortName} ${[item.posMasterNoPrefix, item.posMasterNo, item.posMasterNoSuffix]
|
||||
.filter((p) => p !== null && p !== undefined && p !== "")
|
||||
.join(" ")}`,
|
||||
position: item.position,
|
||||
positionLevel: item.posLevel,
|
||||
positionType: item.posType,
|
||||
|
|
|
|||
|
|
@ -621,13 +621,13 @@ export class PosMasterActController extends Controller {
|
|||
let conditionGroup = "";
|
||||
if (body.type.trim().toUpperCase() == "GROUP1.1") {
|
||||
conditionGroup =
|
||||
"(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ชำนาญงาน') OR (posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ปฏิบัติงาน') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ปฏิบัติการ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการ')";
|
||||
"(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ชำนาญงาน') OR (posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ปฏิบัติงาน') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ปฏิบัติการ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการ')";
|
||||
} else if (body.type.trim().toUpperCase() == "GROUP1.2") {
|
||||
conditionGroup =
|
||||
"(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'อาวุโส') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ชำนาญการพิเศษ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'ต้น')";
|
||||
"(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'อาวุโส') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ชำนาญการพิเศษ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'ต้น')";
|
||||
} else if (body.type.trim().toUpperCase() == "GROUP2") {
|
||||
conditionGroup =
|
||||
"(posTypeAct.posTypeName = 'ทั่วไป' AND posLevelAct.posLevelName = 'ทักษะพิเศษ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'เชี่ยวชาญ') OR (posTypeAct.posTypeName = 'วิชาการ' AND posLevelAct.posLevelName = 'ทรงคุณวุฒิ') OR (posTypeAct.posTypeName = 'อำนวยการ' AND posLevelAct.posLevelName = 'สูง') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'ต้น') OR (posTypeAct.posTypeName = 'บริหาร' AND posLevelAct.posLevelName = 'สูง')";
|
||||
"(posType.posTypeName = 'ทั่วไป' AND posLevel.posLevelName = 'ทักษะพิเศษ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'เชี่ยวชาญ') OR (posType.posTypeName = 'วิชาการ' AND posLevel.posLevelName = 'ทรงคุณวุฒิ') OR (posType.posTypeName = 'อำนวยการ' AND posLevel.posLevelName = 'สูง') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'ต้น') OR (posType.posTypeName = 'บริหาร' AND posLevel.posLevelName = 'สูง')";
|
||||
} else {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "กลุ่มเป้าหมายไม่ถูกต้อง");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { PosMaster } from "../entities/PosMaster";
|
|||
import { Position } from "../entities/Position";
|
||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
import { EmployeePosition } from "../entities/EmployeePosition";
|
||||
import { In, IsNull, MoreThan, Not } from "typeorm";
|
||||
import { EntityManager, In, IsNull, MoreThan, Not } from "typeorm";
|
||||
import { RequestWithUser } from "../middlewares/user";
|
||||
import { Command } from "../entities/Command";
|
||||
import { ProfileSalary } from "../entities/ProfileSalary";
|
||||
|
|
@ -254,14 +254,23 @@ export function calculateRetireYear(birthDate: Date) {
|
|||
|
||||
return yy + 61;
|
||||
}
|
||||
export async function removeProfileInOrganize(profileId: string, type: string) {
|
||||
const currentRevision = await AppDataSource.getRepository(OrgRevision)
|
||||
export async function removeProfileInOrganize(
|
||||
profileId: string,
|
||||
type: string,
|
||||
manager?: EntityManager,
|
||||
) {
|
||||
// ถ้าส่ง manager เข้ามา → ทุก query/update อยู่ใน transaction ของ caller (all-or-nothing)
|
||||
// ถ้าไม่ส่ง → ใช้ global DataSource เหมือนเดิม (backward compatible)
|
||||
const ds = manager ?? AppDataSource;
|
||||
const currentRevision = await ds
|
||||
.getRepository(OrgRevision)
|
||||
.createQueryBuilder("orgRevision")
|
||||
.where("orgRevision.orgRevisionIsDraft = false")
|
||||
.andWhere("orgRevision.orgRevisionIsCurrent = true")
|
||||
.getOne();
|
||||
|
||||
const draftRevision = await AppDataSource.getRepository(OrgRevision)
|
||||
const draftRevision = await ds
|
||||
.getRepository(OrgRevision)
|
||||
.createQueryBuilder("orgRevision")
|
||||
.where("orgRevision.orgRevisionIsDraft = true")
|
||||
.andWhere("orgRevision.orgRevisionIsCurrent = false")
|
||||
|
|
@ -271,26 +280,30 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
return;
|
||||
}
|
||||
if (type === "OFFICER") {
|
||||
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster)
|
||||
const findProfileInposMaster = await ds
|
||||
.getRepository(PosMaster)
|
||||
.createQueryBuilder("posMaster")
|
||||
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
|
||||
.andWhere("posMaster.current_holderId = :profileId", { profileId })
|
||||
.getOne();
|
||||
|
||||
await AppDataSource.getRepository(PosMaster)
|
||||
await ds
|
||||
.getRepository(PosMaster)
|
||||
.createQueryBuilder()
|
||||
.update(PosMaster)
|
||||
.set({ current_holderId: null, isSit: false })
|
||||
.where("id = :id", { id: findProfileInposMaster?.id })
|
||||
.execute();
|
||||
|
||||
const findProfileInposMasterDraft = await AppDataSource.getRepository(PosMaster)
|
||||
const findProfileInposMasterDraft = await ds
|
||||
.getRepository(PosMaster)
|
||||
.createQueryBuilder("posMaster")
|
||||
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: draftRevision?.id })
|
||||
.andWhere("posMaster.next_holderId = :profileId", { profileId })
|
||||
.getOne();
|
||||
|
||||
await AppDataSource.getRepository(PosMaster)
|
||||
await ds
|
||||
.getRepository(PosMaster)
|
||||
.createQueryBuilder()
|
||||
.update(PosMaster)
|
||||
.set({ next_holderId: null, isSit: false })
|
||||
|
|
@ -300,7 +313,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
if (!findProfileInposMaster && !findProfileInposMasterDraft) {
|
||||
return;
|
||||
}
|
||||
const findPosition = await AppDataSource.getRepository(Position)
|
||||
const findPosition = await ds
|
||||
.getRepository(Position)
|
||||
.createQueryBuilder("position")
|
||||
.where("position.posMasterId = :posMasterId", { posMasterId: findProfileInposMaster?.id })
|
||||
.getMany();
|
||||
|
|
@ -308,7 +322,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
if (!findPosition) {
|
||||
return;
|
||||
}
|
||||
await AppDataSource.getRepository(Position)
|
||||
await ds
|
||||
.getRepository(Position)
|
||||
.createQueryBuilder()
|
||||
.update(Position)
|
||||
.set({ positionIsSelected: false })
|
||||
|
|
@ -316,14 +331,16 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
.execute();
|
||||
}
|
||||
if (type === "EMPLOYEE") {
|
||||
const findProfileInEmpPosMaster = await AppDataSource.getRepository(EmployeePosMaster)
|
||||
const findProfileInEmpPosMaster = await ds
|
||||
.getRepository(EmployeePosMaster)
|
||||
.createQueryBuilder("employeePosMaster")
|
||||
.where("employeePosMaster.orgRevisionId = :orgRevisionId", {
|
||||
orgRevisionId: currentRevision?.id,
|
||||
})
|
||||
.andWhere("employeePosMaster.current_holderId = :profileId", { profileId })
|
||||
.getOne();
|
||||
await AppDataSource.getRepository(EmployeePosMaster)
|
||||
await ds
|
||||
.getRepository(EmployeePosMaster)
|
||||
.createQueryBuilder()
|
||||
.update(EmployeePosMaster)
|
||||
.set({ current_holderId: null, isSit: false })
|
||||
|
|
@ -333,7 +350,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
if (!findProfileInEmpPosMaster) {
|
||||
return;
|
||||
}
|
||||
const findEmpPosition = await AppDataSource.getRepository(EmployeePosition)
|
||||
const findEmpPosition = await ds
|
||||
.getRepository(EmployeePosition)
|
||||
.createQueryBuilder("employeePosition")
|
||||
.where("employeePosition.posMasterId = :posMasterId", {
|
||||
posMasterId: findProfileInEmpPosMaster?.id,
|
||||
|
|
@ -344,7 +362,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
await AppDataSource.getRepository(EmployeePosition)
|
||||
await ds
|
||||
.getRepository(EmployeePosition)
|
||||
.createQueryBuilder()
|
||||
.update(EmployeePosition)
|
||||
.set({ positionIsSelected: false })
|
||||
|
|
@ -353,8 +372,10 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function removePostMasterAct(profileId: string) {
|
||||
const currentRevision = await AppDataSource.getRepository(OrgRevision)
|
||||
export async function removePostMasterAct(profileId: string, manager?: EntityManager) {
|
||||
const ds = manager ?? AppDataSource;
|
||||
const currentRevision = await ds
|
||||
.getRepository(OrgRevision)
|
||||
.createQueryBuilder("orgRevision")
|
||||
.where("orgRevision.orgRevisionIsDraft = false")
|
||||
.andWhere("orgRevision.orgRevisionIsCurrent = true")
|
||||
|
|
@ -364,7 +385,8 @@ export async function removePostMasterAct(profileId: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster)
|
||||
const findProfileInposMaster = await ds
|
||||
.getRepository(PosMaster)
|
||||
.createQueryBuilder("posMaster")
|
||||
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
|
||||
.andWhere("posMaster.current_holderId = :profileId", { profileId })
|
||||
|
|
@ -374,11 +396,12 @@ export async function removePostMasterAct(profileId: string) {
|
|||
return;
|
||||
}
|
||||
|
||||
const posMasterAct = await AppDataSource.getRepository(PosMasterAct)
|
||||
const posMasterAct = await ds
|
||||
.getRepository(PosMasterAct)
|
||||
.createQueryBuilder("posMasterAct")
|
||||
.where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id })
|
||||
.getMany();
|
||||
await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct);
|
||||
await ds.getRepository(PosMasterAct).remove(posMasterAct);
|
||||
}
|
||||
|
||||
export async function checkReturnCommandType(commandId: string) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { AppDataSource } from "../database/data-source";
|
||||
import { CommandRecive } from "../entities/CommandRecive";
|
||||
import { Command } from "../entities/Command";
|
||||
import { CommandOperator } from "../entities/CommandOperator";
|
||||
import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { Position } from "../entities/Position";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { RequestWithUser } from "../middlewares/user";
|
||||
import { EntityManager } from "typeorm";
|
||||
|
||||
export interface PosNumCodeSitResult {
|
||||
posNumCodeSit: string;
|
||||
|
|
@ -17,21 +21,23 @@ export interface PosNumCodeSitResult {
|
|||
* เรียงลำดับผู้ได้รับคำสั่งใหม่หลังจากลบรายการ และอัพเดทสถานะคำสั่งถ้าไม่มีผู้ได้รับคำสั่งเหลือ
|
||||
* @param reciveId commandRecive.Id ของผู้ได้รับคำสั่ง
|
||||
* @param code ประเภทคำสั่ง
|
||||
* @param manager ถ้าส่งเข้ามา → ทุก operation อยู่ใน transaction ของ caller (all-or-nothing)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function reOrderCommandRecivesAndDelete(
|
||||
reciveId: string
|
||||
reciveId: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const commandReciveRepo = AppDataSource.getRepository(CommandRecive);
|
||||
const commandRepo = AppDataSource.getRepository(Command);
|
||||
const ds = manager ?? AppDataSource;
|
||||
const commandReciveRepo = ds.getRepository(CommandRecive);
|
||||
const commandRepo = ds.getRepository(Command);
|
||||
|
||||
// ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId
|
||||
const commandRecive = await commandReciveRepo.findOne({
|
||||
where: { id: reciveId }
|
||||
where: { id: reciveId },
|
||||
});
|
||||
|
||||
if (commandRecive == null)
|
||||
return;
|
||||
if (commandRecive == null) return;
|
||||
|
||||
const commandId = commandRecive.commandId;
|
||||
// ลบตาม refId
|
||||
|
|
@ -43,17 +49,14 @@ export async function reOrderCommandRecivesAndDelete(
|
|||
});
|
||||
// ลำดับผู้ได้รับคำสั่งใหม่
|
||||
if (commandReciveList.length > 0) {
|
||||
await Promise.all(
|
||||
commandReciveList.map(async (p, i) => {
|
||||
p.order = i + 1;
|
||||
await commandReciveRepo.save(p);
|
||||
})
|
||||
);
|
||||
for (let i = 0; i < commandReciveList.length; i++) {
|
||||
commandReciveList[i].order = i + 1;
|
||||
await commandReciveRepo.save(commandReciveList[i]);
|
||||
}
|
||||
} else {
|
||||
// ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง
|
||||
await commandRepo.update({ id: commandId }, { status: "CANCEL" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -144,3 +147,101 @@ export async function getPosNumCodeSit(
|
|||
commandExcecuteDate,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้าง/insert CommandOperator "เจ้าหน้าที่ดำเนินการ" สำหรับ command
|
||||
* ใช้ userProfile ที่ query ไปแล้วถ้ามี ถ้าไม่มีค่อย query ใหม่
|
||||
* @param userProfile profile ที่ query ไปแล้ว (หรือ null ถ้ายังไม่ได้ query)
|
||||
* @param commandId command id ที่จะผูกกับ operator
|
||||
* @param request request context (สำหรับ user.sub / user.name)
|
||||
* @param now timestamp สำหรับ audit fields
|
||||
* @param manager ถ้าส่งเข้ามา → ทุก operation อยู่ใน transaction ของ caller (all-or-nothing)
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function ensureCommandOperator(
|
||||
userProfile: Profile | null,
|
||||
commandId: string,
|
||||
request: RequestWithUser,
|
||||
now: Date,
|
||||
manager?: EntityManager,
|
||||
): Promise<void> {
|
||||
const ds = manager ?? AppDataSource;
|
||||
const profileRepo = ds.getRepository(Profile);
|
||||
const positionRepo = ds.getRepository(Position);
|
||||
const commandOperatorRepo = ds.getRepository(CommandOperator);
|
||||
|
||||
if (!request.user.sub) return;
|
||||
// ใช้ userProfile ที่ query ไปแล้วถ้ามี ถ้าไม่มีค่อย query ใหม่
|
||||
let profile = userProfile;
|
||||
if (!profile) {
|
||||
profile = await profileRepo.findOne({
|
||||
where: { keycloak: request.user.sub },
|
||||
relations: {
|
||||
posLevel: true,
|
||||
posType: true,
|
||||
current_holders: {
|
||||
orgRevision: true,
|
||||
orgRoot: true,
|
||||
orgChild1: true,
|
||||
orgChild2: true,
|
||||
orgChild3: true,
|
||||
orgChild4: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
if (!profile) return;
|
||||
|
||||
const currentHolder = profile.current_holders?.find(
|
||||
(x) =>
|
||||
x.orgRevision?.orgRevisionIsDraft === false &&
|
||||
x.orgRevision?.orgRevisionIsCurrent === true,
|
||||
);
|
||||
|
||||
const posNo =
|
||||
currentHolder != null && currentHolder.orgChild4 != null
|
||||
? `${currentHolder.orgChild4.orgChild4ShortName} ${currentHolder.posMasterNo}`
|
||||
: currentHolder != null && currentHolder.orgChild3 != null
|
||||
? `${currentHolder.orgChild3.orgChild3ShortName} ${currentHolder.posMasterNo}`
|
||||
: currentHolder != null && currentHolder.orgChild2 != null
|
||||
? `${currentHolder.orgChild2.orgChild2ShortName} ${currentHolder.posMasterNo}`
|
||||
: currentHolder != null && currentHolder.orgChild1 != null
|
||||
? `${currentHolder.orgChild1.orgChild1ShortName} ${currentHolder.posMasterNo}`
|
||||
: currentHolder != null && currentHolder?.orgRoot != null
|
||||
? `${currentHolder.orgRoot.orgRootShortName} ${currentHolder.posMasterNo}`
|
||||
: null;
|
||||
|
||||
const position = await positionRepo.findOne({
|
||||
where: {
|
||||
positionIsSelected: true,
|
||||
posMaster: {
|
||||
orgRevisionId: currentHolder?.orgRevisionId,
|
||||
current_holderId: profile.id,
|
||||
},
|
||||
},
|
||||
order: { createdAt: "DESC" },
|
||||
relations: { posExecutive: true },
|
||||
});
|
||||
|
||||
const operator = Object.assign(new CommandOperator(), {
|
||||
profileId: profile?.id,
|
||||
prefix: profile?.prefix,
|
||||
firstName: profile?.firstName,
|
||||
lastName: profile?.lastName,
|
||||
posNo: posNo,
|
||||
posType: profile?.posType?.posTypeName ?? null,
|
||||
posLevel: profile?.posLevel?.posLevelName ?? null,
|
||||
position: position?.positionName ?? null,
|
||||
positionExecutive: position?.posExecutive?.posExecutiveName ?? null,
|
||||
roleName: "เจ้าหน้าที่ดำเนินการ",
|
||||
orderNo: 1,
|
||||
commandId: commandId,
|
||||
createdUserId: request.user.sub,
|
||||
createdFullName: request.user.name,
|
||||
createdAt: now,
|
||||
lastUpdateUserId: request.user.sub,
|
||||
lastUpdateFullName: request.user.name,
|
||||
lastUpdatedAt: now,
|
||||
});
|
||||
await commandOperatorRepo.save(operator);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { In, Like } from "typeorm";
|
||||
import { EntityManager, In, Like } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
|
|
@ -129,14 +129,23 @@ export interface ExecutionContext {
|
|||
/**
|
||||
* Service สำหรับสร้าง/อัปเดตทะเบียนประวัติข้าราชการ (Profile) หลังออกคำสั่งบรรจุ
|
||||
*
|
||||
* ถูกออกแบบมาเพื่อแก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ
|
||||
* โดยให้ฝั่งบรรจุส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เอง
|
||||
* ที่ต้นทาง (Linear Flow) แทนการเรียกซ้อนกันกลับไปมา
|
||||
* ใช้กับ commandType: C-PM-01, 02, 14
|
||||
*
|
||||
* - endpoint /org/command/excexute/create-officer-profile เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (no HTTP loopback)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.CreateOfficeProfileExcecute ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operations (createUser/addUserRoles/removeUserRoles/updateUserAttributes)
|
||||
* ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้
|
||||
* ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*
|
||||
* Design note: แก้ปัญหา "Circular Dependency" ระหว่าง API Org กับ API บรรจุ โดยให้ฝั่งบรรจุ
|
||||
* ส่ง resultData กลับมา แล้วฝั่ง Org ประมวลผลสร้าง profile เองที่ต้นทาง แทนการเรียกซ้อนกัน
|
||||
*/
|
||||
export class ExecuteOfficerProfileService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
|
|
@ -177,8 +186,12 @@ export class ExecuteOfficerProfileService {
|
|||
data: OfficerProfileItem[],
|
||||
ctx: ExecutionContext,
|
||||
): Promise<void> {
|
||||
console.log("[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile");
|
||||
console.log("[ExecuteOfficerProfileService] Request body count:", data?.length);
|
||||
const commandId =
|
||||
data?.find((x: any) => x.bodySalarys?.commandId)?.bodySalarys?.commandId ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile — commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteOfficerProfileService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields
|
||||
|
|
@ -326,20 +339,101 @@ export class ExecuteOfficerProfileService {
|
|||
data.length,
|
||||
"profile(s)",
|
||||
);
|
||||
await Promise.all(
|
||||
data.map(async (item, index) => {
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
roleKeycloak,
|
||||
list,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
meta,
|
||||
before,
|
||||
commandId,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteOfficerProfileService] Failed commandId=${commandId}, citizenId=${item.bodyProfile?.citizenId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของ batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: OfficerProfileItem,
|
||||
ctx: ExecutionContext,
|
||||
manager: EntityManager,
|
||||
roleKeycloak: RoleKeycloak | null,
|
||||
list: any[],
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
meta: any,
|
||||
before: any,
|
||||
commandId: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// repo ทั้งหมดสร้างจาก manager เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const posLevelRepo = manager.getRepository(PosLevel);
|
||||
const posTypeRepo = manager.getRepository(PosType);
|
||||
const provinceRepo = manager.getRepository(Province);
|
||||
const districtRepo = manager.getRepository(District);
|
||||
const subDistrictRepo = manager.getRepository(SubDistrict);
|
||||
const posMasterRepository = manager.getRepository(PosMaster);
|
||||
const positionRepository = manager.getRepository(Position);
|
||||
const profileEducationRepo = manager.getRepository(ProfileEducation);
|
||||
const profileEducationHistoryRepo = manager.getRepository(ProfileEducationHistory);
|
||||
const certificateRepo = manager.getRepository(ProfileCertificate);
|
||||
const certificateHistoryRepo = manager.getRepository(ProfileCertificateHistory);
|
||||
const profileFamilyCoupleRepo = manager.getRepository(ProfileFamilyCouple);
|
||||
const profileFamilyCoupleHistoryRepo = manager.getRepository(ProfileFamilyCoupleHistory);
|
||||
const profileFamilyFatherRepo = manager.getRepository(ProfileFamilyFather);
|
||||
const profileFamilyFatherHistoryRepo = manager.getRepository(ProfileFamilyFatherHistory);
|
||||
const profileFamilyMotherRepo = manager.getRepository(ProfileFamilyMother);
|
||||
const profileFamilyMotherHistoryRepo = manager.getRepository(ProfileFamilyMotherHistory);
|
||||
const insigniaRepo = manager.getRepository(ProfileInsignia);
|
||||
const insigniaHistoryRepo = manager.getRepository(ProfileInsigniaHistory);
|
||||
const avatarRepository = manager.getRepository(ProfileAvatar);
|
||||
|
||||
console.log(
|
||||
"[ExecuteOfficerProfileService] Processing item",
|
||||
index + 1,
|
||||
"of",
|
||||
data.length,
|
||||
"[ExecuteOfficerProfileService] Processing citizenId:",
|
||||
item.bodyProfile.citizenId,
|
||||
);
|
||||
const _null: any = null;
|
||||
if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null;
|
||||
if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null;
|
||||
if (
|
||||
item.bodyProfile.posLevelId &&
|
||||
!(await this.posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId }))
|
||||
!(await posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId }))
|
||||
) {
|
||||
console.error(
|
||||
"[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:",
|
||||
|
|
@ -349,7 +443,7 @@ export class ExecuteOfficerProfileService {
|
|||
}
|
||||
if (
|
||||
item.bodyProfile.posTypeId &&
|
||||
!(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId }))
|
||||
!(await posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId }))
|
||||
) {
|
||||
console.error(
|
||||
"[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:",
|
||||
|
|
@ -362,22 +456,22 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Processing citizenId:",
|
||||
item.bodyProfile.citizenId,
|
||||
);
|
||||
let registrationProvinceId = await this.provinceRepo.findOneBy({
|
||||
let registrationProvinceId = await provinceRepo.findOneBy({
|
||||
id: item.bodyProfile.registrationProvinceId ?? "",
|
||||
});
|
||||
let registrationDistrictId = await this.districtRepo.findOneBy({
|
||||
let registrationDistrictId = await districtRepo.findOneBy({
|
||||
id: item.bodyProfile.registrationDistrictId ?? "",
|
||||
});
|
||||
let registrationSubDistrictId = await this.subDistrictRepo.findOneBy({
|
||||
let registrationSubDistrictId = await subDistrictRepo.findOneBy({
|
||||
id: item.bodyProfile.registrationSubDistrictId ?? "",
|
||||
});
|
||||
let currentProvinceId = await this.provinceRepo.findOneBy({
|
||||
let currentProvinceId = await provinceRepo.findOneBy({
|
||||
id: item.bodyProfile.currentProvinceId ?? "",
|
||||
});
|
||||
let currentDistrictId = await this.districtRepo.findOneBy({
|
||||
let currentDistrictId = await districtRepo.findOneBy({
|
||||
id: item.bodyProfile.currentDistrictId ?? "",
|
||||
});
|
||||
let currentSubDistrictId = await this.subDistrictRepo.findOneBy({
|
||||
let currentSubDistrictId = await subDistrictRepo.findOneBy({
|
||||
id: item.bodyProfile.currentSubDistrictId ?? "",
|
||||
});
|
||||
console.log("[ExecuteOfficerProfileService] Address validation completed");
|
||||
|
|
@ -502,7 +596,7 @@ export class ExecuteOfficerProfileService {
|
|||
);
|
||||
}
|
||||
|
||||
let profile: any = await this.profileRepository.findOne({
|
||||
let profile: any = await profileRepository.findOne({
|
||||
where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ },
|
||||
relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"],
|
||||
});
|
||||
|
|
@ -520,7 +614,7 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] No existing profile found, creating new profile",
|
||||
);
|
||||
//กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม
|
||||
let profileEmployee: any = await this.profileEmployeeRepository.findOne({
|
||||
let profileEmployee: any = await profileEmployeeRepository.findOne({
|
||||
where: { citizenId: item.bodyProfile.citizenId },
|
||||
relations: ["profileInsignias", "roleKeycloaks"],
|
||||
});
|
||||
|
|
@ -532,7 +626,7 @@ export class ExecuteOfficerProfileService {
|
|||
console.log(
|
||||
"[ExecuteOfficerProfileService] Converting employee profile to officer profile",
|
||||
);
|
||||
const _order = await this.salaryRepo.findOne({
|
||||
const _order = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: profileEmployee.id },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -550,15 +644,15 @@ export class ExecuteOfficerProfileService {
|
|||
Object.assign(history, { ...profileEmpSalary, id: undefined });
|
||||
profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt;
|
||||
(profileEmpSalary.profileId = _null),
|
||||
await this.salaryRepo.save(profileEmpSalary, { data: req });
|
||||
await salaryRepo.save(profileEmpSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileEmpSalary });
|
||||
history.profileSalaryId = profileEmpSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
if (profileEmployee.profileInsignias.length > 0) {
|
||||
_oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? [];
|
||||
}
|
||||
await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE");
|
||||
await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE", manager);
|
||||
if (profileEmployee.keycloak != null) {
|
||||
// const delUserKeycloak = await deleteUser(profileEmployee.keycloak);
|
||||
// if (delUserKeycloak) {
|
||||
|
|
@ -573,7 +667,7 @@ export class ExecuteOfficerProfileService {
|
|||
profileEmployee.lastUpdateUserId = ctx.user.sub;
|
||||
profileEmployee.lastUpdateFullName = ctx.user.name;
|
||||
profileEmployee.lastUpdatedAt = new Date();
|
||||
await this.profileEmployeeRepository.save(profileEmployee);
|
||||
await profileEmployeeRepository.save(profileEmployee);
|
||||
setLogDataDiff(req, { before, after: profileEmployee });
|
||||
}
|
||||
profile = Object.assign({ ...item.bodyProfile, ...meta });
|
||||
|
|
@ -619,7 +713,7 @@ export class ExecuteOfficerProfileService {
|
|||
profile.phone = item.bodyProfile.phone ?? null;
|
||||
|
||||
console.log("[ExecuteOfficerProfileService] Saving new profile");
|
||||
await this.profileRepository.save(profile);
|
||||
await profileRepository.save(profile);
|
||||
console.log(
|
||||
"[ExecuteOfficerProfileService] New profile saved, profileId:",
|
||||
profile.id,
|
||||
|
|
@ -649,7 +743,7 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record",
|
||||
);
|
||||
//ดึง profileSalary เดิม
|
||||
_oldSalaries = await this.salaryRepo.find({
|
||||
_oldSalaries = await salaryRepo.find({
|
||||
where: { profileId: profile.id },
|
||||
order: { order: "ASC" },
|
||||
});
|
||||
|
|
@ -696,7 +790,7 @@ export class ExecuteOfficerProfileService {
|
|||
profile.nationality = item.bodyProfile.nationality ?? null;
|
||||
profile.bloodGroup = item.bodyProfile.bloodGroup ?? null;
|
||||
profile.phone = item.bodyProfile.phone ?? null;
|
||||
await this.profileRepository.save(profile);
|
||||
await profileRepository.save(profile);
|
||||
console.log(
|
||||
"[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:",
|
||||
profile.id,
|
||||
|
|
@ -791,7 +885,7 @@ export class ExecuteOfficerProfileService {
|
|||
item.bodyProfile.phone && item.bodyProfile.phone != ""
|
||||
? item.bodyProfile.phone
|
||||
: profile.phone;
|
||||
await this.profileRepository.save(profile);
|
||||
await profileRepository.save(profile);
|
||||
console.log(
|
||||
"[ExecuteOfficerProfileService] Existing active profile updated, profileId:",
|
||||
profile.id,
|
||||
|
|
@ -811,25 +905,23 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Processing educations, count:",
|
||||
item.bodyEducations.length,
|
||||
);
|
||||
await Promise.all(
|
||||
item.bodyEducations.map(async (education) => {
|
||||
for (const education of item.bodyEducations) {
|
||||
const profileEdu = new ProfileEducation();
|
||||
Object.assign(profileEdu, { ...education, ...meta });
|
||||
const eduHistory = new ProfileEducationHistory();
|
||||
Object.assign(eduHistory, { ...profileEdu, id: undefined });
|
||||
profileEdu.profileId = profile.id;
|
||||
const educationLevel = await this.profileEducationRepo.findOne({
|
||||
const educationLevel = await profileEducationRepo.findOne({
|
||||
select: ["id", "level", "profileId"],
|
||||
where: { profileId: profile.id, isDeleted: false },
|
||||
order: { level: "DESC" },
|
||||
});
|
||||
profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1;
|
||||
await this.profileEducationRepo.save(profileEdu, { data: req });
|
||||
await profileEducationRepo.save(profileEdu, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileEdu });
|
||||
eduHistory.profileEducationId = profileEdu.id;
|
||||
await this.profileEducationHistoryRepo.save(eduHistory, { data: req });
|
||||
}),
|
||||
);
|
||||
await profileEducationHistoryRepo.save(eduHistory, { data: req });
|
||||
}
|
||||
}
|
||||
//Certificates
|
||||
if (item.bodyCertificates && item.bodyCertificates.length > 0) {
|
||||
|
|
@ -837,19 +929,17 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Processing certificates, count:",
|
||||
item.bodyCertificates.length,
|
||||
);
|
||||
await Promise.all(
|
||||
item.bodyCertificates.map(async (cer) => {
|
||||
for (const cer of item.bodyCertificates) {
|
||||
const profileCer = new ProfileCertificate();
|
||||
Object.assign(profileCer, { ...cer, ...meta });
|
||||
const cerHistory = new ProfileCertificateHistory();
|
||||
Object.assign(cerHistory, { ...profileCer, id: undefined });
|
||||
profileCer.profileId = profile.id;
|
||||
await this.certificateRepo.save(profileCer, { data: req });
|
||||
await certificateRepo.save(profileCer, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileCer });
|
||||
cerHistory.profileCertificateId = profileCer.id;
|
||||
await this.certificateHistoryRepo.save(cerHistory, { data: req });
|
||||
}),
|
||||
);
|
||||
await certificateHistoryRepo.save(cerHistory, { data: req });
|
||||
}
|
||||
}
|
||||
//FamilyCouple
|
||||
if (item.bodyMarry != null) {
|
||||
|
|
@ -868,10 +958,10 @@ export class ExecuteOfficerProfileService {
|
|||
const coupleHistory = new ProfileFamilyCoupleHistory();
|
||||
Object.assign(coupleHistory, { ...profileCouple, id: undefined });
|
||||
profileCouple.profileId = profile.id;
|
||||
await this.profileFamilyCoupleRepo.save(profileCouple, { data: req });
|
||||
await profileFamilyCoupleRepo.save(profileCouple, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileCouple });
|
||||
coupleHistory.profileFamilyCoupleId = profileCouple.id;
|
||||
await this.profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req });
|
||||
await profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req });
|
||||
}
|
||||
//FamilyFather
|
||||
if (item.bodyFather != null) {
|
||||
|
|
@ -889,10 +979,10 @@ export class ExecuteOfficerProfileService {
|
|||
const fatherHistory = new ProfileFamilyFatherHistory();
|
||||
Object.assign(fatherHistory, { ...profileFather, id: undefined });
|
||||
profileFather.profileId = profile.id;
|
||||
await this.profileFamilyFatherRepo.save(profileFather, { data: req });
|
||||
await profileFamilyFatherRepo.save(profileFather, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileFather });
|
||||
fatherHistory.profileFamilyFatherId = profileFather.id;
|
||||
await this.profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req });
|
||||
await profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req });
|
||||
}
|
||||
//FamilyMother
|
||||
if (item.bodyMother != null) {
|
||||
|
|
@ -910,10 +1000,10 @@ export class ExecuteOfficerProfileService {
|
|||
const motherHistory = new ProfileFamilyMotherHistory();
|
||||
Object.assign(motherHistory, { ...profileMother, id: undefined });
|
||||
profileMother.profileId = profile.id;
|
||||
await this.profileFamilyMotherRepo.save(profileMother, { data: req });
|
||||
await profileFamilyMotherRepo.save(profileMother, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileMother });
|
||||
motherHistory.profileFamilyMotherId = profileMother.id;
|
||||
await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req });
|
||||
await profileFamilyMotherHistoryRepo.save(motherHistory, { data: req });
|
||||
}
|
||||
//Salary
|
||||
//insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ
|
||||
|
|
@ -922,24 +1012,22 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Restoring old salaries, count:",
|
||||
_oldSalaries.length,
|
||||
);
|
||||
await Promise.all(
|
||||
_oldSalaries.map(async (oldSal) => {
|
||||
for (const oldSal of _oldSalaries) {
|
||||
const profileSal: any = new ProfileSalary();
|
||||
Object.assign(profileSal, { ...oldSal, ...meta });
|
||||
const salaryHistory = new ProfileSalaryHistory();
|
||||
Object.assign(salaryHistory, { ...profileSal, id: undefined });
|
||||
profileSal.profileId = profile.id;
|
||||
await this.salaryRepo.save(profileSal, { data: req });
|
||||
await salaryRepo.save(profileSal, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileSal });
|
||||
salaryHistory.profileSalaryId = profileSal.id;
|
||||
await this.salaryHistoryRepo.save(salaryHistory, { data: req });
|
||||
}),
|
||||
);
|
||||
await salaryHistoryRepo.save(salaryHistory, { data: req });
|
||||
}
|
||||
}
|
||||
//insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว
|
||||
if (item.bodySalarys && item.bodySalarys != null) {
|
||||
console.log("[ExecuteOfficerProfileService] Processing new salary data");
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: profile.id },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -956,10 +1044,10 @@ export class ExecuteOfficerProfileService {
|
|||
profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null;
|
||||
profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null;
|
||||
profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? null;
|
||||
await this.salaryRepo.save(profileSal, { data: req });
|
||||
await salaryRepo.save(profileSal, { data: req });
|
||||
setLogDataDiff(req, { before, after: profileSal });
|
||||
salaryHistory.profileSalaryId = profileSal.id;
|
||||
await this.salaryHistoryRepo.save(salaryHistory, { data: req });
|
||||
await salaryHistoryRepo.save(salaryHistory, { data: req });
|
||||
}
|
||||
//Position
|
||||
if (item.bodyPosition && item.bodyPosition != null) {
|
||||
|
|
@ -969,7 +1057,7 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:",
|
||||
item.bodyPosition.posmasterId,
|
||||
);
|
||||
let posMaster = await this.posMasterRepository.findOne({
|
||||
let posMaster = await posMasterRepository.findOne({
|
||||
where: {
|
||||
id: item.bodyPosition.posmasterId,
|
||||
},
|
||||
|
|
@ -995,7 +1083,7 @@ export class ExecuteOfficerProfileService {
|
|||
console.log(
|
||||
"[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA",
|
||||
);
|
||||
posMaster = await this.posMasterRepository.findOne({
|
||||
posMaster = await posMasterRepository.findOne({
|
||||
where: {
|
||||
ancestorDNA: posMaster.ancestorDNA,
|
||||
orgRevision: {
|
||||
|
|
@ -1027,7 +1115,7 @@ export class ExecuteOfficerProfileService {
|
|||
|
||||
// STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน
|
||||
console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data");
|
||||
const posMasterOld = await this.posMasterRepository.findOne({
|
||||
const posMasterOld = await posMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
|
|
@ -1040,7 +1128,7 @@ export class ExecuteOfficerProfileService {
|
|||
}
|
||||
|
||||
// หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก
|
||||
const positionOld = await this.positionRepository.findOne({
|
||||
const positionOld = await positionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMasterOld?.id,
|
||||
positionIsSelected: true,
|
||||
|
|
@ -1048,14 +1136,14 @@ export class ExecuteOfficerProfileService {
|
|||
});
|
||||
if (positionOld != null) {
|
||||
positionOld.positionIsSelected = false;
|
||||
await this.positionRepository.save(positionOld);
|
||||
await positionRepository.save(positionOld);
|
||||
}
|
||||
|
||||
// STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่
|
||||
console.log(
|
||||
"[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster",
|
||||
);
|
||||
const checkPosition = await this.positionRepository.find({
|
||||
const checkPosition = await positionRepository.find({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionIsSelected: true,
|
||||
|
|
@ -1066,7 +1154,7 @@ export class ExecuteOfficerProfileService {
|
|||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await this.positionRepository.save(clearPosition);
|
||||
await positionRepository.save(clearPosition);
|
||||
}
|
||||
|
||||
// STEP 4: กำหนดคนครองใหม่ให้กับ posMaster
|
||||
|
|
@ -1078,10 +1166,13 @@ export class ExecuteOfficerProfileService {
|
|||
// posMaster.conditionReason = _null;
|
||||
// posMaster.isCondition = false;
|
||||
if (posMasterOld != null) {
|
||||
await this.posMasterRepository.save(posMasterOld);
|
||||
await CreatePosMasterHistoryOfficer(posMasterOld.id, req);
|
||||
await posMasterRepository.save(posMasterOld);
|
||||
console.log(
|
||||
`[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, citizenId: ${item.bodyProfile?.citizenId} (old)`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
|
||||
}
|
||||
await this.posMasterRepository.save(posMaster);
|
||||
await posMasterRepository.save(posMaster);
|
||||
console.log("[ExecuteOfficerProfileService] posMaster saved with new holder");
|
||||
|
||||
// STEP 5: กำหนด position ใหม่
|
||||
|
|
@ -1104,7 +1195,7 @@ export class ExecuteOfficerProfileService {
|
|||
item.bodyPosition?.positionId,
|
||||
);
|
||||
if (item.bodyPosition?.positionId) {
|
||||
const positionById = await this.positionRepository.findOne({
|
||||
const positionById = await positionRepository.findOne({
|
||||
where: {
|
||||
id: item.bodyPosition.positionId,
|
||||
posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง
|
||||
|
|
@ -1149,7 +1240,7 @@ export class ExecuteOfficerProfileService {
|
|||
whereCondition.positionArea = item.bodyPosition.positionArea;
|
||||
}
|
||||
|
||||
const positionBy7Fields = await this.positionRepository.findOne({
|
||||
const positionBy7Fields = await positionRepository.findOne({
|
||||
where: whereCondition,
|
||||
relations: ["posExecutive"],
|
||||
order: { orderNo: "ASC" },
|
||||
|
|
@ -1171,7 +1262,7 @@ export class ExecuteOfficerProfileService {
|
|||
console.log(
|
||||
"[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields",
|
||||
);
|
||||
const positionBy3Fields = await this.positionRepository.findOne({
|
||||
const positionBy3Fields = await positionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionName: item.bodyPosition.positionName,
|
||||
|
|
@ -1196,26 +1287,6 @@ export class ExecuteOfficerProfileService {
|
|||
}
|
||||
}
|
||||
|
||||
// // ═══════════════════════════════════════════════════════════
|
||||
// // FALLBACK: ถ้าทั้ง 3 ไม่ match ให้เลือก position แรกใน posMaster
|
||||
// // ═══════════════════════════════════════════════════════════
|
||||
// if (!positionNew) {
|
||||
// const fallbackPositions = await this.positionRepository.find({
|
||||
// where: {
|
||||
// posMasterId: posMaster.id,
|
||||
// },
|
||||
// relations: ["posExecutive"],
|
||||
// order: {
|
||||
// orderNo: "ASC",
|
||||
// },
|
||||
// take: 1,
|
||||
// });
|
||||
|
||||
// if (fallbackPositions.length > 0) {
|
||||
// positionNew = fallbackPositions[0];
|
||||
// }
|
||||
// }
|
||||
|
||||
// อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
|
||||
profile.posMasterNo = getPosMasterNo(posMaster);
|
||||
profile.org = getOrgFullName(posMaster);
|
||||
|
|
@ -1238,7 +1309,7 @@ export class ExecuteOfficerProfileService {
|
|||
profile.positionExecutiveField = positionNew.positionExecutiveField ?? null;
|
||||
// profile.dateStart = new Date();
|
||||
}
|
||||
await this.positionRepository.save(positionNew, { data: req });
|
||||
await positionRepository.save(positionNew, { data: req });
|
||||
} else if (!posMaster.isSit) {
|
||||
// fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา
|
||||
console.log(
|
||||
|
|
@ -1251,12 +1322,15 @@ export class ExecuteOfficerProfileService {
|
|||
profile.positionArea = item.bodyPosition.positionArea ?? null;
|
||||
profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? null;
|
||||
}
|
||||
await this.profileRepository.save(profile, { data: req });
|
||||
await profileRepository.save(profile, { data: req });
|
||||
setLogDataDiff(req, { before, after: profile });
|
||||
// await CreatePosMasterHistoryOfficer(posMaster.id, req);
|
||||
console.log(
|
||||
`[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, citizenId: ${item.bodyProfile?.citizenId}`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, {
|
||||
positionId: positionNew?.id,
|
||||
});
|
||||
}, manager);
|
||||
}
|
||||
// Insignia
|
||||
if (_oldInsigniaIds.length > 0) {
|
||||
|
|
@ -1264,7 +1338,7 @@ export class ExecuteOfficerProfileService {
|
|||
"[ExecuteOfficerProfileService] Processing old insignias, count:",
|
||||
_oldInsigniaIds.length,
|
||||
);
|
||||
const _insignias = await this.insigniaRepo.find({
|
||||
const _insignias = await insigniaRepo.find({
|
||||
where: { id: In(_oldInsigniaIds), isDeleted: false },
|
||||
order: { createdAt: "ASC" },
|
||||
});
|
||||
|
|
@ -1290,10 +1364,10 @@ export class ExecuteOfficerProfileService {
|
|||
Object.assign(insignia, { ...newInsigniaData, ...meta });
|
||||
const history = new ProfileInsigniaHistory();
|
||||
Object.assign(history, { ...insignia, id: undefined });
|
||||
await this.insigniaRepo.save(insignia, { data: req });
|
||||
await insigniaRepo.save(insignia, { data: req });
|
||||
setLogDataDiff(req, { before, after: insignia });
|
||||
history.profileInsigniaId = insignia.id;
|
||||
await this.insigniaHistoryRepo.save(history, { data: req });
|
||||
await insigniaHistoryRepo.save(history, { data: req });
|
||||
}
|
||||
}
|
||||
// เพิ่มรูปภาพโปรไฟล์
|
||||
|
|
@ -1309,29 +1383,27 @@ export class ExecuteOfficerProfileService {
|
|||
profileEmployeeId: undefined,
|
||||
});
|
||||
if (profile.profileAvatars && profile.profileAvatars.length > 0) {
|
||||
await Promise.all(
|
||||
profile.profileAvatars.map(async (item: any) => {
|
||||
item.isActive = false;
|
||||
await this.avatarRepository.save(item);
|
||||
}),
|
||||
);
|
||||
for (const avatarItem of profile.profileAvatars) {
|
||||
avatarItem.isActive = false;
|
||||
await avatarRepository.save(avatarItem);
|
||||
}
|
||||
await this.avatarRepository.save(_profileAvatar);
|
||||
}
|
||||
await avatarRepository.save(_profileAvatar);
|
||||
let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`;
|
||||
let fileName = `profile-${_profileAvatar.id}`;
|
||||
_profileAvatar.isActive = true;
|
||||
_profileAvatar.avatar = avatar;
|
||||
_profileAvatar.avatarName = fileName;
|
||||
await this.avatarRepository.save(_profileAvatar, { data: req });
|
||||
await avatarRepository.save(_profileAvatar, { data: req });
|
||||
profile.avatar = avatar;
|
||||
profile.avatarName = fileName;
|
||||
await this.profileRepository.save(profile, { data: req });
|
||||
const checkAvatar = await this.avatarRepository.findOne({
|
||||
await profileRepository.save(profile, { data: req });
|
||||
const checkAvatar = await avatarRepository.findOne({
|
||||
where: { avatar: avatar, avatarName: fileName },
|
||||
});
|
||||
if (checkAvatar && checkAvatar.profileId == null) {
|
||||
checkAvatar.profileId = profile.id;
|
||||
await this.avatarRepository.save(checkAvatar);
|
||||
await avatarRepository.save(checkAvatar);
|
||||
}
|
||||
//duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม
|
||||
await new CallAPI()
|
||||
|
|
@ -1343,8 +1415,9 @@ export class ExecuteOfficerProfileService {
|
|||
.catch(() => {});
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
console.log(
|
||||
`[ExecuteOfficerProfileService] Completed processOne — citizenId: ${item.bodyProfile?.citizenId}`,
|
||||
);
|
||||
console.log("[ExecuteOfficerProfileService] executeCreateOfficerProfile completed successfully");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
860
src/services/ExecuteOrgCommandService.ts
Normal file
860
src/services/ExecuteOrgCommandService.ts
Normal file
|
|
@ -0,0 +1,860 @@
|
|||
import { Double, EntityManager, In, Like } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import Extension from "../interfaces/extension";
|
||||
import CallAPI from "../interfaces/call-api";
|
||||
import { setLogDataDiff } from "../interfaces/utils";
|
||||
import {
|
||||
CreatePosMasterHistoryEmployee,
|
||||
CreatePosMasterHistoryEmployeeTemp,
|
||||
} from "./PositionService";
|
||||
import {
|
||||
addUserRoles,
|
||||
createUser,
|
||||
getRoles,
|
||||
getUserByUsername,
|
||||
getRoleMappings,
|
||||
} from "../keycloak";
|
||||
import { Command } from "../entities/Command";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
||||
import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
import { RoleKeycloak } from "../entities/RoleKeycloak";
|
||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
import { EmployeeTempPosMaster } from "../entities/EmployeeTempPosMaster";
|
||||
import { EmployeePosition } from "../entities/EmployeePosition";
|
||||
import { PosMaster } from "../entities/PosMaster";
|
||||
import { Position } from "../entities/Position";
|
||||
import { ProfileSalary } from "../entities/ProfileSalary";
|
||||
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
|
||||
import { PosMasterAct } from "../entities/PosMasterAct";
|
||||
import { ProfileActposition } from "../entities/ProfileActposition";
|
||||
import { ProfileActpositionHistory } from "../entities/ProfileActpositionHistory";
|
||||
import { promisify } from "util";
|
||||
|
||||
const redis = require("redis");
|
||||
const REDIS_HOST = process.env.REDIS_HOST;
|
||||
const REDIS_PORT = process.env.REDIS_PORT;
|
||||
|
||||
/**
|
||||
* Input: refIds ที่ consumer ใน rabbitmq build ขึ้น (เดิมคือ body.refIds ของ endpoint /excecute)
|
||||
* ใช้กับ C-PM-21, C-PM-38, C-PM-40
|
||||
*/
|
||||
export interface CommandRefItem {
|
||||
refId: string;
|
||||
commandId?: string | null;
|
||||
amount: Double | null;
|
||||
amountSpecial?: Double | null;
|
||||
positionSalaryAmount: Double | null;
|
||||
mouthSalaryAmount: Double | null;
|
||||
commandNo: string | null;
|
||||
commandYear: number;
|
||||
commandDateAffect?: Date | string | null;
|
||||
commandDateSign?: Date | string | null;
|
||||
commandCode?: string | null;
|
||||
commandName?: string | null;
|
||||
remark: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context สำหรับ audit/log (เหมือน ExecuteSalaryService)
|
||||
*/
|
||||
export interface OrgCommandExecutionContext {
|
||||
user: { sub: string; name: string };
|
||||
req?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service สำหรับคำสั่งที่เดิม "ยิงเข้าตัว" (HTTP loopback เข้า org เอง)
|
||||
*
|
||||
* ใช้กับ commandType:
|
||||
* - C-PM-21 : command21/employee/report/excecute (ลูกจ้าง → พนักงานประจำ)
|
||||
* - C-PM-38 : command38/officer/report/excecute (เงินเดือน next_holder ข้าราชการ)
|
||||
* - C-PM-40 : command40/officer/report/excecute (รักษาการ)
|
||||
*
|
||||
* - endpoint commandXX/.../excecute ทั้ง 3 เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow / ทำต่อ)
|
||||
* แทนการ PostData(path + "/excecute") ที่เป็น HTTP loopback เข้า org ตัวเอง
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
*
|
||||
* ⚠️ หมายเหตุ side-effect ที่อยู่นอก DB transaction:
|
||||
* - Keycloak operations (createUser/addUserRoles/getRoleMappings) ใน C-PM-21 ทำภายใน transaction
|
||||
* เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจากนี้
|
||||
* Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
* - .NET call (C-PM-21) ทำหลัง transaction commit แล้ว เพราะ .NET ไม่สามารถ rollback ได้
|
||||
* - Redis cache clear (C-PM-40) ทำหลัง transaction commit (เป็นการ del cache key — idempotent)
|
||||
* - CreatePosMasterHistoryEmployeeTemp สร้าง nested transaction ของตัวเอง (ไม่รับ manager)
|
||||
*/
|
||||
export class ExecuteOrgCommandService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private orgRevisionRepository = AppDataSource.getRepository(OrgRevision);
|
||||
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
|
||||
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
||||
private posMasterActRepository = AppDataSource.getRepository(PosMasterAct);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// แก้ปัญหา _posNumCodeSit/_command resolution ที่ซ้ำกันในทุก endpoint
|
||||
// (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
private async resolvePosNumCodeSit(
|
||||
commandId: string | null | undefined,
|
||||
): Promise<{ command: Command | null; posNumCodeSit: string; posNumCodeSitAbb: string }> {
|
||||
let posNumCodeSit = "";
|
||||
let posNumCodeSitAbb = "";
|
||||
const command = commandId
|
||||
? await this.commandRepository.findOne({ where: { id: commandId } })
|
||||
: null;
|
||||
if (command) {
|
||||
if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
|
||||
const orgRootDeputy = await this.orgRootRepository.findOne({
|
||||
where: {
|
||||
isDeputy: true,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: ["orgRevision"],
|
||||
});
|
||||
posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
|
||||
posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป.";
|
||||
} else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
|
||||
posNumCodeSit = "กรุงเทพมหานคร";
|
||||
posNumCodeSitAbb = "กทม.";
|
||||
} else {
|
||||
let profileAdmin = await this.profileRepository.findOne({
|
||||
where: {
|
||||
keycloak: command?.createdUserId.toString(),
|
||||
current_holders: {
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
|
||||
});
|
||||
posNumCodeSit =
|
||||
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
|
||||
"";
|
||||
posNumCodeSitAbb =
|
||||
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
|
||||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
return { command, posNumCodeSit, posNumCodeSitAbb };
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-21 : command21/employee/report/excecute
|
||||
// ลูกจ้างชั่วคราว → พนักงานประจำ (บรรจุ)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
/**
|
||||
* @returns profileEmps ที่จะส่งต่อให้ .NET (สำหรับ consumer rabbitmq เรียก .NET เอง)
|
||||
* ถ้าเรียกจาก thin-wrapper endpoint จะเรียก .NET ภายใน method นี้เอง
|
||||
*/
|
||||
async executeCommand21Employee(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
options?: { callDotNet?: boolean },
|
||||
): Promise<{ profileEmps: any[] }> {
|
||||
const req = ctx.req;
|
||||
const callDotNet = options?.callDotNet ?? true;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand21Employee — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
const roleKeycloak = await this.roleKeycloakRepo.findOne({
|
||||
where: { name: Like("USER") },
|
||||
});
|
||||
const { command: _command, posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
|
||||
await this.resolvePosNumCodeSit(commandId);
|
||||
|
||||
const profileEmps: any[] = [];
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOneCommand21(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
roleKeycloak,
|
||||
_command,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
profileEmps,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteOrgCommandService] Failed C-PM-21, commandId=${commandId}, refId=${item.refId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// .NET call ทำหลัง commit (เหมือน endpoint เดิมที่เรียกหลัง Promise.all) — .NET ไม่ rollback ได้
|
||||
if (callDotNet && profileEmps.length > 0) {
|
||||
await new CallAPI()
|
||||
.PostData(req, "/placement/appointment/employee-appoint-21/report/excecute", {
|
||||
profileEmps,
|
||||
})
|
||||
.catch((error) => {
|
||||
throw new Error(`Failed. Cannot update status. ${error?.message ?? ""}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] Completed C-PM-21 — ${profileEmps.length} profiles sent to .NET`,
|
||||
);
|
||||
return { profileEmps };
|
||||
}
|
||||
|
||||
private async processOneCommand21(
|
||||
item: CommandRefItem,
|
||||
ctx: OrgCommandExecutionContext,
|
||||
manager: EntityManager,
|
||||
roleKeycloak: RoleKeycloak | null,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
profileEmps: any[],
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
const employeeTempPosMasterRepository = manager.getRepository(EmployeeTempPosMaster);
|
||||
const employeePositionRepository = manager.getRepository(EmployeePosition);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
|
||||
const profile = await profileEmployeeRepository.findOne({
|
||||
where: { id: item.refId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
|
||||
}
|
||||
const orgRevision = await this.orgRevisionRepository.findOne({
|
||||
where: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
});
|
||||
const _posMaster = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
orgRevisionId: orgRevision?.id,
|
||||
id: profile.posmasterIdTemp,
|
||||
},
|
||||
relations: {
|
||||
orgRoot: true,
|
||||
orgChild1: true,
|
||||
orgChild2: true,
|
||||
orgChild3: true,
|
||||
orgChild4: true,
|
||||
},
|
||||
});
|
||||
const orgRootRef = _posMaster?.orgRoot ?? null;
|
||||
const orgChild1Ref = _posMaster?.orgChild1 ?? null;
|
||||
const orgChild2Ref = _posMaster?.orgChild2 ?? null;
|
||||
const orgChild3Ref = _posMaster?.orgChild3 ?? null;
|
||||
const orgChild4Ref = _posMaster?.orgChild4 ?? null;
|
||||
let orgShortName = "";
|
||||
if (_posMaster != null) {
|
||||
if (_posMaster.orgChild1Id === null) {
|
||||
orgShortName = _posMaster.orgRoot?.orgRootShortName;
|
||||
} else if (_posMaster.orgChild2Id === null) {
|
||||
orgShortName = _posMaster.orgChild1?.orgChild1ShortName;
|
||||
} else if (_posMaster.orgChild3Id === null) {
|
||||
orgShortName = _posMaster.orgChild2?.orgChild2ShortName;
|
||||
} else if (_posMaster.orgChild4Id === null) {
|
||||
orgShortName = _posMaster.orgChild3?.orgChild3ShortName;
|
||||
} else {
|
||||
orgShortName = _posMaster.orgChild4?.orgChild4ShortName;
|
||||
}
|
||||
}
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.refId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileEmployeeId: profile.id,
|
||||
amount: item.amount,
|
||||
amountSpecial: item.amountSpecial,
|
||||
positionSalaryAmount: item.positionSalaryAmount,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount,
|
||||
position: profile.positionTemp,
|
||||
positionName: profile.positionTemp,
|
||||
positionType: profile.posTypeNameTemp,
|
||||
positionLevel: profile.posLevelNameTemp,
|
||||
order: dest_item == null ? 1 : dest_item.order + 1,
|
||||
orgRoot: orgRootRef?.orgRootName ?? null,
|
||||
orgChild1: orgChild1Ref?.orgChild1Name ?? null,
|
||||
orgChild2: orgChild2Ref?.orgChild2Name ?? null,
|
||||
orgChild3: orgChild3Ref?.orgChild3Name ?? null,
|
||||
orgChild4: orgChild4Ref?.orgChild4Name ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: profile.posMasterNoTemp ?? "",
|
||||
posNoAbb: orgShortName,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
|
||||
Object.assign(dataSalary, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
const posMaster = await employeePosMasterRepository.findOne({
|
||||
where: { id: profile.posmasterIdTemp },
|
||||
relations: ["orgRoot", "orgChild1", "orgChild2", "orgChild3", "orgChild4"],
|
||||
});
|
||||
if (posMaster == null)
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
|
||||
|
||||
const posMasterOld = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
},
|
||||
});
|
||||
if (posMasterOld != null) {
|
||||
posMasterOld.current_holderId = null;
|
||||
posMasterOld.lastUpdatedAt = new Date();
|
||||
}
|
||||
|
||||
const positionOld = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMasterOld?.id,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (positionOld != null) {
|
||||
positionOld.positionIsSelected = false;
|
||||
await employeePositionRepository.save(positionOld);
|
||||
}
|
||||
|
||||
const checkPosition = await employeePositionRepository.find({
|
||||
where: {
|
||||
posMasterId: profile.posmasterIdTemp,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (checkPosition.length > 0) {
|
||||
const clearPosition = checkPosition.map((positions) => ({
|
||||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await employeePositionRepository.save(clearPosition);
|
||||
}
|
||||
|
||||
posMaster.current_holderId = profile.id;
|
||||
posMaster.lastUpdatedAt = new Date();
|
||||
posMaster.next_holderId = null;
|
||||
if (posMasterOld != null) {
|
||||
await employeePosMasterRepository.save(posMasterOld);
|
||||
await CreatePosMasterHistoryEmployee(posMasterOld.id, req, undefined, manager);
|
||||
}
|
||||
await employeePosMasterRepository.save(posMaster);
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager);
|
||||
|
||||
const clsTempPosmaster = await employeeTempPosMasterRepository.find({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (clsTempPosmaster.length > 0) {
|
||||
const clearTempPosmaster = clsTempPosmaster.map((posMasterTemp) => ({
|
||||
...posMasterTemp,
|
||||
current_holderId: null,
|
||||
next_holderId: null,
|
||||
}));
|
||||
await employeeTempPosMasterRepository.save(clearTempPosmaster);
|
||||
|
||||
const checkTempPosition = await employeePositionRepository.find({
|
||||
where: {
|
||||
posMasterTempId: In(clearTempPosmaster.map((x) => x.id)),
|
||||
positionIsSelected: true,
|
||||
},
|
||||
});
|
||||
if (checkTempPosition.length > 0) {
|
||||
const clearTempPosition = checkTempPosition.map((positions) => ({
|
||||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await employeePositionRepository.save(clearTempPosition);
|
||||
}
|
||||
await Promise.all(
|
||||
clsTempPosmaster.map(
|
||||
async (posMasterTemp) =>
|
||||
await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const positionNew = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
id: profile.positionIdTemp,
|
||||
posMasterId: profile.posmasterIdTemp,
|
||||
},
|
||||
});
|
||||
|
||||
if (positionNew != null) {
|
||||
// Create Keycloak
|
||||
const checkUser = await getUserByUsername(profile.citizenId);
|
||||
if (checkUser.length == 0) {
|
||||
let password = profile.citizenId;
|
||||
if (profile.birthDate != null) {
|
||||
const _date = new Date(profile.birthDate.toDateString())
|
||||
.getDate()
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
const _month = (new Date(profile.birthDate.toDateString()).getMonth() + 1)
|
||||
.toString()
|
||||
.padStart(2, "0");
|
||||
const _year = new Date(profile.birthDate.toDateString()).getFullYear() + 543;
|
||||
password = `${_date}${_month}${_year}`;
|
||||
}
|
||||
// กรอง "." ออกจาก firstName ก่อนส่งไป keycloak
|
||||
const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? "";
|
||||
const userKeycloakId = await createUser(profile.citizenId, password, {
|
||||
firstName: sanitizedFirstName,
|
||||
lastName: profile.lastName,
|
||||
});
|
||||
const list = await getRoles();
|
||||
if (!Array.isArray(list))
|
||||
throw new Error("Failed. Cannot get role(s) data from the server.");
|
||||
const result = await addUserRoles(
|
||||
userKeycloakId,
|
||||
list
|
||||
.filter((v) => v.name === "USER")
|
||||
.map((x) => ({
|
||||
id: x.id,
|
||||
name: x.name,
|
||||
})),
|
||||
);
|
||||
profile.keycloak =
|
||||
userKeycloakId && typeof userKeycloakId == "string" ? userKeycloakId : "";
|
||||
profile.roleKeycloaks = result && roleKeycloak ? [roleKeycloak] : [];
|
||||
// End Create Keycloak
|
||||
} else {
|
||||
const rolesData = await getRoleMappings(checkUser[0].id);
|
||||
if (rolesData) {
|
||||
const _roleKeycloak = await this.roleKeycloakRepo.find({
|
||||
where: { name: In(rolesData.map((x: any) => x.name)) },
|
||||
});
|
||||
profile.roleKeycloaks =
|
||||
_roleKeycloak && _roleKeycloak.length > 0 ? _roleKeycloak : [];
|
||||
}
|
||||
profile.keycloak = checkUser[0].id;
|
||||
}
|
||||
positionNew.positionIsSelected = true;
|
||||
profile.posLevelId = positionNew.posLevelId;
|
||||
profile.posTypeId = positionNew.posTypeId;
|
||||
profile.position = positionNew.positionName;
|
||||
profile.employeeOc = posMaster?.orgRoot?.orgRootName ?? null;
|
||||
profile.positionEmployeePositionId = positionNew.positionName;
|
||||
profile.statusTemp = "DONE";
|
||||
profile.employeeClass = "PERM";
|
||||
const _null: any = null;
|
||||
profile.employeeWage = item.amount == null ? _null : item.amount.toString();
|
||||
profile.dateStart = _command ? _command.commandExcecuteDate : new Date();
|
||||
profile.dateAppoint = _command ? _command.commandExcecuteDate : new Date();
|
||||
profile.amount = item.amount == null ? _null : item.amount;
|
||||
profile.amountSpecial = item.amountSpecial == null ? _null : item.amountSpecial;
|
||||
profileEmps.push({
|
||||
profileId: profile.id,
|
||||
prefix: profile.prefix,
|
||||
firstName: profile.firstName,
|
||||
lastName: profile.lastName,
|
||||
citizenId: profile.citizenId,
|
||||
root: posMaster.orgRoot.orgRootName,
|
||||
rootId: posMaster.orgRootId,
|
||||
rootShortName: posMaster.orgRoot.orgRootShortName,
|
||||
rootDnaId: posMaster.orgRoot?.ancestorDNA ?? _null,
|
||||
child1DnaId: posMaster.orgChild1?.ancestorDNA ?? _null,
|
||||
child2DnaId: posMaster.orgChild2?.ancestorDNA ?? _null,
|
||||
child3DnaId: posMaster.orgChild3?.ancestorDNA ?? _null,
|
||||
child4DnaId: posMaster.orgChild4?.ancestorDNA ?? _null,
|
||||
});
|
||||
await profileEmployeeRepository.save(profile);
|
||||
await employeePositionRepository.save(positionNew);
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req, undefined, manager);
|
||||
//ลบออกคนออกจากโครงสร้างลูกจ้างชั่วคราว
|
||||
const posMasterTemp = await employeeTempPosMasterRepository.findOne({
|
||||
where: {
|
||||
orgRevisionId: orgRevision?.id,
|
||||
current_holderId: profile.id,
|
||||
},
|
||||
});
|
||||
if (posMasterTemp) {
|
||||
await employeeTempPosMasterRepository.update(posMasterTemp.id, {
|
||||
current_holderId: _null,
|
||||
});
|
||||
await CreatePosMasterHistoryEmployeeTemp(posMasterTemp.id, req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-38 : command38/officer/report/excecute
|
||||
// เงินเดือน next_holder ของข้าราชการ
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
async executeCommand38Officer(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand38Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
|
||||
await this.resolvePosNumCodeSit(commandId);
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOneCommand38(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteOrgCommandService] Failed C-PM-38, commandId=${commandId}, refId=${item.refId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[ExecuteOrgCommandService] Completed C-PM-38 — ${data?.length ?? 0} items`);
|
||||
}
|
||||
|
||||
private async processOneCommand38(
|
||||
item: CommandRefItem,
|
||||
ctx: OrgCommandExecutionContext,
|
||||
manager: EntityManager,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const posMasterRepository = manager.getRepository(PosMaster);
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const positionRepository = manager.getRepository(Position);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
|
||||
const posMaster = await posMasterRepository.findOne({
|
||||
where: { id: item.refId },
|
||||
relations: [
|
||||
"orgRoot",
|
||||
"orgChild1",
|
||||
"orgChild2",
|
||||
"orgChild3",
|
||||
"orgChild4",
|
||||
"current_holder",
|
||||
"current_holder.posLevel",
|
||||
"current_holder.posType",
|
||||
],
|
||||
});
|
||||
if (!posMaster) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบตำแหน่งดังกล่าว");
|
||||
}
|
||||
if (posMaster.next_holderId != null) {
|
||||
const orgRootRef = posMaster?.orgRoot ?? null;
|
||||
const orgChild1Ref = posMaster?.orgChild1 ?? null;
|
||||
const orgChild2Ref = posMaster?.orgChild2 ?? null;
|
||||
const orgChild3Ref = posMaster?.orgChild3 ?? null;
|
||||
const orgChild4Ref = posMaster?.orgChild4 ?? null;
|
||||
const shortName =
|
||||
posMaster != null && posMaster.orgChild4 != null
|
||||
? `${posMaster.orgChild4.orgChild4ShortName}`
|
||||
: posMaster != null && posMaster.orgChild3 != null
|
||||
? `${posMaster.orgChild3.orgChild3ShortName}`
|
||||
: posMaster != null && posMaster.orgChild2 != null
|
||||
? `${posMaster.orgChild2.orgChild2ShortName}`
|
||||
: posMaster != null && posMaster.orgChild1 != null
|
||||
? `${posMaster.orgChild1.orgChild1ShortName}`
|
||||
: posMaster != null && posMaster?.orgRoot != null
|
||||
? `${posMaster.orgRoot.orgRootShortName}`
|
||||
: null;
|
||||
const profile = await profileRepository.findOne({
|
||||
where: { id: posMaster.next_holderId },
|
||||
});
|
||||
const position = await positionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionIsSelected: true,
|
||||
},
|
||||
relations: ["posType", "posLevel"],
|
||||
});
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: profile?.id },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const before = null;
|
||||
const dataSalary = new ProfileSalary();
|
||||
dataSalary.posNumCodeSit = _posNumCodeSit;
|
||||
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileId: profile?.id,
|
||||
date: new Date(),
|
||||
amount: item.amount,
|
||||
commandId: item.commandId,
|
||||
positionSalaryAmount: item.positionSalaryAmount,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount,
|
||||
position: position?.positionName ?? null,
|
||||
positionType: position?.posType?.posTypeName ?? null,
|
||||
positionLevel: position?.posLevel?.posLevelName ?? null,
|
||||
order: dest_item == null ? 1 : dest_item.order + 1,
|
||||
orgRoot: orgRootRef?.orgRootName ?? null,
|
||||
orgChild1: orgChild1Ref?.orgChild1Name ?? null,
|
||||
orgChild2: orgChild2Ref?.orgChild2Name ?? null,
|
||||
orgChild3: orgChild3Ref?.orgChild3Name ?? null,
|
||||
orgChild4: orgChild4Ref?.orgChild4Name ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: posMaster.posMasterNo,
|
||||
posNoAbb: shortName,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(dataSalary, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// C-PM-40 : command40/officer/report/excecute
|
||||
// รักษาการ (ProfileActposition)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
async executeCommand40Officer(
|
||||
data: CommandRefItem[],
|
||||
ctx: OrgCommandExecutionContext,
|
||||
): Promise<void> {
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
|
||||
console.log(
|
||||
`[ExecuteOrgCommandService] executeCommand40Officer — commandId: ${commandId}, count: ${data?.length ?? 0}`,
|
||||
);
|
||||
|
||||
// 3. ตรวจสอบว่ามี data[0] หรือไม่
|
||||
const firstRef = data[0];
|
||||
if (!firstRef) {
|
||||
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบข้อมูล refIds");
|
||||
}
|
||||
|
||||
const profileIdsToClearCache = new Set<string>();
|
||||
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
// 1. Bulk update status
|
||||
await manager.getRepository(PosMasterAct).update(
|
||||
{ id: In(data.map((x) => x.refId)) },
|
||||
{ statusReport: "DONE" },
|
||||
);
|
||||
|
||||
// 2. ดึงข้อมูลครบทุก relation ที่จำเป็น
|
||||
const posMasters = await manager.getRepository(PosMasterAct).find({
|
||||
where: { id: In(data.map((x) => x.refId)) },
|
||||
relations: [
|
||||
"posMasterChild",
|
||||
"posMasterChild.current_holder",
|
||||
"posMaster",
|
||||
"posMaster.current_holder",
|
||||
"posMaster.positions",
|
||||
"posMaster.orgRoot",
|
||||
"posMaster.orgChild1",
|
||||
"posMaster.orgChild2",
|
||||
"posMaster.orgChild3",
|
||||
"posMaster.orgChild4",
|
||||
],
|
||||
});
|
||||
|
||||
for (const item of posMasters) {
|
||||
try {
|
||||
// 4. ตรวจสอบข้อมูลที่จำเป็นทั้งหมด
|
||||
if (!item.posMasterChild?.current_holderId || !item.posMaster) {
|
||||
console.warn(`ข้ามรายการ ${item.id}: ข้อมูลไม่ครบ`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.posMasterChild.current_holderId) {
|
||||
profileIdsToClearCache.add(item.posMasterChild.current_holderId);
|
||||
}
|
||||
|
||||
// 5. สร้าง orgShortName แบบปลอดภัย
|
||||
const orgShortName =
|
||||
[
|
||||
item.posMaster?.orgChild4?.orgChild4ShortName,
|
||||
item.posMaster?.orgChild3?.orgChild3ShortName,
|
||||
item.posMaster?.orgChild2?.orgChild2ShortName,
|
||||
item.posMaster?.orgChild1?.orgChild1ShortName,
|
||||
item.posMaster?.orgRoot?.orgRootShortName,
|
||||
].find(Boolean) ?? "";
|
||||
|
||||
// 6. หา position ที่ถูกเลือกแบบปลอดภัย
|
||||
const selectedPosition = item.posMaster?.positions;
|
||||
const positionName =
|
||||
selectedPosition
|
||||
?.map((pos) => pos.positionName)
|
||||
.filter(Boolean)
|
||||
.join(", ") ?? "-";
|
||||
|
||||
// 7. สร้าง metaAct แบบปลอดภัย
|
||||
const metaAct = {
|
||||
profileId: item.posMasterChild.current_holderId,
|
||||
dateStart: firstRef.commandDateAffect ?? null,
|
||||
dateEnd: null,
|
||||
position: positionName,
|
||||
status: true,
|
||||
commandId: firstRef.commandId ?? null,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
commandNo: firstRef.commandNo ?? null,
|
||||
refCommandNo: `${firstRef.commandNo ?? ""}/${firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : ""}`,
|
||||
commandYear: firstRef.commandYear ? Extension.ToThaiYear(firstRef.commandYear) : null,
|
||||
posNo:
|
||||
orgShortName && item.posMaster?.posMasterNo
|
||||
? `${orgShortName} ${item.posMaster.posMasterNo}`
|
||||
: item.posMaster?.posMasterNo ?? "-",
|
||||
posNoAbb: orgShortName,
|
||||
commandDateAffect: firstRef.commandDateAffect ?? null,
|
||||
commandDateSign: firstRef.commandDateSign ?? null,
|
||||
commandCode: firstRef.commandCode ?? null,
|
||||
commandName: firstRef.commandName ?? null,
|
||||
remark: firstRef.remark ?? null,
|
||||
};
|
||||
|
||||
// 8. ปิดสถานะรักษาการ
|
||||
const actpositionRepository = manager.getRepository(ProfileActposition);
|
||||
const actpositionHistoryRepository = manager.getRepository(ProfileActpositionHistory);
|
||||
|
||||
const existingActPositions = await actpositionRepository.find({
|
||||
where: {
|
||||
profileId: item.posMasterChild.current_holderId,
|
||||
status: true,
|
||||
isDeleted: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingActPositions.length > 0) {
|
||||
const updatedActPositions = existingActPositions.map((_data) => ({
|
||||
..._data,
|
||||
status: false,
|
||||
dateEnd: new Date(),
|
||||
}));
|
||||
|
||||
await actpositionRepository.save(updatedActPositions);
|
||||
}
|
||||
|
||||
// 9. บันทึกข้อมูลใหม่
|
||||
const dataAct = new ProfileActposition();
|
||||
Object.assign(dataAct, metaAct);
|
||||
|
||||
const historyAct = new ProfileActpositionHistory();
|
||||
Object.assign(historyAct, { ...dataAct, id: undefined });
|
||||
|
||||
await actpositionRepository.save(dataAct);
|
||||
historyAct.profileActpositionId = dataAct.id;
|
||||
await actpositionHistoryRepository.save(historyAct);
|
||||
} catch (error) {
|
||||
console.error(`Error processing item ${item.id}:`, error);
|
||||
throw new HttpError(
|
||||
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
||||
`เกิดข้อผิดพลาดในการประมวลผลรายการ ${item.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Redis cache clear ทำหลัง commit (del cache key — idempotent)
|
||||
if (profileIdsToClearCache.size > 0) {
|
||||
await Promise.all(
|
||||
Array.from(profileIdsToClearCache).map(async (profileId) => {
|
||||
const redisClient = await redis.createClient({
|
||||
host: REDIS_HOST,
|
||||
port: REDIS_PORT,
|
||||
});
|
||||
|
||||
const delAsync = promisify(redisClient.del).bind(redisClient);
|
||||
await delAsync("role_" + profileId);
|
||||
await delAsync("menu_" + profileId);
|
||||
|
||||
redisClient.quit();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`[ExecuteOrgCommandService] Completed C-PM-40 — ${data?.length ?? 0} items`);
|
||||
}
|
||||
}
|
||||
|
|
@ -60,17 +60,6 @@ export interface SalaryCurrentExecutionContext {
|
|||
req?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* ผลลัพธ์การประมวลผล batch — all-or-nothing (single transaction ครอบทั้ง batch)
|
||||
* ถ้าทุกคนสำเร็จจะ return result; ถ้ามีคนใด throw จะ rollback ทั้ง batch
|
||||
* และ propagate error ออกไป (caller เห็นเป็น failure ทั้งหมด)
|
||||
*/
|
||||
export interface ExecuteSalaryResult {
|
||||
successCount: number;
|
||||
failureCount: number;
|
||||
failures: { profileId: string; reason: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Service สำหรับสร้าง ProfileSalary ของข้าราชการ + อัปเดตตำแหน่งปัจจุบัน (เปลี่ยนตำแหน่ง)
|
||||
*
|
||||
|
|
@ -98,7 +87,7 @@ export class ExecuteSalaryCurrentService {
|
|||
async executeSalaryCurrent(
|
||||
data: SalaryCurrentItem[],
|
||||
ctx: SalaryCurrentExecutionContext,
|
||||
): Promise<ExecuteSalaryResult> {
|
||||
): Promise<void> {
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
|
|
@ -170,12 +159,10 @@ export class ExecuteSalaryCurrentService {
|
|||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
let successCount = 0;
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb);
|
||||
successCount++;
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
|
|
@ -191,12 +178,6 @@ export class ExecuteSalaryCurrentService {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[ExecuteSalaryCurrentService] executeSalaryCurrent completed — success: ${successCount}, failure: 0`,
|
||||
);
|
||||
|
||||
return { successCount, failureCount: 0, failures: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -373,10 +354,10 @@ export class ExecuteSalaryCurrentService {
|
|||
if (posMasterOld != null) {
|
||||
await posMasterRepository.save(posMasterOld);
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
|
||||
console.log(
|
||||
`[ExecuteSalaryCurrentService] PosMasterOldId: ${posMasterOld.id}, profileId: ${item.profileId}`,
|
||||
`[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
|
||||
}
|
||||
await posMasterRepository.save(posMaster);
|
||||
|
||||
|
|
@ -504,11 +485,11 @@ export class ExecuteSalaryCurrentService {
|
|||
profile.amountSpecial = item.amountSpecial ?? null;
|
||||
await profileRepository.save(profile);
|
||||
await positionRepository.save(positionNew);
|
||||
console.log(
|
||||
`[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`,
|
||||
);
|
||||
}
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
|
||||
|
||||
console.log(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Double } from "typeorm";
|
||||
import { Double, EntityManager } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
|
|
@ -63,28 +63,29 @@ export interface SalaryEmployeeCurrentExecutionContext {
|
|||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateCurrent ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*/
|
||||
export class ExecuteSalaryEmployeeCurrentService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
|
||||
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
|
||||
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
|
||||
private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
|
||||
private employeePositionRepository = AppDataSource.getRepository(EmployeePosition);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
|
||||
/**
|
||||
* ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของลูกจ้าง
|
||||
* ประมวลผลสร้าง ProfileSalary + อัปเดตตำแหน่งปัจจุบันของลูกจ้างทั้ง batch
|
||||
*/
|
||||
async executeSalaryEmployeeCurrent(
|
||||
data: SalaryEmployeeCurrentItem[],
|
||||
ctx: SalaryEmployeeCurrentExecutionContext,
|
||||
): Promise<void> {
|
||||
console.log("[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent");
|
||||
console.log("[ExecuteSalaryEmployeeCurrentService] Request body count:", data?.length);
|
||||
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryEmployeeCurrentService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
|
|
@ -144,14 +145,59 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const profile: any = await this.profileEmployeeRepository.findOneBy({ id: item.profileId });
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryEmployeeCurrentService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryEmployeeCurrentItem,
|
||||
ctx: SalaryEmployeeCurrentExecutionContext,
|
||||
manager: EntityManager,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
const employeePositionRepository = manager.getRepository(EmployeePosition);
|
||||
|
||||
const profile: any = await profileEmployeeRepository.findOneBy({ id: item.profileId });
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
|
||||
}
|
||||
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -178,19 +224,19 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
|
||||
await this.salaryRepo.save(dataSalary, { data: req });
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
const posMaster = await this.employeePosMasterRepository.findOne({
|
||||
const posMaster = await employeePosMasterRepository.findOne({
|
||||
where: { id: item.posmasterId },
|
||||
relations: ["orgRoot"],
|
||||
});
|
||||
if (posMaster == null)
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
|
||||
|
||||
const posMasterOld = await this.employeePosMasterRepository.findOne({
|
||||
const posMasterOld = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: item.profileId,
|
||||
orgRevisionId: posMaster.orgRevisionId,
|
||||
|
|
@ -202,7 +248,7 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
}
|
||||
// if (posMasterOld != null) posMasterOld.next_holderId = null;
|
||||
|
||||
const positionOld = await this.employeePositionRepository.findOne({
|
||||
const positionOld = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMasterOld?.id,
|
||||
positionIsSelected: true,
|
||||
|
|
@ -210,10 +256,10 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
});
|
||||
if (positionOld != null) {
|
||||
positionOld.positionIsSelected = false;
|
||||
await this.employeePositionRepository.save(positionOld);
|
||||
await employeePositionRepository.save(positionOld);
|
||||
}
|
||||
|
||||
const checkPosition = await this.employeePositionRepository.find({
|
||||
const checkPosition = await employeePositionRepository.find({
|
||||
where: {
|
||||
posMasterId: item.posmasterId,
|
||||
positionIsSelected: true,
|
||||
|
|
@ -224,18 +270,22 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await this.employeePositionRepository.save(clearPosition);
|
||||
await employeePositionRepository.save(clearPosition);
|
||||
}
|
||||
|
||||
posMaster.current_holderId = item.profileId;
|
||||
posMaster.lastUpdatedAt = new Date();
|
||||
posMaster.next_holderId = null;
|
||||
if (posMasterOld != null) {
|
||||
await this.employeePosMasterRepository.save(posMasterOld);
|
||||
await CreatePosMasterHistoryEmployee(posMasterOld.id, req);
|
||||
await employeePosMasterRepository.save(posMasterOld);
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMasterOld.id}, profileId: ${item.profileId} (old)`,
|
||||
);
|
||||
await CreatePosMasterHistoryEmployee(posMasterOld.id, req, null, manager);
|
||||
}
|
||||
await this.employeePosMasterRepository.save(posMaster);
|
||||
const positionNew = await this.employeePositionRepository.findOne({
|
||||
await employeePosMasterRepository.save(posMaster);
|
||||
const positionNew = await employeePositionRepository.findOne({
|
||||
where: {
|
||||
id: item.positionId,
|
||||
posMasterId: item.posmasterId,
|
||||
|
|
@ -250,13 +300,17 @@ export class ExecuteSalaryEmployeeCurrentService {
|
|||
profile.positionEmployeePositionId = positionNew.positionName;
|
||||
profile.amount = item.amount ?? null;
|
||||
profile.amountSpecial = item.amountSpecial ?? null;
|
||||
await this.profileEmployeeRepository.save(profile);
|
||||
await this.employeePositionRepository.save(positionNew);
|
||||
await profileEmployeeRepository.save(profile);
|
||||
await employeePositionRepository.save(positionNew);
|
||||
}
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req);
|
||||
}),
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
|
||||
);
|
||||
await CreatePosMasterHistoryEmployee(posMaster.id, req, null, manager);
|
||||
|
||||
console.log("[ExecuteSalaryEmployeeCurrentService] executeSalaryEmployeeCurrent completed successfully");
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeCurrentService] Completed processOne — profileId: ${item.profileId}, posMasterId: ${posMaster.id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Double } from "typeorm";
|
||||
import { Double, EntityManager } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatus from "../interfaces/http-status";
|
||||
|
|
@ -71,29 +71,34 @@ export interface SalaryEmployeeLeaveExecutionContext {
|
|||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryEmployeeAndUpdateLeave ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryEmployeeLeaveService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private commandReciveRepository = AppDataSource.getRepository(CommandRecive);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
|
||||
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
|
||||
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
|
||||
private employeePosMasterRepository = AppDataSource.getRepository(EmployeePosMaster);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
|
||||
|
||||
/**
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave ของลูกจ้าง
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave ของลูกจ้างทั้ง batch
|
||||
*/
|
||||
async executeSalaryEmployeeLeave(
|
||||
data: SalaryEmployeeLeaveItem[],
|
||||
ctx: SalaryEmployeeLeaveExecutionContext,
|
||||
): Promise<void> {
|
||||
console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave");
|
||||
console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length);
|
||||
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryEmployeeLeaveService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
|
|
@ -156,9 +161,57 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
}
|
||||
}
|
||||
const today = new Date().setHours(0, 0, 0, 0);
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const profile = await this.profileEmployeeRepository.findOne({
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb, today);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryEmployeeLeaveService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryEmployeeLeaveItem,
|
||||
ctx: SalaryEmployeeLeaveExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
today: number,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const commandReciveRepository = manager.getRepository(CommandRecive);
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
const orgRevisionRepo = manager.getRepository(OrgRevision);
|
||||
|
||||
const profile = await profileEmployeeRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: {
|
||||
roleKeycloaks: true,
|
||||
|
|
@ -172,7 +225,7 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
const code = _command?.commandType?.code;
|
||||
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
|
||||
if (item.resignId && code && ["C-PM-42"].includes(code)) {
|
||||
const commandResign = await this.commandReciveRepository.findOne({
|
||||
const commandResign = await commandReciveRepository.findOne({
|
||||
where: { refId: item.resignId },
|
||||
relations: { command: true },
|
||||
});
|
||||
|
|
@ -184,14 +237,15 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
_command.status !== "REPORTED" &&
|
||||
(_command.status !== "WAITING" || today < executeDate)
|
||||
) {
|
||||
await reOrderCommandRecivesAndDelete(commandResign!.id);
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
|
||||
}
|
||||
}
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -218,10 +272,10 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt;
|
||||
await this.salaryRepo.save(dataSalary, { data: req });
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
const _null: any = null;
|
||||
profile.isLeave = item.isLeave;
|
||||
|
|
@ -232,7 +286,7 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
profile.lastUpdatedAt = new Date();
|
||||
// บันทึกประวัติก่อนลบตำแหน่ง
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
const curRevision = await this.orgRevisionRepo.findOne({
|
||||
const curRevision = await orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
||||
});
|
||||
let orgRootRef = null;
|
||||
|
|
@ -241,7 +295,7 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
let orgChild3Ref = null;
|
||||
let orgChild4Ref = null;
|
||||
if (curRevision) {
|
||||
const curPosMaster = await this.employeePosMasterRepository.findOne({
|
||||
const curPosMaster = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: curRevision.id,
|
||||
|
|
@ -260,17 +314,24 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
orgChild3Ref = curPosMaster?.orgChild3 ?? null;
|
||||
orgChild4Ref = curPosMaster?.orgChild4 ?? null;
|
||||
if (curPosMaster) {
|
||||
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE");
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`,
|
||||
);
|
||||
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager);
|
||||
}
|
||||
}
|
||||
|
||||
// ลบตำแหน่ง
|
||||
if (item.isLeave == true) {
|
||||
await removeProfileInOrganize(profile.id, "EMPLOYEE");
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(profile.id, "EMPLOYEE", manager);
|
||||
}
|
||||
|
||||
if (clearProfile.status) {
|
||||
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
|
||||
// Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
|
||||
// (Keycloak ไม่สามารถ rollback ได้)
|
||||
const delUserKeycloak = await deleteUser(profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
|
|
@ -290,7 +351,7 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
// profile.posTypeId = _null;
|
||||
// profile.posLevelId = _null;
|
||||
}
|
||||
await this.profileEmployeeRepository.save(profile);
|
||||
await profileEmployeeRepository.save(profile);
|
||||
|
||||
// if (profile.id) {
|
||||
// await this.keycloakAttributeService.clearOrgDnaAttributes(
|
||||
|
|
@ -313,9 +374,9 @@ export class ExecuteSalaryEmployeeLeaveService {
|
|||
organizeName = names.join(" ");
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("[ExecuteSalaryEmployeeLeaveService] executeSalaryEmployeeLeave completed successfully");
|
||||
console.log(
|
||||
`[ExecuteSalaryEmployeeLeaveService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
615
src/services/ExecuteSalaryLeaveDisciplineService.ts
Normal file
615
src/services/ExecuteSalaryLeaveDisciplineService.ts
Normal file
|
|
@ -0,0 +1,615 @@
|
|||
import { Double, EntityManager } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import { Profile } from "../entities/Profile";
|
||||
import { ProfileEmployee } from "../entities/ProfileEmployee";
|
||||
import { ProfileSalary } from "../entities/ProfileSalary";
|
||||
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
|
||||
import { ProfileDiscipline } from "../entities/ProfileDiscipline";
|
||||
import { ProfileDisciplineHistory } from "../entities/ProfileDisciplineHistory";
|
||||
import { OrgRoot } from "../entities/OrgRoot";
|
||||
import { OrgRevision } from "../entities/OrgRevision";
|
||||
import { EmployeePosMaster } from "../entities/EmployeePosMaster";
|
||||
import { Command } from "../entities/Command";
|
||||
import {
|
||||
checkCommandType,
|
||||
removePostMasterAct,
|
||||
removeProfileInOrganize,
|
||||
setLogDataDiff,
|
||||
} from "../interfaces/utils";
|
||||
import {
|
||||
CreatePosMasterHistoryEmployee,
|
||||
CreatePosMasterHistoryOfficer,
|
||||
} from "./PositionService";
|
||||
import { deleteUser } from "../keycloak";
|
||||
|
||||
/**
|
||||
* Input: ข้อมูล 1 คนสำหรับ endpoint excexute/salary-leave-discipline
|
||||
* (C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 — คำสั่งวินัย ข้าราชการ/ลูกจ้าง)
|
||||
*
|
||||
* profileType "OFFICER" → ข้าราชการ, ค่าอื่น/null → ลูกจ้าง
|
||||
*/
|
||||
export interface SalaryLeaveDisciplineItem {
|
||||
profileId: string;
|
||||
profileType?: string | null;
|
||||
isLeave: boolean | null;
|
||||
leaveReason?: string | null;
|
||||
dateLeave?: Date | string | null;
|
||||
detail?: string | null;
|
||||
level?: string | null;
|
||||
unStigma?: string | null;
|
||||
commandId?: string | null;
|
||||
amount?: Double | null;
|
||||
amountSpecial?: Double | null;
|
||||
positionSalaryAmount?: Double | null;
|
||||
mouthSalaryAmount?: Double | null;
|
||||
isGovernment?: boolean | null;
|
||||
commandNo: string | null;
|
||||
commandYear: number | null;
|
||||
commandDateAffect?: Date | string | null;
|
||||
commandDateSign?: Date | string | null;
|
||||
commandCode?: string | null;
|
||||
commandName?: string | null;
|
||||
remark: string | null;
|
||||
orgRoot?: string | null;
|
||||
orgChild1?: string | null;
|
||||
orgChild2?: string | null;
|
||||
orgChild3?: string | null;
|
||||
orgChild4?: string | null;
|
||||
posNo?: string | null;
|
||||
posNoAbb?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context สำหรับ audit/log
|
||||
*/
|
||||
export interface SalaryLeaveDisciplineExecutionContext {
|
||||
user: { sub: string; name: string };
|
||||
req?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service สำหรับสร้าง ProfileSalary + ProfileDiscipline + handle leave ของคำสั่งวินัย
|
||||
*
|
||||
* ใช้กับ commandType: C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32
|
||||
*
|
||||
* - endpoint /org/command/excexute/salary-leave-discipline เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeaveDiscipline ต้นฉบับ
|
||||
* รวมถึงกรณี OFFICER ที่การ save ProfileSalary + ProfileDiscipline ถูก comment out ไว้
|
||||
* (เก็บไว้เพื่อ preserve behavior เดิม — มีเพียง EMPLOYEE เท่านั้นที่ save จริง)
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryLeaveDisciplineService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
|
||||
/**
|
||||
* ประมวลผลคำสั่งวินัยทั้ง batch
|
||||
*
|
||||
* @returns สรุปผล success/failure ต่อคน
|
||||
*/
|
||||
async executeSalaryLeaveDiscipline(
|
||||
data: SalaryLeaveDisciplineItem[],
|
||||
ctx: SalaryLeaveDisciplineExecutionContext,
|
||||
): Promise<void> {
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Starting executeSalaryLeaveDiscipline — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryLeaveDisciplineService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const toDate = (v: any): Date | null => {
|
||||
if (v == null || v === "") return null;
|
||||
if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
|
||||
const d = new Date(v);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
};
|
||||
for (const item of data ?? []) {
|
||||
const it = item as any;
|
||||
it.dateLeave = toDate(it.dateLeave);
|
||||
it.commandDateAffect = toDate(it.commandDateAffect);
|
||||
it.commandDateSign = toDate(it.commandDateSign);
|
||||
}
|
||||
|
||||
let _posNumCodeSit: string = "";
|
||||
let _posNumCodeSitAbb: string = "";
|
||||
const _command = await this.commandRepository.findOne({
|
||||
relations: ["commandType"],
|
||||
where: { id: data.find((x) => x.commandId)?.commandId ?? "" },
|
||||
});
|
||||
if (_command) {
|
||||
if (_command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
|
||||
const orgRootDeputy = await this.orgRootRepository.findOne({
|
||||
where: {
|
||||
isDeputy: true,
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
relations: ["orgRevision"],
|
||||
});
|
||||
_posNumCodeSit = orgRootDeputy ? orgRootDeputy?.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
|
||||
_posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy?.orgRootShortName : "สนป.";
|
||||
} else if (_command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
|
||||
_posNumCodeSit = "กรุงเทพมหานคร";
|
||||
_posNumCodeSitAbb = "กทม.";
|
||||
} else {
|
||||
let _profileAdmin = await this.profileRepository.findOne({
|
||||
where: {
|
||||
keycloak: _command?.createdUserId.toString(),
|
||||
current_holders: {
|
||||
orgRevision: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
|
||||
});
|
||||
_posNumCodeSit =
|
||||
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
|
||||
"";
|
||||
_posNumCodeSitAbb =
|
||||
_profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
|
||||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryLeaveDisciplineItem,
|
||||
ctx: SalaryLeaveDisciplineExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const disciplineRepository = manager.getRepository(ProfileDiscipline);
|
||||
const disciplineHistoryRepository = manager.getRepository(ProfileDisciplineHistory);
|
||||
const orgRevisionRepo = manager.getRepository(OrgRevision);
|
||||
const employeePosMasterRepository = manager.getRepository(EmployeePosMaster);
|
||||
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
|
||||
const orgRevision = await orgRevisionRepo.findOne({
|
||||
where: {
|
||||
orgRevisionIsCurrent: true,
|
||||
orgRevisionIsDraft: false,
|
||||
},
|
||||
});
|
||||
|
||||
let orgRootRef: any = null;
|
||||
let orgChild1Ref: any = null;
|
||||
let orgChild2Ref: any = null;
|
||||
let orgChild3Ref: any = null;
|
||||
let orgChild4Ref: any = null;
|
||||
|
||||
const code = _command?.commandType?.code;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// OFFICER (ข้าราชการ)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (item.profileType && item.profileType.trim().toUpperCase() == "OFFICER") {
|
||||
const profile: any = await profileRepository.findOne({
|
||||
relations: [
|
||||
"posLevel",
|
||||
"posType",
|
||||
"current_holders",
|
||||
"current_holders.orgRoot",
|
||||
"current_holders.orgChild1",
|
||||
"current_holders.orgChild2",
|
||||
"current_holders.orgChild3",
|
||||
"current_holders.orgChild4",
|
||||
"current_holders.positions",
|
||||
"current_holders.positions.posExecutive",
|
||||
"roleKeycloaks",
|
||||
],
|
||||
where: { id: item.profileId },
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const lastSalary = await salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
select: ["order"],
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
|
||||
|
||||
//ลบตำแหน่งที่รักษาการแทน (await + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน)
|
||||
if (code && ["C-PM-19", "C-PM-20"].includes(code)) {
|
||||
await removePostMasterAct(profile.id, manager);
|
||||
}
|
||||
|
||||
const orgRevisionRef =
|
||||
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
|
||||
orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
const position =
|
||||
profile.current_holders
|
||||
.filter((x: any) => x.orgRevisionId == orgRevision?.id)[0]
|
||||
?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null;
|
||||
|
||||
// ประวัติตำแหน่ง
|
||||
const data = new ProfileSalary();
|
||||
data.posNumCodeSit = _posNumCodeSit;
|
||||
data.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileId: profile.id,
|
||||
commandId: item.commandId,
|
||||
position: profile.position,
|
||||
positionName: profile.position,
|
||||
positionType: profile?.posType?.posTypeName ?? null,
|
||||
positionLevel: profile?.posLevel?.posLevelName ?? null,
|
||||
positionExecutive: position?.posExecutive?.posExecutiveName ?? null,
|
||||
amount: item.amount ? item.amount : null,
|
||||
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
|
||||
amountSpecial: item.amountSpecial ? item.amountSpecial : null,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
|
||||
order: nextOrder,
|
||||
orgRoot: item.orgRoot,
|
||||
orgChild1: item.orgChild1,
|
||||
orgChild2: item.orgChild2,
|
||||
orgChild3: item.orgChild3,
|
||||
orgChild4: item.orgChild4,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
dateGovernment: item.commandDateAffect ?? new Date(),
|
||||
isGovernment: item.isGovernment,
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: item.posNo,
|
||||
posNoAbb: item.posNoAbb,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(data, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...data, id: undefined });
|
||||
// ── preserve: OFFICER branch ไม่ save ProfileSalary (comment ตามต้นฉบับ) ──
|
||||
// await salaryRepo.save(data, { data: req });
|
||||
// history.profileSalaryId = data.id;
|
||||
// await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
// ประวัติวินัย
|
||||
const dataDis = new ProfileDiscipline();
|
||||
const metaDis = {
|
||||
date: item.commandDateAffect,
|
||||
refCommandDate: item.commandDateSign,
|
||||
refCommandNo: `${item.commandNo}/${item.commandYear}`,
|
||||
refCommandId: item.commandId,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
};
|
||||
Object.assign(dataDis, { ...item, ...metaDis });
|
||||
const historyDis = new ProfileDisciplineHistory();
|
||||
Object.assign(historyDis, { ...dataDis, id: undefined });
|
||||
// ── preserve: OFFICER branch ไม่ save ProfileDiscipline (comment ตามต้นฉบับ) ──
|
||||
// await disciplineRepository.save(dataDis, { data: req });
|
||||
// historyDis.profileDisciplineId = dataDis.id;
|
||||
// await disciplineHistoryRepository.save(historyDis, { data: req });
|
||||
|
||||
// ทะเบียนประวัติ
|
||||
if (item.isLeave != null) {
|
||||
const _profile: any = await profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!_profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const _null: any = null;
|
||||
_profile.isLeave = item.isLeave;
|
||||
_profile.leaveReason = item.leaveReason ?? _null;
|
||||
_profile.dateLeave = item.dateLeave ?? _null;
|
||||
_profile.lastUpdateUserId = ctx.user.sub;
|
||||
_profile.lastUpdateFullName = ctx.user.name;
|
||||
_profile.lastUpdatedAt = new Date();
|
||||
if (item.isLeave == true) {
|
||||
if (orgRevisionRef) {
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager);
|
||||
}
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(_profile.id, "OFFICER", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
if (clearProfile.status) {
|
||||
if (
|
||||
_profile.keycloak != null &&
|
||||
_profile.keycloak != "" &&
|
||||
_profile.isDelete === false
|
||||
) {
|
||||
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
|
||||
const delUserKeycloak = await deleteUser(_profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// _profile.keycloak = _null;
|
||||
_profile.roleKeycloaks = [];
|
||||
_profile.isActive = false;
|
||||
_profile.isDelete = true;
|
||||
}
|
||||
}
|
||||
_profile.leaveCommandId = item.commandId ?? _null;
|
||||
_profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`;
|
||||
_profile.leaveRemark = clearProfile.leaveRemark ?? _null;
|
||||
_profile.leaveDate = item.commandDateAffect ?? _null;
|
||||
_profile.leaveType = clearProfile.LeaveType ?? _null;
|
||||
//ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516)
|
||||
// _profile.position = _null;
|
||||
// _profile.posTypeId = _null;
|
||||
// _profile.posLevelId = _null;
|
||||
}
|
||||
await profileRepository.save(_profile, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: _profile });
|
||||
}
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// EMPLOYEE (ลูกจ้าง)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
else {
|
||||
const profile: any = await profileEmployeeRepository.findOne({
|
||||
relations: [
|
||||
"posLevel",
|
||||
"posType",
|
||||
"current_holders",
|
||||
"current_holders.orgRoot",
|
||||
"current_holders.orgChild1",
|
||||
"current_holders.orgChild2",
|
||||
"current_holders.orgChild3",
|
||||
"current_holders.orgChild4",
|
||||
"roleKeycloaks",
|
||||
],
|
||||
where: { id: item.profileId },
|
||||
});
|
||||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const lastSalary = await salaryRepo.findOne({
|
||||
where: { profileEmployeeId: item.profileId },
|
||||
select: ["order"],
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
|
||||
const orgRevisionRef =
|
||||
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
|
||||
orgRootRef = orgRevisionRef?.orgRoot ?? null;
|
||||
orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
|
||||
orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
|
||||
orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
|
||||
orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
|
||||
|
||||
// ประวัติตำแหน่ง
|
||||
const data = new ProfileSalary();
|
||||
data.posNumCodeSit = _posNumCodeSit;
|
||||
data.posNumCodeSitAbb = _posNumCodeSitAbb;
|
||||
const meta = {
|
||||
profileEmployeeId: profile.id,
|
||||
commandId: item.commandId,
|
||||
position: profile.position,
|
||||
positionName: profile.position,
|
||||
positionType: profile?.posType?.posTypeName ?? null,
|
||||
positionLevel:
|
||||
profile?.posType && profile?.posLevel
|
||||
? `${profile?.posType?.posTypeShortName} ${profile?.posLevel?.posLevelName}`
|
||||
: null,
|
||||
amount: item.amount ? item.amount : null,
|
||||
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
|
||||
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
|
||||
order: nextOrder,
|
||||
orgRoot: item.orgRoot,
|
||||
orgChild1: item.orgChild1,
|
||||
orgChild2: item.orgChild2,
|
||||
orgChild3: item.orgChild3,
|
||||
orgChild4: item.orgChild4,
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
dateGovernment: item.commandDateAffect ?? new Date(),
|
||||
isGovernment: item.isGovernment,
|
||||
commandNo: item.commandNo,
|
||||
commandYear: item.commandYear,
|
||||
posNo: item.posNo,
|
||||
posNoAbb: item.posNoAbb,
|
||||
commandDateAffect: item.commandDateAffect,
|
||||
commandDateSign: item.commandDateSign,
|
||||
commandCode: item.commandCode,
|
||||
commandName: item.commandName,
|
||||
remark: item.remark,
|
||||
};
|
||||
Object.assign(data, meta);
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...data, id: undefined });
|
||||
await salaryRepo.save(data, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: data });
|
||||
history.profileSalaryId = data.id;
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
// ประวัติวินัย
|
||||
const dataDis = new ProfileDiscipline();
|
||||
const metaDis = {
|
||||
createdUserId: ctx.user.sub,
|
||||
createdFullName: ctx.user.name,
|
||||
lastUpdateUserId: ctx.user.sub,
|
||||
lastUpdateFullName: ctx.user.name,
|
||||
createdAt: new Date(),
|
||||
lastUpdatedAt: new Date(),
|
||||
};
|
||||
Object.assign(dataDis, {
|
||||
...item,
|
||||
...metaDis,
|
||||
date: item.commandDateAffect,
|
||||
refCommandDate: item.commandDateSign,
|
||||
refCommandNo: item.commandNo,
|
||||
profileEmployeeId: item.profileId,
|
||||
profileId: undefined,
|
||||
});
|
||||
const historyDis = new ProfileDisciplineHistory();
|
||||
Object.assign(historyDis, { ...dataDis, id: undefined });
|
||||
await disciplineRepository.save(dataDis, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: dataDis });
|
||||
historyDis.profileDisciplineId = dataDis.id;
|
||||
await disciplineHistoryRepository.save(historyDis, { data: req });
|
||||
|
||||
// ทะเบียนประวัติ
|
||||
if (item.isLeave != null) {
|
||||
const _profile: any = await profileEmployeeRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: ["roleKeycloaks"],
|
||||
});
|
||||
if (!_profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const _null: any = null;
|
||||
_profile.isLeave = item.isLeave;
|
||||
_profile.leaveReason = item.leaveReason ?? _null;
|
||||
_profile.dateLeave = item.dateLeave ?? _null;
|
||||
_profile.lastUpdateUserId = ctx.user.sub;
|
||||
_profile.lastUpdateFullName = ctx.user.name;
|
||||
_profile.lastUpdatedAt = new Date();
|
||||
if (item.isLeave == true) {
|
||||
// บันทึกประวัติก่อนลบตำแหน่ง
|
||||
const curRevision = await orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
||||
});
|
||||
if (curRevision) {
|
||||
const curPosMaster = await employeePosMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: _profile.id,
|
||||
orgRevisionId: curRevision.id,
|
||||
},
|
||||
});
|
||||
if (curPosMaster) {
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await CreatePosMasterHistoryEmployee(curPosMaster.id, req, "DELETE", manager);
|
||||
}
|
||||
}
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(_profile.id, "EMPLOYEE", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
if (clearProfile.status) {
|
||||
if (
|
||||
_profile.keycloak != null &&
|
||||
_profile.keycloak != "" &&
|
||||
_profile.isDelete === false
|
||||
) {
|
||||
// Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
|
||||
// (Keycloak ไม่สามารถ rollback ได้)
|
||||
const delUserKeycloak = await deleteUser(_profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
// _profile.keycloak = _null;
|
||||
_profile.roleKeycloaks = [];
|
||||
_profile.isActive = false;
|
||||
_profile.isDelete = true;
|
||||
}
|
||||
}
|
||||
_profile.leaveCommandId = item.commandId ?? _null;
|
||||
_profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`;
|
||||
_profile.leaveRemark = clearProfile.leaveRemark ?? _null;
|
||||
_profile.leaveDate = item.commandDateAffect ?? _null;
|
||||
_profile.leaveType = clearProfile.LeaveType ?? _null;
|
||||
//ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516)
|
||||
// _profile.position = _null;
|
||||
// _profile.posTypeId = _null;
|
||||
// _profile.posLevelId = _null;
|
||||
}
|
||||
await profileEmployeeRepository.save(_profile, { data: req });
|
||||
setLogDataDiff(req, { before: null, after: _profile });
|
||||
}
|
||||
}
|
||||
|
||||
// Task #2190 (preserve: organizeName computed แต่ยังไม่ได้ใช้ในต้นฉบับ — เก็บไว้ตาม behavior เดิม)
|
||||
if (_command && ["C-PM-19", "C-PM-20"].includes(_command.commandType.code)) {
|
||||
let organizeName = "";
|
||||
if (orgRootRef) {
|
||||
const names = [
|
||||
orgChild4Ref?.orgChild4Name,
|
||||
orgChild3Ref?.orgChild3Name,
|
||||
orgChild2Ref?.orgChild2Name,
|
||||
orgChild1Ref?.orgChild1Name,
|
||||
orgRootRef?.orgRootName,
|
||||
].filter(Boolean);
|
||||
organizeName = names.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveDisciplineService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Double, In, Like } from "typeorm";
|
||||
import { Double, EntityManager, In, Like } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
|
|
@ -103,27 +103,34 @@ export interface SalaryLeaveExecutionContext {
|
|||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdateLeave ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operations (deleteUser/createUser/addUserRoles/updateUserAttributes)
|
||||
* ทำภายใน transaction เพื่อ preserve behavior เดิม — Keycloak ไม่สามารถ rollback ได้
|
||||
* ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryLeaveService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private commandReciveRepository = AppDataSource.getRepository(CommandRecive);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
|
||||
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
|
||||
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
||||
private positionRepository = AppDataSource.getRepository(Position);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
|
||||
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
|
||||
|
||||
/**
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการ
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/กลับเข้าราชการ ของข้าราชการทั้ง batch
|
||||
*
|
||||
* @returns สรุปผล success/failure ต่อคน
|
||||
*/
|
||||
async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise<void> {
|
||||
console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave");
|
||||
console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length);
|
||||
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveService] Starting executeSalaryLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryLeaveService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
|
|
@ -189,9 +196,69 @@ export class ExecuteSalaryLeaveService {
|
|||
}
|
||||
}
|
||||
const today = new Date().setHours(0, 0, 0, 0);
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const profile = await this.profileRepository.findOne({
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(
|
||||
item,
|
||||
ctx,
|
||||
manager,
|
||||
_command,
|
||||
_posNumCodeSit,
|
||||
_posNumCodeSitAbb,
|
||||
today,
|
||||
roleKeycloak,
|
||||
);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryLeaveService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryLeaveItem,
|
||||
ctx: SalaryLeaveExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
today: number,
|
||||
roleKeycloak: RoleKeycloak | null,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const commandReciveRepository = manager.getRepository(CommandRecive);
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const posMasterRepository = manager.getRepository(PosMaster);
|
||||
const positionRepository = manager.getRepository(Position);
|
||||
const orgRevisionRepo = manager.getRepository(OrgRevision);
|
||||
const roleKeycloakRepo = manager.getRepository(RoleKeycloak);
|
||||
|
||||
const profile = await profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: {
|
||||
roleKeycloaks: true,
|
||||
|
|
@ -203,11 +270,12 @@ export class ExecuteSalaryLeaveService {
|
|||
//ลบตำแหน่งที่รักษาการแทน
|
||||
const code = _command?.commandType?.code;
|
||||
if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(code)) {
|
||||
removePostMasterAct(profile.id);
|
||||
// await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction
|
||||
await removePostMasterAct(profile.id, manager);
|
||||
}
|
||||
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
|
||||
else if (item.resignId && code && ["C-PM-41"].includes(code)) {
|
||||
const commandResign = await this.commandReciveRepository.findOne({
|
||||
const commandResign = await commandReciveRepository.findOne({
|
||||
where: { refId: item.resignId },
|
||||
relations: { command: true },
|
||||
});
|
||||
|
|
@ -219,7 +287,8 @@ export class ExecuteSalaryLeaveService {
|
|||
_command.status !== "REPORTED" &&
|
||||
(_command.status !== "WAITING" || today < executeDate)
|
||||
) {
|
||||
await reOrderCommandRecivesAndDelete(commandResign!.id);
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
|
||||
}
|
||||
}
|
||||
let _commandYear = item.commandYear;
|
||||
|
|
@ -227,7 +296,7 @@ export class ExecuteSalaryLeaveService {
|
|||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
const returnWork = await checkReturnCommandType(String(item.commandId));
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -249,10 +318,10 @@ export class ExecuteSalaryLeaveService {
|
|||
Object.assign(dataSalary, { ...item, ...meta });
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
await this.salaryRepo.save(dataSalary, { data: req });
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
}
|
||||
const _null: any = null;
|
||||
profile.isLeave = item.isLeave;
|
||||
|
|
@ -264,7 +333,7 @@ export class ExecuteSalaryLeaveService {
|
|||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
|
||||
//ปั๊มประวัติก่อนลบตำแหน่ง
|
||||
const curRevision = await this.orgRevisionRepo.findOne({
|
||||
const curRevision = await orgRevisionRepo.findOne({
|
||||
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
|
||||
});
|
||||
let orgRootRef = null;
|
||||
|
|
@ -273,7 +342,7 @@ export class ExecuteSalaryLeaveService {
|
|||
let orgChild3Ref = null;
|
||||
let orgChild4Ref = null;
|
||||
if (curRevision) {
|
||||
const curPosMaster = await this.posMasterRepository.findOne({
|
||||
const curPosMaster = await posMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: profile.id,
|
||||
orgRevisionId: curRevision.id,
|
||||
|
|
@ -292,16 +361,22 @@ export class ExecuteSalaryLeaveService {
|
|||
orgChild3Ref = curPosMaster?.orgChild3 ?? null;
|
||||
orgChild4Ref = curPosMaster?.orgChild4 ?? null;
|
||||
if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") {
|
||||
await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE");
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${curPosMaster.id}, profileId: ${item.profileId}, type: DELETE`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(curPosMaster.id, req, "DELETE", null, manager);
|
||||
}
|
||||
}
|
||||
|
||||
//ลบตำแหน่ง
|
||||
if (item.isLeave == true) {
|
||||
await removeProfileInOrganize(profile.id, "OFFICER");
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
await removeProfileInOrganize(profile.id, "OFFICER", manager);
|
||||
}
|
||||
if (clearProfile.status) {
|
||||
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
|
||||
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
|
||||
const delUserKeycloak = await deleteUser(profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
// Task #228
|
||||
|
|
@ -325,10 +400,10 @@ export class ExecuteSalaryLeaveService {
|
|||
if (item.isGovernment == true) {
|
||||
if (returnWork) {
|
||||
//ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้
|
||||
await removeProfileInOrganize(profile.id, "OFFICER");
|
||||
await removeProfileInOrganize(profile.id, "OFFICER", manager);
|
||||
//ปั๊มตำแหน่งใหม่
|
||||
// หา posMaster และเช็ค orgRevisionIsCurrent
|
||||
let posMaster = await this.posMasterRepository.findOne({
|
||||
let posMaster = await posMasterRepository.findOne({
|
||||
where: { id: item.posmasterId?.toString() },
|
||||
relations: {
|
||||
orgRevision: true,
|
||||
|
|
@ -347,7 +422,7 @@ export class ExecuteSalaryLeaveService {
|
|||
|
||||
// ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA
|
||||
if (!isCurrent && posMaster?.ancestorDNA) {
|
||||
posMaster = await this.posMasterRepository.findOne({
|
||||
posMaster = await posMasterRepository.findOne({
|
||||
where: {
|
||||
ancestorDNA: posMaster.ancestorDNA,
|
||||
orgRevision: {
|
||||
|
|
@ -367,7 +442,7 @@ export class ExecuteSalaryLeaveService {
|
|||
}
|
||||
|
||||
if (posMaster) {
|
||||
const checkPosition = await this.positionRepository.find({
|
||||
const checkPosition = await positionRepository.find({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionIsSelected: true,
|
||||
|
|
@ -378,13 +453,13 @@ export class ExecuteSalaryLeaveService {
|
|||
...positions,
|
||||
positionIsSelected: false,
|
||||
}));
|
||||
await this.positionRepository.save(clearPosition);
|
||||
await positionRepository.save(clearPosition);
|
||||
}
|
||||
posMaster.current_holderId = profile.id;
|
||||
posMaster.lastUpdatedAt = new Date();
|
||||
// posMaster.conditionReason = _null;
|
||||
// posMaster.isCondition = false;
|
||||
await this.posMasterRepository.save(posMaster);
|
||||
await posMasterRepository.save(posMaster);
|
||||
|
||||
// Match position ตามลำดับ priority:
|
||||
// Condition 1: match จาก positionId
|
||||
|
|
@ -398,7 +473,7 @@ export class ExecuteSalaryLeaveService {
|
|||
// CONDITION 1: เช็คจาก positionId ตรง
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (item.positionId) {
|
||||
const positionById = await this.positionRepository.findOne({
|
||||
const positionById = await positionRepository.findOne({
|
||||
where: {
|
||||
id: item.positionId,
|
||||
posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง
|
||||
|
|
@ -436,7 +511,7 @@ export class ExecuteSalaryLeaveService {
|
|||
whereCondition.positionArea = item.positionArea;
|
||||
}
|
||||
|
||||
const positionBy7Fields = await this.positionRepository.findOne({
|
||||
const positionBy7Fields = await positionRepository.findOne({
|
||||
where: whereCondition,
|
||||
relations: ["posExecutive"],
|
||||
order: { orderNo: "ASC" },
|
||||
|
|
@ -451,7 +526,7 @@ export class ExecuteSalaryLeaveService {
|
|||
// CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) {
|
||||
const positionBy3Fields = await this.positionRepository.findOne({
|
||||
const positionBy3Fields = await positionRepository.findOne({
|
||||
where: {
|
||||
posMasterId: posMaster.id,
|
||||
positionName: item.positionNameNew,
|
||||
|
|
@ -469,7 +544,7 @@ export class ExecuteSalaryLeaveService {
|
|||
|
||||
// // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition)
|
||||
// if (!positionNew) {
|
||||
// const fallbackPositions = await this.positionRepository.find({
|
||||
// const fallbackPositions = await positionRepository.find({
|
||||
// where: {
|
||||
// posMasterId: posMaster.id,
|
||||
// },
|
||||
|
|
@ -487,9 +562,13 @@ export class ExecuteSalaryLeaveService {
|
|||
|
||||
if (positionNew) {
|
||||
positionNew.positionIsSelected = true;
|
||||
await this.positionRepository.save(positionNew, { data: req });
|
||||
await positionRepository.save(positionNew, { data: req });
|
||||
}
|
||||
await CreatePosMasterHistoryOfficer(posMaster.id, req);
|
||||
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
|
||||
profile.posMasterNo = getPosMasterNo(posMaster);
|
||||
profile.org = getOrgFullName(posMaster);
|
||||
}
|
||||
|
|
@ -522,9 +601,9 @@ export class ExecuteSalaryLeaveService {
|
|||
Object.assign(dataSalary, { ...newMapProfileSalary, ...meta });
|
||||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
await this.salaryRepo.save(dataSalary);
|
||||
await salaryRepo.save(dataSalary);
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history);
|
||||
await salaryHistoryRepo.save(history);
|
||||
profile.leaveReason = _null;
|
||||
profile.leaveCommandId = _null;
|
||||
profile.leaveCommandNo = _null;
|
||||
|
|
@ -553,6 +632,7 @@ export class ExecuteSalaryLeaveService {
|
|||
}
|
||||
// กรอง "." ออกจาก firstName ก่อนส่งไป keycloak
|
||||
const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? "";
|
||||
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
|
||||
userKeycloakId = await createUser(profile.citizenId, password, {
|
||||
firstName: sanitizedFirstName,
|
||||
lastName: profile.lastName,
|
||||
|
|
@ -578,7 +658,7 @@ export class ExecuteSalaryLeaveService {
|
|||
else {
|
||||
const rolesData = await getRoleMappings(checkUser[0].id);
|
||||
if (rolesData) {
|
||||
const _roleKeycloak = await this.roleKeycloakRepo.find({
|
||||
const _roleKeycloak = await roleKeycloakRepo.find({
|
||||
where: { name: In(rolesData.map((x: any) => x.name)) },
|
||||
});
|
||||
profile.roleKeycloaks =
|
||||
|
|
@ -591,7 +671,7 @@ export class ExecuteSalaryLeaveService {
|
|||
profile.isActive = true;
|
||||
profile.isDelete = false;
|
||||
}
|
||||
await this.profileRepository.save(profile);
|
||||
await profileRepository.save(profile);
|
||||
|
||||
// if (profile.id) {
|
||||
// await this.keycloakAttributeService.clearOrgDnaAttributes(
|
||||
|
|
@ -620,9 +700,9 @@ export class ExecuteSalaryLeaveService {
|
|||
organizeName = names.join(" ");
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully");
|
||||
console.log(
|
||||
`[ExecuteSalaryLeaveService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
539
src/services/ExecuteSalaryProbationService.ts
Normal file
539
src/services/ExecuteSalaryProbationService.ts
Normal 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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
318
src/services/ExecuteSalaryReportService.ts
Normal file
318
src/services/ExecuteSalaryReportService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Double } from "typeorm";
|
||||
import { Double, EntityManager } from "typeorm";
|
||||
import { AppDataSource } from "../database/data-source";
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
|
|
@ -70,31 +70,38 @@ export interface SalaryExecutionContext {
|
|||
/**
|
||||
* Service สำหรับสร้าง ProfileSalary ของข้าราชการ + handle leave/ออกจากราชการ/ช่วยราชการ
|
||||
*
|
||||
* ใช้กับ commandType: C-PM-13 (โอน), C-PM-15 (ช่วยราชการ), C-PM-16 (เกษียณ)
|
||||
* ใช้กับ commandType: C-PM-13, 15, 16
|
||||
*
|
||||
* - endpoint /org/command/excexute/salary เรียกผ่าน service นี้ (thin wrapper)
|
||||
* - consumer ใน rabbitmq handler เรียกผ่าน service นี้โดยตรง (Linear Flow)
|
||||
*
|
||||
* Behavior ทั้งหมด preserve จาก CommandController.newSalaryAndUpdate ต้นฉบับ
|
||||
*
|
||||
* Batch semantics: all-or-nothing — ประมวลผลทุกคนภายใต้ transaction เดียว (sequential)
|
||||
* ถ้าคนใด throw จะ rollback ทั้ง batch และ propagate error ออกไป (ล้มเหลวทั้งหมด)
|
||||
* ถ้าทุกคนสำเร็จจะ return result รายงาน success count
|
||||
*
|
||||
* ⚠️ หมายเหตุ Keycloak: operation (deleteUser) ทำภายใน transaction เพื่อ preserve behavior
|
||||
* เดิม — Keycloak ไม่สามารถ rollback ได้ ถ้า DB rollback หลังจาก Keycloak operation สำเร็จ
|
||||
* → Keycloak จะถูกเปลี่ยนไปแล้ว
|
||||
*/
|
||||
export class ExecuteSalaryService {
|
||||
private commandRepository = AppDataSource.getRepository(Command);
|
||||
private profileRepository = AppDataSource.getRepository(Profile);
|
||||
private salaryRepo = AppDataSource.getRepository(ProfileSalary);
|
||||
private salaryHistoryRepo = AppDataSource.getRepository(ProfileSalaryHistory);
|
||||
private posMasterRepository = AppDataSource.getRepository(PosMaster);
|
||||
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
|
||||
private assistanceRepository = AppDataSource.getRepository(ProfileAssistance);
|
||||
private assistanceHistoryRepository = AppDataSource.getRepository(ProfileAssistanceHistory);
|
||||
|
||||
/**
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/assistance
|
||||
* ประมวลผลสร้าง ProfileSalary + handle leave/assistance ทั้ง batch
|
||||
*
|
||||
* @returns สรุปผล success/failure ต่อคน
|
||||
*/
|
||||
async executeSalary(data: SalaryItem[], ctx: SalaryExecutionContext): Promise<void> {
|
||||
console.log("[ExecuteSalaryService] Starting executeSalary");
|
||||
console.log("[ExecuteSalaryService] Request body count:", data?.length);
|
||||
|
||||
const req = ctx.req;
|
||||
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
|
||||
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Starting executeSalary — commandCode: ${commandCode}, commandId: ${commandId}`,
|
||||
);
|
||||
console.log(`[ExecuteSalaryService] Request body count: ${data?.length ?? 0}`);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
|
||||
|
|
@ -158,9 +165,59 @@ export class ExecuteSalaryService {
|
|||
.orgRootShortName ?? "";
|
||||
}
|
||||
}
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const profile: any = await this.profileRepository.findOne({
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// Single transaction ครอบทั้ง batch (all-or-nothing)
|
||||
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
|
||||
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
let successCount = 0;
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
for (const item of data ?? []) {
|
||||
try {
|
||||
await this.processOne(item, ctx, manager, _command, _posNumCodeSit, _posNumCodeSitAbb);
|
||||
} catch (err) {
|
||||
const reason =
|
||||
err instanceof HttpError
|
||||
? err.message
|
||||
: err instanceof Error
|
||||
? err.message
|
||||
: "unexpected error";
|
||||
console.error(
|
||||
`[ExecuteSalaryService] Failed commandCode=${commandCode}, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
|
||||
err,
|
||||
);
|
||||
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* ประมวลผล 1 คน ภายใน transaction เดียว (manager)
|
||||
* ทุก save ใช้ manager.getRepository(...) เพื่อให้อยู่ใน transaction เดียวกัน
|
||||
* ถ้า throw ระหว่างทาง → rollback ทั้งหมดของคนนี้ + ทั้ง batch (กัน partial commit)
|
||||
*
|
||||
* หมายเหตุ: Keycloak deleteUser ทำก่อนเข้า transaction เพราะไม่สามารถ rollback ได้
|
||||
*/
|
||||
private async processOne(
|
||||
item: SalaryItem,
|
||||
ctx: SalaryExecutionContext,
|
||||
manager: EntityManager,
|
||||
_command: Command | null,
|
||||
_posNumCodeSit: string,
|
||||
_posNumCodeSitAbb: string,
|
||||
): Promise<void> {
|
||||
const req = ctx.req;
|
||||
|
||||
const profileRepository = manager.getRepository(Profile);
|
||||
const salaryRepo = manager.getRepository(ProfileSalary);
|
||||
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
|
||||
const posMasterRepository = manager.getRepository(PosMaster);
|
||||
const assistanceRepository = manager.getRepository(ProfileAssistance);
|
||||
const assistanceHistoryRepository = manager.getRepository(ProfileAssistanceHistory);
|
||||
|
||||
const profile: any = await profileRepository.findOne({
|
||||
where: { id: item.profileId },
|
||||
relations: {
|
||||
roleKeycloaks: true,
|
||||
|
|
@ -171,7 +228,7 @@ export class ExecuteSalaryService {
|
|||
if (!profile) {
|
||||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
|
||||
}
|
||||
const posMaster: any = await this.posMasterRepository.findOne({
|
||||
const posMaster: any = await posMasterRepository.findOne({
|
||||
where: {
|
||||
current_holderId: item.profileId,
|
||||
orgRevision: {
|
||||
|
|
@ -199,14 +256,15 @@ export class ExecuteSalaryService {
|
|||
//ลบตำแหน่งที่รักษาการแทน
|
||||
const code = _command?.commandType?.code;
|
||||
if (code && ["C-PM-13"].includes(code)) {
|
||||
removePostMasterAct(profile.id);
|
||||
// await (เดิมไม่ await = fire-and-forget bug) + ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction
|
||||
await removePostMasterAct(profile.id, manager);
|
||||
}
|
||||
|
||||
let _commandYear = item.commandYear;
|
||||
if (item.commandYear) {
|
||||
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
|
||||
}
|
||||
const dest_item = await this.salaryRepo.findOne({
|
||||
const dest_item = await salaryRepo.findOne({
|
||||
where: { profileId: item.profileId },
|
||||
order: { order: "DESC" },
|
||||
});
|
||||
|
|
@ -224,12 +282,18 @@ export class ExecuteSalaryService {
|
|||
lastUpdatedAt: new Date(),
|
||||
};
|
||||
if (item.isLeave != undefined && item.isLeave == true) {
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE");
|
||||
await removeProfileInOrganize(profile.id, "OFFICER");
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Creating PosMasterHistory — posMasterId: ${orgRevisionRef}, profileId: ${item.profileId}, type: DELETE`,
|
||||
);
|
||||
await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE", null, manager);
|
||||
await removeProfileInOrganize(profile.id, "OFFICER", manager);
|
||||
}
|
||||
const clearProfile = await checkCommandType(String(item.commandId));
|
||||
const _null: any = null;
|
||||
if (clearProfile.status) {
|
||||
// Keycloak deleteUser ทำก่อนเข้า transaction-bound save ด้านล่าง
|
||||
// (ทำภายใน transaction เดียวกัน เพราะถ้า fail ต้อง rollback DB ด้วย)
|
||||
// หมายเหตุ: Keycloak ไม่สามารถ rollback ได้ → ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
|
||||
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
|
||||
const delUserKeycloak = await deleteUser(profile.keycloak);
|
||||
if (delUserKeycloak) {
|
||||
|
|
@ -254,7 +318,7 @@ export class ExecuteSalaryService {
|
|||
profile.dateLeave = item.dateLeave ?? _null;
|
||||
profile.amount = item.amount ?? _null;
|
||||
profile.amountSpecial = item.amountSpecial ?? _null;
|
||||
await this.profileRepository.save(profile, { data: req });
|
||||
await profileRepository.save(profile, { data: req });
|
||||
|
||||
// if (profile.id) {
|
||||
// await this.keycloakAttributeService.clearOrgDnaAttributes(
|
||||
|
|
@ -267,10 +331,10 @@ export class ExecuteSalaryService {
|
|||
const history = new ProfileSalaryHistory();
|
||||
Object.assign(history, { ...dataSalary, id: undefined });
|
||||
|
||||
await this.salaryRepo.save(dataSalary, { data: req });
|
||||
await salaryRepo.save(dataSalary, { data: req });
|
||||
setLogDataDiff(req, { before, after: dataSalary });
|
||||
history.profileSalaryId = dataSalary.id;
|
||||
await this.salaryHistoryRepo.save(history, { data: req });
|
||||
await salaryHistoryRepo.save(history, { data: req });
|
||||
|
||||
if (_command) {
|
||||
if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) {
|
||||
|
|
@ -300,9 +364,9 @@ export class ExecuteSalaryService {
|
|||
const historyAssis = new ProfileAssistanceHistory();
|
||||
Object.assign(historyAssis, { ...dataAssis, id: undefined });
|
||||
|
||||
await this.assistanceRepository.save(dataAssis);
|
||||
await assistanceRepository.save(dataAssis);
|
||||
historyAssis.profileAssistanceId = dataAssis.id;
|
||||
await this.assistanceHistoryRepository.save(historyAssis);
|
||||
await assistanceHistoryRepository.save(historyAssis);
|
||||
}
|
||||
// Task #2190
|
||||
else if (_command.commandType.code == "C-PM-13") {
|
||||
|
|
@ -319,9 +383,9 @@ export class ExecuteSalaryService {
|
|||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
console.log("[ExecuteSalaryService] executeSalary completed successfully");
|
||||
console.log(
|
||||
`[ExecuteSalaryService] Completed processOne — profileId: ${item.profileId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -217,12 +217,12 @@ export async function CreatePosMasterHistoryEmployee(
|
|||
posMasterId: string,
|
||||
request: RequestWithUser | null,
|
||||
type?: string | null,
|
||||
manager?: EntityManager,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await AppDataSource.transaction(async (manager) => {
|
||||
const repoPosmaster = manager.getRepository(EmployeePosMaster);
|
||||
const repoHistory = manager.getRepository(PosMasterEmployeeHistory);
|
||||
const repoProfileEmployee = manager.getRepository(ProfileEmployee);
|
||||
const execute = async (transactionManager: EntityManager) => {
|
||||
const repoPosmaster = transactionManager.getRepository(EmployeePosMaster);
|
||||
const repoHistory = transactionManager.getRepository(PosMasterEmployeeHistory);
|
||||
const repoProfileEmployee = transactionManager.getRepository(ProfileEmployee);
|
||||
|
||||
const pm = await repoPosmaster.findOne({
|
||||
where: { id: posMasterId },
|
||||
|
|
@ -239,8 +239,8 @@ export async function CreatePosMasterHistoryEmployee(
|
|||
"current_holder",
|
||||
],
|
||||
});
|
||||
if (!pm) return false;
|
||||
if (!pm.ancestorDNA) return false;
|
||||
if (!pm) return;
|
||||
if (!pm.ancestorDNA) return;
|
||||
const _null: any = null;
|
||||
const h = new PosMasterEmployeeHistory();
|
||||
const selectedPosition =
|
||||
|
|
@ -299,10 +299,23 @@ export async function CreatePosMasterHistoryEmployee(
|
|||
h.createdAt = new Date();
|
||||
h.lastUpdatedAt = new Date();
|
||||
await repoHistory.save(h);
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
if (manager) {
|
||||
await execute(manager);
|
||||
return true;
|
||||
}
|
||||
|
||||
await AppDataSource.transaction(async (transactionManager) => {
|
||||
await execute(transactionManager);
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
if (manager) {
|
||||
console.error("CreatePosMasterHistoryEmployee error (external transaction):", err);
|
||||
throw err;
|
||||
}
|
||||
console.error("CreatePosMasterHistoryEmployee transaction error:", err);
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService";
|
|||
import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService";
|
||||
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;
|
||||
|
|
@ -172,6 +176,23 @@ function createConsumer( //----> consumer
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* สร้าง pseudo Express request สำหรับ service ที่ถูกเรียกจาก RabbitMQ (ไม่ใช่ HTTP)
|
||||
* ต้องมี `.app.locals.logData.sequence` เพราะ addLogSequence (ใน CallAPI) อ่านค่านี้
|
||||
* และ `.headers.authorization` + `.user` สำหรับ audit/auth
|
||||
*/
|
||||
function buildPseudoReq(token: string, user: any) {
|
||||
return {
|
||||
headers: { authorization: token },
|
||||
user,
|
||||
app: {
|
||||
locals: {
|
||||
logData: { sequence: [] as any[] },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
||||
//----> condition before process consumer
|
||||
// const repo = AppDataSource.getRepository(Command);
|
||||
|
|
@ -335,6 +356,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
// - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ)
|
||||
// - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ)
|
||||
// - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง leave)
|
||||
// - ExecuteSalaryLeaveDisciplineService : C-PM-19, 20, 25, 26, 27, 28, 29, 30, 31, 32 (คำสั่งวินัย)
|
||||
// - ExecuteOrgCommandService : C-PM-21, 38, 40 (org-self — path ชี้กลับ org เอง
|
||||
// เรียก Service ตรงๆ ไม่ผ่าน HTTP loopback เพราะ PostData(path+"/excecute") = ยิงเข้าตัว)
|
||||
// - คำสั่งอื่น ยังใช้ Circular Flow เดิม
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const code = command.commandType?.code;
|
||||
|
|
@ -344,17 +368,148 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
const isSalary = ["C-PM-13", "C-PM-15", "C-PM-16"].includes(code);
|
||||
const isSalaryLeave = ["C-PM-08", "C-PM-09", "C-PM-17", "C-PM-18", "C-PM-41", "C-PM-48"].includes(code);
|
||||
const isSalaryEmployeeLeave = ["C-PM-23", "C-PM-42", "C-PM-43"].includes(code);
|
||||
const isSalaryLeaveDiscipline = ["C-PM-19", "C-PM-20", "C-PM-25", "C-PM-26", "C-PM-27", "C-PM-28",
|
||||
"C-PM-29", "C-PM-30", "C-PM-31", "C-PM-32",
|
||||
].includes(code);
|
||||
// C-PM-21/38/40: path ชี้กลับ org เอง (ไม่ใช่ .NET) → ต้องเรียก Service ตรงๆ ไม่ผ่าน loopback
|
||||
const isCommand21 = code === "C-PM-21";
|
||||
const isCommand38 = code === "C-PM-38";
|
||||
const isCommand40 = code === "C-PM-40";
|
||||
const isOrgSelfLinear = isCommand21 || isCommand38 || isCommand40;
|
||||
// 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 ||
|
||||
isSalaryEmployeeCurrent ||
|
||||
isSalary ||
|
||||
isSalaryLeave ||
|
||||
isSalaryEmployeeLeave;
|
||||
isSalaryEmployeeLeave ||
|
||||
isSalaryLeaveDiscipline;
|
||||
|
||||
if (isLinearFlow) {
|
||||
console.log(`[AMQ] Linear Flow (${code})`);
|
||||
// Org-self (C-PM-21/38/40): เรียก Service ตรงๆ (Linear Flow / ทำต่อ) ไม่ผ่าน HTTP loopback
|
||||
// เพราะ path ของ command เหล่านี้ชี้กลับ org เอง → PostData(path + "/excecute") = ยิงเข้าตัว
|
||||
if (isOrgSelfLinear) {
|
||||
console.log(`[AMQ] Linear Flow org-self (${code}) — เรียก Service ตรงๆ (no loopback)`);
|
||||
const pseudoReq = buildPseudoReq(token, user);
|
||||
const ctx = {
|
||||
user: { sub: user?.sub ?? "system", name: user?.name ?? "System" },
|
||||
req: pseudoReq,
|
||||
};
|
||||
const flatRefIds = chunks.flat();
|
||||
if (isCommand21) {
|
||||
await new ExecuteOrgCommandService().executeCommand21Employee(flatRefIds, ctx);
|
||||
} else if (isCommand38) {
|
||||
await new ExecuteOrgCommandService().executeCommand38Officer(flatRefIds, ctx);
|
||||
} else if (isCommand40) {
|
||||
await new ExecuteOrgCommandService().executeCommand40Officer(flatRefIds, ctx);
|
||||
}
|
||||
console.log(`[AMQ] Processed ${flatRefIds.length} items via ExecuteOrgCommandService (${code})`);
|
||||
} else if (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 = buildPseudoReq(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 = buildPseudoReq(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";
|
||||
let resultData: any[] = [];
|
||||
let resultData1: any[] = []; //เฉพาะ C-PM-32 (ฝั่ง "การพิจารณาลงโทษ")
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const res = await new CallAPI().PostData(
|
||||
|
|
@ -363,9 +518,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
{ refIds: chunk },
|
||||
false,
|
||||
);
|
||||
// response (resultData) จาก .NET
|
||||
if (Array.isArray(res)) {
|
||||
console.log(`[AMQ] Push result data`);
|
||||
if (isCpm32 && res && !Array.isArray(res)) {
|
||||
// C-PM-32: response เป็น object { data, data1 } → แยก 2 track
|
||||
console.log(
|
||||
`[AMQ] C-PM-32 split response — data: ${res.data?.length ?? 0}, data1: ${res.data1?.length ?? 0}`,
|
||||
);
|
||||
if (Array.isArray(res.data)) resultData.push(...res.data);
|
||||
if (Array.isArray(res.data1)) resultData1.push(...res.data1);
|
||||
} else if (Array.isArray(res)) {
|
||||
console.log(`[AMQ] Push result data (${res.length})`);
|
||||
resultData.push(...res);
|
||||
}
|
||||
}
|
||||
|
|
@ -373,12 +534,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`);
|
||||
|
||||
// Route ไป service ที่ถูกต้องตาม commandType
|
||||
if (resultData.length > 0) {
|
||||
if (resultData.length > 0 || resultData1.length > 0) {
|
||||
// สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req})
|
||||
const pseudoReq = {
|
||||
headers: { authorization: token },
|
||||
user,
|
||||
};
|
||||
const pseudoReq = buildPseudoReq(token, user);
|
||||
const ctx = {
|
||||
user: { sub: user?.sub ?? "system", name: user?.name ?? "System" },
|
||||
req: pseudoReq,
|
||||
|
|
@ -402,6 +560,21 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
|
|||
} else if (isSalaryEmployeeLeave) {
|
||||
await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx);
|
||||
console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`);
|
||||
} else if (isSalaryLeaveDiscipline) {
|
||||
// C-PM-32 (คำสั่งยุติเรื่อง): response เป็น object { data, data1 }
|
||||
// profileId เดียวกันอาจอยู่ในทั้ง 2 track → ต้องส่งให้ org แยก 2 ครั้ง ห้าม merge
|
||||
if (resultData.length > 0) {
|
||||
await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData, ctx);
|
||||
console.log(
|
||||
`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryLeaveDisciplineService`,
|
||||
);
|
||||
}
|
||||
if (isCpm32 && resultData1.length > 0) {
|
||||
await new ExecuteSalaryLeaveDisciplineService().executeSalaryLeaveDiscipline(resultData1, ctx);
|
||||
console.log(
|
||||
`[AMQ] Processed resultData1: ${resultData1.length} profiles via ExecuteSalaryLeaveDisciplineService`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue