12.6 · Study Leave (การลาศึกษา)
บทนี้เจาะลึก business logic ของการลาศึกษา — composite model (parent + 8 children), transaction-based creation, dropdown-driven status, และ search cascade
ภาพรวม
| ด้าน | ค่า |
|---|---|
| ไฟล์ controller หลัก | lib/controllers/study/studyLeaveRequest.js (~1,280 บรรทัด) |
| Sub-controllers (8) | studyLeaveDetail.js, studyStrategy.js, studyEducationFee.js (V2), studyExtend.js, studyHistory.js, studyLearnRecord.js, studyScholarship.js, studyAttachment.js |
| Forward controller | centralize/controllers/studyLeaveRequest.js |
| Status | dropdown-driven (MasterDropDown) |
| Composite model | parent + 8 children ใน transaction |
| Limit | 50 รายการต่อ query |
1. Composite model — โครงสร้าง
StudyLeaveRequest (parent)
│
├── StudyLeaveDetail (1:1)
│ └── ระดับปริญญา, สถาบัน, สาขา, ระยะเวลา
│
├── StudyStrategy (1:N)
│ └── กลยุทธ์/เป้าหมายการศึกษา
│
├── StudyEducationFee (1:N)
│ └── ค่าธรรมเนียมการศึกษา (ภาคเรียนละรายการ)
│
├── StudyHistory (1:N)
│ └── ประวัติ action (submit, approve, reject, ...)
│
├── StudyLearnRecord (1:N)
│ └── บันทึกผลการเรียน (ภาคเรียนละรายการ)
│
├── StudyScholarship (1:N)
│ └── ทุนการศึกษา (ถ้ามี)
│
└── StudyAttachment (1:N)
└── เอกสารแนบ (ร่างคำขอ, หนังสือตอบรับจากสถาบัน, ...)2. การสร้าง — composite + transaction
const result = await create(args, t) // parent
await createStudyLeaveDetail(result, args, context, t) // 1
await createStudyStrategy(result, args, context, t) // 2
await createStudyEducationFee(result, args, context, t) // 3
await createStudyHistory(result.studyLeaveRequestId, args, context, t) // 4
await saveStudyLearnRecord(args.studyLearnRecord, result.studyLeaveRequestId, context, t) // 5
await createStudyScholarship(result, args, context, t) // 6
await saveAttachments(args.studyAttachment, result.studyLeaveRequestId, context, t) // 7Transaction — ทั้งหมดหรือไม่มีเลย
การสร้างทั้ง parent + 8 children อยู่ใน transaction เดียวกัน — ถ้า child ตัวใดตัวหนึ่งล้มเหลว parent ก็ rollback ด้วย
const createStudyLeaveRequest = async (args, context) => {
return await sequelize.transaction(async (t) => {
const result = await create(args, t)
// ... create children
return result
})
}3. Status — dropdown-driven (ไม่ fixed)
ไม่เหมือน domain อื่นที่มี status enum ชัดเจน — study leave ดึง status จาก MasterDropDown:
// concept — fetchListStudyLeaveRequest filter
const statusFilter = args.status
? { status: args.status }
: {}
const result = await StudyLeaveRequest.findAll({
where: {
...statusFilter,
// ...
}
})Dropdown types
dropDownType: 'studyStatus'— สถานะคำขอ (DRAFT, PENDING, APPROVE, REJECT, ...)dropDownType: 'leaveResult'— ผลการลา (สำเร็จ, ไม่สำเร็จ, ขยายเวลา)dropDownType: 'studyType'— ประเภทการศึกษา
4. DocNo generation — runningNo.js
// studyLeaveRequest.js (concept)
const generateStudyLeaveNo = async (context) => {
return await runningNoRepo.generate('STUDY', context)
}ใช้ repository runningNo.js แทนที่จะ hardcode format เหมือน domain อื่น
5. Validation — validateData
// concept
const validateData = async (args) => {
const errors = []
// กรณี DRAFT — lenient
if (args.status === 'DRAFT') {
return errors
}
// กรณี submit — strict
if (!args.studyLeaveDetail.degree) errors.push('degree required')
if (!args.studyLeaveDetail.academy) errors.push('academy required')
// ... อีกหลายฟิลด์
return errors
}6. Search cascade
// concept
// 1. ค้นใน StudyLeaveRequest (parent)
const results = await StudyLeaveRequest.findAll({ where: searchCondition })
if (results.length === 0) {
// 2. ถ้าไม่พบ → ค้นใน StudyLeaveDetail (child)
const detailResults = await StudyLeaveDetail.findAll({
where: detailSearchCondition
})
// 3. map detail → parent
return await StudyLeaveRequest.findAll({
where: { studyLeaveRequestId: detailResults.map(d => d.studyLeaveRequestId) }
})
}
return resultsค้นใน parent ก่อน ถ้าไม่พบจะค้นใน child แล้ววนกลับมาหา parent — เพื่อให้ค้นด้วยชื่อสถาบันหรือสาขาได้
7. Permission scope
// concept
if (context.role === 'ADMIN' || context.role === 'SUPER_ADMIN') {
// admin เห็นทุก org
} else {
condition.where.organizationCode = context.organizationCode
}8. Soft delete — cascade
// concept — destroyStudyLeaveRequest
const destroyStudyLeaveRequest = async (id) => {
await sequelize.transaction(async (t) => {
await StudyLeaveRequest.destroy({ where: { studyLeaveRequestId: id } }, { transaction: t })
await StudyLeaveDetail.destroy({ where: { studyLeaveRequestId: id } }, { transaction: t })
await StudyStrategy.destroy({ where: { studyLeaveRequestId: id } }, { transaction: t })
// ... cascade ทุก children
})
}9. History + revision
// concept
const result = await create({
...args,
currentRevision: 1, // เริ่มต้น revision 1
status: args.status || 'DRAFT'
}, t)แต่ละครั้งที่แก้ไข — currentRevision เพิ่มขึ้น
10. Magic numbers / hardcoded values
| Value | ความหมาย | ที่มา |
|---|---|---|
limit ≤ 50 | จำกัดผลลัพธ์ต่อ query | studyLeaveRequest.js:51-55 |
currentRevision: 1 | revision เริ่มต้น | studyLeaveRequest.js:393 |
'DRAFT' | status เริ่มต้น | — |
11. Cross-service calls
| vtrc-api function | cu-central-api | วัตถุประสงค์ |
|---|---|---|
getEmpStudyLeaveRequestCD | (GraphQL) | sync ข้อมูลไป CD |
getPersonalCD | personal | ดึง profile พื้นฐาน |
12. ข้อระวังทั้งหมด
1. ไม่มี multi-level approval
study leave ดูเหมือนจะใช้ approval แบบ single-level (ผ่าน History action) — ไม่ซับซ้อนเท่า leave
2. Composite model ทำให้ query ช้า
parent + 8 children — ถ้า fetch พร้อมกันทั้งหมด อาจเป็น N+1 problem
3. validateData สร้าง error message dynamic
// concept — dynamic error message
const errorMsg = errors.map((e, i) => `${i + 1}. ${e}`).join('\n')
throw new Error(errorMsg)ไม่ได้ใช้ responError ทำให้ error format ไม่สม่ำเสมอ
4. หลาย function ไม่มี try/catch
destroyStudyLeaveRequest และ function อื่น ๆ ไม่ catch error — ถ้า DB ล้มเหลว จะเป็น unhandled rejection
5. ไม่พบ enum ของ study type (full-time / part-time)
น่าจะอยู่ใน studyLeaveDetail.studyType field แต่ไม่มี enum ชัดเจนใน controller — ดึงจาก dropdown
13. Summary — study domain
| ด้าน | ทำใน vtrc-api | ส่งต่อ cu-central-api |
|---|---|---|
| Composite model + transaction | ใช่ | — |
| Status (dropdown-driven) | ใช่ | — |
| Validation | ใช่ | — |
| DocNo generation | ใช่ (ผ่าน runningNo repo) | — |
| Sync ข้อมูลไป CD | ใช่ | รับ |
| Profile พื้นฐาน | — | ใช่ |