Compare commits

...

9 commits

Author SHA1 Message Date
harid
759e69d44d fix
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m7s
2026-07-03 17:30:44 +07:00
harid
cf161f9809 fix #240
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-07-03 10:34:20 +07:00
harid
30ddb4e3f3 fix #240
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-07-02 18:11:59 +07:00
harid
005f3f1334 fix เพิ่มรายการออกคำสั่ง ตัวย่อหน่วยงานไม่แสดง #2582
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m2s
2026-07-02 14:46:57 +07:00
harid
8807ee3226 revert add password redis
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m5s
2026-06-30 11:04:19 +07:00
harid
2f7ec40e78 add pass redis test
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m6s
2026-06-29 16:01:16 +07:00
harid
7b37cc37db Linear Flow Probation+Salary #224 2026-06-29 14:28:50 +07:00
harid
3d2fc5128a Linear Flow discipline + organization #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m11s
2026-06-26 18:09:45 +07:00
harid
832c5d2cb3 add transaction #224
All checks were successful
Build & Deploy on Dev / build (push) Successful in 1m10s
2026-06-24 18:05:54 +07:00
19 changed files with 5051 additions and 3863 deletions

File diff suppressed because it is too large Load diff

View file

@ -7227,7 +7227,9 @@ export class OrganizationDotnetController extends Controller {
dateStart: profileEmp?.dateStart ?? null, dateStart: profileEmp?.dateStart ?? null,
dateAppoint: profileEmp?.dateAppoint ?? null, dateAppoint: profileEmp?.dateAppoint ?? null,
keycloak: profileEmp?.keycloak ?? 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, position: item.position,
positionLevel: item.posLevel, positionLevel: item.posLevel,
positionType: item.posType, positionType: item.posType,
@ -8900,7 +8902,7 @@ export class OrganizationDotnetController extends Controller {
const profiles = await this.profileRepo.find({ const profiles = await this.profileRepo.find({
where: { id: In(profileIds) }, 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])); const profileMap = new Map(profiles.map((p) => [p.id, p]));
@ -8918,7 +8920,11 @@ export class OrganizationDotnetController extends Controller {
dateStart: profile?.dateStart ?? null, dateStart: profile?.dateStart ?? null,
dateAppoint: profile?.dateAppoint ?? null, dateAppoint: profile?.dateAppoint ?? null,
keycloak: profile?.keycloak ?? 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, position: item.position,
positionLevel: item.posLevel, positionLevel: item.posLevel,
positionType: item.posType, positionType: item.posType,

View file

@ -621,13 +621,13 @@ export class PosMasterActController extends Controller {
let conditionGroup = ""; let conditionGroup = "";
if (body.type.trim().toUpperCase() == "GROUP1.1") { if (body.type.trim().toUpperCase() == "GROUP1.1") {
conditionGroup = 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") { } else if (body.type.trim().toUpperCase() == "GROUP1.2") {
conditionGroup = 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") { } else if (body.type.trim().toUpperCase() == "GROUP2") {
conditionGroup = 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 { } else {
throw new HttpError(HttpStatusCode.NOT_FOUND, "กลุ่มเป้าหมายไม่ถูกต้อง"); throw new HttpError(HttpStatusCode.NOT_FOUND, "กลุ่มเป้าหมายไม่ถูกต้อง");
} }

View file

@ -23,6 +23,7 @@ import { ProfileEmployee } from "../entities/ProfileEmployee";
import { In, IsNull, LessThan, MoreThan, Not } from "typeorm"; import { In, IsNull, LessThan, MoreThan, Not } from "typeorm";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService";
import { normalizeDurationSumSimple } from "../utils/tenure"; import { normalizeDurationSumSimple } from "../utils/tenure";
import { import {
TenurePositionOfficer, TenurePositionOfficer,
@ -1380,91 +1381,10 @@ export class ProfileSalaryController extends Controller {
@Post("update") @Post("update")
public async updateSalary(@Request() req: RequestWithUser, @Body() body: CreateProfileSalary) { public async updateSalary(@Request() req: RequestWithUser, @Body() body: CreateProfileSalary) {
if (!body.profileId) { await new ExecuteSalaryReportService().executeOfficerSalaryUpdate([body], {
throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileId"); user: { sub: req.user.sub, name: req.user.name },
} req,
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" },
}); });
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(); return new HttpSuccess();
} }

View file

@ -27,6 +27,7 @@ import { Profile } from "../entities/Profile";
import { In, LessThan, IsNull, MoreThan } from "typeorm"; import { In, LessThan, IsNull, MoreThan } from "typeorm";
import permission from "../interfaces/permission"; import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils"; import { setLogDataDiff } from "../interfaces/utils";
import { ExecuteSalaryReportService } from "../services/ExecuteSalaryReportService";
import { normalizeDurationSumSimple } from "../utils/tenure"; import { normalizeDurationSumSimple } from "../utils/tenure";
import { Command } from "../entities/Command"; import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
@ -507,94 +508,10 @@ export class ProfileSalaryEmployeeController extends Controller {
@Request() req: RequestWithUser, @Request() req: RequestWithUser,
@Body() body: CreateProfileSalaryEmployee, @Body() body: CreateProfileSalaryEmployee,
) { ) {
if (!body.profileEmployeeId) { await new ExecuteSalaryReportService().executeEmployeeSalaryUpdate([body], {
throw new HttpError(HttpStatus.BAD_REQUEST, "กรุณากรอก profileEmployeeId"); user: { sub: req.user.sub, name: req.user.name },
} req,
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" },
}); });
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(); return new HttpSuccess();
} }

View file

@ -4,7 +4,7 @@ import { PosMaster } from "../entities/PosMaster";
import { Position } from "../entities/Position"; import { Position } from "../entities/Position";
import { EmployeePosMaster } from "../entities/EmployeePosMaster"; import { EmployeePosMaster } from "../entities/EmployeePosMaster";
import { EmployeePosition } from "../entities/EmployeePosition"; 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 { RequestWithUser } from "../middlewares/user";
import { Command } from "../entities/Command"; import { Command } from "../entities/Command";
import { ProfileSalary } from "../entities/ProfileSalary"; import { ProfileSalary } from "../entities/ProfileSalary";
@ -254,14 +254,23 @@ export function calculateRetireYear(birthDate: Date) {
return yy + 61; return yy + 61;
} }
export async function removeProfileInOrganize(profileId: string, type: string) { export async function removeProfileInOrganize(
const currentRevision = await AppDataSource.getRepository(OrgRevision) 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") .createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false") .where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true") .andWhere("orgRevision.orgRevisionIsCurrent = true")
.getOne(); .getOne();
const draftRevision = await AppDataSource.getRepository(OrgRevision) const draftRevision = await ds
.getRepository(OrgRevision)
.createQueryBuilder("orgRevision") .createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = true") .where("orgRevision.orgRevisionIsDraft = true")
.andWhere("orgRevision.orgRevisionIsCurrent = false") .andWhere("orgRevision.orgRevisionIsCurrent = false")
@ -271,26 +280,30 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
return; return;
} }
if (type === "OFFICER") { if (type === "OFFICER") {
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) const findProfileInposMaster = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster") .createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
.andWhere("posMaster.current_holderId = :profileId", { profileId }) .andWhere("posMaster.current_holderId = :profileId", { profileId })
.getOne(); .getOne();
await AppDataSource.getRepository(PosMaster) await ds
.getRepository(PosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(PosMaster) .update(PosMaster)
.set({ current_holderId: null, isSit: false }) .set({ current_holderId: null, isSit: false })
.where("id = :id", { id: findProfileInposMaster?.id }) .where("id = :id", { id: findProfileInposMaster?.id })
.execute(); .execute();
const findProfileInposMasterDraft = await AppDataSource.getRepository(PosMaster) const findProfileInposMasterDraft = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster") .createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: draftRevision?.id }) .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: draftRevision?.id })
.andWhere("posMaster.next_holderId = :profileId", { profileId }) .andWhere("posMaster.next_holderId = :profileId", { profileId })
.getOne(); .getOne();
await AppDataSource.getRepository(PosMaster) await ds
.getRepository(PosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(PosMaster) .update(PosMaster)
.set({ next_holderId: null, isSit: false }) .set({ next_holderId: null, isSit: false })
@ -300,7 +313,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findProfileInposMaster && !findProfileInposMasterDraft) { if (!findProfileInposMaster && !findProfileInposMasterDraft) {
return; return;
} }
const findPosition = await AppDataSource.getRepository(Position) const findPosition = await ds
.getRepository(Position)
.createQueryBuilder("position") .createQueryBuilder("position")
.where("position.posMasterId = :posMasterId", { posMasterId: findProfileInposMaster?.id }) .where("position.posMasterId = :posMasterId", { posMasterId: findProfileInposMaster?.id })
.getMany(); .getMany();
@ -308,7 +322,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findPosition) { if (!findPosition) {
return; return;
} }
await AppDataSource.getRepository(Position) await ds
.getRepository(Position)
.createQueryBuilder() .createQueryBuilder()
.update(Position) .update(Position)
.set({ positionIsSelected: false }) .set({ positionIsSelected: false })
@ -316,14 +331,16 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
.execute(); .execute();
} }
if (type === "EMPLOYEE") { if (type === "EMPLOYEE") {
const findProfileInEmpPosMaster = await AppDataSource.getRepository(EmployeePosMaster) const findProfileInEmpPosMaster = await ds
.getRepository(EmployeePosMaster)
.createQueryBuilder("employeePosMaster") .createQueryBuilder("employeePosMaster")
.where("employeePosMaster.orgRevisionId = :orgRevisionId", { .where("employeePosMaster.orgRevisionId = :orgRevisionId", {
orgRevisionId: currentRevision?.id, orgRevisionId: currentRevision?.id,
}) })
.andWhere("employeePosMaster.current_holderId = :profileId", { profileId }) .andWhere("employeePosMaster.current_holderId = :profileId", { profileId })
.getOne(); .getOne();
await AppDataSource.getRepository(EmployeePosMaster) await ds
.getRepository(EmployeePosMaster)
.createQueryBuilder() .createQueryBuilder()
.update(EmployeePosMaster) .update(EmployeePosMaster)
.set({ current_holderId: null, isSit: false }) .set({ current_holderId: null, isSit: false })
@ -333,7 +350,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
if (!findProfileInEmpPosMaster) { if (!findProfileInEmpPosMaster) {
return; return;
} }
const findEmpPosition = await AppDataSource.getRepository(EmployeePosition) const findEmpPosition = await ds
.getRepository(EmployeePosition)
.createQueryBuilder("employeePosition") .createQueryBuilder("employeePosition")
.where("employeePosition.posMasterId = :posMasterId", { .where("employeePosition.posMasterId = :posMasterId", {
posMasterId: findProfileInEmpPosMaster?.id, posMasterId: findProfileInEmpPosMaster?.id,
@ -344,7 +362,8 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
return; return;
} }
await AppDataSource.getRepository(EmployeePosition) await ds
.getRepository(EmployeePosition)
.createQueryBuilder() .createQueryBuilder()
.update(EmployeePosition) .update(EmployeePosition)
.set({ positionIsSelected: false }) .set({ positionIsSelected: false })
@ -353,8 +372,10 @@ export async function removeProfileInOrganize(profileId: string, type: string) {
} }
} }
export async function removePostMasterAct(profileId: string) { export async function removePostMasterAct(profileId: string, manager?: EntityManager) {
const currentRevision = await AppDataSource.getRepository(OrgRevision) const ds = manager ?? AppDataSource;
const currentRevision = await ds
.getRepository(OrgRevision)
.createQueryBuilder("orgRevision") .createQueryBuilder("orgRevision")
.where("orgRevision.orgRevisionIsDraft = false") .where("orgRevision.orgRevisionIsDraft = false")
.andWhere("orgRevision.orgRevisionIsCurrent = true") .andWhere("orgRevision.orgRevisionIsCurrent = true")
@ -364,7 +385,8 @@ export async function removePostMasterAct(profileId: string) {
return; return;
} }
const findProfileInposMaster = await AppDataSource.getRepository(PosMaster) const findProfileInposMaster = await ds
.getRepository(PosMaster)
.createQueryBuilder("posMaster") .createQueryBuilder("posMaster")
.where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id }) .where("posMaster.orgRevisionId = :orgRevisionId", { orgRevisionId: currentRevision?.id })
.andWhere("posMaster.current_holderId = :profileId", { profileId }) .andWhere("posMaster.current_holderId = :profileId", { profileId })
@ -374,11 +396,12 @@ export async function removePostMasterAct(profileId: string) {
return; return;
} }
const posMasterAct = await AppDataSource.getRepository(PosMasterAct) const posMasterAct = await ds
.getRepository(PosMasterAct)
.createQueryBuilder("posMasterAct") .createQueryBuilder("posMasterAct")
.where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id }) .where("posMasterAct.posMasterChildId = :posMasterChildId", { posMasterChildId: findProfileInposMaster.id })
.getMany(); .getMany();
await AppDataSource.getRepository(PosMasterAct).remove(posMasterAct); await ds.getRepository(PosMasterAct).remove(posMasterAct);
} }
export async function checkReturnCommandType(commandId: string) { export async function checkReturnCommandType(commandId: string) {

View file

@ -1,8 +1,12 @@
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import { CommandRecive } from "../entities/CommandRecive"; import { CommandRecive } from "../entities/CommandRecive";
import { Command } from "../entities/Command"; import { Command } from "../entities/Command";
import { CommandOperator } from "../entities/CommandOperator";
import { OrgRoot } from "../entities/OrgRoot"; import { OrgRoot } from "../entities/OrgRoot";
import { Position } from "../entities/Position";
import { Profile } from "../entities/Profile"; import { Profile } from "../entities/Profile";
import { RequestWithUser } from "../middlewares/user";
import { EntityManager } from "typeorm";
export interface PosNumCodeSitResult { export interface PosNumCodeSitResult {
posNumCodeSit: string; posNumCodeSit: string;
@ -17,21 +21,23 @@ export interface PosNumCodeSitResult {
* *
* @param reciveId commandRecive.Id * @param reciveId commandRecive.Id
* @param code * @param code
* @param manager operation transaction caller (all-or-nothing)
* @returns Promise<void> * @returns Promise<void>
*/ */
export async function reOrderCommandRecivesAndDelete( export async function reOrderCommandRecivesAndDelete(
reciveId: string reciveId: string,
manager?: EntityManager,
): Promise<void> { ): Promise<void> {
const commandReciveRepo = AppDataSource.getRepository(CommandRecive); const ds = manager ?? AppDataSource;
const commandRepo = AppDataSource.getRepository(Command); const commandReciveRepo = ds.getRepository(CommandRecive);
const commandRepo = ds.getRepository(Command);
// ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId // ค้นหาข้อมูลผู้ได้รับคำสั่งตาม reciveId
const commandRecive = await commandReciveRepo.findOne({ const commandRecive = await commandReciveRepo.findOne({
where: { id: reciveId } where: { id: reciveId },
}); });
if (commandRecive == null) if (commandRecive == null) return;
return;
const commandId = commandRecive.commandId; const commandId = commandRecive.commandId;
// ลบตาม refId // ลบตาม refId
@ -43,17 +49,14 @@ export async function reOrderCommandRecivesAndDelete(
}); });
// ลำดับผู้ได้รับคำสั่งใหม่ // ลำดับผู้ได้รับคำสั่งใหม่
if (commandReciveList.length > 0) { if (commandReciveList.length > 0) {
await Promise.all( for (let i = 0; i < commandReciveList.length; i++) {
commandReciveList.map(async (p, i) => { commandReciveList[i].order = i + 1;
p.order = i + 1; await commandReciveRepo.save(commandReciveList[i]);
await commandReciveRepo.save(p); }
})
);
} else { } else {
// ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง // ถ้าไม่มีผู้ได้รับคำสั่งเหลือเลย ให้ยกเลิกคำสั่ง
await commandRepo.update({ id: commandId }, { status: "CANCEL" }); await commandRepo.update({ id: commandId }, { status: "CANCEL" });
} }
} }
/** /**
@ -144,3 +147,101 @@ export async function getPosNumCodeSit(
commandExcecuteDate, 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);
}

View file

@ -1,4 +1,4 @@
import { In, Like } from "typeorm"; import { EntityManager, In, Like } from "typeorm";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
@ -129,14 +129,23 @@ export interface ExecutionContext {
/** /**
* Service / (Profile) * Service / (Profile)
* *
* "Circular Dependency" API Org API * commandType: C-PM-01, 02, 14
* resultData Org profile
* (Linear Flow)
* *
* - endpoint /org/command/excexute/create-officer-profile service (thin wrapper) * - 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 * 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 { export class ExecuteOfficerProfileService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
@ -177,8 +186,12 @@ export class ExecuteOfficerProfileService {
data: OfficerProfileItem[], data: OfficerProfileItem[],
ctx: ExecutionContext, ctx: ExecutionContext,
): Promise<void> { ): Promise<void> {
console.log("[ExecuteOfficerProfileService] Starting executeCreateOfficerProfile"); const commandId =
console.log("[ExecuteOfficerProfileService] Request body count:", data?.length); 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 // Normalize date fields
@ -326,20 +339,101 @@ export class ExecuteOfficerProfileService {
data.length, data.length,
"profile(s)", "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( console.log(
"[ExecuteOfficerProfileService] Processing item", "[ExecuteOfficerProfileService] Processing citizenId:",
index + 1, item.bodyProfile.citizenId,
"of",
data.length,
); );
const _null: any = null; const _null: any = null;
if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null; if (item.bodyProfile.posLevelId === "") item.bodyProfile.posLevelId = null;
if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null; if (item.bodyProfile.posTypeId === "") item.bodyProfile.posTypeId = null;
if ( if (
item.bodyProfile.posLevelId && item.bodyProfile.posLevelId &&
!(await this.posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId })) !(await posLevelRepo.findOneBy({ id: item.bodyProfile.posLevelId }))
) { ) {
console.error( console.error(
"[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:", "[ExecuteOfficerProfileService] ไม่พบข้อมูลระดับตำแหน่งนี้ posLevelId:",
@ -349,7 +443,7 @@ export class ExecuteOfficerProfileService {
} }
if ( if (
item.bodyProfile.posTypeId && item.bodyProfile.posTypeId &&
!(await this.posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId })) !(await posTypeRepo.findOneBy({ id: item.bodyProfile.posTypeId }))
) { ) {
console.error( console.error(
"[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:", "[ExecuteOfficerProfileService] ไม่พบข้อมูลประเภทตำแหน่งนี้ posTypeId:",
@ -362,22 +456,22 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Processing citizenId:", "[ExecuteOfficerProfileService] Processing citizenId:",
item.bodyProfile.citizenId, item.bodyProfile.citizenId,
); );
let registrationProvinceId = await this.provinceRepo.findOneBy({ let registrationProvinceId = await provinceRepo.findOneBy({
id: item.bodyProfile.registrationProvinceId ?? "", id: item.bodyProfile.registrationProvinceId ?? "",
}); });
let registrationDistrictId = await this.districtRepo.findOneBy({ let registrationDistrictId = await districtRepo.findOneBy({
id: item.bodyProfile.registrationDistrictId ?? "", id: item.bodyProfile.registrationDistrictId ?? "",
}); });
let registrationSubDistrictId = await this.subDistrictRepo.findOneBy({ let registrationSubDistrictId = await subDistrictRepo.findOneBy({
id: item.bodyProfile.registrationSubDistrictId ?? "", id: item.bodyProfile.registrationSubDistrictId ?? "",
}); });
let currentProvinceId = await this.provinceRepo.findOneBy({ let currentProvinceId = await provinceRepo.findOneBy({
id: item.bodyProfile.currentProvinceId ?? "", id: item.bodyProfile.currentProvinceId ?? "",
}); });
let currentDistrictId = await this.districtRepo.findOneBy({ let currentDistrictId = await districtRepo.findOneBy({
id: item.bodyProfile.currentDistrictId ?? "", id: item.bodyProfile.currentDistrictId ?? "",
}); });
let currentSubDistrictId = await this.subDistrictRepo.findOneBy({ let currentSubDistrictId = await subDistrictRepo.findOneBy({
id: item.bodyProfile.currentSubDistrictId ?? "", id: item.bodyProfile.currentSubDistrictId ?? "",
}); });
console.log("[ExecuteOfficerProfileService] Address validation completed"); 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 */ }, where: { citizenId: item.bodyProfile.citizenId /*, isActive: true */ },
relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"], relations: ["roleKeycloaks", "profileInsignias", "profileAvatars"],
}); });
@ -520,7 +614,7 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] No existing profile found, creating new profile", "[ExecuteOfficerProfileService] No existing profile found, creating new profile",
); );
//กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม //กรณีลูกจ้างประจำมาสอบเป็นข้าราชการ ต้อง update สถานะโปรไฟล์เดิม
let profileEmployee: any = await this.profileEmployeeRepository.findOne({ let profileEmployee: any = await profileEmployeeRepository.findOne({
where: { citizenId: item.bodyProfile.citizenId }, where: { citizenId: item.bodyProfile.citizenId },
relations: ["profileInsignias", "roleKeycloaks"], relations: ["profileInsignias", "roleKeycloaks"],
}); });
@ -532,7 +626,7 @@ export class ExecuteOfficerProfileService {
console.log( console.log(
"[ExecuteOfficerProfileService] Converting employee profile to officer profile", "[ExecuteOfficerProfileService] Converting employee profile to officer profile",
); );
const _order = await this.salaryRepo.findOne({ const _order = await salaryRepo.findOne({
where: { profileEmployeeId: profileEmployee.id }, where: { profileEmployeeId: profileEmployee.id },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -550,15 +644,15 @@ export class ExecuteOfficerProfileService {
Object.assign(history, { ...profileEmpSalary, id: undefined }); Object.assign(history, { ...profileEmpSalary, id: undefined });
profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt; profileEmpSalary.dateGovernment = item.bodySalarys?.commandDateAffect ?? meta.createdAt;
(profileEmpSalary.profileId = _null), (profileEmpSalary.profileId = _null),
await this.salaryRepo.save(profileEmpSalary, { data: req }); await salaryRepo.save(profileEmpSalary, { data: req });
setLogDataDiff(req, { before, after: profileEmpSalary }); setLogDataDiff(req, { before, after: profileEmpSalary });
history.profileSalaryId = profileEmpSalary.id; history.profileSalaryId = profileEmpSalary.id;
await this.salaryHistoryRepo.save(history, { data: req }); await salaryHistoryRepo.save(history, { data: req });
if (profileEmployee.profileInsignias.length > 0) { if (profileEmployee.profileInsignias.length > 0) {
_oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? []; _oldInsigniaIds = profileEmployee.profileInsignias?.map((x: any) => x.id) ?? [];
} }
await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE"); await removeProfileInOrganize(profileEmployee.id, "EMPLOYEE", manager);
if (profileEmployee.keycloak != null) { if (profileEmployee.keycloak != null) {
// const delUserKeycloak = await deleteUser(profileEmployee.keycloak); // const delUserKeycloak = await deleteUser(profileEmployee.keycloak);
// if (delUserKeycloak) { // if (delUserKeycloak) {
@ -573,7 +667,7 @@ export class ExecuteOfficerProfileService {
profileEmployee.lastUpdateUserId = ctx.user.sub; profileEmployee.lastUpdateUserId = ctx.user.sub;
profileEmployee.lastUpdateFullName = ctx.user.name; profileEmployee.lastUpdateFullName = ctx.user.name;
profileEmployee.lastUpdatedAt = new Date(); profileEmployee.lastUpdatedAt = new Date();
await this.profileEmployeeRepository.save(profileEmployee); await profileEmployeeRepository.save(profileEmployee);
setLogDataDiff(req, { before, after: profileEmployee }); setLogDataDiff(req, { before, after: profileEmployee });
} }
profile = Object.assign({ ...item.bodyProfile, ...meta }); profile = Object.assign({ ...item.bodyProfile, ...meta });
@ -619,7 +713,7 @@ export class ExecuteOfficerProfileService {
profile.phone = item.bodyProfile.phone ?? null; profile.phone = item.bodyProfile.phone ?? null;
console.log("[ExecuteOfficerProfileService] Saving new profile"); console.log("[ExecuteOfficerProfileService] Saving new profile");
await this.profileRepository.save(profile); await profileRepository.save(profile);
console.log( console.log(
"[ExecuteOfficerProfileService] New profile saved, profileId:", "[ExecuteOfficerProfileService] New profile saved, profileId:",
profile.id, profile.id,
@ -649,7 +743,7 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record", "[ExecuteOfficerProfileService] Profile is leaving with eligible leave type, creating new profile record",
); );
//ดึง profileSalary เดิม //ดึง profileSalary เดิม
_oldSalaries = await this.salaryRepo.find({ _oldSalaries = await salaryRepo.find({
where: { profileId: profile.id }, where: { profileId: profile.id },
order: { order: "ASC" }, order: { order: "ASC" },
}); });
@ -696,7 +790,7 @@ export class ExecuteOfficerProfileService {
profile.nationality = item.bodyProfile.nationality ?? null; profile.nationality = item.bodyProfile.nationality ?? null;
profile.bloodGroup = item.bodyProfile.bloodGroup ?? null; profile.bloodGroup = item.bodyProfile.bloodGroup ?? null;
profile.phone = item.bodyProfile.phone ?? null; profile.phone = item.bodyProfile.phone ?? null;
await this.profileRepository.save(profile); await profileRepository.save(profile);
console.log( console.log(
"[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:", "[ExecuteOfficerProfileService] New profile record saved for leaving officer, profileId:",
profile.id, profile.id,
@ -791,7 +885,7 @@ export class ExecuteOfficerProfileService {
item.bodyProfile.phone && item.bodyProfile.phone != "" item.bodyProfile.phone && item.bodyProfile.phone != ""
? item.bodyProfile.phone ? item.bodyProfile.phone
: profile.phone; : profile.phone;
await this.profileRepository.save(profile); await profileRepository.save(profile);
console.log( console.log(
"[ExecuteOfficerProfileService] Existing active profile updated, profileId:", "[ExecuteOfficerProfileService] Existing active profile updated, profileId:",
profile.id, profile.id,
@ -811,25 +905,23 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Processing educations, count:", "[ExecuteOfficerProfileService] Processing educations, count:",
item.bodyEducations.length, item.bodyEducations.length,
); );
await Promise.all( for (const education of item.bodyEducations) {
item.bodyEducations.map(async (education) => {
const profileEdu = new ProfileEducation(); const profileEdu = new ProfileEducation();
Object.assign(profileEdu, { ...education, ...meta }); Object.assign(profileEdu, { ...education, ...meta });
const eduHistory = new ProfileEducationHistory(); const eduHistory = new ProfileEducationHistory();
Object.assign(eduHistory, { ...profileEdu, id: undefined }); Object.assign(eduHistory, { ...profileEdu, id: undefined });
profileEdu.profileId = profile.id; profileEdu.profileId = profile.id;
const educationLevel = await this.profileEducationRepo.findOne({ const educationLevel = await profileEducationRepo.findOne({
select: ["id", "level", "profileId"], select: ["id", "level", "profileId"],
where: { profileId: profile.id, isDeleted: false }, where: { profileId: profile.id, isDeleted: false },
order: { level: "DESC" }, order: { level: "DESC" },
}); });
profileEdu.level = educationLevel == null ? 1 : educationLevel.level + 1; 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 }); setLogDataDiff(req, { before, after: profileEdu });
eduHistory.profileEducationId = profileEdu.id; eduHistory.profileEducationId = profileEdu.id;
await this.profileEducationHistoryRepo.save(eduHistory, { data: req }); await profileEducationHistoryRepo.save(eduHistory, { data: req });
}), }
);
} }
//Certificates //Certificates
if (item.bodyCertificates && item.bodyCertificates.length > 0) { if (item.bodyCertificates && item.bodyCertificates.length > 0) {
@ -837,19 +929,17 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Processing certificates, count:", "[ExecuteOfficerProfileService] Processing certificates, count:",
item.bodyCertificates.length, item.bodyCertificates.length,
); );
await Promise.all( for (const cer of item.bodyCertificates) {
item.bodyCertificates.map(async (cer) => {
const profileCer = new ProfileCertificate(); const profileCer = new ProfileCertificate();
Object.assign(profileCer, { ...cer, ...meta }); Object.assign(profileCer, { ...cer, ...meta });
const cerHistory = new ProfileCertificateHistory(); const cerHistory = new ProfileCertificateHistory();
Object.assign(cerHistory, { ...profileCer, id: undefined }); Object.assign(cerHistory, { ...profileCer, id: undefined });
profileCer.profileId = profile.id; profileCer.profileId = profile.id;
await this.certificateRepo.save(profileCer, { data: req }); await certificateRepo.save(profileCer, { data: req });
setLogDataDiff(req, { before, after: profileCer }); setLogDataDiff(req, { before, after: profileCer });
cerHistory.profileCertificateId = profileCer.id; cerHistory.profileCertificateId = profileCer.id;
await this.certificateHistoryRepo.save(cerHistory, { data: req }); await certificateHistoryRepo.save(cerHistory, { data: req });
}), }
);
} }
//FamilyCouple //FamilyCouple
if (item.bodyMarry != null) { if (item.bodyMarry != null) {
@ -868,10 +958,10 @@ export class ExecuteOfficerProfileService {
const coupleHistory = new ProfileFamilyCoupleHistory(); const coupleHistory = new ProfileFamilyCoupleHistory();
Object.assign(coupleHistory, { ...profileCouple, id: undefined }); Object.assign(coupleHistory, { ...profileCouple, id: undefined });
profileCouple.profileId = profile.id; profileCouple.profileId = profile.id;
await this.profileFamilyCoupleRepo.save(profileCouple, { data: req }); await profileFamilyCoupleRepo.save(profileCouple, { data: req });
setLogDataDiff(req, { before, after: profileCouple }); setLogDataDiff(req, { before, after: profileCouple });
coupleHistory.profileFamilyCoupleId = profileCouple.id; coupleHistory.profileFamilyCoupleId = profileCouple.id;
await this.profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req }); await profileFamilyCoupleHistoryRepo.save(coupleHistory, { data: req });
} }
//FamilyFather //FamilyFather
if (item.bodyFather != null) { if (item.bodyFather != null) {
@ -889,10 +979,10 @@ export class ExecuteOfficerProfileService {
const fatherHistory = new ProfileFamilyFatherHistory(); const fatherHistory = new ProfileFamilyFatherHistory();
Object.assign(fatherHistory, { ...profileFather, id: undefined }); Object.assign(fatherHistory, { ...profileFather, id: undefined });
profileFather.profileId = profile.id; profileFather.profileId = profile.id;
await this.profileFamilyFatherRepo.save(profileFather, { data: req }); await profileFamilyFatherRepo.save(profileFather, { data: req });
setLogDataDiff(req, { before, after: profileFather }); setLogDataDiff(req, { before, after: profileFather });
fatherHistory.profileFamilyFatherId = profileFather.id; fatherHistory.profileFamilyFatherId = profileFather.id;
await this.profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req }); await profileFamilyFatherHistoryRepo.save(fatherHistory, { data: req });
} }
//FamilyMother //FamilyMother
if (item.bodyMother != null) { if (item.bodyMother != null) {
@ -910,10 +1000,10 @@ export class ExecuteOfficerProfileService {
const motherHistory = new ProfileFamilyMotherHistory(); const motherHistory = new ProfileFamilyMotherHistory();
Object.assign(motherHistory, { ...profileMother, id: undefined }); Object.assign(motherHistory, { ...profileMother, id: undefined });
profileMother.profileId = profile.id; profileMother.profileId = profile.id;
await this.profileFamilyMotherRepo.save(profileMother, { data: req }); await profileFamilyMotherRepo.save(profileMother, { data: req });
setLogDataDiff(req, { before, after: profileMother }); setLogDataDiff(req, { before, after: profileMother });
motherHistory.profileFamilyMotherId = profileMother.id; motherHistory.profileFamilyMotherId = profileMother.id;
await this.profileFamilyMotherHistoryRepo.save(motherHistory, { data: req }); await profileFamilyMotherHistoryRepo.save(motherHistory, { data: req });
} }
//Salary //Salary
//insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ //insert profileSalary อันเก่า กรณีพ้นราชการแล้วกลับมาบรรจุ
@ -922,24 +1012,22 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Restoring old salaries, count:", "[ExecuteOfficerProfileService] Restoring old salaries, count:",
_oldSalaries.length, _oldSalaries.length,
); );
await Promise.all( for (const oldSal of _oldSalaries) {
_oldSalaries.map(async (oldSal) => {
const profileSal: any = new ProfileSalary(); const profileSal: any = new ProfileSalary();
Object.assign(profileSal, { ...oldSal, ...meta }); Object.assign(profileSal, { ...oldSal, ...meta });
const salaryHistory = new ProfileSalaryHistory(); const salaryHistory = new ProfileSalaryHistory();
Object.assign(salaryHistory, { ...profileSal, id: undefined }); Object.assign(salaryHistory, { ...profileSal, id: undefined });
profileSal.profileId = profile.id; profileSal.profileId = profile.id;
await this.salaryRepo.save(profileSal, { data: req }); await salaryRepo.save(profileSal, { data: req });
setLogDataDiff(req, { before, after: profileSal }); setLogDataDiff(req, { before, after: profileSal });
salaryHistory.profileSalaryId = profileSal.id; salaryHistory.profileSalaryId = profileSal.id;
await this.salaryHistoryRepo.save(salaryHistory, { data: req }); await salaryHistoryRepo.save(salaryHistory, { data: req });
}), }
);
} }
//insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว //insert item.bodySalarys ต่อจากที่ insert เดิมไปแล้ว
if (item.bodySalarys && item.bodySalarys != null) { if (item.bodySalarys && item.bodySalarys != null) {
console.log("[ExecuteOfficerProfileService] Processing new salary data"); console.log("[ExecuteOfficerProfileService] Processing new salary data");
const dest_item = await this.salaryRepo.findOne({ const dest_item = await salaryRepo.findOne({
where: { profileId: profile.id }, where: { profileId: profile.id },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -956,10 +1044,10 @@ export class ExecuteOfficerProfileService {
profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null; profileSal.amountSpecial = item.bodySalarys.amountSpecial ?? null;
profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null; profileSal.positionSalaryAmount = item.bodySalarys.positionSalaryAmount ?? null;
profileSal.mouthSalaryAmount = item.bodySalarys.mouthSalaryAmount ?? 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 }); setLogDataDiff(req, { before, after: profileSal });
salaryHistory.profileSalaryId = profileSal.id; salaryHistory.profileSalaryId = profileSal.id;
await this.salaryHistoryRepo.save(salaryHistory, { data: req }); await salaryHistoryRepo.save(salaryHistory, { data: req });
} }
//Position //Position
if (item.bodyPosition && item.bodyPosition != null) { if (item.bodyPosition && item.bodyPosition != null) {
@ -969,7 +1057,7 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:", "[ExecuteOfficerProfileService] STEP 1: Finding posMaster, posmasterId:",
item.bodyPosition.posmasterId, item.bodyPosition.posmasterId,
); );
let posMaster = await this.posMasterRepository.findOne({ let posMaster = await posMasterRepository.findOne({
where: { where: {
id: item.bodyPosition.posmasterId, id: item.bodyPosition.posmasterId,
}, },
@ -995,7 +1083,7 @@ export class ExecuteOfficerProfileService {
console.log( console.log(
"[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA", "[ExecuteOfficerProfileService] Finding current posMaster from ancestorDNA",
); );
posMaster = await this.posMasterRepository.findOne({ posMaster = await posMasterRepository.findOne({
where: { where: {
ancestorDNA: posMaster.ancestorDNA, ancestorDNA: posMaster.ancestorDNA,
orgRevision: { orgRevision: {
@ -1027,7 +1115,7 @@ export class ExecuteOfficerProfileService {
// STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน // STEP 2: เคลียร์ข้อมูลตำแหน่งเก่าที่ครองอยู่ ในโครงสร้างปัจจุบัน
console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data"); console.log("[ExecuteOfficerProfileService] STEP 2: Clearing old position data");
const posMasterOld = await this.posMasterRepository.findOne({ const posMasterOld = await posMasterRepository.findOne({
where: { where: {
current_holderId: profile.id, current_holderId: profile.id,
orgRevisionId: posMaster.orgRevisionId, orgRevisionId: posMaster.orgRevisionId,
@ -1040,7 +1128,7 @@ export class ExecuteOfficerProfileService {
} }
// หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก // หา position เก่าที่เลือกไว้ แล้วเคลียร์การเลือก
const positionOld = await this.positionRepository.findOne({ const positionOld = await positionRepository.findOne({
where: { where: {
posMasterId: posMasterOld?.id, posMasterId: posMasterOld?.id,
positionIsSelected: true, positionIsSelected: true,
@ -1048,14 +1136,14 @@ export class ExecuteOfficerProfileService {
}); });
if (positionOld != null) { if (positionOld != null) {
positionOld.positionIsSelected = false; positionOld.positionIsSelected = false;
await this.positionRepository.save(positionOld); await positionRepository.save(positionOld);
} }
// STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่ // STEP 3: เคลียร์ position ที่เลือกไว้อื่นๆ ใน posMaster ตัวใหม่
console.log( console.log(
"[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster", "[ExecuteOfficerProfileService] STEP 3: Clearing other selected positions in new posMaster",
); );
const checkPosition = await this.positionRepository.find({ const checkPosition = await positionRepository.find({
where: { where: {
posMasterId: posMaster.id, posMasterId: posMaster.id,
positionIsSelected: true, positionIsSelected: true,
@ -1066,7 +1154,7 @@ export class ExecuteOfficerProfileService {
...positions, ...positions,
positionIsSelected: false, positionIsSelected: false,
})); }));
await this.positionRepository.save(clearPosition); await positionRepository.save(clearPosition);
} }
// STEP 4: กำหนดคนครองใหม่ให้กับ posMaster // STEP 4: กำหนดคนครองใหม่ให้กับ posMaster
@ -1078,10 +1166,13 @@ export class ExecuteOfficerProfileService {
// posMaster.conditionReason = _null; // posMaster.conditionReason = _null;
// posMaster.isCondition = false; // posMaster.isCondition = false;
if (posMasterOld != null) { if (posMasterOld != null) {
await this.posMasterRepository.save(posMasterOld); await posMasterRepository.save(posMasterOld);
await CreatePosMasterHistoryOfficer(posMasterOld.id, req); 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"); console.log("[ExecuteOfficerProfileService] posMaster saved with new holder");
// STEP 5: กำหนด position ใหม่ // STEP 5: กำหนด position ใหม่
@ -1104,7 +1195,7 @@ export class ExecuteOfficerProfileService {
item.bodyPosition?.positionId, item.bodyPosition?.positionId,
); );
if (item.bodyPosition?.positionId) { if (item.bodyPosition?.positionId) {
const positionById = await this.positionRepository.findOne({ const positionById = await positionRepository.findOne({
where: { where: {
id: item.bodyPosition.positionId, id: item.bodyPosition.positionId,
posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง
@ -1149,7 +1240,7 @@ export class ExecuteOfficerProfileService {
whereCondition.positionArea = item.bodyPosition.positionArea; whereCondition.positionArea = item.bodyPosition.positionArea;
} }
const positionBy7Fields = await this.positionRepository.findOne({ const positionBy7Fields = await positionRepository.findOne({
where: whereCondition, where: whereCondition,
relations: ["posExecutive"], relations: ["posExecutive"],
order: { orderNo: "ASC" }, order: { orderNo: "ASC" },
@ -1171,7 +1262,7 @@ export class ExecuteOfficerProfileService {
console.log( console.log(
"[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields", "[ExecuteOfficerProfileService] CONDITION 2 not matched, trying CONDITION 3: Match 3 fields",
); );
const positionBy3Fields = await this.positionRepository.findOne({ const positionBy3Fields = await positionRepository.findOne({
where: { where: {
posMasterId: posMaster.id, posMasterId: posMaster.id,
positionName: item.bodyPosition.positionName, 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 // อัพเดท org และ posMasterNo ตลอดไม่ต้องดัก isSit
profile.posMasterNo = getPosMasterNo(posMaster); profile.posMasterNo = getPosMasterNo(posMaster);
profile.org = getOrgFullName(posMaster); profile.org = getOrgFullName(posMaster);
@ -1238,7 +1309,7 @@ export class ExecuteOfficerProfileService {
profile.positionExecutiveField = positionNew.positionExecutiveField ?? null; profile.positionExecutiveField = positionNew.positionExecutiveField ?? null;
// profile.dateStart = new Date(); // profile.dateStart = new Date();
} }
await this.positionRepository.save(positionNew, { data: req }); await positionRepository.save(positionNew, { data: req });
} else if (!posMaster.isSit) { } else if (!posMaster.isSit) {
// fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา // fallback: ตำแหน่งในโครงสร้างถูกแก้ไข ใช้ข้อมูลตำแหน่งที่สมัครสอบมา
console.log( console.log(
@ -1251,12 +1322,15 @@ export class ExecuteOfficerProfileService {
profile.positionArea = item.bodyPosition.positionArea ?? null; profile.positionArea = item.bodyPosition.positionArea ?? null;
profile.positionExecutiveField = item.bodyPosition.positionExecutiveField ?? 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 }); setLogDataDiff(req, { before, after: profile });
// await CreatePosMasterHistoryOfficer(posMaster.id, req); // await CreatePosMasterHistoryOfficer(posMaster.id, req);
console.log(
`[ExecuteOfficerProfileService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, citizenId: ${item.bodyProfile?.citizenId}`,
);
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, { await CreatePosMasterHistoryOfficer(posMaster.id, req, null, {
positionId: positionNew?.id, positionId: positionNew?.id,
}); }, manager);
} }
// Insignia // Insignia
if (_oldInsigniaIds.length > 0) { if (_oldInsigniaIds.length > 0) {
@ -1264,7 +1338,7 @@ export class ExecuteOfficerProfileService {
"[ExecuteOfficerProfileService] Processing old insignias, count:", "[ExecuteOfficerProfileService] Processing old insignias, count:",
_oldInsigniaIds.length, _oldInsigniaIds.length,
); );
const _insignias = await this.insigniaRepo.find({ const _insignias = await insigniaRepo.find({
where: { id: In(_oldInsigniaIds), isDeleted: false }, where: { id: In(_oldInsigniaIds), isDeleted: false },
order: { createdAt: "ASC" }, order: { createdAt: "ASC" },
}); });
@ -1290,10 +1364,10 @@ export class ExecuteOfficerProfileService {
Object.assign(insignia, { ...newInsigniaData, ...meta }); Object.assign(insignia, { ...newInsigniaData, ...meta });
const history = new ProfileInsigniaHistory(); const history = new ProfileInsigniaHistory();
Object.assign(history, { ...insignia, id: undefined }); Object.assign(history, { ...insignia, id: undefined });
await this.insigniaRepo.save(insignia, { data: req }); await insigniaRepo.save(insignia, { data: req });
setLogDataDiff(req, { before, after: insignia }); setLogDataDiff(req, { before, after: insignia });
history.profileInsigniaId = insignia.id; 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, profileEmployeeId: undefined,
}); });
if (profile.profileAvatars && profile.profileAvatars.length > 0) { if (profile.profileAvatars && profile.profileAvatars.length > 0) {
await Promise.all( for (const avatarItem of profile.profileAvatars) {
profile.profileAvatars.map(async (item: any) => { avatarItem.isActive = false;
item.isActive = false; await avatarRepository.save(avatarItem);
await this.avatarRepository.save(item);
}),
);
} }
await this.avatarRepository.save(_profileAvatar); }
await avatarRepository.save(_profileAvatar);
let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`; let avatar = `ทะเบียนประวัติ/โปรไฟล์/${profile.id}`;
let fileName = `profile-${_profileAvatar.id}`; let fileName = `profile-${_profileAvatar.id}`;
_profileAvatar.isActive = true; _profileAvatar.isActive = true;
_profileAvatar.avatar = avatar; _profileAvatar.avatar = avatar;
_profileAvatar.avatarName = fileName; _profileAvatar.avatarName = fileName;
await this.avatarRepository.save(_profileAvatar, { data: req }); await avatarRepository.save(_profileAvatar, { data: req });
profile.avatar = avatar; profile.avatar = avatar;
profile.avatarName = fileName; profile.avatarName = fileName;
await this.profileRepository.save(profile, { data: req }); await profileRepository.save(profile, { data: req });
const checkAvatar = await this.avatarRepository.findOne({ const checkAvatar = await avatarRepository.findOne({
where: { avatar: avatar, avatarName: fileName }, where: { avatar: avatar, avatarName: fileName },
}); });
if (checkAvatar && checkAvatar.profileId == null) { if (checkAvatar && checkAvatar.profileId == null) {
checkAvatar.profileId = profile.id; checkAvatar.profileId = profile.id;
await this.avatarRepository.save(checkAvatar); await avatarRepository.save(checkAvatar);
} }
//duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม //duplicate รูปภาพโปรไฟล์โดยอิงจากรูปภาพเดิม
await new CallAPI() await new CallAPI()
@ -1343,8 +1415,9 @@ export class ExecuteOfficerProfileService {
.catch(() => {}); .catch(() => {});
} }
} }
}),
console.log(
`[ExecuteOfficerProfileService] Completed processOne — citizenId: ${item.bodyProfile?.citizenId}`,
); );
console.log("[ExecuteOfficerProfileService] executeCreateOfficerProfile completed successfully");
} }
} }

View 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`);
}
}

View file

@ -60,17 +60,6 @@ export interface SalaryCurrentExecutionContext {
req?: any; 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 + () * Service ProfileSalary + ()
* *
@ -98,7 +87,7 @@ export class ExecuteSalaryCurrentService {
async executeSalaryCurrent( async executeSalaryCurrent(
data: SalaryCurrentItem[], data: SalaryCurrentItem[],
ctx: SalaryCurrentExecutionContext, ctx: SalaryCurrentExecutionContext,
): Promise<ExecuteSalaryResult> { ): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown"; const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown"; const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log( console.log(
@ -170,12 +159,10 @@ export class ExecuteSalaryCurrentService {
// ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch // ทุกคนใช้ manager ตัวเดียวกัน — คนใด throw จะ rollback ทั้ง batch
// และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow // และ propagate error ออกไป (ล้มเหลวทั้งหมด) โดย log error ของคนที่ทำให้ fail ก่อน rethrow
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
let successCount = 0;
await AppDataSource.transaction(async (manager) => { await AppDataSource.transaction(async (manager) => {
for (const item of data ?? []) { for (const item of data ?? []) {
try { try {
await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb); await this.processOne(item, ctx, manager, _posNumCodeSit, _posNumCodeSitAbb);
successCount++;
} catch (err) { } catch (err) {
const reason = const reason =
err instanceof HttpError 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) { if (posMasterOld != null) {
await posMasterRepository.save(posMasterOld); await posMasterRepository.save(posMasterOld);
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await CreatePosMasterHistoryOfficer(posMasterOld.id, req, null, null, manager);
console.log( 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); await posMasterRepository.save(posMaster);
@ -504,11 +485,11 @@ export class ExecuteSalaryCurrentService {
profile.amountSpecial = item.amountSpecial ?? null; profile.amountSpecial = item.amountSpecial ?? null;
await profileRepository.save(profile); await profileRepository.save(profile);
await positionRepository.save(positionNew); await positionRepository.save(positionNew);
console.log(
`[ExecuteSalaryCurrentService] Applied new position — profileId: ${item.profileId}, positionId: ${positionNew.id}, posMasterId: ${posMaster.id}`,
);
} }
// ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
console.log(
`[ExecuteSalaryCurrentService] Creating PosMasterHistory — posMasterId: ${posMaster.id}, profileId: ${item.profileId}`,
);
await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager); await CreatePosMasterHistoryOfficer(posMaster.id, req, null, null, manager);
console.log( console.log(

View file

@ -1,4 +1,4 @@
import { Double } from "typeorm"; import { Double, EntityManager } from "typeorm";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
@ -63,28 +63,29 @@ export interface SalaryEmployeeCurrentExecutionContext {
* - consumer rabbitmq handler service (Linear Flow) * - consumer rabbitmq handler service (Linear Flow)
* *
* Behavior preserve CommandController.newSalaryEmployeeAndUpdateCurrent * Behavior preserve CommandController.newSalaryEmployeeAndUpdateCurrent
*
* Batch semantics: all-or-nothing transaction (sequential)
* throw rollback batch propagate error ()
* return result success count
*/ */
export class ExecuteSalaryEmployeeCurrentService { export class ExecuteSalaryEmployeeCurrentService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
private profileRepository = AppDataSource.getRepository(Profile); 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); private orgRootRepository = AppDataSource.getRepository(OrgRoot);
/** /**
* ProfileSalary + * ProfileSalary + batch
*/ */
async executeSalaryEmployeeCurrent( async executeSalaryEmployeeCurrent(
data: SalaryEmployeeCurrentItem[], data: SalaryEmployeeCurrentItem[],
ctx: SalaryEmployeeCurrentExecutionContext, ctx: SalaryEmployeeCurrentExecutionContext,
): Promise<void> { ): Promise<void> {
console.log("[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent"); const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
console.log("[ExecuteSalaryEmployeeCurrentService] Request body count:", data?.length); const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
const req = ctx.req; `[ExecuteSalaryEmployeeCurrentService] Starting executeSalaryEmployeeCurrent — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryEmployeeCurrentService] Request body count: ${data?.length ?? 0}`);
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -144,14 +145,59 @@ export class ExecuteSalaryEmployeeCurrentService {
.orgRootShortName ?? ""; .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) { if (!profile) {
throw new HttpError(HttpStatus.BAD_REQUEST, "ไม่พบ 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 }, where: { profileEmployeeId: item.profileId },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -178,19 +224,19 @@ export class ExecuteSalaryEmployeeCurrentService {
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined }); Object.assign(history, { ...dataSalary, id: undefined });
await this.salaryRepo.save(dataSalary, { data: req }); await salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary }); setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id; 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 }, where: { id: item.posmasterId },
relations: ["orgRoot"], relations: ["orgRoot"],
}); });
if (posMaster == null) if (posMaster == null)
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลตำแหน่งนี้");
const posMasterOld = await this.employeePosMasterRepository.findOne({ const posMasterOld = await employeePosMasterRepository.findOne({
where: { where: {
current_holderId: item.profileId, current_holderId: item.profileId,
orgRevisionId: posMaster.orgRevisionId, orgRevisionId: posMaster.orgRevisionId,
@ -202,7 +248,7 @@ export class ExecuteSalaryEmployeeCurrentService {
} }
// if (posMasterOld != null) posMasterOld.next_holderId = null; // if (posMasterOld != null) posMasterOld.next_holderId = null;
const positionOld = await this.employeePositionRepository.findOne({ const positionOld = await employeePositionRepository.findOne({
where: { where: {
posMasterId: posMasterOld?.id, posMasterId: posMasterOld?.id,
positionIsSelected: true, positionIsSelected: true,
@ -210,10 +256,10 @@ export class ExecuteSalaryEmployeeCurrentService {
}); });
if (positionOld != null) { if (positionOld != null) {
positionOld.positionIsSelected = false; positionOld.positionIsSelected = false;
await this.employeePositionRepository.save(positionOld); await employeePositionRepository.save(positionOld);
} }
const checkPosition = await this.employeePositionRepository.find({ const checkPosition = await employeePositionRepository.find({
where: { where: {
posMasterId: item.posmasterId, posMasterId: item.posmasterId,
positionIsSelected: true, positionIsSelected: true,
@ -224,18 +270,22 @@ export class ExecuteSalaryEmployeeCurrentService {
...positions, ...positions,
positionIsSelected: false, positionIsSelected: false,
})); }));
await this.employeePositionRepository.save(clearPosition); await employeePositionRepository.save(clearPosition);
} }
posMaster.current_holderId = item.profileId; posMaster.current_holderId = item.profileId;
posMaster.lastUpdatedAt = new Date(); posMaster.lastUpdatedAt = new Date();
posMaster.next_holderId = null; posMaster.next_holderId = null;
if (posMasterOld != null) { if (posMasterOld != null) {
await this.employeePosMasterRepository.save(posMasterOld); await employeePosMasterRepository.save(posMasterOld);
await CreatePosMasterHistoryEmployee(posMasterOld.id, req); // ส่ง 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); await employeePosMasterRepository.save(posMaster);
const positionNew = await this.employeePositionRepository.findOne({ const positionNew = await employeePositionRepository.findOne({
where: { where: {
id: item.positionId, id: item.positionId,
posMasterId: item.posmasterId, posMasterId: item.posmasterId,
@ -250,13 +300,17 @@ export class ExecuteSalaryEmployeeCurrentService {
profile.positionEmployeePositionId = positionNew.positionName; profile.positionEmployeePositionId = positionNew.positionName;
profile.amount = item.amount ?? null; profile.amount = item.amount ?? null;
profile.amountSpecial = item.amountSpecial ?? null; profile.amountSpecial = item.amountSpecial ?? null;
await this.profileEmployeeRepository.save(profile); await profileEmployeeRepository.save(profile);
await this.employeePositionRepository.save(positionNew); 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}`,
);
} }
} }

View file

@ -1,4 +1,4 @@
import { Double } from "typeorm"; import { Double, EntityManager } from "typeorm";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
@ -71,29 +71,34 @@ export interface SalaryEmployeeLeaveExecutionContext {
* - consumer rabbitmq handler service (Linear Flow) * - consumer rabbitmq handler service (Linear Flow)
* *
* Behavior preserve CommandController.newSalaryEmployeeAndUpdateLeave * 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 { export class ExecuteSalaryEmployeeLeaveService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
private commandReciveRepository = AppDataSource.getRepository(CommandRecive); private commandReciveRepository = AppDataSource.getRepository(CommandRecive);
private profileRepository = AppDataSource.getRepository(Profile); 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 orgRootRepository = AppDataSource.getRepository(OrgRoot);
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
/** /**
* ProfileSalary + handle leave * ProfileSalary + handle leave batch
*/ */
async executeSalaryEmployeeLeave( async executeSalaryEmployeeLeave(
data: SalaryEmployeeLeaveItem[], data: SalaryEmployeeLeaveItem[],
ctx: SalaryEmployeeLeaveExecutionContext, ctx: SalaryEmployeeLeaveExecutionContext,
): Promise<void> { ): Promise<void> {
console.log("[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave"); const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
console.log("[ExecuteSalaryEmployeeLeaveService] Request body count:", data?.length); const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
const req = ctx.req; `[ExecuteSalaryEmployeeLeaveService] Starting executeSalaryEmployeeLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryEmployeeLeaveService] Request body count: ${data?.length ?? 0}`);
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -156,9 +161,57 @@ export class ExecuteSalaryEmployeeLeaveService {
} }
} }
const today = new Date().setHours(0, 0, 0, 0); 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 }, where: { id: item.profileId },
relations: { relations: {
roleKeycloaks: true, roleKeycloaks: true,
@ -172,7 +225,7 @@ export class ExecuteSalaryEmployeeLeaveService {
const code = _command?.commandType?.code; const code = _command?.commandType?.code;
//ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก //ออกคำสั่งยกเลิกลาออก ลบเฉพาะคนที่ขอยกเลิกลาออก
if (item.resignId && code && ["C-PM-42"].includes(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 }, where: { refId: item.resignId },
relations: { command: true }, relations: { command: true },
}); });
@ -184,14 +237,15 @@ export class ExecuteSalaryEmployeeLeaveService {
_command.status !== "REPORTED" && _command.status !== "REPORTED" &&
(_command.status !== "WAITING" || today < executeDate) (_command.status !== "WAITING" || today < executeDate)
) { ) {
await reOrderCommandRecivesAndDelete(commandResign!.id); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
} }
} }
let _commandYear = item.commandYear; let _commandYear = item.commandYear;
if (item.commandYear) { if (item.commandYear) {
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; _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 }, where: { profileEmployeeId: item.profileId },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -218,10 +272,10 @@ export class ExecuteSalaryEmployeeLeaveService {
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined }); Object.assign(history, { ...dataSalary, id: undefined });
dataSalary.dateGovernment = (item.commandDateAffect as Date) ?? meta.createdAt; 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 }); setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id; history.profileSalaryId = dataSalary.id;
await this.salaryHistoryRepo.save(history, { data: req }); await salaryHistoryRepo.save(history, { data: req });
const _null: any = null; const _null: any = null;
profile.isLeave = item.isLeave; profile.isLeave = item.isLeave;
@ -232,7 +286,7 @@ export class ExecuteSalaryEmployeeLeaveService {
profile.lastUpdatedAt = new Date(); profile.lastUpdatedAt = new Date();
// บันทึกประวัติก่อนลบตำแหน่ง // บันทึกประวัติก่อนลบตำแหน่ง
const clearProfile = await checkCommandType(String(item.commandId)); const clearProfile = await checkCommandType(String(item.commandId));
const curRevision = await this.orgRevisionRepo.findOne({ const curRevision = await orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
}); });
let orgRootRef = null; let orgRootRef = null;
@ -241,7 +295,7 @@ export class ExecuteSalaryEmployeeLeaveService {
let orgChild3Ref = null; let orgChild3Ref = null;
let orgChild4Ref = null; let orgChild4Ref = null;
if (curRevision) { if (curRevision) {
const curPosMaster = await this.employeePosMasterRepository.findOne({ const curPosMaster = await employeePosMasterRepository.findOne({
where: { where: {
current_holderId: profile.id, current_holderId: profile.id,
orgRevisionId: curRevision.id, orgRevisionId: curRevision.id,
@ -260,17 +314,24 @@ export class ExecuteSalaryEmployeeLeaveService {
orgChild3Ref = curPosMaster?.orgChild3 ?? null; orgChild3Ref = curPosMaster?.orgChild3 ?? null;
orgChild4Ref = curPosMaster?.orgChild4 ?? null; orgChild4Ref = curPosMaster?.orgChild4 ?? null;
if (curPosMaster) { 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) { if (item.isLeave == true) {
await removeProfileInOrganize(profile.id, "EMPLOYEE"); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await removeProfileInOrganize(profile.id, "EMPLOYEE", manager);
} }
if (clearProfile.status) { if (clearProfile.status) {
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
// Keycloak deleteUser ทำภายใน transaction — ถ้า DB rollback หลังจากนี้ Keycloak จะถูกลบไปแล้ว
// (Keycloak ไม่สามารถ rollback ได้)
const delUserKeycloak = await deleteUser(profile.keycloak); const delUserKeycloak = await deleteUser(profile.keycloak);
if (delUserKeycloak) { if (delUserKeycloak) {
// Task #228 // Task #228
@ -290,7 +351,7 @@ export class ExecuteSalaryEmployeeLeaveService {
// profile.posTypeId = _null; // profile.posTypeId = _null;
// profile.posLevelId = _null; // profile.posLevelId = _null;
} }
await this.profileEmployeeRepository.save(profile); await profileEmployeeRepository.save(profile);
// if (profile.id) { // if (profile.id) {
// await this.keycloakAttributeService.clearOrgDnaAttributes( // await this.keycloakAttributeService.clearOrgDnaAttributes(
@ -313,9 +374,9 @@ export class ExecuteSalaryEmployeeLeaveService {
organizeName = names.join(" "); organizeName = names.join(" ");
} }
} }
}),
);
console.log("[ExecuteSalaryEmployeeLeaveService] executeSalaryEmployeeLeave completed successfully"); console.log(
`[ExecuteSalaryEmployeeLeaveService] Completed processOne — profileId: ${item.profileId}`,
);
} }
} }

View 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}`,
);
}
}

View file

@ -1,4 +1,4 @@
import { Double, In, Like } from "typeorm"; import { Double, EntityManager, In, Like } from "typeorm";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
@ -103,27 +103,34 @@ export interface SalaryLeaveExecutionContext {
* - consumer rabbitmq handler service (Linear Flow) * - consumer rabbitmq handler service (Linear Flow)
* *
* Behavior preserve CommandController.newSalaryAndUpdateLeave * 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 { export class ExecuteSalaryLeaveService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
private commandReciveRepository = AppDataSource.getRepository(CommandRecive); private commandReciveRepository = AppDataSource.getRepository(CommandRecive);
private profileRepository = AppDataSource.getRepository(Profile); 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 orgRootRepository = AppDataSource.getRepository(OrgRoot);
private orgRevisionRepo = AppDataSource.getRepository(OrgRevision);
private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak); private roleKeycloakRepo = AppDataSource.getRepository(RoleKeycloak);
/** /**
* ProfileSalary + handle leave/ * ProfileSalary + handle leave/ batch
*
* @returns success/failure
*/ */
async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise<void> { async executeSalaryLeave(data: SalaryLeaveItem[], ctx: SalaryLeaveExecutionContext): Promise<void> {
console.log("[ExecuteSalaryLeaveService] Starting executeSalaryLeave"); const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
console.log("[ExecuteSalaryLeaveService] Request body count:", data?.length); const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
const req = ctx.req; `[ExecuteSalaryLeaveService] Starting executeSalaryLeave — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryLeaveService] Request body count: ${data?.length ?? 0}`);
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -189,9 +196,69 @@ export class ExecuteSalaryLeaveService {
} }
} }
const today = new Date().setHours(0, 0, 0, 0); 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 }, where: { id: item.profileId },
relations: { relations: {
roleKeycloaks: true, roleKeycloaks: true,
@ -203,11 +270,12 @@ export class ExecuteSalaryLeaveService {
//ลบตำแหน่งที่รักษาการแทน //ลบตำแหน่งที่รักษาการแทน
const code = _command?.commandType?.code; const code = _command?.commandType?.code;
if (code && ["C-PM-08", "C-PM-17", "C-PM-18", "C-PM-48"].includes(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)) { 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 }, where: { refId: item.resignId },
relations: { command: true }, relations: { command: true },
}); });
@ -219,7 +287,8 @@ export class ExecuteSalaryLeaveService {
_command.status !== "REPORTED" && _command.status !== "REPORTED" &&
(_command.status !== "WAITING" || today < executeDate) (_command.status !== "WAITING" || today < executeDate)
) { ) {
await reOrderCommandRecivesAndDelete(commandResign!.id); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await reOrderCommandRecivesAndDelete(commandResign!.id, manager);
} }
} }
let _commandYear = item.commandYear; let _commandYear = item.commandYear;
@ -227,7 +296,7 @@ export class ExecuteSalaryLeaveService {
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; _commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
} }
const returnWork = await checkReturnCommandType(String(item.commandId)); const returnWork = await checkReturnCommandType(String(item.commandId));
const dest_item = await this.salaryRepo.findOne({ const dest_item = await salaryRepo.findOne({
where: { profileId: item.profileId }, where: { profileId: item.profileId },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -249,10 +318,10 @@ export class ExecuteSalaryLeaveService {
Object.assign(dataSalary, { ...item, ...meta }); Object.assign(dataSalary, { ...item, ...meta });
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined }); Object.assign(history, { ...dataSalary, id: undefined });
await this.salaryRepo.save(dataSalary, { data: req }); await salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary }); setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id; history.profileSalaryId = dataSalary.id;
await this.salaryHistoryRepo.save(history, { data: req }); await salaryHistoryRepo.save(history, { data: req });
} }
const _null: any = null; const _null: any = null;
profile.isLeave = item.isLeave; profile.isLeave = item.isLeave;
@ -264,7 +333,7 @@ export class ExecuteSalaryLeaveService {
const clearProfile = await checkCommandType(String(item.commandId)); const clearProfile = await checkCommandType(String(item.commandId));
//ปั๊มประวัติก่อนลบตำแหน่ง //ปั๊มประวัติก่อนลบตำแหน่ง
const curRevision = await this.orgRevisionRepo.findOne({ const curRevision = await orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false }, where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
}); });
let orgRootRef = null; let orgRootRef = null;
@ -273,7 +342,7 @@ export class ExecuteSalaryLeaveService {
let orgChild3Ref = null; let orgChild3Ref = null;
let orgChild4Ref = null; let orgChild4Ref = null;
if (curRevision) { if (curRevision) {
const curPosMaster = await this.posMasterRepository.findOne({ const curPosMaster = await posMasterRepository.findOne({
where: { where: {
current_holderId: profile.id, current_holderId: profile.id,
orgRevisionId: curRevision.id, orgRevisionId: curRevision.id,
@ -292,16 +361,22 @@ export class ExecuteSalaryLeaveService {
orgChild3Ref = curPosMaster?.orgChild3 ?? null; orgChild3Ref = curPosMaster?.orgChild3 ?? null;
orgChild4Ref = curPosMaster?.orgChild4 ?? null; orgChild4Ref = curPosMaster?.orgChild4 ?? null;
if (curPosMaster && clearProfile.LeaveType != "RETIRE_OUT_EMP") { 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) { if (item.isLeave == true) {
await removeProfileInOrganize(profile.id, "OFFICER"); // ส่ง manager เข้าไปเพื่อให้อยู่ใน transaction เดียวกัน
await removeProfileInOrganize(profile.id, "OFFICER", manager);
} }
if (clearProfile.status) { if (clearProfile.status) {
if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) { if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
const delUserKeycloak = await deleteUser(profile.keycloak); const delUserKeycloak = await deleteUser(profile.keycloak);
if (delUserKeycloak) { if (delUserKeycloak) {
// Task #228 // Task #228
@ -325,10 +400,10 @@ export class ExecuteSalaryLeaveService {
if (item.isGovernment == true) { if (item.isGovernment == true) {
if (returnWork) { if (returnWork) {
//ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้ //ปลดตำแหน่งเดิมที่ไม่ถูกปลดออกจากกิ่งครั้งเมื่อออกคำสั่งพักราชการหรือออกราชการไว้
await removeProfileInOrganize(profile.id, "OFFICER"); await removeProfileInOrganize(profile.id, "OFFICER", manager);
//ปั๊มตำแหน่งใหม่ //ปั๊มตำแหน่งใหม่
// หา posMaster และเช็ค orgRevisionIsCurrent // หา posMaster และเช็ค orgRevisionIsCurrent
let posMaster = await this.posMasterRepository.findOne({ let posMaster = await posMasterRepository.findOne({
where: { id: item.posmasterId?.toString() }, where: { id: item.posmasterId?.toString() },
relations: { relations: {
orgRevision: true, orgRevision: true,
@ -347,7 +422,7 @@ export class ExecuteSalaryLeaveService {
// ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA // ถ้าไม่อยู่ในโครงสร้างปัจจุบัน ให้หาตัวใหม่จาก ancestorDNA
if (!isCurrent && posMaster?.ancestorDNA) { if (!isCurrent && posMaster?.ancestorDNA) {
posMaster = await this.posMasterRepository.findOne({ posMaster = await posMasterRepository.findOne({
where: { where: {
ancestorDNA: posMaster.ancestorDNA, ancestorDNA: posMaster.ancestorDNA,
orgRevision: { orgRevision: {
@ -367,7 +442,7 @@ export class ExecuteSalaryLeaveService {
} }
if (posMaster) { if (posMaster) {
const checkPosition = await this.positionRepository.find({ const checkPosition = await positionRepository.find({
where: { where: {
posMasterId: posMaster.id, posMasterId: posMaster.id,
positionIsSelected: true, positionIsSelected: true,
@ -378,13 +453,13 @@ export class ExecuteSalaryLeaveService {
...positions, ...positions,
positionIsSelected: false, positionIsSelected: false,
})); }));
await this.positionRepository.save(clearPosition); await positionRepository.save(clearPosition);
} }
posMaster.current_holderId = profile.id; posMaster.current_holderId = profile.id;
posMaster.lastUpdatedAt = new Date(); posMaster.lastUpdatedAt = new Date();
// posMaster.conditionReason = _null; // posMaster.conditionReason = _null;
// posMaster.isCondition = false; // posMaster.isCondition = false;
await this.posMasterRepository.save(posMaster); await posMasterRepository.save(posMaster);
// Match position ตามลำดับ priority: // Match position ตามลำดับ priority:
// Condition 1: match จาก positionId // Condition 1: match จาก positionId
@ -398,7 +473,7 @@ export class ExecuteSalaryLeaveService {
// CONDITION 1: เช็คจาก positionId ตรง // CONDITION 1: เช็คจาก positionId ตรง
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (item.positionId) { if (item.positionId) {
const positionById = await this.positionRepository.findOne({ const positionById = await positionRepository.findOne({
where: { where: {
id: item.positionId, id: item.positionId,
posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง posMasterId: posMaster.id, // ต้องอยู่ใน posMaster ที่ถูกต้อง
@ -436,7 +511,7 @@ export class ExecuteSalaryLeaveService {
whereCondition.positionArea = item.positionArea; whereCondition.positionArea = item.positionArea;
} }
const positionBy7Fields = await this.positionRepository.findOne({ const positionBy7Fields = await positionRepository.findOne({
where: whereCondition, where: whereCondition,
relations: ["posExecutive"], relations: ["posExecutive"],
order: { orderNo: "ASC" }, order: { orderNo: "ASC" },
@ -451,7 +526,7 @@ export class ExecuteSalaryLeaveService {
// CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match) // CONDITION 3: Match 3 ฟิลด์ (ถ้า Condition 2 ไม่ match)
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) { if (!positionNew && item.positionNameNew && item.positionTypeNew && item.positionLevelNew) {
const positionBy3Fields = await this.positionRepository.findOne({ const positionBy3Fields = await positionRepository.findOne({
where: { where: {
posMasterId: posMaster.id, posMasterId: posMaster.id,
positionName: item.positionNameNew, positionName: item.positionNameNew,
@ -469,7 +544,7 @@ export class ExecuteSalaryLeaveService {
// // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition) // // FALLBACK: เลือก position แรก (ถ้าไม่เจอทั้ง 2 condition)
// if (!positionNew) { // if (!positionNew) {
// const fallbackPositions = await this.positionRepository.find({ // const fallbackPositions = await positionRepository.find({
// where: { // where: {
// posMasterId: posMaster.id, // posMasterId: posMaster.id,
// }, // },
@ -487,9 +562,13 @@ export class ExecuteSalaryLeaveService {
if (positionNew) { if (positionNew) {
positionNew.positionIsSelected = true; 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.posMasterNo = getPosMasterNo(posMaster);
profile.org = getOrgFullName(posMaster); profile.org = getOrgFullName(posMaster);
} }
@ -522,9 +601,9 @@ export class ExecuteSalaryLeaveService {
Object.assign(dataSalary, { ...newMapProfileSalary, ...meta }); Object.assign(dataSalary, { ...newMapProfileSalary, ...meta });
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined }); Object.assign(history, { ...dataSalary, id: undefined });
await this.salaryRepo.save(dataSalary); await salaryRepo.save(dataSalary);
history.profileSalaryId = dataSalary.id; history.profileSalaryId = dataSalary.id;
await this.salaryHistoryRepo.save(history); await salaryHistoryRepo.save(history);
profile.leaveReason = _null; profile.leaveReason = _null;
profile.leaveCommandId = _null; profile.leaveCommandId = _null;
profile.leaveCommandNo = _null; profile.leaveCommandNo = _null;
@ -553,6 +632,7 @@ export class ExecuteSalaryLeaveService {
} }
// กรอง "." ออกจาก firstName ก่อนส่งไป keycloak // กรอง "." ออกจาก firstName ก่อนส่งไป keycloak
const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? ""; const sanitizedFirstName = profile.firstName?.replace(/\./g, "") ?? "";
// Keycloak ทำภายใน transaction — ไม่สามารถ rollback ได้ (ดู docstring ของ class)
userKeycloakId = await createUser(profile.citizenId, password, { userKeycloakId = await createUser(profile.citizenId, password, {
firstName: sanitizedFirstName, firstName: sanitizedFirstName,
lastName: profile.lastName, lastName: profile.lastName,
@ -578,7 +658,7 @@ export class ExecuteSalaryLeaveService {
else { else {
const rolesData = await getRoleMappings(checkUser[0].id); const rolesData = await getRoleMappings(checkUser[0].id);
if (rolesData) { if (rolesData) {
const _roleKeycloak = await this.roleKeycloakRepo.find({ const _roleKeycloak = await roleKeycloakRepo.find({
where: { name: In(rolesData.map((x: any) => x.name)) }, where: { name: In(rolesData.map((x: any) => x.name)) },
}); });
profile.roleKeycloaks = profile.roleKeycloaks =
@ -591,7 +671,7 @@ export class ExecuteSalaryLeaveService {
profile.isActive = true; profile.isActive = true;
profile.isDelete = false; profile.isDelete = false;
} }
await this.profileRepository.save(profile); await profileRepository.save(profile);
// if (profile.id) { // if (profile.id) {
// await this.keycloakAttributeService.clearOrgDnaAttributes( // await this.keycloakAttributeService.clearOrgDnaAttributes(
@ -620,9 +700,9 @@ export class ExecuteSalaryLeaveService {
organizeName = names.join(" "); organizeName = names.join(" ");
} }
} }
}),
);
console.log("[ExecuteSalaryLeaveService] executeSalaryLeave completed successfully"); console.log(
`[ExecuteSalaryLeaveService] Completed processOne — profileId: ${item.profileId}`,
);
} }
} }

View file

@ -0,0 +1,539 @@
import { Double, EntityManager, In, Repository } from "typeorm";
import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { Profile } from "../entities/Profile";
import { ProfileSalary } from "../entities/ProfileSalary";
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
import { OrgRevision } from "../entities/OrgRevision";
import { checkCommandType, removeProfileInOrganize } from "../interfaces/utils";
import { CreatePosMasterHistoryOfficer } from "./PositionService";
import { deleteUser } from "../keycloak";
/**
* Input: ข้อมูล 1 probation return ( Linear Flow refactor)
* - C-PM-11 : excexute/salary-probation ()
* - C-PM-12 : excexute/salary-probation-leave ( )
*
* shape body.data endpoint /org/command/excexute/salary-probation(-leave)
*/
export interface ProbationSalaryItem {
profileId: string;
commandId?: string | null;
amount?: Double | null;
amountSpecial?: Double | null;
positionSalaryAmount?: Double | null;
mouthSalaryAmount?: Double | null;
commandNo: string | null;
commandYear: number | null;
commandDateAffect?: Date | string | null;
commandDateSign?: Date | string | null;
positionName?: string | null;
commandCode?: string | null;
commandName?: string | null;
remark: string | null;
isGovernment?: boolean | null; // C-PM-12 เท่านั้น
}
/**
* Context audit/log ( ExecuteSalaryService)
*/
export interface ProbationExecutionContext {
user: { sub: string; name: string };
req?: any;
}
/**
* Service (probation) C-PM-11, C-PM-12
*
* circular callback: org AMQ probation PostData("/org/command/excexute/salary-probation(-leave)")
* Linear Flow: org AMQ probation (return salary data) service (no callback)
*
* - C-PM-11 : executeProbationPass ProfileSalary + history, isProbation=false
* - C-PM-12 : executeProbationLeave leave logic + deleteUser(Keycloak) + ProfileSalary + history
*
* - endpoint /org/command/excexute/salary-probation(-leave) service (thin wrapper)
* - consumer rabbitmq handler service (Linear Flow)
*
* Behavior preserve CommandController
* (newSalaryAndUpdateLeaveDisciplinefgh / ExecuteCommand12Async )
*
* Batch semantics: all-or-nothing transaction (sequential)
* throw rollback batch propagate error ()
*
* Keycloak: deleteUser (C-PM-12) transaction preserve behavior
* (consistent ExecuteSalaryService C-PM-13/15/16) Keycloak rollback
* DB rollback deleteUser user Keycloak
*/
export class ExecuteSalaryProbationService {
private commandRepository = AppDataSource.getRepository(Command);
private profileRepository = AppDataSource.getRepository(Profile);
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
// ─────────────────────────────────────────────────────────────
// แก้ปัญหา _posNumCodeSit resolution ที่ซ้ำกันในทุก endpoint
// (เดิมอยู่ใน controller — ย้ายมานี่ ทำครั้งเดียวก่อนเข้า transaction)
// ─────────────────────────────────────────────────────────────
private async resolvePosNumCodeSit(
commandId: string | null | undefined,
): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> {
let posNumCodeSit = "";
let posNumCodeSitAbb = "";
const command = commandId
? await this.commandRepository.findOne({ where: { id: commandId } })
: null;
if (command) {
if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
const orgRootDeputy = await this.orgRootRepository.findOne({
where: {
isDeputy: true,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
relations: ["orgRevision"],
});
posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป.";
} else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
posNumCodeSit = "กรุงเทพมหานคร";
posNumCodeSitAbb = "กทม.";
} else {
const profileAdmin = await this.profileRepository.findOne({
where: {
keycloak: command?.createdUserId.toString(),
current_holders: {
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
},
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
});
posNumCodeSit =
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
"";
posNumCodeSitAbb =
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
.orgRootShortName ?? "";
}
}
return { posNumCodeSit, posNumCodeSitAbb };
}
// normalize date (AMQ path ส่ง string มา → แปลงเป็น Date / null ถ้า invalid)
private toDate(v: any): Date | null {
if (v == null || v === "") return null;
if (v instanceof Date) return isNaN(v.getTime()) ? null : v;
const d = new Date(v);
return isNaN(d.getTime()) ? null : d;
}
// ═══════════════════════════════════════════════════════════════
// C-PM-11 : ผ่านทดลองปฏิบัติหน้าที่ราชการ
// สร้าง ProfileSalary + history แล้ว set isProbation=false
// ═══════════════════════════════════════════════════════════════
async executeProbationPass(
data: ProbationSalaryItem[],
ctx: ProbationExecutionContext,
): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
console.log(
`[ExecuteSalaryProbationService] executeProbationPass (C-PM-11) — commandId: ${commandId}, count: ${data?.length ?? 0}`,
);
// ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน AMQ handler จะได้ string → ต้องแปลงเป็น Date)
// ─────────────────────────────────────────────────────────────
for (const item of data ?? []) {
const it = item as any;
it.commandDateAffect = this.toDate(it.commandDateAffect);
it.commandDateSign = this.toDate(it.commandDateSign);
}
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
await this.resolvePosNumCodeSit(commandId);
const profileIds = (data ?? []).map((x) => x.profileId).filter(Boolean);
await AppDataSource.transaction(async (manager) => {
const profileRepository = manager.getRepository(Profile);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
const orgRevisionRepo = manager.getRepository(OrgRevision);
for (const item of data ?? []) {
try {
await this.processOneProbationPass(
item,
ctx,
_posNumCodeSit,
_posNumCodeSitAbb,
salaryRepo,
salaryHistoryRepo,
profileRepository,
orgRevisionRepo,
);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryProbationService] Failed C-PM-11, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
// C-PM-11: ผ่านทดลองงาน → isProbation = false (bulk update ใน transaction เดียวกัน)
if (profileIds.length > 0) {
await profileRepository.update({ id: In(profileIds) }, { isProbation: false });
}
});
console.log(`[ExecuteSalaryProbationService] Completed C-PM-11 — ${data?.length ?? 0} items`);
}
private async processOneProbationPass(
item: ProbationSalaryItem,
ctx: ProbationExecutionContext,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
profileRepository: Repository<Profile>,
orgRevisionRepo: Repository<OrgRevision>,
): Promise<void> {
// current orgRevision (อ่านครั้งเดียวต่อคน — preserve query pattern ของ endpoint เดิม)
const orgRevision = await orgRevisionRepo.findOne({
where: { orgRevisionIsCurrent: true, orgRevisionIsDraft: false },
});
const profile: any = await profileRepository.findOne({
relations: [
"posType",
"posLevel",
"current_holders",
"current_holders.orgRoot",
"current_holders.orgChild1",
"current_holders.orgChild2",
"current_holders.orgChild3",
"current_holders.orgChild4",
],
where: { id: item.profileId },
});
if (!profile) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
}
const lastSalary = await salaryRepo.findOne({
where: { profileId: item.profileId },
select: ["order"],
order: { order: "DESC" },
});
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
const orgRevisionRef =
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
const shortName =
orgRevisionRef?.orgChild4?.orgChild4ShortName ??
orgRevisionRef?.orgChild3?.orgChild3ShortName ??
orgRevisionRef?.orgChild2?.orgChild2ShortName ??
orgRevisionRef?.orgChild1?.orgChild1ShortName ??
orgRevisionRef?.orgRoot?.orgRootShortName ??
null;
const posNo = orgRevisionRef?.posMasterNo?.toString() ?? null;
// NOTE: endpoint เดิมไม่ได้ load relation "current_holders.positions" → position เป็น null (preserve)
const position =
profile.current_holders
?.filter((x: any) => x.orgRevisionId == orgRevision?.id)[0]
?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null;
const dataSalary = new ProfileSalary();
dataSalary.posNumCodeSit = _posNumCodeSit;
dataSalary.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
profileId: item.profileId,
commandId: item.commandId,
positionName: profile.position,
positionType: profile?.posType?.posTypeName ?? null,
positionLevel: profile?.posLevel?.posLevelName ?? null,
positionExecutive: position?.posExecutive?.posExecutiveName ?? null,
amount: item.amount ? item.amount : null,
amountSpecial: item.amountSpecial ? item.amountSpecial : null,
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
order: nextOrder,
orgRoot: orgRevisionRef?.orgRoot?.orgRootName ?? null,
orgChild1: orgRevisionRef?.orgChild1?.orgChild1Name ?? null,
orgChild2: orgRevisionRef?.orgChild2?.orgChild2Name ?? null,
orgChild3: orgRevisionRef?.orgChild3?.orgChild3Name ?? null,
orgChild4: orgRevisionRef?.orgChild4?.orgChild4Name ?? null,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
commandNo: item.commandNo,
commandYear: item.commandYear,
posNo: posNo,
posNoAbb: shortName,
commandDateAffect: item.commandDateAffect,
commandDateSign: item.commandDateSign,
commandCode: item.commandCode,
commandName: item.commandName,
remark: item.remark,
};
Object.assign(dataSalary, meta);
const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined });
await salaryRepo.save(dataSalary);
history.profileSalaryId = dataSalary.id;
await salaryHistoryRepo.save(history);
}
// ═══════════════════════════════════════════════════════════════
// C-PM-12 : ออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐาน
// leave logic (removeProfileInOrganize + deleteUser Keycloak) + สร้าง ProfileSalary + history
// ═══════════════════════════════════════════════════════════════
async executeProbationLeave(
data: ProbationSalaryItem[],
ctx: ProbationExecutionContext,
): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
console.log(
`[ExecuteSalaryProbationService] executeProbationLeave (C-PM-12) — commandId: ${commandId}, count: ${data?.length ?? 0}`,
);
for (const item of data ?? []) {
const it = item as any;
it.commandDateAffect = this.toDate(it.commandDateAffect);
it.commandDateSign = this.toDate(it.commandDateSign);
}
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
await this.resolvePosNumCodeSit(commandId);
await AppDataSource.transaction(async (manager) => {
const profileRepository = manager.getRepository(Profile);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
const orgRevisionRepo = manager.getRepository(OrgRevision);
for (const item of data ?? []) {
try {
await this.processOneProbationLeave(
item,
ctx,
manager,
_posNumCodeSit,
_posNumCodeSitAbb,
salaryRepo,
salaryHistoryRepo,
profileRepository,
orgRevisionRepo,
);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryProbationService] Failed C-PM-12, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
});
console.log(`[ExecuteSalaryProbationService] Completed C-PM-12 — ${data?.length ?? 0} items`);
}
private async processOneProbationLeave(
item: ProbationSalaryItem,
ctx: ProbationExecutionContext,
manager: EntityManager,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
profileRepository: Repository<Profile>,
orgRevisionRepo: Repository<OrgRevision>,
): Promise<void> {
const req = ctx.req;
const profile: any = await profileRepository.findOne({
relations: [
"posType",
"posLevel",
"current_holders",
"current_holders.orgRoot",
"current_holders.orgChild1",
"current_holders.orgChild2",
"current_holders.orgChild3",
"current_holders.orgChild4",
"current_holders.positions",
"current_holders.positions.posExecutive",
],
where: { id: item.profileId },
});
if (!profile) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์");
}
const lastSalary = await salaryRepo.findOne({
where: { profileId: item.profileId },
select: ["order"],
order: { order: "DESC" },
});
const nextOrder = lastSalary ? lastSalary.order + 1 : 1;
let _commandYear = item.commandYear;
if (item.commandYear) {
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543;
}
// _profile (load แยกสำหรับ mutation เกี่ยวกับ Keycloak/leave — preserve pattern เดิม)
const _profile: any = await profileRepository.findOne({
where: { id: item.profileId },
relations: ["roleKeycloaks"],
});
if (!_profile) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลโปรไฟล์");
}
let dateLeave_: any = item.commandDateAffect;
_profile.isLeave = true;
_profile.leaveReason =
"คำสั่งให้ข้าราชการออกจากราชการเพราะผลการทดลองปฏิบัติหน้าที่ราชการต่ำกว่ามาตรฐานที่กำหนด";
_profile.dateLeave = dateLeave_;
_profile.lastUpdateUserId = ctx.user.sub;
_profile.lastUpdateFullName = ctx.user.name;
_profile.lastUpdatedAt = new Date();
const orgRevision = await orgRevisionRepo.findOne({
where: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
});
const orgRevisionRef =
profile?.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id) ?? null;
const orgRootRef = orgRevisionRef?.orgRoot ?? null;
const orgChild1Ref = orgRevisionRef?.orgChild1 ?? null;
const orgChild2Ref = orgRevisionRef?.orgChild2 ?? null;
const orgChild3Ref = orgRevisionRef?.orgChild3 ?? null;
const orgChild4Ref = orgRevisionRef?.orgChild4 ?? null;
const matchHolder = profile.current_holders?.find((x: any) => x.orgRevisionId == orgRevision?.id);
const shortName =
!profile.current_holders || profile.current_holders.length == 0
? null
: matchHolder != null && matchHolder?.orgChild4 != null
? `${matchHolder.orgChild4.orgChild4ShortName}`
: matchHolder != null && matchHolder?.orgChild3 != null
? `${matchHolder.orgChild3.orgChild3ShortName}`
: matchHolder != null && matchHolder?.orgChild2 != null
? `${matchHolder.orgChild2.orgChild2ShortName}`
: matchHolder != null && matchHolder?.orgChild1 != null
? `${matchHolder.orgChild1.orgChild1ShortName}`
: matchHolder != null && matchHolder?.orgRoot != null
? `${matchHolder.orgRoot.orgRootShortName}`
: null;
const posNo = `${matchHolder?.posMasterNo}`;
const position =
matchHolder?.positions?.filter((pos: any) => pos.positionIsSelected === true)[0] ?? null;
const profileSalary: ProfileSalary = Object.assign(new ProfileSalary(), {
profileId: item.profileId,
commandId: item.commandId,
positionName: profile.position,
positionType: profile?.posType?.posTypeName ?? null,
positionLevel: profile?.posLevel?.posLevelName ?? null,
positionExecutive: position?.posExecutive?.posExecutiveName ?? null,
amount: item.amount ? item.amount : null,
amountSpecial: item.amountSpecial ? item.amountSpecial : null,
positionSalaryAmount: item.positionSalaryAmount ? item.positionSalaryAmount : null,
mouthSalaryAmount: item.mouthSalaryAmount ? item.mouthSalaryAmount : null,
order: nextOrder,
orgRoot: orgRootRef?.orgRootName ?? null,
orgChild1: orgChild1Ref?.orgChild1Name ?? null,
orgChild2: orgChild2Ref?.orgChild2Name ?? null,
orgChild3: orgChild3Ref?.orgChild3Name ?? null,
orgChild4: orgChild4Ref?.orgChild4Name ?? null,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
dateGovernment: item.commandDateAffect ?? new Date(),
isGovernment: item.isGovernment,
commandNo: item.commandNo,
commandYear: item.commandYear,
posNo: posNo,
posNoAbb: shortName,
commandDateAffect: item.commandDateAffect,
commandDateSign: item.commandDateSign,
commandCode: item.commandCode,
commandName: item.commandName,
remark: item.remark,
posNumCodeSit: _posNumCodeSit,
posNumCodeSitAbb: _posNumCodeSitAbb,
});
if (orgRevisionRef) {
await CreatePosMasterHistoryOfficer(orgRevisionRef.id, req, "DELETE", null, manager);
}
await removeProfileInOrganize(profile.id, "OFFICER", manager);
const clearProfile = await checkCommandType(String(item.commandId));
const _null: any = null;
if (clearProfile.status) {
// Keycloak deleteUser ทำภายใน transaction (preserve behavior เดิม — Keycloak ไม่ rollback ได้)
if (_profile.keycloak != null && _profile.keycloak != "" && _profile.isDelete === false) {
const delUserKeycloak = await deleteUser(_profile.keycloak);
if (delUserKeycloak) {
// Task #228
_profile.roleKeycloaks = [];
_profile.isActive = false;
_profile.isDelete = true;
}
}
_profile.leaveCommandId = item.commandId ?? _null;
_profile.leaveCommandNo = `${item.commandNo}/${_commandYear}`;
_profile.leaveRemark = clearProfile.leaveRemark ?? _null;
_profile.leaveDate = item.commandDateAffect ?? _null;
_profile.leaveType = clearProfile.LeaveType ?? _null;
//ออกจากราชการ ไม่ต้องลบตำแหน่งในทะเบียน (issue #1516)
// _profile.position = _null;
// _profile.posTypeId = _null;
// _profile.posLevelId = _null;
}
await Promise.all([
profileRepository.save(_profile),
salaryRepo.save(profileSalary),
]);
const history = new ProfileSalaryHistory();
Object.assign(history, { ...profileSalary, id: undefined });
history.profileSalaryId = profileSalary.id;
await salaryHistoryRepo.save(history);
console.log(
`[ExecuteSalaryProbationService] processOneProbationLeave done — profileId: ${item.profileId}`,
);
}
}

View file

@ -0,0 +1,318 @@
import { EntityManager, Repository } from "typeorm";
import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import permission from "../interfaces/permission";
import { setLogDataDiff } from "../interfaces/utils";
import { Profile } from "../entities/Profile";
import { ProfileEmployee } from "../entities/ProfileEmployee";
import {
CreateProfileSalary,
CreateProfileSalaryEmployee,
ProfileSalary,
} from "../entities/ProfileSalary";
import { ProfileSalaryHistory } from "../entities/ProfileSalaryHistory";
import { Command } from "../entities/Command";
import { OrgRoot } from "../entities/OrgRoot";
/**
* Context audit/log ( ExecuteSalaryService)
*/
export interface SalaryReportExecutionContext {
user: { sub: string; name: string };
req?: any;
}
/**
* Service salary service
*
* - C-PM-33, C-PM-34, C-PM-35, C-PM-45 : officer /org/profile/salary/update
* - C-PM-36, C-PM-37, C-PM-46 : employee /org/profile-employee/salary/update
*
* circular callback: org AMQ salary service PostData("/org/profile/(employee/)salary/update")
* Linear Flow: org AMQ salary service (return salary data) service (no callback)
*
* - executeOfficerSalaryUpdate : สร้าง ProfileSalary + history + Profile (amount*)
* - executeEmployeeSalaryUpdate : สร้าง ProfileSalary + history + ProfileEmployee (amount* + salaryLevel/group)
*
* Behavior preserve
* ProfileSalaryController.updateSalary / ProfileSalaryEmployeeController.updateSalary
* ( permission check + setLogDataDiff + save({data: req}))
*
* Batch semantics: all-or-nothing transaction batch
* throw (validation/permission) rollback batch propagate error
*
* permission check: ทำ per-item transaction (preserve behavior )
* HTTP loopback /org/permission/user/... token ctx.req batch
* ( salary endpoint permission check)
*/
export class ExecuteSalaryReportService {
private commandRepository = AppDataSource.getRepository(Command);
private profileRepository = AppDataSource.getRepository(Profile);
private profileEmployeeRepository = AppDataSource.getRepository(ProfileEmployee);
private orgRootRepository = AppDataSource.getRepository(OrgRoot);
// ─────────────────────────────────────────────────────────────
// resolve _posNumCodeSit/Abb จาก command (ทำครั้งเดียวก่อนเข้า transaction)
// admin-lookup ใช้ Profile (officer: profileRepo / employee: profileGovementRepo — ทั้งคู่คือ Profile)
// ─────────────────────────────────────────────────────────────
private async resolvePosNumCodeSit(
commandId: string | null | undefined,
): Promise<{ posNumCodeSit: string; posNumCodeSitAbb: string }> {
let posNumCodeSit = "";
let posNumCodeSitAbb = "";
const command = commandId
? await this.commandRepository.findOne({ where: { id: commandId } })
: null;
if (command) {
if (command?.isBangkok?.toLocaleUpperCase() == "OFFICE") {
const orgRootDeputy = await this.orgRootRepository.findOne({
where: {
isDeputy: true,
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
relations: ["orgRevision"],
});
posNumCodeSit = orgRootDeputy ? orgRootDeputy.orgRootName : "สำนักปลัดกรุงเทพมหานคร";
posNumCodeSitAbb = orgRootDeputy ? orgRootDeputy.orgRootShortName : "สนป.";
} else if (command?.isBangkok?.toLocaleUpperCase() == "BANGKOK") {
posNumCodeSit = "กรุงเทพมหานคร";
posNumCodeSitAbb = "กทม.";
} else {
const profileAdmin = await this.profileRepository.findOne({
where: {
keycloak: command?.createdUserId.toString(),
current_holders: {
orgRevision: {
orgRevisionIsCurrent: true,
orgRevisionIsDraft: false,
},
},
},
relations: ["current_holders", "current_holders.orgRevision", "current_holders.orgRoot"],
});
posNumCodeSit =
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootName)?.orgRoot.orgRootName ??
"";
posNumCodeSitAbb =
profileAdmin?.current_holders.find((x) => x.orgRoot.orgRootShortName)?.orgRoot
.orgRootShortName ?? "";
}
}
return { posNumCodeSit, posNumCodeSitAbb };
}
// ═══════════════════════════════════════════════════════════════
// C-PM-33/34/35/45 : officer salary update
// ═══════════════════════════════════════════════════════════════
async executeOfficerSalaryUpdate(
data: CreateProfileSalary[],
ctx: SalaryReportExecutionContext,
): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
console.log(
`[ExecuteSalaryReportService] executeOfficerSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`,
);
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
await this.resolvePosNumCodeSit(commandId);
await AppDataSource.transaction(async (manager) => {
const profileRepository = manager.getRepository(Profile);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
for (const item of data ?? []) {
try {
await this.processOneOfficer(
item,
ctx,
_posNumCodeSit,
_posNumCodeSitAbb,
profileRepository,
salaryRepo,
salaryHistoryRepo,
);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryReportService] Failed officer, commandId=${commandId}, profileId=${item.profileId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
});
console.log(`[ExecuteSalaryReportService] Completed officer — ${data?.length ?? 0} items`);
}
private async processOneOfficer(
item: CreateProfileSalary,
ctx: SalaryReportExecutionContext,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
profileRepository: Repository<Profile>,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
): Promise<void> {
const req = ctx.req;
if (!item.profileId) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileId");
}
const profile = await profileRepository.findOneBy({ id: item.profileId });
if (!profile) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_OFFICER", profile.id);
const dest_item = await salaryRepo.findOne({
where: { profileId: item.profileId },
order: { order: "DESC" },
});
const before = null;
const data = new ProfileSalary();
data.posNumCodeSit = _posNumCodeSit;
data.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(data, { ...item, ...meta });
const history = new ProfileSalaryHistory();
Object.assign(history, { ...data, id: undefined });
await salaryRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
history.profileSalaryId = data.id;
await salaryHistoryRepo.save(history, { data: req });
const _null: any = null;
profile.amount = item.amount ?? _null;
profile.amountSpecial = item.amountSpecial ?? _null;
profile.positionSalaryAmount = item.positionSalaryAmount ?? _null;
profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null;
await profileRepository.save(profile);
}
// ═══════════════════════════════════════════════════════════════
// C-PM-36/37/46 : employee salary update
// ═══════════════════════════════════════════════════════════════
async executeEmployeeSalaryUpdate(
data: CreateProfileSalaryEmployee[],
ctx: SalaryReportExecutionContext,
): Promise<void> {
const commandId = data?.find((x) => x.commandId)?.commandId ?? "";
console.log(
`[ExecuteSalaryReportService] executeEmployeeSalaryUpdate — commandId: ${commandId}, count: ${data?.length ?? 0}`,
);
const { posNumCodeSit: _posNumCodeSit, posNumCodeSitAbb: _posNumCodeSitAbb } =
await this.resolvePosNumCodeSit(commandId);
await AppDataSource.transaction(async (manager) => {
const profileEmployeeRepository = manager.getRepository(ProfileEmployee);
const salaryRepo = manager.getRepository(ProfileSalary);
const salaryHistoryRepo = manager.getRepository(ProfileSalaryHistory);
for (const item of data ?? []) {
try {
await this.processOneEmployee(
item,
ctx,
_posNumCodeSit,
_posNumCodeSitAbb,
profileEmployeeRepository,
salaryRepo,
salaryHistoryRepo,
);
} catch (err) {
const reason =
err instanceof HttpError
? err.message
: err instanceof Error
? err.message
: "unexpected error";
console.error(
`[ExecuteSalaryReportService] Failed employee, commandId=${commandId}, profileEmployeeId=${item.profileEmployeeId}: ${reason}`,
err,
);
throw err; // → rollback ทั้ง transaction + propagate เป็น batch failure
}
}
});
console.log(`[ExecuteSalaryReportService] Completed employee — ${data?.length ?? 0} items`);
}
private async processOneEmployee(
item: CreateProfileSalaryEmployee,
ctx: SalaryReportExecutionContext,
_posNumCodeSit: string,
_posNumCodeSitAbb: string,
profileEmployeeRepository: Repository<ProfileEmployee>,
salaryRepo: Repository<ProfileSalary>,
salaryHistoryRepo: Repository<ProfileSalaryHistory>,
): Promise<void> {
const req = ctx.req;
if (!item.profileEmployeeId) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "กรุณากรอก profileEmployeeId");
}
const profile = await profileEmployeeRepository.findOneBy({ id: item.profileEmployeeId });
if (!profile) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่พบ profile ดังกล่าว");
}
await new permission().PermissionOrgUserUpdate(req, "SYS_REGISTRY_EMP", profile.id);
const dest_item = await salaryRepo.findOne({
where: { profileEmployeeId: item.profileEmployeeId },
order: { order: "DESC" },
});
const before = null;
const data = new ProfileSalary();
data.posNumCodeSit = _posNumCodeSit;
data.posNumCodeSitAbb = _posNumCodeSitAbb;
const meta = {
order: dest_item == null ? 1 : dest_item.order + 1,
createdUserId: ctx.user.sub,
createdFullName: ctx.user.name,
lastUpdateUserId: ctx.user.sub,
lastUpdateFullName: ctx.user.name,
createdAt: new Date(),
lastUpdatedAt: new Date(),
};
Object.assign(data, { ...item, ...meta });
const history = new ProfileSalaryHistory();
Object.assign(history, { ...data, id: undefined });
await salaryRepo.save(data, { data: req });
setLogDataDiff(req, { before, after: data });
history.profileSalaryId = data.id;
await salaryHistoryRepo.save(history, { data: req });
const _null: any = null;
profile.amount = item.amount ?? _null;
profile.amountSpecial = item.amountSpecial ?? _null;
profile.positionSalaryAmount = item.positionSalaryAmount ?? _null;
profile.mouthSalaryAmount = item.mouthSalaryAmount ?? _null;
profile.salaryLevel = item.salaryLevel ?? _null;
profile.group = item.group ?? _null;
await profileEmployeeRepository.save(profile);
}
}

View file

@ -1,4 +1,4 @@
import { Double } from "typeorm"; import { Double, EntityManager } from "typeorm";
import { AppDataSource } from "../database/data-source"; import { AppDataSource } from "../database/data-source";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status"; import HttpStatusCode from "../interfaces/http-status";
@ -70,31 +70,38 @@ export interface SalaryExecutionContext {
/** /**
* Service ProfileSalary + handle leave// * 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) * - endpoint /org/command/excexute/salary service (thin wrapper)
* - consumer rabbitmq handler service (Linear Flow) * - consumer rabbitmq handler service (Linear Flow)
* *
* Behavior preserve CommandController.newSalaryAndUpdate * 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 { export class ExecuteSalaryService {
private commandRepository = AppDataSource.getRepository(Command); private commandRepository = AppDataSource.getRepository(Command);
private profileRepository = AppDataSource.getRepository(Profile); 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 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> { async executeSalary(data: SalaryItem[], ctx: SalaryExecutionContext): Promise<void> {
console.log("[ExecuteSalaryService] Starting executeSalary"); const commandId = data?.find((x) => x.commandId)?.commandId ?? "unknown";
console.log("[ExecuteSalaryService] Request body count:", data?.length); const commandCode = data?.find((x) => x.commandCode)?.commandCode ?? "unknown";
console.log(
const req = ctx.req; `[ExecuteSalaryService] Starting executeSalary — commandCode: ${commandCode}, commandId: ${commandId}`,
);
console.log(`[ExecuteSalaryService] Request body count: ${data?.length ?? 0}`);
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date) // Normalize date fields (ผ่าน handler จะได้ string → ต้องแปลงเป็น Date)
@ -158,9 +165,59 @@ export class ExecuteSalaryService {
.orgRootShortName ?? ""; .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 }, where: { id: item.profileId },
relations: { relations: {
roleKeycloaks: true, roleKeycloaks: true,
@ -171,7 +228,7 @@ export class ExecuteSalaryService {
if (!profile) { if (!profile) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้"); throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบข้อมูลทะเบียนประวัตินี้");
} }
const posMaster: any = await this.posMasterRepository.findOne({ const posMaster: any = await posMasterRepository.findOne({
where: { where: {
current_holderId: item.profileId, current_holderId: item.profileId,
orgRevision: { orgRevision: {
@ -199,14 +256,15 @@ export class ExecuteSalaryService {
//ลบตำแหน่งที่รักษาการแทน //ลบตำแหน่งที่รักษาการแทน
const code = _command?.commandType?.code; const code = _command?.commandType?.code;
if (code && ["C-PM-13"].includes(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; let _commandYear = item.commandYear;
if (item.commandYear) { if (item.commandYear) {
_commandYear = item.commandYear > 2500 ? item.commandYear : item.commandYear + 543; _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 }, where: { profileId: item.profileId },
order: { order: "DESC" }, order: { order: "DESC" },
}); });
@ -224,12 +282,18 @@ export class ExecuteSalaryService {
lastUpdatedAt: new Date(), lastUpdatedAt: new Date(),
}; };
if (item.isLeave != undefined && item.isLeave == true) { if (item.isLeave != undefined && item.isLeave == true) {
await CreatePosMasterHistoryOfficer(orgRevisionRef, req, "DELETE"); console.log(
await removeProfileInOrganize(profile.id, "OFFICER"); `[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 clearProfile = await checkCommandType(String(item.commandId));
const _null: any = null; const _null: any = null;
if (clearProfile.status) { 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) { if (profile.keycloak != null && profile.keycloak != "" && profile.isDelete === false) {
const delUserKeycloak = await deleteUser(profile.keycloak); const delUserKeycloak = await deleteUser(profile.keycloak);
if (delUserKeycloak) { if (delUserKeycloak) {
@ -254,7 +318,7 @@ export class ExecuteSalaryService {
profile.dateLeave = item.dateLeave ?? _null; profile.dateLeave = item.dateLeave ?? _null;
profile.amount = item.amount ?? _null; profile.amount = item.amount ?? _null;
profile.amountSpecial = item.amountSpecial ?? _null; profile.amountSpecial = item.amountSpecial ?? _null;
await this.profileRepository.save(profile, { data: req }); await profileRepository.save(profile, { data: req });
// if (profile.id) { // if (profile.id) {
// await this.keycloakAttributeService.clearOrgDnaAttributes( // await this.keycloakAttributeService.clearOrgDnaAttributes(
@ -267,10 +331,10 @@ export class ExecuteSalaryService {
const history = new ProfileSalaryHistory(); const history = new ProfileSalaryHistory();
Object.assign(history, { ...dataSalary, id: undefined }); Object.assign(history, { ...dataSalary, id: undefined });
await this.salaryRepo.save(dataSalary, { data: req }); await salaryRepo.save(dataSalary, { data: req });
setLogDataDiff(req, { before, after: dataSalary }); setLogDataDiff(req, { before, after: dataSalary });
history.profileSalaryId = dataSalary.id; history.profileSalaryId = dataSalary.id;
await this.salaryHistoryRepo.save(history, { data: req }); await salaryHistoryRepo.save(history, { data: req });
if (_command) { if (_command) {
if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) { if (["C-PM-15", "C-PM-16"].includes(_command.commandType.code)) {
@ -300,9 +364,9 @@ export class ExecuteSalaryService {
const historyAssis = new ProfileAssistanceHistory(); const historyAssis = new ProfileAssistanceHistory();
Object.assign(historyAssis, { ...dataAssis, id: undefined }); Object.assign(historyAssis, { ...dataAssis, id: undefined });
await this.assistanceRepository.save(dataAssis); await assistanceRepository.save(dataAssis);
historyAssis.profileAssistanceId = dataAssis.id; historyAssis.profileAssistanceId = dataAssis.id;
await this.assistanceHistoryRepository.save(historyAssis); await assistanceHistoryRepository.save(historyAssis);
} }
// Task #2190 // Task #2190
else if (_command.commandType.code == "C-PM-13") { 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}`,
);
} }
} }

View file

@ -217,12 +217,12 @@ export async function CreatePosMasterHistoryEmployee(
posMasterId: string, posMasterId: string,
request: RequestWithUser | null, request: RequestWithUser | null,
type?: string | null, type?: string | null,
manager?: EntityManager,
): Promise<boolean> { ): Promise<boolean> {
try { const execute = async (transactionManager: EntityManager) => {
await AppDataSource.transaction(async (manager) => { const repoPosmaster = transactionManager.getRepository(EmployeePosMaster);
const repoPosmaster = manager.getRepository(EmployeePosMaster); const repoHistory = transactionManager.getRepository(PosMasterEmployeeHistory);
const repoHistory = manager.getRepository(PosMasterEmployeeHistory); const repoProfileEmployee = transactionManager.getRepository(ProfileEmployee);
const repoProfileEmployee = manager.getRepository(ProfileEmployee);
const pm = await repoPosmaster.findOne({ const pm = await repoPosmaster.findOne({
where: { id: posMasterId }, where: { id: posMasterId },
@ -239,8 +239,8 @@ export async function CreatePosMasterHistoryEmployee(
"current_holder", "current_holder",
], ],
}); });
if (!pm) return false; if (!pm) return;
if (!pm.ancestorDNA) return false; if (!pm.ancestorDNA) return;
const _null: any = null; const _null: any = null;
const h = new PosMasterEmployeeHistory(); const h = new PosMasterEmployeeHistory();
const selectedPosition = const selectedPosition =
@ -299,10 +299,23 @@ export async function CreatePosMasterHistoryEmployee(
h.createdAt = new Date(); h.createdAt = new Date();
h.lastUpdatedAt = new Date(); h.lastUpdatedAt = new Date();
await repoHistory.save(h); await repoHistory.save(h);
}); };
try {
if (manager) {
await execute(manager);
return true;
}
await AppDataSource.transaction(async (transactionManager) => {
await execute(transactionManager);
});
return true; return true;
} catch (err) { } catch (err) {
if (manager) {
console.error("CreatePosMasterHistoryEmployee error (external transaction):", err);
throw err;
}
console.error("CreatePosMasterHistoryEmployee transaction error:", err); console.error("CreatePosMasterHistoryEmployee transaction error:", err);
return false; return false;
} }

View file

@ -35,6 +35,10 @@ import { ExecuteSalaryCurrentService } from "./ExecuteSalaryCurrentService";
import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService"; import { ExecuteSalaryEmployeeCurrentService } from "./ExecuteSalaryEmployeeCurrentService";
import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService"; import { ExecuteSalaryLeaveService } from "./ExecuteSalaryLeaveService";
import { ExecuteSalaryEmployeeLeaveService } from "./ExecuteSalaryEmployeeLeaveService"; 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 = require("redis");
const REDIS_HOST = process.env.REDIS_HOST; 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> { async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
//----> condition before process consumer //----> condition before process consumer
// const repo = AppDataSource.getRepository(Command); // const repo = AppDataSource.getRepository(Command);
@ -335,6 +356,9 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
// - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ) // - ExecuteSalaryService : C-PM-13, 15, 16 (ให้โอน/ให้ช่วยราชการ/ให้กลับเข้าราชการ)
// - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ) // - ExecuteSalaryLeaveService : C-PM-08, 09, 17, 18, 41, 48 (ข้าราชการ leave/กลับเข้าราชการ)
// - ExecuteSalaryEmployeeLeaveService : C-PM-23, 42, 43 (ลูกจ้าง 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 เดิม // - คำสั่งอื่น ยังใช้ Circular Flow เดิม
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
const code = command.commandType?.code; 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 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 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 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 = const isLinearFlow =
isOfficerProfile || isOfficerProfile ||
isSalaryCurrent || isSalaryCurrent ||
isSalaryEmployeeCurrent || isSalaryEmployeeCurrent ||
isSalary || isSalary ||
isSalaryLeave || isSalaryLeave ||
isSalaryEmployeeLeave; isSalaryEmployeeLeave ||
isSalaryLeaveDiscipline;
if (isLinearFlow) { // Org-self (C-PM-21/38/40): เรียก Service ตรงๆ (Linear Flow / ทำต่อ) ไม่ผ่าน HTTP loopback
console.log(`[AMQ] Linear Flow (${code})`); // เพราะ 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[] = []; 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) { for (const chunk of chunks) {
const res = await new CallAPI().PostData( const res = await new CallAPI().PostData(
@ -363,9 +518,15 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
{ refIds: chunk }, { refIds: chunk },
false, false,
); );
// response (resultData) จาก .NET if (isCpm32 && res && !Array.isArray(res)) {
if (Array.isArray(res)) { // C-PM-32: response เป็น object { data, data1 } → แยก 2 track
console.log(`[AMQ] Push result data`); 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); 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})`); console.log(`[AMQ] Received ${resultData.length} profiles from .NET (${code})`);
// Route ไป service ที่ถูกต้องตาม commandType // Route ไป service ที่ถูกต้องตาม commandType
if (resultData.length > 0) { if (resultData.length > 0 || resultData1.length > 0) {
// สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req}) // สร้าง pseudo-req สำหรับ setLogDataDiff/save({data: req})
const pseudoReq = { const pseudoReq = buildPseudoReq(token, user);
headers: { authorization: token },
user,
};
const ctx = { const ctx = {
user: { sub: user?.sub ?? "system", name: user?.name ?? "System" }, user: { sub: user?.sub ?? "system", name: user?.name ?? "System" },
req: pseudoReq, req: pseudoReq,
@ -402,6 +560,21 @@ async function handler(msg: amqp.ConsumeMessage): Promise<boolean> {
} else if (isSalaryEmployeeLeave) { } else if (isSalaryEmployeeLeave) {
await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx); await new ExecuteSalaryEmployeeLeaveService().executeSalaryEmployeeLeave(resultData, ctx);
console.log(`[AMQ] Processed ${resultData.length} profiles via ExecuteSalaryEmployeeLeaveService`); 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 { } else {