Skip to content

12.3 · Welfare / WDHospital

บทนี้เจาะลึก business logic ของระบบเบิกค่ารักษาพยาบาล (Welfare / WDHospital) — state machine ที่ซับซ้อนที่สุดในระบบ VTRC, payment export 3 ระบบ (KBank, SmartCredit, FMIS), และการ import ผลจ่ายจากธนาคาร


ภาพรวม

ด้านค่า
ไฟล์ controller หลักlib/controllers/withdraw/hospital.js (~3,200 บรรทัด)
ไฟล์ exportlib/controllers/excel/excel.js, lib/repositories/excel/{kPayrollDataType,smartCreditDataType,fmisDataType}.js
Repositorylib/repositories/withdraw/{hospital,slip,export,historyAction,approver,delegate}.js
Forward controllercentralize/controllers/hrmi.js (ใช้ getMedical)
State machine10 สถานะ (มากที่สุดในระบบ)
Approvalsingle-level (ผู้อนุมัติจาก MasterApprovers table)
Payment3 channels (KBank, SmartCredit, FMIS) + cash

State machine — 10 สถานะ

                    createWDHospital


                     [DRAFT] ──────── submit ──────► [PENDING]
                          │                             │
                          │                     ┌───────┼───────┬──────────┐
                          │                     ▼       ▼       ▼          ▼
                          │              [MOREINFO] [REJECT] [APPROVE]  [CANCELDOC]
                          │                     │                  │
                          │                     └── edit ──►[PENDING] ├── PAID ──► [SUCCESS]
                          │                                        │         └── [FAILED]
                          │                                        │
                          │                                        ├── PAIDCASH ──► [CASH]
                          │                                        │
                          │                                        └── EDIT (แก้ยอด)

                          └────────── cancel ──► [CANCELDOC]

Transition map (action label ภาษาไทย)

2912:2948:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const getActionStatus = (currentStatus, previousStatus) => {
  const ACTION_STATUS = {
    "DRAFT": {
      "PENDING": "ทำเรื่องเบิก",
      "DRAFT": "บันทึกร่าง",
      "CANCELDOC": "ยกเลิกรายการเบิก",
    },
    "PENDING": {
      "MOREINFO": "เอกสารไม่ครบถ้วน",
      "APPROVE": "อนุมัติรายการเบิก",
      "REJECT": "ไม่อนุมัติรายการเบิก",
      "CANCELDOC": "ยกเลิกรายการเบิก"
    },
    "MOREINFO": {
      "PENDING": "แก้ไข",
      "CANCELDOC": "ยกเลิกรายการเบิก",
    },
    "APPROVE": {
      "CANCELDOC": "ยกเลิกรายการเบิก",
      "EDIT": "แก้ไขจำนวนเงินที่เบิกได้",
      "PAIDCASH": "ทำเรื่องจ่ายเงินสด",
      "PAID": "ทำเรื่องจ่าย"
    },
    "PAID": {
      "SUCCESS": "ดำเนินการโอนเงิน",
      "FAILED": "ดำเนินการโอนเงิน",
      "APPROVE": "ยกเลิกเอกสารจ่าย"
    },
    "FAILED": {
      "CASH": "ทำเรื่องจ่ายเงินสด",
      "CANCEL": "ยกเลิกการจ่ายเงินสด"
    },
    "PAIDCASH": {
      "CASH": "ดำเนินการสำเร็จ",
      "CANCEL": "ยกเลิกการจ่ายเงินสด"
    },
  }

1. การสร้าง — createWDHospital

ที่อยู่: hospital.js:1357

Flow

1. รับ args:
   - empCode, orgCode, hospitalCode
   - slipNo, slipDate, slipAmount
   - totalCurrentWithdraw (ยอดที่จะเบิก — มาจาก client)
   - medicalExpenseId (ความสัมพันธ์ — บุตร, คู่สมรส)
   - wdSlipId ( FK ไฟล์ใบเสร็จ)

2. validate:
   - checkSlipDate — ใบเสร็จต้องไม่เก่ากว่า 1 ปี
   - ตรวจ slipNo ซ้ำในโรงพยาบาลเดียวกัน
   - checkResignationRight — พ้นสภาพเกิน 30 วัน → ห้ามเบิก
   - checkMedicalExpenseRight — บุตรอายุ > 20 ปี → ยกเลิกสิทธิ์

3. generate withdrawNo format `MD[YYMM]-[6-digit]`

4. INSERT WDHospital + WDHistory (status=DRAFT)

5. INSERT WDSlip (เก็บ slipNo/slipDate/slipAmount)

6. INSERT WDApprove (รายชื่อผู้อนุมัติ)

7. ถ้า status === 'PENDING':
   a. notify approvers

2. Validation — checkSlipDate

ที่อยู่: hospital.js:2745-2826

Rules

Ruleความหมายแหล่งข้อมูล
อายุใบเสร็จ ≤ 1 ปีใบเสร็จต้องไม่เก่ากว่า 1 ปีนับจากวันเอกสารhospital.js:2784, 2822-2826
SlipNo ไม่ซ้ำใบเสร็จเลขเดียวกันในโรงพยาบาลเดียวกัน = ผิดhospital.js:2776-2782, 2818-2820
วันที่เอกสารห้ามล่วงหน้าhospital.js:2765
สิทธิ์บุตรอายุ ≤ 20 ปีhospital.js:3126 (checkMedicalExpenseRight)
พ้นสภาพห้ามเกิน 30 วันหลังลาออกhospital.js:3158, 3172 (checkResignationRight)

สิทธิ์การเบิก — vtrc-api ไม่ได้คำนวณเอง

js
// hospital.js:2748 (commented out — ไม่ทำงาน)
// if (args.totalCurrentWithdraw > 8000) {
//   throw responError('AMOUNT_OVER_LIMIT', {}, null, "ยอดที่เบิกได้ไม่เกิน 8000 บาท")
// }

vtrc-api ไม่ได้คำนวณ cap หรือเปอร์เซ็นต์เบิก — ยอดเบิกมาจาก client ทั้งหมด และ cap logic (ถ้ามี) น่าจะอยู่ที่ cu-central-api หรือ frontend


3. Approval — single-level จาก MasterApprovers

1441:1449:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
      const conditionMasterApprover = {
        where: {
          orgCode: args.orgCode,
          isActive: 1,
          type: "MEDICAL"
        }
      }
      let approvers = await MasterApprover.findAllMasterApprover(conditionMasterApprover)

ไม่มี multi-level เหมือน leave — ผู้อนุมัติคนใดคนหนึ่งใน MasterApprovers (type=MEDICAL) สามารถ approve/reject ได้

Permission scope

js
// hospital.js:107-120 (concept)
// approver orgCode '02'/'03' → เห็นเฉพาะของ org ตัวเอง
// approver orgCode อื่น ๆ → เห็นทุกอย่างยกเว้น 02/03
if (userOrgCode === '02' || userOrgCode === '03') {
  condition.where.orgCode = userOrgCode
} else {
  condition.where.orgCode = { [Op.notIn]: ['02', '03'] }
}

4. Payment export — 3 channels

ที่อยู่: routes.js:772-833 + lib/controllers/excel/excel.js:26

Flow

1. HR คลิก "export" → /api/wdHospital/export?id=...

2. createExportFile(condition) สร้างไฟล์ 3 ชนิด:
   a. KBank (kPayroll) — fixed-width text TIS-620 encoding
   b. SmartCredit — fixed-width text TIS-620 encoding
   c. FMIS — Excel (.xlsx)

3. บีบอัดเป็น ZIP พร้อม PDF reports (sumOrganize, sumCoA, ...)

4. ส่งกลับให้ HR ดาวน์โหลด

5. INSERT WDExport (เก็บ exportCode + รายการที่ export)

ตัวอย่าง route

776:811:vtrc-api/api/src/routes.js
const { excelBuffer, kPayrollText, smartCreditText, exportCode, WDExportData } = await createExportFile(condition)

const bufferKcrash = iconv.encode(kPayrollText, 'TIS-620')
const bufferSmartCredit = iconv.encode(smartCreditText, 'TIS-620')

if (kPayrollText.length > 179) {
  archive.append(bufferKcrash, { name: `${exportCode}_KBank_welfare.txt` })
}
if (smartCreditText.length > 83) {
  archive.append(bufferSmartCredit, { name: `${exportCode}_Smartcredit_welfare.txt` })
}
archive.append(excelBuffer, { name: `${exportCode}_FMIS.xlsx` })

Magic thresholds

Thresholdความหมายที่มา
kPayrollText.length > 179skip KBank file ถ้าว่างเกินไปroutes.js:803
smartCreditText.length > 83skip SmartCredit file ถ้าว่างเกินไปroutes.js:807

Encoding — TIS-620 สำคัญ

ไฟล์ KBank และ SmartCredit เป็น fixed-width text ที่ต้องเข้ารหัส TIS-620 (เพราะระบบเก่าของธนาคาร) — ใช้ iconv-lite:

js
iconv.encode(text, 'TIS-620')

ถ้า encode ผิด (เช่น UTF-8) ตัวอักษรไทยจะเพี้ยน และธนาคารจะปฏิเสธไฟล์


5. Import ผลจ่าย — importUpdateStatus

ที่อยู่: hospital.js:2160

หลังจากที่ธนาคารโอนเงินแล้ว จะส่งไฟล์ผลจ่ายกลับมา — HR อัปโหลดไฟล์เข้าระบบ:

1. HR อัปโหลดไฟล์ผลจ่าย (ผ่าน GraphQL Upload)

2. parse ไฟล์ (fixed-width text)

3. map bank code → statusKcash (config.js):
   - filter by element.status === statusKcash[].id
   - ใช้ statusByKcash[0].status เป็น WDHospital.status (SUCCESS หรือ FAILED)
   - เก็บ importStatus = bank code id, importLabel = labelTH

4. UPDATE WDHospital status:
   - id '9' → status='SUCCESS'
   - id '6','7','8','10' → status='FAILED'

5. INSERT WDHistory บันทึกผลจ่าย

6. notify เจ้าของเอกสาร:
   - SUCCESS → "โอนเงินสำเร็จ"
   - FAILED → "โอนเงินไม่สำเร็จ กรุณาติดต่อ..."

statusKcash mapping

ที่มา: vtrc-api/api/src/config.js lines 488–518 — ids ที่มีจริงมีแค่ '6''10' (ไม่มี id '0')

idlabellabelTHstatus
6ISSUED and/or STOPPAY WITH DUPLICATEการดำเนินการและ/หรือหยุดการจ่ายจากการทำซ้ำFAILED
7CANCELLEDยกเลิกรายการFAILED
8STOPPAYหยุดการจ่ายFAILED
9PAIDโอนสำเร็จSUCCESS
10UNSUCCESS CREDITโอนไม่สำเร็จFAILED

Import (hospital.js:2210-2235) ใช้ statusKcash.filter(value => value.id === element.status) แล้วเขียน status: statusByKcash[0].status และ importLabel: statusByKcash[0].labelTH


6. Notification triggers

แต่ละ status transition ส่ง notification:

ActionNotify ใครตัวอย่างข้อความ
submit → PENDINGapprovers"รออนุมัติการเบิก..."
need more info → MOREINFOเจ้าของ"เอกสารไม่ครบ..."
reject → REJECTเจ้าของ"ไม่อนุมัติการเบิก..."
paid → PAIDเจ้าของ"รอการโอนเงิน"
paid cash → PAIDCASHเจ้าของ"รอรับเงินสด"
success → SUCCESSเจ้าของ"โอนเงินสำเร็จ"
failed → FAILEDเจ้าของ"โอนเงินไม่สำเร็จ"

ที่มา: hospital.js:1566-1649

MEDICAL_NOTI_TYPE

31:34:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const MEDICAL_NOTI_TYPE = { 
  detail: "MEDICAL_DETAIL", 
  approving: "MEDICAL_APPROVING" 
}

7. Helper functions

getWithdrawNo — generate running no

2858:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const getWithdrawNo = async (context) => {
  // format MD[YYMM]-[6-digit]
  // reset เมื่อเดือนเปลี่ยน
}

getFiscalYear — ปีงบประมาณไทย

2901:2910:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const getFiscalYear = (date) => {
  const payDate = new Date(date)
  const month = payDate.getMonth() + 1
  // เดือน ม.ค. - ก.ย. เป็นปีงบประมาณปัจจุบัน
  // ตั้งแต่เดือน ต.ค. เป็นปีต่อไป
  if (month < 10) {
    return payDate.getFullYear()
  } else {
    return payDate.getFullYear() + 1
  }
}

getDiffYear — คำนวณอายุ

3144:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const getDiffYear = (birthDate, calDate) => {
  return Math.floor((calDate - birthDate) / 3.15576e+10)  // ms/year
}

3.15576e+10 = มิลลิวินาทีต่อปี (365.25 วัน)

getNextMonthDate — เดือนถัดไป

3146:vtrc-api/api/src/lib/controllers/withdraw/hospital.js
const getNextMonthDate = (startDate) => {
  // handle month wrap (12 → 1, year + 1)
}

ใช้สำหรับเช็ค "พ้นสภาพเกิน 30 วัน" — คือ resignationDate + 1 เดือน


8. Magic numbers / hardcoded values

Valueความหมายที่มา
'88' orgCodeempType = "บำนาญ"hospital.js:1266, 1388
'02', '03' orgCodepermission scope พิเศษhospital.js:107-120
1 ปี (slip date)อายุใบเสร็จสูงสุดhospital.js:2784
30 วัน (resignation)ระยะเวลาหลังพ้นสภาพที่ยังเบิกได้hospital.js:3158
20 ปี (child age)อายุบุตรสูงสุดที่มีสิทธิ์hospital.js:3126
8000 บาทcap ยอดเบิก (ถูก comment ปิด)hospital.js:2748
> 179, > 83threshold สำหรับ skip empty export fileroutes.js:803, 807
3.15576e+10ms/year สำหรับคำนวณอายุhospital.js:3144

9. Cross-service calls

vtrc-api functioncu-central-apiวัตถุประสงค์
getMedical()getMedicalดึงข้อมูลสิทธิ์ + ความสัมพันธ์ + birthDate
employeeDetailCDemployeeProfileดึง profile พนักงาน (resignationDate, workingStatus)

10. Payment lifecycle — ตัวอย่าง end-to-end

Day 1: พนักงานสร้างคำขอ → DRAFT
Day 1: submit → PENDING → notify approver
Day 3: approver อนุมัติ → APPROVE → notify เจ้าของ "อนุมัติแล้ว"
Day 5: HR export ไฟล์ → สร้าง KBank.txt + SmartCredit.txt + FMIS.xlsx
Day 5: status ทุกรายการ → PAID → notify "รอการโอนเงิน"
Day 8: ธนาคารโอนเงินเสร็จ → ส่งไฟล์ผลจ่ายกลับ
Day 8: HR import ไฟล์ → รายการสำเร็จ → SUCCESS / รายการ fail → FAILED
Day 8: notify เจ้าของแต่ละรายการ

(FAILED → CASH ได้ ถ้า HR เลือกจ่ายเงินสดแทน — ACTION_STATUS: FAILED→CASH = "ทำเรื่องจ่ายเงินสด"; ไม่ใช่ PAIDCASH)

11. ข้อระวังทั้งหมด

1. Validation ลดลง (cap ถูกปิด)

  • cap ยอดเบิก 8,000 บาทถูก comment ปิด
  • ปัจจุบัน client ส่งยอดมาเท่าไรระบบรับหมด
  • ความเสี่ยง: พนักงานเบิกเกินสิทธิ์ได้

2. Magic org codes กระจายหลายจุด

'02', '03', '88', '0734' ฝังใน controller หลายจุด — ถ้า org code เปลี่ยน ต้องแก้ทุกที่

3. Encoding TIS-620

ถ้าใครแก้โค้ดส่วน export และใช้ UTF-8 โดยไม่ระวัง — ไฟล์จะใช้งานไม่ได้

4. ไฟล์ controller ใหญ่มาก

hospital.js 3,200 บรรทัด — ยากต่อการ maintain ควรแยกเป็นหลายไฟล์ตาม concern (validation, payment, notification)

5. Dead code ใน hospital.js

มี block ที่ comment ปิดหลายจุด (hospital.js:1690-1738) — ควรลบหรือย้ายไป git history


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

ไป บท 11.4 Payroll / Payslip / Tax