Skip to content

9.7 · ERD — Welfare withdrawal (WDHospital)

โดเมนนี้คือ financial flow ที่สำคัญที่สุดของระบบ — พนักงานเบิกค่ารักษาพยาบาล → ส่งห่อง chain → export ไปธนาคาร เงินจริงไหลผ่านตรงนี้ เลยต้องระวังเป็นพิเศษ

บทนี้เป็น ERD ที่ใหญ่ที่สุดใน Volume — มี relation มาก + state machine ซับซ้อน (13 สถานะ)


Tables ใน domain

ตารางPKหน้าที่
WDHospitalwdHospitalId UUIDใบเบิกหลัก (พนักงาน / ผู้รับการรักษา / ยอดเงิน)
WDSlipwdSlipId UUIDใบเสร็จที่แนบมากับใบเบิก
WDHospitalDocumentdocumentId UUIDเอกสารแนบอื่นๆ (ใบรับรองแพทย์, สำเนาบัตร)
WDHistoryhistoryId UUIDaudit trail
WDExportwdExportId UUIDbatch export ไปธนาคาร (KBANK / OTHER)
ApproverapproverId UUIDผู้อนุมัติ (shared กับ LeaveDocument)
MasterApproverapproverId UUIDconfig ว่า orgCode ไหนมี approver ใคร
SicknesssicknessId STRINGmaster data ของโรค
HospitalhospitalId UUIDmaster data ของโรงพยาบาล
HospitalBankAccounthospitalBankAccountId UUIDบัญชีธนาคารของโรงพยาบาล
DelegateUUIDผู้รับมอบฉันทะ (ใช้ตอน sign บางกรณี)

ERD — ASCII vertical flow

                    vtrc schema (db)
   ┌─────────────────────────────────────────────────────────────┐
   │                                                              │
   │  WDHospital                                                  │
   │  ├── wdHospitalId (PK, UUID)                                 │
   │  ├── empCode / empFirstName / empLastName / empFullName      │
   │  ├── empType / workingStatus / resignationDate               │
   │  ├── withdrawFullName / withdrawFirstName / withdrawLastName │
   │  ├── withdrawIdCard (STRING — บัตรประชาชนผู้ป่วย)            │
   │  ├── relation (STRING — self|spouse|child|parent)            │
   │  ├── age (INTEGER)                                           │
   │  ├── orgCode / orgName / department / division               │
   │  ├── totalWithdraw (DECIMAL 12,2 — ยอดรวมที่เบิก)             │
   │  ├── totalCurrentWithdraw (DECIMAL 12,2)                     │
   │  ├── previousCurrentWithdraw (DECIMAL 12,2)                  │
   │  ├── status (ENUM — 13 ค่า!)                                  │
   │  ├── level (INTEGER — current approval level)                │
   │  ├── isHistory (INTEGER)                                     │
   │  ├── sicknessId (FK) + sicknessName (STRING — legacy)        │
   │  ├── hospitalId (FK) + hospitalName (STRING — legacy)        │
   │  ├── hospitalBankAccountId (FK)                              │
   │  ├── treatmentStartDate / treatmentEndDate (DATE)            │
   │  ├── effectivePayDate (DATE)                                 │
   │  ├── wdExportId (FK → WDExport — null ถ้ายังไม่ export)        │
   │  ├── refId (UUID — link ไป record ก่อนแก้)                    │
   │  ├── documentNo / withdrawNo (STRING)                        │
   │  ├── bank (ENUM 'KBANK' | 'OTHER')                           │
   │  ├── cashFlg (STRING)                                        │
   │  ├── coaCodeName (STRING — ซ้ำ! declare สองครั้ง)             │
   │  └── refDocument (STRING)                                    │
   │      │                                                       │
   │      │ hasMany                                                │
   │      ▼                                                       │
   │  WDSlip (ใบเสร็จ)                                             │
   │  ├── wdSlipId (PK, UUID)                                     │
   │  ├── wdHospitalId (FK logical)                               │
   │  ├── slipNo / numberSlip (STRING)                            │
   │  ├── date (DATE)                                             │
   │  ├── totalPrice (DECIMAL 12,2 — ยอดรวมใบเสร็จ)               │
   │  ├── totalSlipWithdraw (DECIMAL 12,2 — ยอดที่เบิกจากใบเสร็จนี้) │
   │  └── fileId (FK → File)                                      │
   │                                                              │
   │  WDHospitalDocument (เอกสารแนบ)                               │
   │  ├── documentId (PK, UUID)                                   │
   │  ├── wdHospitalId (FK logical)                               │
   │  └── fileId (FK → File)                                      │
   │                                                              │
   │  Approver (shared กับ LeaveDocument)                          │
   │  ├── approverId (PK, UUID)                                   │
   │  ├── wdHospitalId (FK logical)                               │
   │  ├── wdLeaveId (FK logical ของฝั่ง leave — null ถ้าเป็น WD)   │
   │  ├── empCode (STRING)                                        │
   │  ├── level / role / canEdit / isAction                       │
   │                                                              │
   │  WDHistory                                                   │
   │  ├── historyId (PK, UUID)                                    │
   │  ├── refTaskHistoryId (UUID — targetKey)                     │
   │  ├── payload (JSON)                                          │
   │  └── ...                                                     │
   │                                                              │
   │  WDExport (batch ไปธนาคาร)                                   │
   │  ├── wdExportId (PK, UUID)                                   │
   │  ├── exportCode (STRING — unique)                            │
   │  ├── years (STRING)                                          │
   │  ├── payDate (DATE)                                          │
   │  ├── isImport (INTEGER — 0|1, นำเข้าแล้วหรือยัง)               │
   │  ├── isCancel (INTEGER — 0|1, ยกเลิก batch หรือยัง)            │
   │  ├── cancelNote / cancelBy / cancelDate                      │
   │  ├── totalExport / totalImport (INTEGER — count)             │
   │  ├── wdHospitalIds (TEXT — JSON list ของ hospital ids)        │
   │  ├── exportBy (UUID)                                         │
   │  └── totalCurrentWithdraw (DECIMAL 12,2 — ยอดรวม batch)      │
   │                                                              │
   │  MasterApprover (config)                                     │
   │  ├── approverId (PK, UUID)                                   │
   │  ├── empCode (STRING)                                        │
   │  ├── userId (STRING — ผิด type! ควรเป็น UUID)                 │
   │  ├── orgCode (STRING — key หลัก)                              │
   │  ├── orgName (STRING)                                        │
   │  ├── isActive (BOOLEAN)                                      │
   │  ├── type (ENUM "MEDICAL")                                   │
   │  └── createdBy / updatedBy                                   │
   │                                                              │
   │  Sickness (master)                                           │
   │  ├── sicknessId (PK, STRING)                                 │
   │  └── sicknessName (STRING)                                   │
   │                                                              │
   │  Hospital (master)                                           │
   │  ├── hospitalId (PK, UUID)                                   │
   │  ├── hospitalCode / hospitalName / hospitalShortName         │
   │  ├── affiliation / affiliationNew (STRING — legacy migration)│
   │  ├── province / address / phoneNumber                        │
   │  ├── isDeleted (BOOLEAN — soft delete)                       │
   │  │      │                                                    │
   │  │      │ hasMany                                            │
   │  │      ▼                                                    │
   │  │  HospitalBankAccount                                       │
   │  │  ├── hospitalBankAccountId (PK, UUID)                     │
   │  │  ├── hospitalId (FK → Hospital)                           │
   │  │  ├── accountNo / accountName (STRING)                     │
   │  │  └── bank (ENUM)                                          │
   │  │                                                           │
   └─────────────────────────────────────────────────────────────┘

State machine — ENUM 13 ค่า

WDHospital.status เป็น ENUM ที่ยาวที่สุดในระบบ

ค่าความหมาย
DRAFTร่าง (ยังไม่ส่ง)
PENDINGรออนุมัติ
MOREINFOต้องการข้อมูลเพิ่ม
APPROVEอนุมัติแล้ว (รอ export)
REJECTปฏิเสธ
CANCELยกเลิก
CANCELDOCยกเลิกเอกสาร
PAIDจ่ายเงินแล้ว (หลัง export)
PAIDCASHจ่ายเงินสดแล้ว
CASHรอจ่ายเงินสด
EDITแก้ไข (หลัง approve แล้วกลับมาแก้)
SUCCESSสำเร็จ (final)
FAILEDล้มเหลว (export fail)
              submit
   DRAFT ─────────────► PENDING ──────► APPROVE ──────► PAID ──────► SUCCESS
                          │                │              │              │
                          │ reject         │ cancel       │ cash         │
                          ▼                ▼              ▼              ▼
                       REJECT           CANCEL        PAIDCASH       (end)

              moreinfo                                    edit
   PENDING ──────────► MOREINFO ─────► (resubmit)   APPROVE ─────► EDIT ─► (re-approve)

(transition จริงซับซ้อนกว่านี้ — นี่คือ overview)


Duplicated columns — bug ที่ซ่อนอยู่

WDHospital มี column coaCodeName และ isJuristic declare สองครั้งในไฟล์เดียวกัน

// hospital.js (WDHospital)
coaCodeName: { type: Sequelize.STRING, ... }   // บรรทัด ~14
// ... 100 บรรทัด ...
coaCodeName: { type: Sequelize.STRING, ... }   // บรรทัด ~138 (ซ้ำ)

Sequelize define() merge attribute declarations — declaration ตัวหลังชนะ ตัวแรกหายไปอย่างเงียบ

ผลกระทบ:

  • ถ้า declaration ทั้งสองเหมือนกันทุกประการ → ไม่มีผล
  • ถ้าต่างกัน (เช่น allowNull ต่างกัน) → behavior ผิดไปจากที่คาด
  • code review ที่ไม่สังเกต → bug ที่หายากมาก

Double belongsTo(Hospital) — migration half-step

256:257:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.belongsTo(models.Hospital, { as: "hospitalWithName", foreignKey: 'hospitalName', targetKey: 'hospitalId' })
    models.WDHospital.belongsTo(models.Hospital, { as: "hospital", foreignKey: 'hospitalId', targetKey: 'hospitalId' })

WDHospital reference Hospital สองทาง:

  • hospital — join ผ่าน hospitalId (UUID FK)
  • hospitalWithName — join ผ่าน hospitalName (string)

อย่างเดียวกันกับ Sickness:

254:255:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.belongsTo(models.Sickness, { as: "sicknessWithName", foreignKey: 'sicknessName', targetKey: 'sicknessId' })
    models.WDHospital.belongsTo(models.Sickness, { as: "sickness", foreignKey: 'sicknessId', targetKey: 'sicknessId' })

เหตุผล — migration ครึ่งทาง

เดิม:

  • WDHospital เก็บ hospitalName (string) โดยตรง ไม่มี FK

ทีหลัง:

  • เพิ่ม hospitalId (UUID) เป็น reference ที่ถูกต้อง
  • แต่ยังไม่ลบ hospitalName เพราะ:
    • record เก่าที่ hospitalId null ยังอ่านได้ผ่าน hospitalName
    • ไม่มี migration script ที่จะ backfill hospitalId จาก hospitalName

ผลกระทบ:

  • บาง query ใช้ hospitalId, บาง query ใช้ hospitalName → inconsistent
  • ถ้าชื่อโรงพยาบาลเปลี่ยน → hospitalWithName join ไม่ติด (string เปลี่ยน)
  • ไม่รู้ว่าข้อมูลใหม่ใช้ relation ไหน — ต้องเช็กที่ controller

MasterApprover — config-driven approver chain

1:42:vtrc-api/api/src/models/core/masterData/masterApprovers.js
export default (db, Sequelize) => {
  var MasterApprover = db.define('MasterApprover', {
    // ...
    empCode: { type: Sequelize.STRING, allowNull: false },
    userId: { type: Sequelize.STRING, allowNull: false },     // ← ผิด type! ควรเป็น UUID
    orgCode: { type: Sequelize.STRING, allowNull: false },
    orgName: { type: Sequelize.STRING, allowNull: false },
    isActive: { type: Sequelize.BOOLEAN, ... },
    type: { type: Sequelize.ENUM("MEDICAL"), ... },

config ว่า orgCode ไหนมี approver ใคร — ใช้ตอน auto-generate Approver row ตอนสร้าง WDHospital

การ join:

251:251:vtrc-api/api/src/models/core/withdraw/hospital.js
    models.WDHospital.hasMany(models.MasterApprover, { as: 'masterApprover', foreignKey: 'orgCode', targetKey: 'orgCode' })

WDHospital.orgCode ←→ MasterApprover.orgCode — ไม่ใช่ PK join แต่ใช้ business key

Bug — userId type ผิด

userId declare เป็น STRING แต่ทุกที่ในระบบใช้ UUID — ทำให้:

  • index size ใหญ่กว่าที่ควร
  • ไม่ enforce format
  • ถ้ามีการ cast เป็น UUID ใน query → silent fail

WDSlip มี relation ซ้อน

48:52:vtrc-api/api/src/models/core/withdraw/slip.js
  WDSlip.associate = (models) => {
    models.WDSlip.belongsTo(models.File, { as: "slipFile", foreignKey: 'fileId' })
    models.WDSlip.hasMany(models.File, { as: "slipMultiFile", foreignKey: 'wdSlipId' })
    models.WDSlip.belongsTo(models.WDHospital, { as: "wdHospital", foreignKey: 'wdHospitalId'})
  }

WDSlip reference File สองแบบ:

  • slipFile — ไฟล์เดียวผ่าน fileId (legacy)
  • slipMultiFile — หลายไฟล์ผ่าน wdSlipId (ใหม่)

อีก half-step migration — เดิม slip มีไฟล์เดียว ทีหลังเปลี่ยนเป็นหลายไฟล์ แต่ยังไม่ลบ relation เดิม


Money columns — ถูกต้อง

สำคัญมาก — field ที่เกี่ยวกับเงินทั้งหมดใช้ DECIMAL(12,2) ไม่ใช่ FLOAT

FieldType
WDHospital.totalWithdrawDECIMAL(12,2)
WDHospital.totalCurrentWithdrawDECIMAL(12,2)
WDHospital.previousCurrentWithdrawDECIMAL(12,2)
WDSlip.totalPriceDECIMAL(12,2)
WDSlip.totalSlipWithdrawDECIMAL(12,2)
WDExport.totalCurrentWithdrawDECIMAL(12,2)

รองรับยอดสูงสุด 999,999,999,999.99 (เกินพอ)


WDExport.wdHospitalIds — JSON-encoded list

51:54:vtrc-api/api/src/models/core/withdraw/export.js
    wdHospitalIds: {
      type: Sequelize.TEXT,
      allowNull: true
    },

เก็บ list ของ hospital ID ใน TEXT column เป็น JSON — เช่น ["uuid1","uuid2","uuid3"]

ผลกระทบ:

  • query "export batch นี้มี hospital อะไรบ้าง" → ต้อง parse JSON ในแอป
  • ไม่ enforce relation จริง → ถ้า hospital ถูกลบ IDs ยังค้าง
  • index ไม่ได้

เคล็ดลับตอนทำงานจริง

ถ้า query WDHospital แล้ว hospital relation null

  • เช็กว่าใช้ alias hospital (FK hospitalId) หรือ hospitalWithName (string hospitalName)
  • ถ้า record เก่า hospitalId อาจจะ null — ใช้ hospitalWithName แทน

ถ้าเจอ WDHospital ที่ status ไม่ logical (เช่น PAID แต่ยังไม่ APPROVE)

state inconsistency — เกิดจาก manual update ใน DB หรือ bug ใน controller ต้อง manual fix

ถ้า approver chain ไม่ครบ

  • เช็ก MasterApprover ของ orgCode นั้น — ถ้าไม่มี row → chain ไม่ generate
  • ถ้า MasterApprover.isActive = false → ข้าม

อย่าเชื่อ totalWithdraw ใน WDHospital

ยอดรวมควรเป็น sum ของ WDSlip.totalSlipWithdraw ทั้งหมด — ถ้าไม่ตรงแปลว่ามี denormalized field ที่ไม่ sync

ถ้า export batch fail

เช็ก WDExport.isImport — ถ้าเป็น 0 แปลว่ายังไม่ import กลับมาจากธนาคาร ถ้า isCancel = 1 แปลว่า batch ถูกยกเลิก แต่ WDHospital ใน batch จะกลับไป status APPROVE


ไป 8.8 ERD — Study, Fund, Notification