Skip to content

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/createcandidate.service.ts:442-530
blacklistโปรไฟล์ถูก blacklist ตอน createcandidate.service.ts:532-540
exam / interview / passSelectionประกาศผล confirm ตาม recordTypeannouncement.service.ts:1061-1098
backupสำรองรายชื่อตอน passSelectionannouncement.service.ts:1123-1146
declineสัญญาสละสิทธิ์contract.service.ts:702-723
inContractสร้าง/อัปเดตสัญญา status ไม่ใช่ confirmcontract.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, AuthRepository

recruitment-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-66POST /jobRequest/create_job_request (ต้อง JWT; ไม่มี @Public())

Create flow (code trace)

recruitment-api/src/modules/jobRequest/jobRequest.service.ts (branch: prod):403-526:

  1. validateData(body) — ตรวจเลขตำแหน่งซ้ำ
  2. เตรียม JobReqPosition audit fields จาก body.createdBy*
  3. updateWorkforceManpowerNo(null, position.manpowerNo, ...) — เรียก workforce-api ให้เลขอัตราเป็น recruitmentStatus: 'inProgress' ก่อน save (:419, รายละเอียด :948-991)
  4. map children: jobReqDegree, jobReqField, jobReqCapability, jobReqCapabilityLang, jobReqProLicense ด้วย StampAudit
  5. สร้าง JobReqHistory (action: 'create', status: body.status)
  6. สร้าง JobReqAdvertisement ว่าง (จะเติมเนื้อหา draft-js ทีหลังผ่าน update_job_advertisement)
  7. อัปโหลด rcAttachFile ผ่าน uploadService.create(..., 'jobRequest') ถ้ามี base64
  8. documentNo = generateRunningNo(JobRequestHd, 'documentNo', '0001', 'RCM') — prefix RCM (:513)
  9. entityManager.save(JobRequestHd, hd) — cascade ทุก relation ที่ประกาศใน entity
typescript
// สรุปลำดับใน 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-991PUT ${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():

MethodPathหน้าที่
GET/jobList/get_by_org_unit_idรายการประกาศตามหน่วยงาน
GET/jobList/get_job_detail_by_id/:idรายละเอียดประกาศ
GET/jobList/get_default_datadropdown/default ของหน้า list
GET/jobList/sent_otp/:emailส่ง OTP
GET/jobList/validate_otpตรวจ OTP → ออก JWT
PUT/jobList/logoutลบแถว authData
GET/jobList/export_other_docPDF แบบฟอร์มอื่น

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:

  1. sendOtpotpService.sendOtpauthDataService.delete แล้ว create เก็บ OTP ในตาราง authData
  2. validateOTP → ถ้า OTP ถูกต้องและไม่หมดอายุ (OTP_EXPIRE_IN นาที, default 5 ใน auth.service.ts:19)
  3. generateToken({ email, client: 'rc-web' }) ด้วย jsonwebtoken + SECRET_KEY / REFRESH_TOKEN_SECRET_KEY
  4. updateToken เขียน 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-53POST /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):

  1. repo.getByJobRequestNo — ห้ามสมัครตำแหน่งเดิมซ้ำ → message คุณได้สมัครตำแหน่งนี้แล้ว
  2. ถ้า 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:
    • มี emailappProfileRepo.getByEmail (portal)
    • มี appProfileIdgetById (back-office / ESS)
  • hd.appProfileId = appProfile?.id

Step D — VTRC sync (พนักงานภายใน)

ถ้า appProfile.profileKey && appProfile.empCode (:509-527):

  1. appProfileService.verifyUserAndDifferentData(appProfile, headersAuth)
  2. ถ้าต่าง → saveProfileAfterCompareVTRC แล้วเขียนทับ field ตำแหน่งปัจจุบันบนใบสมัคร (applyPositionCode, applyPositionName, ...)

แหล่งข้อมูล VTRC จริงอยู่ที่ appProfile.repositories.ts:422-430POST ${vtrcServer}employeeDetailForRecruit (ดูบท 4)

Step E — save + blacklist side-effect

  • entityManager.save(CandidateHd, hd) (:530)
  • ถ้า appProfile.isBlacklist → แทรก CandidateStatus { status: 'blacklist' } (:532-540)
typescript
// โครง 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 profile
  • GET /candidate/get_candidate_compare + export PDF/Excel — เปรียบเทียบผู้สมัคร
  • GET /candidate/get_candidate_Per_Job_ById — เรียก stored procedure AI บน connection ai (ดูบท 3)

Stage 4 — Announcement / screening / interview / passSelection

Endpoint

recruitment-api/src/modules/announcement/announcement.controller.ts (branch: prod):55-67:

  • POST /announcement/create
  • PUT /announcement/update
  • POST /announcement/send_email — ส่งอีเมลอย่างเดียว (onlySendEmail)

recordType และ document prefix

announcement.service.ts (branch: prod):1023-1025:

recordTypeความหมายdocumentNo prefix
examสอบข้อเขียนWTT
interview + type=onsiteสัมภาษณ์ onsiteIOS
interview + ไม่ใช่ onsiteสัมภาษณ์ onlineIOL
อื่น (รวม passSelection)ผลการคัดเลือกPSL

Dropdown ของประเภทประกาศกรองจาก candidateStatus codes exam|interview|passSelection (announcement.service.ts:268-270)

Create + confirm side effects

announcement.service.ts (branch: prod):957-1151:

  1. validateData (รวมเช็คซ้ำเลขใบสมัครต่อ recordType)
  2. map qualifiedCandidate / qualifiedCandidateBackup ตั้ง isSentEmail: body.status === 'confirm'
  3. history action: 'create'
  4. generate running no ด้วย where clause ที่รวม recordType (+ type ถ้า interview) — string interpolate เข้า where (:1027-1037; ดูความเสี่ยงในบท 3)
  5. save(QualifiedHd)
  6. ถ้า 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-39POST /contract/create

contract.service.ts (branch: prod):354-476:

  1. validate
  2. เตรียม contractDt + contractEmployment
  3. HRMI mapping: getWorkforceHRMiMapping(contractEmployment) → เขียน field *HRMI ลง employment (:380-389)
  4. history action: 'create'
  5. attach files (รวม copy จาก appProfileHd)
  6. save(ContractHd) cascade
  7. หา CandidateHd จาก contractDt.candidateDocumentNo
  8. updateCandidate(..., body.status)hired ถ้า confirm, ไม่เช่นนั้น inContract (:766-773)
  9. ถ้า confirm และมี manpowerNoupdateRemainingManpower(..., isDecline=false) ลดโควต้า (:466-468, :740-763)

HRMI mapping logic

contract.service.ts (branch: prod):1803-1893GET workforce-api/.../fetchMappingTableRc ด้วย header apiKey: rcWebAPIKey แล้ว map:

ตารางใน responseเขียนลง field
tbMappingPositionCareerpositionCareerCodeHRMI (exact → equal → first sorted)
tbMappingOrgUnitorgUnitCodeHRMI
tbMappingEmployeeLevelemployeeLevelCodeHRMI
tbMappingEmploymentTypeemploymentTypeHRMI (match ชื่อ)
tbMappingPositionManagepositionManageCodeHRMI
tbMappingPositionTechnicalpositionTechnicalCodeHRMI

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):

  1. บันทึก ContractHistory action exportToHRMi หรือ cancelExportToHRMi
  2. ถ้าสัญญา status === 'confirm'prepareEmailContract อ่าน emailConfig ตาม emailType แล้ว contractEmailService.sendEmail
    • แยกเมล "บุคลากรใหม่" กับ "ปรับตำแหน่ง" (แก้ subject string) (:1610-1641)
  3. stamp exportToHRMiDate หลังส่งเมลครั้งแรก (:1646-1659)
  4. updateManpowerNoRecruitmentStatus → workforce recruitmentStatus: '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-712PUT /candidate/cancel_apply

  1. findOne(CandidateHd, { id })
  2. ตั้ง status = 'cancelApply'
  3. save — ไม่พบการคืน remainingManpower ใน method นี้ (โควต้าลดเฉพาะตอน contract confirm)

Update candidate status (bulk ได้)

candidate.service.ts (branch: prod):738-771PUT /candidate/update_candidate_status

  1. repo.checkCandidateByOldStatus + checkQualifindDraftByStatus — กันอัปเดตถ้ามีประกาศผล draft ค้าง / blacklist
  2. รองรับ param.id เดี่ยว หรือ param.multipleId คั่นด้วย comma
  3. แทรก CandidateStatus แถวใหม่ต่อคน + อัปเดต candidateHd.status ด้วย QueryBuilder IN (:...id) (parameterized)

Favorite / blacklist / qualified status

EndpointService methodผลลัพธ์หลัก
PUT /candidate/update_favoriteupdateFavorite (:835)ตั้ง isFavorite + favoriteReason
PUT /candidate/update_blacklistupdateBlacklistผูก blacklist กับ app profile / สถานะใบสมัคร
PUT /candidate/update_candidate_qualified_statusupdateCandidateQualifiedStatussync สถานะจากหน้า qualified

Compare / export / AI assist

Endpointหน้าที่dependency พิเศษ
GET /candidate/get_candidate_compareเปรียบเทียบผู้สมัครหลายคนmain DB
GET export_excel_compare_candidateExcelJS export
GET export_pdf_compare_candidatepuppeteer PDF
GET /candidate/get_candidate_Per_Job_ByIdคะแนน match จาก AI SPconnection aiModule3_3_Get_Selected_Candidate_Per_Job
GET export_pdf_compare_candidate_ByAiPDF เทียบผล AIai + 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_emailonlySendEmail โดยไม่ต้อง 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+):

  1. validate + remap HRMI codes เหมือน create
  2. updateCandidate ตาม body.status
  3. ถ้ายังไม่เคย confirm มาก่อน หรือเป็น decline → updateRemainingManpower(..., isDecline)
    • isDecline=trueremainingManpower + 1 (คืนโควต้า)
    • ไม่เช่นนั้น → ลด 1 (floor ที่ 0)

Decline email (:695-737): บันทึก CandidateStatus status decline + อัปเดต candidateHd + ส่งเมล decline เฉพาะเมื่อสัญญาเดิมเป็น confirm

AppProfile vs CandidateHd (อย่าสับสนใน pipeline)

แนวคิดEntityCardinalityใช้ตอน
คนสมัคร (PII + ประวัติ)appProfileHd + children1 คนสมัครครั้งแรก / แก้โปรไฟล์ / PDPA delete
ใบสมัครต่อตำแหน่งcandidateHdN ใบต่อ 1 คนสมัครแต่ละ job request
Session portalauthDataต่อ emailOTP / 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-55generateRunningNo:

  1. prefix = {letter}{YY}{MM} เช่น RCC2607
  2. query แถวล่าสุดที่ column LIKE '{prefix}-%' เรียง running DESC
  3. คืน {prefix}-{padded} เช่น RCC2607-0001

พารามิเตอร์ whereClause ถูกแปะเป็น `${tableAlias}.${whereClause}` (running.util.ts:26) — announcement ส่ง where ยาวที่มี recordType/type จึงจำกัด running ต่อประเภทประกาศ (และเป็นจุด string-inject ที่บท 3 เตือน)

Candidate application E2E (สรุปเส้นทางเดียว)

ลำดับเรียกจริงที่ผู้สมัครทั่วไปใช้:

  1. GET /jobList/get_* (public) เลือกประกาศ
  2. GET /jobList/sent_otp/{email} → เก็บ OTP ใน authData
  3. GET /jobList/validate_otp?email=&otp= → ได้ { token, refreshToken }
  4. (optional) POST /upload หรือแนบ base64 ใน body create
  5. POST /candidate/create + Authorization: Bearer {token} (+ apiKey ตามที่ frontend ส่ง)
  6. ทีมสรรหา: announcement create/confirm → contract create/confirm → export_to_hrmi

เส้นทางพนักงานภายใน (ESS / bo):

  1. Login ที่ VTRC ได้ JWT (iss=redcross.or.th:vtrc)
  2. เรียก recruitment-api ด้วย Bearer เดิม (shared SECRET_KEY)
  3. POST /candidate/create พร้อม appProfileId / client ที่ไม่ใช่ default web
  4. verifyUserAndDifferentData ดึง 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/updatePUT /updateManpowerNoJobRequestRcjobRequest.service.ts:979
Job list org/contactGET /fetchListOrgUnitDataRc, fetchOrgUnitByIdJobListRcjobList.repositories.ts
Candidate apply checkGET /checkEmpRedCrossRc, fetchEmployeeCandidateRCcandidate.service.ts:241,432
Announcement dropdownGET /fetchListOrgUnitDataRc, fetchEmployeeCandidateRCannouncement.service.ts
Contract map + empGET /fetchMappingTableRc, fetchEmployeeCandidateRCcontract.service.ts:891,1808
Export manpowerPUT /updateManpowerNoJobRequestRccontract.service.ts:825
Guard role (ถ้ามี apiKey)GET /checkRoleRclibs/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