2 · Core pipeline — เปิดรับสมัคร → คัดเลือก → สัมภาษณ์ → สัญญา → hire / HRMI
บทนี้ trace business flow ของ recruitment-api (branch: prod) แบบ end-to-end จากคำขอเปิดรับสมัครจนถึงสัญญาจ้างและแจ้งทีมฐานให้บันทึก HRMI พร้อม module call graph และจุดเชื่อมภายนอกที่พิสูจน์ได้จากโค้ด
ภาพรวม pipeline ทั้งระบบ
[หน่วยงาน] JobRequestModule.create
│ documentNo prefix RCM, cascade save children, update workforce manpower = inProgress
▼
[ทีมสรรหา] updateAdvertisement / isPublish=1 → JobListModule (@Public)
│
▼
[ผู้สมัคร] OTP (jobList) → JWT client=rc-web → CandidateModule.create (documentNo RCC)
│ validate duplicate + checkEmpRedCross → link appProfileId → optional VTRC sync
▼
[ทีมสรรหา] AnnouncementModule.create (recordType: exam | interview | passSelection)
│ confirm → email + update candidateHd.status + CandidateStatus row
▼
[ทีมสรรหา] ContractModule.create (map HRMI codes via workforce-api)
│ status confirm → candidate status hired + remainingManpower - 1
▼
[ทีมฐาน] ContractModule.updateExportToHRMi
→ email (export-to-hrmi) + manpower recruitmentStatus=activeสถานะหลักของใบสมัคร (candidateHd.status) ที่พบจากการอัปเดตในโค้ด (ไม่มี TypeScript enum รวมศูนย์ — เป็น string literal):
| status | เกิดเมื่อ | หลักฐาน |
|---|---|---|
| (ค่าเริ่มจาก body create) | POST /candidate/create | candidate.service.ts:442-530 |
blacklist | โปรไฟล์ถูก blacklist ตอน create | candidate.service.ts:532-540 |
exam / interview / passSelection | ประกาศผล confirm ตาม recordType | announcement.service.ts:1061-1098 |
backup | สำรองรายชื่อตอน passSelection | announcement.service.ts:1123-1146 |
decline | สัญญาสละสิทธิ์ | contract.service.ts:702-723 |
inContract | สร้าง/อัปเดตสัญญา status ไม่ใช่ confirm | contract.service.ts:766-773 |
hired | สัญญา status === 'confirm' | contract.service.ts:773 |
cancelApply | ผู้สมัครยกเลิกใบสมัคร | candidate.service.ts (cancelApply) |
Module call graph
AppModule
├─ JobRequestModule ──► UploadModule, DropdownModule, workforce-api (axios)
├─ JobListModule ─────► AuthDataService, OtpService, DropdownModule, workforce-api, DB ai (AI recommend)
├─ CandidateModule ───► AppProfileModule, UploadModule, AuthDataService, workforce-api, DB ai
├─ AppProfileModule ──► AuthDataService, workforce-api, vtrc-api HTTP (employeeDetailForRecruit), reCAPTCHA
├─ AnnouncementModule ► CandidateRepository, UploadModule, workforce-api, announcementEmailService
├─ ContractModule ────► Candidate/JobRequest/AppProfile repos, workforce-api, contractEmailService
├─ BlacklistModule ───► AppProfileHd / CandidateStatus updates
├─ DeleteHistoryModule ► PDPA delete cascade + email
├─ AuthModule ────────► authData table + jsonwebtoken
├─ DropdownModule / UploadModule / ShareModule / CheckApiOnModule
└─ providers กลาง: JwtStrategy, AuthDataService, AuthRepositoryrecruitment-api/src/app.module.ts (branch: prod):54-69 ลงทะเบียน 13 feature module และ provider กลาง 4 ตัว — AuthDataService ถูก inject ข้าม module (เช่น candidate / jobList / appProfile / contract)
Stage 1 — Job request (เปิดรับสมัคร)
Endpoint
recruitment-api/src/modules/jobRequest/jobRequest.controller.ts (branch: prod):63-66 — POST /jobRequest/create_job_request (ต้อง JWT; ไม่มี @Public())
Create flow (code trace)
recruitment-api/src/modules/jobRequest/jobRequest.service.ts (branch: prod):403-526:
validateData(body)— ตรวจเลขตำแหน่งซ้ำ- เตรียม
JobReqPositionaudit fields จากbody.createdBy* updateWorkforceManpowerNo(null, position.manpowerNo, ...)— เรียก workforce-api ให้เลขอัตราเป็นrecruitmentStatus: 'inProgress'ก่อน save (:419, รายละเอียด:948-991)- map children:
jobReqDegree,jobReqField,jobReqCapability,jobReqCapabilityLang,jobReqProLicenseด้วยStampAudit - สร้าง
JobReqHistory(action: 'create',status: body.status) - สร้าง
JobReqAdvertisementว่าง (จะเติมเนื้อหา draft-js ทีหลังผ่านupdate_job_advertisement) - อัปโหลด
rcAttachFileผ่านuploadService.create(..., 'jobRequest')ถ้ามี base64 documentNo = generateRunningNo(JobRequestHd, 'documentNo', '0001', 'RCM')— prefix RCM (:513)entityManager.save(JobRequestHd, hd)— cascade ทุก relation ที่ประกาศใน entity
// สรุปลำดับใน create() — ไม่ใช่ copy เต็ม
await this.updateWorkforceManpowerNo(null, position.manpowerNo, ...);
const hd: JobRequestHd = {
...hdSave,
documentNo: await this.running.generateRunningNo(JobRequestHd, 'documentNo', '0001', 'RCM'),
jobReqCapability, jobReqCapabilityLang, jobReqProLicense,
jobReqDegree, jobReqField, jobReqHistory: [history],
jobReqPosition: position, jobReqAdvertisement, rcAttachFile,
};
return await this.entityManager.save(JobRequestHd, hd);Workforce manpower sync
recruitment-api/src/modules/jobRequest/jobRequest.service.ts (branch: prod):948-991 — PUT ${workforceEndPoint}/updateManpowerNoJobRequestRc
- create ใหม่: ส่ง
recruitmentStatus: 'inProgress' - update/delete ที่มี
id: ปลดเลขเดิมเป็นapprovedก่อน แล้วค่อยตั้งเลขใหม่เป็นinProgress(ถ้าไม่ใช่ delete)
workforceEndPoint = `${config.get('workforceServer')}/workforce-api/api/restful` (jobRequest.service.ts:74)
Publish / visit
- ประกาศเผยแพร่ควบคุมด้วย
jobRequestHd.isPublish+ ช่วงstartDate/endDate/isNoEndDate(ใช้ใน jobList query) PUT /jobRequest/update_visitเป็น@Public()(jobRequest.controller.ts:93-96) — นับจำนวนเข้าชมประกาศโดยไม่ต้อง login
Stage 2 — Public job list + OTP login (candidate portal)
Public listing
recruitment-api/src/modules/jobList/jobList.controller.ts (branch: prod):16-56 — endpoints ที่ @Public():
| Method | Path | หน้าที่ |
|---|---|---|
| GET | /jobList/get_by_org_unit_id | รายการประกาศตามหน่วยงาน |
| GET | /jobList/get_job_detail_by_id/:id | รายละเอียดประกาศ |
| GET | /jobList/get_default_data | dropdown/default ของหน้า list |
| GET | /jobList/sent_otp/:email | ส่ง OTP |
| GET | /jobList/validate_otp | ตรวจ OTP → ออก JWT |
| PUT | /jobList/logout | ลบแถว authData |
| GET | /jobList/export_other_doc | PDF แบบฟอร์มอื่น |
GET /jobList/get_job_recommend_ai/:id ไม่ public (:58-61) — ต้อง JWT และ email ใน token ต้องตรงกับ appProfileHd.email
OTP → JWT (จุดที่ผู้สมัครได้ token)
recruitment-api/src/modules/jobList/jobList.service.ts (branch: prod):70-108:
sendOtp→otpService.sendOtp→authDataService.deleteแล้วcreateเก็บ OTP ในตารางauthDatavalidateOTP→ ถ้า OTP ถูกต้องและไม่หมดอายุ (OTP_EXPIRE_INนาที, default 5 ในauth.service.ts:19)generateToken({ email, client: 'rc-web' })ด้วยjsonwebtoken+SECRET_KEY/REFRESH_TOKEN_SECRET_KEYupdateTokenเขียน access + refresh กลับauthData
นี่คือเหตุผลที่ CandidateController ไม่มี @Public() — frontend (recruitment-web) ต้องถือ JWT จาก validate_otp ก่อนเรียก POST /candidate/create
Refresh แยกอยู่ที่ POST /auth/refresh_token (auth.controller.ts:7-12) และ guard ยอมผ่านถ้ามี header x-refresh-token (jwt-auth.guard.ts:30-32)
Stage 3 — Candidate application (ใบสมัคร)
Endpoint
recruitment-api/src/modules/candidate/candidate.controller.ts (branch: prod):50-53 — POST /candidate/create
มี endpoint อีก 21 ตัวใน controller เดียวกัน (list, update, cancel, status, compare PDF/Excel/AI, etc.) — ทั้งหมดต้องการ JWT ตาม global guard
End-to-end create trace
recruitment-api/src/modules/candidate/candidate.service.ts (branch: prod):442-548:
Step A — validate
validateData (:551-569):
repo.getByJobRequestNo— ห้ามสมัครตำแหน่งเดิมซ้ำ → messageคุณได้สมัครตำแหน่งนี้แล้ว- ถ้า
isCreate:checkEmpRedcross(:400-439)- ถ้า
appProfile.isEmpRedcrossและ token ไม่ใช่ issuer VTRC → ปฏิเสธทันที - ถ้ายังไม่เคยเช็ควันนี้ →
GET workforce-api/.../checkEmpRedCrossRc?idCard=... - ถ้าพบใน workforce → อัปเดต
isEmpRedcross=trueแล้วปฏิเสธ
- ถ้า
Step B — normalize + attach files
body.client = body.client ?? 'web'(:456) — ค่าที่พบใน flow:web/bo/ess(อ้างจาก comment ในโค้ด:492-499)- email → lowercase
- map
rcAttachFile: ถ้ามีfileBase64เรียกuploadService.create(att, 'candidate')แล้วตั้งrelatedTable: 'candidateHd'(:459-478)
Step C — running number + profile link
documentNo = generateRunningNo(CandidateHd, 'documentNo', '0001', 'RCC')— prefix RCC (:487)- หา
appProfile:- มี
email→appProfileRepo.getByEmail(portal) - มี
appProfileId→getById(back-office / ESS)
- มี
hd.appProfileId = appProfile?.id
Step D — VTRC sync (พนักงานภายใน)
ถ้า appProfile.profileKey && appProfile.empCode (:509-527):
appProfileService.verifyUserAndDifferentData(appProfile, headersAuth)- ถ้าต่าง →
saveProfileAfterCompareVTRCแล้วเขียนทับ field ตำแหน่งปัจจุบันบนใบสมัคร (applyPositionCode,applyPositionName, ...)
แหล่งข้อมูล VTRC จริงอยู่ที่ appProfile.repositories.ts:422-430 — POST ${vtrcServer}employeeDetailForRecruit (ดูบท 4)
Step E — save + blacklist side-effect
entityManager.save(CandidateHd, hd)(:530)- ถ้า
appProfile.isBlacklist→ แทรกCandidateStatus { status: 'blacklist' }(:532-540)
// โครง create สำคัญ
const validate = await this.validateData(body, headersAuth, true);
const hd: CandidateHd = {
...hdSave,
rcAttachFile: attachFile ?? null,
documentNo: await this.running.generateRunningNo(CandidateHd, 'documentNo', '0001', 'RCC'),
};
hd.appProfileId = appProfile?.id;
if (appProfile?.profileKey && appProfile?.empCode) {
const checkedData = await this.appProfileService.verifyUserAndDifferentData(...);
// sync profile + applyPosition* ถ้าต่างจาก VTRC
}
const result = await this.entityManager.save(CandidateHd, hd);Screening helpers ใน CandidateModule
หลังสมัคร ทีมสรรหาใช้ endpoint ใน controller เดียวกัน:
PUT /candidate/update_candidate_status— เปลี่ยนสถานะมือPUT /candidate/update_favorite— ติดดาวPUT /candidate/update_blacklist— blacklist ผ่าน app profileGET /candidate/get_candidate_compare+ export PDF/Excel — เปรียบเทียบผู้สมัครGET /candidate/get_candidate_Per_Job_ById— เรียก stored procedure AI บน connectionai(ดูบท 3)
Stage 4 — Announcement / screening / interview / passSelection
Endpoint
recruitment-api/src/modules/announcement/announcement.controller.ts (branch: prod):55-67:
POST /announcement/createPUT /announcement/updatePOST /announcement/send_email— ส่งอีเมลอย่างเดียว (onlySendEmail)
recordType และ document prefix
announcement.service.ts (branch: prod):1023-1025:
| recordType | ความหมาย | documentNo prefix |
|---|---|---|
exam | สอบข้อเขียน | WTT |
interview + type=onsite | สัมภาษณ์ onsite | IOS |
interview + ไม่ใช่ onsite | สัมภาษณ์ online | IOL |
อื่น (รวม passSelection) | ผลการคัดเลือก | PSL |
Dropdown ของประเภทประกาศกรองจาก candidateStatus codes exam|interview|passSelection (announcement.service.ts:268-270)
Create + confirm side effects
announcement.service.ts (branch: prod):957-1151:
validateData(รวมเช็คซ้ำเลขใบสมัครต่อ recordType)- map
qualifiedCandidate/qualifiedCandidateBackupตั้งisSentEmail: body.status === 'confirm' - history
action: 'create' - generate running no ด้วย where clause ที่รวม
recordType(+typeถ้า interview) — string interpolate เข้า where (:1027-1037; ดูความเสี่ยงในบท 3) save(QualifiedHd)- ถ้า
status === 'confirm':sendEmailToCandidate(+ backup email ถ้ามีสำรองรายชื่อ)- อัปเดต
candidateHd.statusตาม recordType - บันทึก
CandidateStatusพร้อม score / interviewScore - ถ้า
passSelection: ตั้งjobRequestHd.isQualified = true - สำรองรายชื่อ →
candidateHd.status = 'backup'
PDF / email
- PDF: puppeteer + ejs ภายใต้
libs/pdfTemplate/(exam / interview / passSelection) — เรียกจากexportpdf_*ใน controller - Email:
announcementEmailService(nodemailer) ผ่านsendEmail/sendEmailWithRetry onlySendEmailใน prod จำกัดบาง recordType (announcement.service.ts:1598)
ความเสี่ยง performance: แต่ละ export เปิด puppeteer browser ใหม่ (pattern เดียวกับ jobList exportOtherDocPDF ที่ jobList.service.ts:143-167) — ไม่มี browser pool ในโค้ดที่สำรวจ
Stage 5 — Contract → hire → export HRMI
Create contract
recruitment-api/src/modules/contract/contract.controller.ts (branch: prod):37-39 — POST /contract/create
contract.service.ts (branch: prod):354-476:
- validate
- เตรียม
contractDt+contractEmployment - HRMI mapping:
getWorkforceHRMiMapping(contractEmployment)→ เขียน field*HRMIลง employment (:380-389) - history
action: 'create' - attach files (รวม copy จาก
appProfileHd) save(ContractHd)cascade- หา
CandidateHdจากcontractDt.candidateDocumentNo updateCandidate(..., body.status)→hiredถ้า confirm, ไม่เช่นนั้นinContract(:766-773)- ถ้า
confirmและมีmanpowerNo→updateRemainingManpower(..., isDecline=false)ลดโควต้า (:466-468,:740-763)
HRMI mapping logic
contract.service.ts (branch: prod):1803-1893 — GET workforce-api/.../fetchMappingTableRc ด้วย header apiKey: rcWebAPIKey แล้ว map:
| ตารางใน response | เขียนลง field |
|---|---|
tbMappingPositionCareer | positionCareerCodeHRMI (exact → equal → first sorted) |
tbMappingOrgUnit | orgUnitCodeHRMI |
tbMappingEmployeeLevel | employeeLevelCodeHRMI |
tbMappingEmploymentType | employmentTypeHRMI (match ชื่อ) |
tbMappingPositionManage | positionManageCodeHRMI |
tbMappingPositionTechnical | positionTechnicalCodeHRMI |
remainingManpower race note
updateRemainingManpower (:740-763) ทำ findOne แล้วคำนวณ remaining ± 1 แล้ว update แยก — ไม่มี transaction lock รอบ read-modify-write (อนุมานจากโครงสร้างโค้ด — ไม่ได้ load test)
Export / cancel to HRMI (email-based)
PUT /contract/update_export_to_hrmi (contract.controller.ts:63-65)
contract.service.ts (branch: prod):~1556-1665 (method updateExportToHRMi):
- บันทึก
ContractHistoryactionexportToHRMiหรือcancelExportToHRMi - ถ้าสัญญา
status === 'confirm'→prepareEmailContractอ่านemailConfigตามemailTypeแล้วcontractEmailService.sendEmail- แยกเมล "บุคลากรใหม่" กับ "ปรับตำแหน่ง" (แก้ subject string) (
:1610-1641)
- แยกเมล "บุคลากรใหม่" กับ "ปรับตำแหน่ง" (แก้ subject string) (
- stamp
exportToHRMiDateหลังส่งเมลครั้งแรก (:1646-1659) updateManpowerNoRecruitmentStatus→ workforcerecruitmentStatus: 'active'เมื่อ export หรือคืนinProgressเมื่อ cancel (:792-825)
สำคัญ: runtime ไม่ได้ HTTP push เข้า GCP/HRMI โดยตรง — เป็น email notification ให้ทีมฐานบันทึกมือ; GCP SA key ใน repo ใช้กับ Bitbucket deploy เท่านั้น (ดูบท 1 และบท 4)
Decline path: updateDeclineCandidate ตั้ง status decline + ส่งเมลแบบ decline ถ้าสัญญาเคย confirm (:695-737)
Stage 3b — Post-apply operations (screening มือ)
หลังมีใบสมัครแล้ว ทีมสรรหาใช้ endpoint ใน CandidateController โดยไม่ต้องรอ announcement:
Cancel apply
recruitment-api/src/modules/candidate/candidate.service.ts (branch: prod):700-712 — PUT /candidate/cancel_apply
findOne(CandidateHd, { id })- ตั้ง
status = 'cancelApply' save— ไม่พบการคืนremainingManpowerใน method นี้ (โควต้าลดเฉพาะตอน contract confirm)
Update candidate status (bulk ได้)
candidate.service.ts (branch: prod):738-771 — PUT /candidate/update_candidate_status
repo.checkCandidateByOldStatus+checkQualifindDraftByStatus— กันอัปเดตถ้ามีประกาศผล draft ค้าง / blacklist- รองรับ
param.idเดี่ยว หรือparam.multipleIdคั่นด้วย comma - แทรก
CandidateStatusแถวใหม่ต่อคน + อัปเดตcandidateHd.statusด้วย QueryBuilderIN (:...id)(parameterized)
Favorite / blacklist / qualified status
| Endpoint | Service method | ผลลัพธ์หลัก |
|---|---|---|
PUT /candidate/update_favorite | updateFavorite (:835) | ตั้ง isFavorite + favoriteReason |
PUT /candidate/update_blacklist | updateBlacklist | ผูก blacklist กับ app profile / สถานะใบสมัคร |
PUT /candidate/update_candidate_qualified_status | updateCandidateQualifiedStatus | sync สถานะจากหน้า qualified |
Compare / export / AI assist
| Endpoint | หน้าที่ | dependency พิเศษ |
|---|---|---|
GET /candidate/get_candidate_compare | เปรียบเทียบผู้สมัครหลายคน | main DB |
GET export_excel_compare_candidate | ExcelJS export | |
GET export_pdf_compare_candidate | puppeteer PDF | |
GET /candidate/get_candidate_Per_Job_ById | คะแนน match จาก AI SP | connection ai — Module3_3_Get_Selected_Candidate_Per_Job |
GET export_pdf_compare_candidate_ByAi | PDF เทียบผล AI | ai + puppeteer |
Stage 4b — Announcement update / resend email
PUT /announcement/update (announcement.service.ts:1158+) คล้าย create แต่รองรับ:
- บันทึกคะแนนย่อยผ่าน
qualifiedHistoryที่isSaveScore = true(saveScoreType= recordType) - เมื่อ confirm อีกครั้ง: สำรองรายชื่อลง
qualifiedCandidateBackup, อัปเดตสถานะใบสมัครซ้ำ, ส่งอีเมล POST /announcement/send_email→onlySendEmailโดยไม่ต้อง save เอกสารใหม่ — ใช้ตอน resend
ลำดับ screening ที่ทีมสรรหาใช้จริงบ่อย:
exam (WTT) confirm → candidate.status=exam
→ interview (IOS/IOL) confirm → candidate.status=interview
→ passSelection (PSL) confirm → candidate.status=passSelection
+ jobRequestHd.isQualified=true
+ optional backup list → status=backup
→ เปิดหน้า Contract จากใบสมัครที่ผ่านStage 5b — Contract update / decline / manpower return
PUT /contract/update (contract.service.ts:478+):
- validate + remap HRMI codes เหมือน create
updateCandidateตามbody.status- ถ้ายังไม่เคย confirm มาก่อน หรือเป็น decline →
updateRemainingManpower(..., isDecline)isDecline=true→remainingManpower + 1(คืนโควต้า)- ไม่เช่นนั้น → ลด 1 (floor ที่ 0)
Decline email (:695-737): บันทึก CandidateStatus status decline + อัปเดต candidateHd + ส่งเมล decline เฉพาะเมื่อสัญญาเดิมเป็น confirm
AppProfile vs CandidateHd (อย่าสับสนใน pipeline)
| แนวคิด | Entity | Cardinality | ใช้ตอน |
|---|---|---|---|
| คนสมัคร (PII + ประวัติ) | appProfileHd + children | 1 คน | สมัครครั้งแรก / แก้โปรไฟล์ / PDPA delete |
| ใบสมัครต่อตำแหน่ง | candidateHd | N ใบต่อ 1 คน | สมัครแต่ละ job request |
| Session portal | authData | ต่อ email | OTP / JWT |
candidate.service.ts:490-506 เลือก link profile ด้วย email (web) หรือ appProfileId (bo/ess) — ESS/พนักงานภายในมักพก empCode + profileKey จึงเข้า VTRC sync
Running number format
recruitment-api/libs/utils/running.util.ts (branch: prod):8-55 — generateRunningNo:
- prefix =
{letter}{YY}{MM}เช่นRCC2607 - query แถวล่าสุดที่
column LIKE '{prefix}-%'เรียง running DESC - คืน
{prefix}-{padded}เช่นRCC2607-0001
พารามิเตอร์ whereClause ถูกแปะเป็น `${tableAlias}.${whereClause}` (running.util.ts:26) — announcement ส่ง where ยาวที่มี recordType/type จึงจำกัด running ต่อประเภทประกาศ (และเป็นจุด string-inject ที่บท 3 เตือน)
Candidate application E2E (สรุปเส้นทางเดียว)
ลำดับเรียกจริงที่ผู้สมัครทั่วไปใช้:
GET /jobList/get_*(public) เลือกประกาศGET /jobList/sent_otp/{email}→ เก็บ OTP ในauthDataGET /jobList/validate_otp?email=&otp=→ ได้{ token, refreshToken }- (optional)
POST /uploadหรือแนบ base64 ใน body create POST /candidate/create+Authorization: Bearer {token}(+ apiKey ตามที่ frontend ส่ง)- ทีมสรรหา: announcement create/confirm → contract create/confirm → export_to_hrmi
เส้นทางพนักงานภายใน (ESS / bo):
- Login ที่ VTRC ได้ JWT (
iss=redcross.or.th:vtrc) - เรียก recruitment-api ด้วย Bearer เดิม (shared
SECRET_KEY) POST /candidate/createพร้อมappProfileId/clientที่ไม่ใช่ default webverifyUserAndDifferentDataดึงemployeeDetailForRecruitจากVTRC_SERVERถ้ามีprofileKey+empCode
Side pipelines ที่แตะ core flow
Blacklist
- Import Excel:
POST /blacklist/import_blacklist(@Public— ดูบท 4) - ตอน create ใบสมัคร: ถ้า
appProfile.isBlacklist→ แทรกCandidateStatus=blacklistทันที PUT /candidate/update_blacklistจากหน้า back-office
PDPA delete history
DeleteHistoryModule (service ~1,105 บรรทัด) ไม่ได้อยู่กลาง hire path แต่แตะ appProfileHd / candidateHd / qualifiedHd / contractHd ด้วย soft-delete cascade และส่งอีเมลผ่าน deleteHistoryEmailService — ถ้าโปรไฟล์อยู่ในสถานะลบ อาจกระทบการสมัครใหม่ (ดู delAccStatus ที่ announcement ใช้ระบายสีแถว)
Job request restructure
UpdateJobReqRestructureDto / jobReqRestructureHistory — เปลี่ยน org/position ระหว่างทางโดยไม่สร้าง documentNo ใหม่; มีอีเมลแจ้งผ่าน jobRequestEmailService (ไม่ขยายในบทนี้ — ดู entity ในบท 3)
จุดเชื่อม workforce-api ทั้ง pipeline (สรุป)
| ขั้น | Endpoint path หลัง /workforce-api/api/restful | เรียกจาก |
|---|---|---|
| Job request create/update | PUT /updateManpowerNoJobRequestRc | jobRequest.service.ts:979 |
| Job list org/contact | GET /fetchListOrgUnitDataRc, fetchOrgUnitByIdJobListRc | jobList.repositories.ts |
| Candidate apply check | GET /checkEmpRedCrossRc, fetchEmployeeCandidateRC | candidate.service.ts:241,432 |
| Announcement dropdown | GET /fetchListOrgUnitDataRc, fetchEmployeeCandidateRC | announcement.service.ts |
| Contract map + emp | GET /fetchMappingTableRc, fetchEmployeeCandidateRC | contract.service.ts:891,1808 |
| Export manpower | PUT /updateManpowerNoJobRequestRc | contract.service.ts:825 |
| Guard role (ถ้ามี apiKey) | GET /checkRoleRc | libs/jwt/jwt-auth.guard.ts:80 |
เคล็ดลับตอนทำงานจริง
- อย่าสับสน prefix: job request =
RCM, candidate =RCC, announcement =WTT|IOS|IOL|PSL - ก่อนแก้ mapping HRMI ให้อ่าน
getWorkforceHRMiMappingทั้งก้อน — มี fallback หลายชั้นที่อาจเลือกรหัสเงียบ ๆ - ผู้สมัคร portal ต้องผ่าน OTP ก่อน; token จาก VTRC (
iss: redcross.or.th:vtrc) ใช้คนละกฎกับclient: rc-web(ดูบท 4) isQualifiedบน job request ถูกตั้งตอน passSelection confirm — ใช้เป็นสัญญาณว่ามีผลการคัดเลือกแล้ว ไม่ใช่แค่มีคนสมัคร
ไป บท 3 Persistence →