diff --git a/src/app.ts b/src/app.ts index 06f76548..c2cbe5f7 100644 --- a/src/app.ts +++ b/src/app.ts @@ -19,6 +19,7 @@ import { ScriptProfileOrgController } from "./controllers/ScriptProfileOrgContro import { DateSerializer } from "./interfaces/date-serializer"; import { initWebSocket } from "./services/webSocket"; +import { RetirementService } from "./services/RetirementService"; async function main() { await AppDataSource.initialize(); @@ -114,6 +115,17 @@ async function main() { } }); + // Cron job for posting retirement data to Exprofile - every day at 04:30:00 on the 1st of October + const cronTime_PostRetire = "0 30 4 1 10 *"; + cron.schedule(cronTime_PostRetire, async () => { + try { + const retirementService = new RetirementService(); + await retirementService.cronjobPostRetireToExprofile(); + } catch (error) { + console.error("[Cronjob] Error executing cronjobPostRetireToExprofile:", error); + } + }); + // app.listen(APP_PORT, APP_HOST, () => console.log(`Listening on: http://localhost:${APP_PORT}`)); const server = app.listen( APP_PORT, diff --git a/src/controllers/CommandController.ts b/src/controllers/CommandController.ts index f12df5be..f7e68083 100644 --- a/src/controllers/CommandController.ts +++ b/src/controllers/CommandController.ts @@ -104,6 +104,7 @@ import { PostRetireToExprofile } from "./ExRetirementController"; import { LeaveType } from "../entities/LeaveType"; import { KeycloakAttributeService } from "../services/KeycloakAttributeService"; import { reOrderCommandRecivesAndDelete } from "../services/CommandService"; +import { RetirementService } from "../services/RetirementService"; @Route("api/v1/org/command") @Tags("Command") @Security("bearerAuth") @@ -1608,8 +1609,7 @@ export class CommandController extends Controller { return new HttpSuccess(); } - // @Get("XXX") - async cronjobUpdateRetirementStatus(/*@Request() request: RequestWithUser*/) { + async cronjobUpdateRetirementStatus() { const adminToken = (await getToken()) ?? ""; const today = new Date(); today.setUTCHours(0, 0, 0, 0); @@ -1887,6 +1887,21 @@ export class CommandController extends Controller { return new HttpSuccess(); } + /** + * API ทดสอบ cronjobPostRetireToExprofile + * @summary ทดสอบส่งข้อมูลผู้เกษียณไปยังระบบพ้นราชการ (Exprofile) + */ + @Get("cronjob/cronjobPostRetireToExprofile") + async runCronjobPostRetireToExprofile() { + try { + const retirementService = new RetirementService(); + const result = await retirementService.cronjobPostRetireToExprofile(); + return new HttpSuccess(result); + } catch (error: any) { + throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, error.message || "เกิดข้อผิดพลาด"); + } + } + /** * API รายละเอียดรายการคำสั่ง tab4 คำสั่ง * diff --git a/src/controllers/ExRetirementController.ts b/src/controllers/ExRetirementController.ts index c8ffe5da..6720f19d 100644 --- a/src/controllers/ExRetirementController.ts +++ b/src/controllers/ExRetirementController.ts @@ -237,16 +237,19 @@ export async function PostRetireToExprofile( continue; } - addLogSequence(request, { - action: "request", - status: "error", - description: "unconnected to exprofile api", - request: { - method: "POST", - url: API_URL_BANGKOK + "/importData", - response: JSON.stringify(error), - }, - }); + // เช็ค request ก่อนเรียก addLogSequence (สำหรับ cronjob ที่ส่ง null) + if (request) { + addLogSequence(request, { + action: "request", + status: "error", + description: "unconnected to exprofile api", + request: { + method: "POST", + url: API_URL_BANGKOK + "/importData", + response: JSON.stringify(error), + }, + }); + } throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "ไม่สามารถติดต่อ API ได้"); } diff --git a/src/services/RetirementService.ts b/src/services/RetirementService.ts new file mode 100644 index 00000000..1997c2e3 --- /dev/null +++ b/src/services/RetirementService.ts @@ -0,0 +1,133 @@ +import { AppDataSource } from "../database/data-source"; +import { Profile } from "../entities/Profile"; +import { PostRetireToExprofile } from "../controllers/ExRetirementController"; +import { Between, MoreThanOrEqual } from "typeorm"; + +const BATCH_SIZE = 100; +const CONCURRENT_PER_BATCH = 10; // ส่ง parallel ทีละ 10 คนในแต่ละ batch + +export class RetirementService { + private profileRepository = AppDataSource.getRepository(Profile); + + /** + * Cronjob สำหรับส่งข้อมูลผู้เกษียณไปยังระบบพ้นราชการ (Exprofile) + * ทำงานเวลา 04:30:00 ของทุกวันที่ 1 ตุลาคม + * + * รายละเอียด: + * - Query profiles ที่ leaveDate = วันที่ 1 ตุลาคมของปีนั้น และ leaveType = "RETIRE" + * - Batch ทีละ 100 records + * - Concurrent ทีละ 10 คน (parallel) ในแต่ละ batch + * - ถ้า fail ให้ log error แล้วทำคนต่อไป + */ + async cronjobPostRetireToExprofile(): Promise<{ + success: number; + failed: number; + failedProfiles: Array<{ id: string; name: string; error: string }>; + }> { + const result = { + success: 0, + failed: 0, + failedProfiles: [] as Array<{ id: string; name: string; error: string }>, + }; + + try { + + // หาวันที่ 1 ตุลาคมของปีปัจจุบัน + const now = new Date(); + const currentYear = now.getFullYear(); + + // สร้างวันที่ 1 ตุลาคมของปีปัจจุบัน (เวลา 00:00:00) + const startDate = new Date(currentYear, 9, 1, 0, 0, 0); // Month 9 = October (0-indexed) + const endDate = new Date(currentYear, 9, 1, 23, 59, 59); + + // Query profiles ที่ leaveDate อยู่ในวันที่ 1 ตุลาคม และ leaveType = "RETIRE" + const profiles = await this.profileRepository.find({ + where: [ + { leaveDate: Between(startDate, endDate), leaveType: "RETIRE" as any }, + { leaveDate: MoreThanOrEqual(startDate), leaveType: "RETIRE" as any }, + ], + relations: ["posLevel", "posType"], + }); + + // Filter เอาเฉพาะวันที่ 1 ตุลาคมเท่านั้น + const filteredProfiles = profiles.filter(p => { + if (!p.leaveDate) return false; + const leaveDate = new Date(p.leaveDate); + return ( + leaveDate.getFullYear() === currentYear && + leaveDate.getMonth() === 9 && // October + leaveDate.getDate() === 1 + ); + }); + + if (filteredProfiles.length === 0) { + return result; + } + + // แบ่ง batch ทีละ 100 records + for (let i = 0; i < filteredProfiles.length; i += BATCH_SIZE) { + const batch = filteredProfiles.slice(i, i + BATCH_SIZE); + + // แบ่งเป็น chunk เล็กๆ ทีละ CONCURRENT_PER_BATCH เพื่อส่ง parallel + for (let j = 0; j < batch.length; j += CONCURRENT_PER_BATCH) { + const chunk = batch.slice(j, j + CONCURRENT_PER_BATCH); + + // ส่ง parallel ในแต่ละ chunk + await Promise.all( + chunk.map(async (profile) => { + try { + await this.postSingleProfileToExprofile(profile); + result.success++; + } catch (error: any) { + result.failed++; + const errorInfo = { + id: profile.id, + name: `${profile.prefix}${profile.firstName} ${profile.lastName}`, + error: error.message || String(error), + }; + result.failedProfiles.push(errorInfo); + } + }) + ); + } + } + + } catch (error: any) { + throw error; + } + + return result; + } + + /** + * ส่งข้อมูล profile ไปยัง Exprofile + */ + private async postSingleProfileToExprofile(profile: Profile): Promise { + if (!profile.leaveDate) { + return; + } + + if (!profile.citizenId) { + return; + } + + const retireYear = profile.leaveDate.getFullYear(); + const retireDate = new Date(profile.leaveDate); + + // ส่งไปยัง Exprofile + PostRetireToExprofile( + null, + profile.citizenId, + profile.prefix || "", + profile.firstName || "", + profile.lastName || "", + retireYear.toString(), + profile.position || "", + profile.posType?.posTypeName || "", + profile.posLevel?.posLevelName || "", + retireDate, + profile.org || "", + profile.leaveReason || "เกษียณอายุราชการ" + ); + } +}