Skip to content

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 controllercentralize/controllers/studyLeaveRequest.js
Statusdropdown-driven (MasterDropDown)
Composite modelparent + 8 children ใน transaction
Limit50 รายการต่อ 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

399:406:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
    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)        // 7

Transaction — ทั้งหมดหรือไม่มีเลย

การสร้างทั้ง parent + 8 children อยู่ใน transaction เดียวกัน — ถ้า child ตัวใดตัวหนึ่งล้มเหลว parent ก็ rollback ด้วย

386:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
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:

79:97:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// concept — fetchListStudyLeaveRequest filter
const statusFilter = args.status 
  ? { status: args.status } 
  : {}

const result = await StudyLeaveRequest.findAll({
  where: {
    ...statusFilter,
    // ...
  }
})
  • dropDownType: 'studyStatus' — สถานะคำขอ (DRAFT, PENDING, APPROVE, REJECT, ...)
  • dropDownType: 'leaveResult' — ผลการลา (สำเร็จ, ไม่สำเร็จ, ขยายเวลา)
  • dropDownType: 'studyType' — ประเภทการศึกษา

4. DocNo generation — runningNo.js

js
// studyLeaveRequest.js (concept)
const generateStudyLeaveNo = async (context) => {
  return await runningNoRepo.generate('STUDY', context)
}

ใช้ repository runningNo.js แทนที่จะ hardcode format เหมือน domain อื่น


5. Validation — validateData

900:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// 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

151:196:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// 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

100:108:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// concept
if (context.role === 'ADMIN' || context.role === 'SUPER_ADMIN') {
  // admin เห็นทุก org
} else {
  condition.where.organizationCode = context.organizationCode
}

8. Soft delete — cascade

471:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// 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

393:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// concept
const result = await create({
  ...args,
  currentRevision: 1,   // เริ่มต้น revision 1
  status: args.status || 'DRAFT'
}, t)

แต่ละครั้งที่แก้ไข — currentRevision เพิ่มขึ้น


10. Magic numbers / hardcoded values

Valueความหมายที่มา
limit ≤ 50จำกัดผลลัพธ์ต่อ querystudyLeaveRequest.js:51-55
currentRevision: 1revision เริ่มต้นstudyLeaveRequest.js:393
'DRAFT'status เริ่มต้น

11. Cross-service calls

vtrc-api functioncu-central-apiวัตถุประสงค์
getEmpStudyLeaveRequestCD(GraphQL)sync ข้อมูลไป CD
getPersonalCDpersonalดึง 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

912:925:vtrc-api/api/src/lib/controllers/study/studyLeaveRequest.js
// 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 พื้นฐานใช่

ขั้นตอนถัดไป

ไป บท 11.7 Authentication & Registration