Skip to content

12.4 · Payroll / Payslip / Tax

บทนี้เจาะลึก business logic ของสลิปเงินเดือนและภาษี — การสร้าง PDF, การเข้ารหัส qpdf, การสร้าง signed URL, และชนิดเอกสารทั้ง 6 ประเภท


ภาพรวม

ด้านค่า
ไฟล์ controllerlib/controllers/slip/slip.js (~336 บรรทัด)
Repositorylib/repositories/slip/slip.js (~399 บรรทัด — query CD + signed URL)
PDF builderlib/createslip/index.js, lib/genSlip/index.js
Encryptionlib/encrypt/index.js
Routesroutes.js:51-300
Forward controllercentralize/controllers/slip.js, salaryCert.js
ชนิดเอกสาร6 ประเภท
PDF timeout1 ชั่วโมง (signed URL)
PDF retention5 วินาที (หลัง generate แล้วลบ)
PDF encryptionqpdf (RC4-40)

1. ชนิดเอกสาร — 6 ประเภท

52:77:vtrc-api/api/src/lib/controllers/slip/slip.js
// concept — dropDownSlipPayment
const slipTypes = [
  { id: '0', label: 'สลิปเงินเดือน' },               // /readfile/prs/...
  { id: '1', label: 'ภ.ง.ด. 1ก (50 ทวิ)' },           // /tax/...
  { id: '2', label: 'ค่าประกอบวิชาชีพอิสระ 40(6) ประจำเดือน' },
  { id: '3', label: 'ค่าประกอบวิชาชีพอิสระ 40(6) ม.ค.-มิ.ย.' },  // /tax6month/...
  { id: '4', label: 'ค่าประกอบวิชาชีพอิสระ 40(6) ก.ย.-ธ.ค.' },
  { id: '5', label: 'หนังสือรับรองเงินเดือน' }        // /salarycert/...
]

Route mapping

ประเภทRoute prefixเข้ารหัส qpdfใช้ hash
สลิปเงินเดือน/readfile/prs/ใช่ใช่
ภ.ง.ด. 1 ปี/tax/ใช่ใช่
ภ.ง.ด. 6 เดือน/tax6month/ใช่ใช่
ภ.ง.ด. แบบปี (1-12)/taxyears/ใช่ใช่
หนังสือรับรองเงินเดือน/salarycert/ไม่ใช่ไม่ใช่

2. การคำนวณ — ทั้งหมดอยู่ใน cu-central-api

vtrc-api ไม่ได้คำนวณภาษีเองเลย — ส่งต่อไป cu-central-api:

fetchCDPayRollSlipInfo(tokenCD, { profileKey, year, month })

cu-central-api ส่งกลับ:
{
  income: [{ code, name, amount }],
  deduct: [{ code, name, amount }],
  payrollSlip: {
    totalIncome,
    totalDeduct,
    cumulativeTax,
    cumulativeBaseTax,
    baseTax401,
    baseTax406,
    ...
  }
}

vtrc-api แค่ map ฟิลด์สำหรับแสดงผล:

68:99:vtrc-api/api/src/lib/repositories/slip/slip.js
// concept — map income/deduct
const income = result.income.map(item => ({
  code: item.code,
  name: item.name,
  amount: item.amount
}))
// ลดหย่อน 40(1), 40(6) ฯลฯ มาจาก CD ทั้งหมด

3. PDF generation flow

Pre-check ก่อนออกสลิป

19:23:vtrc-api/api/src/lib/repositories/slip/slip.js
// ตรวจ password = เลขบัตรประชาชน
const verification = await fetchCDpwdVerification(tokenCD, { pwd: identityCard })
if (!verification) throw responError('PWD_INVALID')

Route handler — concept

js
// routes.js:51-99 (concept — สลิปเงินเดือน)
app.get('/readfile/prs/:year/:month/:pid/:empcode/:device/:timestamp/:hash', async (req, res) => {
  const { year, month, pid, empcode, device, timestamp, hash } = req.params
  
  // 1. ตรวจ timeout 1 ชั่วโมง
  const age = Date.now() - parseInt(timestamp)
  if (age > 3600 * 1000) return res.sendStatus(404)
  
  // 2. ตรวจ HMAC hash
  const expected = await EncryptAll(
    `${year}:${month}:${pid}:${empcode}:${device}:${timestamp}:${DATA_ENCRYPT_KEY}`,
    'sha256'
  )
  if (hash !== expected) return res.sendStatus(404)
  
  // 3. ดึงข้อมูล payroll จาก cu-central-api
  const payrollData = await createslip(req)  // → fetchCDPayRollSlipInfo
  
  // 4. Render template → PDF
  //    ใช้ EJS template + html-pdf (PhantomJS)
  //    สร้างไฟล์ชั่วคราว ${file}1.pdf
  
  // 5. qpdf encryption
  const cmd = `qpdf --encrypt ${password} ${password} 40 -- ${file}1.pdf ${file}.pdf && rm ${file}1.pdf`
  execSync(cmd)
  
  // 6. ส่งไฟล์กลับ client
  res.sendFile(pdfPath)
  
  // 7. ลบไฟล์หลัง 5 วินาที
  setTimeout(() => deletePDF(pdfPath), 5000)
})

ที่มา: routes.js:51-99


4. qpdf encryption — password = วันเดือนปีเกิด (พศ.)

73:73:vtrc-api/api/src/routes.js
let cmd = `qpdf --encrypt ${allData.passwordPDF} ${allData.passwordPDF} 40 -- ${allData.pathfilePDF}/${allData.file}1.pdf ${allData.pathfilePDF}/${allData.file}.pdf && rm ${allData.pathfilePDF}/${allData.file}1.pdf`

Password format

  • DDMMYYYY (พศ.) — เช่น 05012511 (5 มกราคม 2511)
  • 8 หลัก

Password hint text

41:43:vtrc-api/api/src/lib/controllers/slip/slip.js
const fixedText = `กรุณาระบุรหัสผ่าน 8 หลัก รหัสของท่านคือ DDMMYYYY
DD: วันที่วันเกิดของท่าน
MM: เดือนเกิดของท่าน
YYYY: ปี พศ. เกิดของท่าน
ตัวอย่างเช่น ถ้าท่านเกิดวันที่ 5 มกราคม 2511
รหัสของท่านคือ 05012511
`

ทำไมต้องเข้ารหัส

  • ถ้า URL รั่ว — ผู้อื่นไม่สามารถเปิด PDF ได้ถ้าไม่รู้วันเกิด
  • เป็นชั้นป้องกันเพิ่มจาก signed URL

จุดอ่อน

  • ถ้าผู้อื่นรู้วันเกิด → เปิดได้
  • วันเกิดเป็นข้อมูลที่เดาได้ง่าย (จาก Facebook, ฯลฯ)

5. Signed URL — createUrlRoutes

300:314:vtrc-api/api/src/lib/repositories/slip/slip.js
const createUrlRoutes = async (context, month, year, profileKey, part) => {
    const user = await Models.User.findOne({ where: { userId: context.userId } })
    const currentSession = await getCurrentProfileSession(context.userId, context.device)
    let currentProfileKey = currentSession.currentProfile
    if (profileKey) { currentProfileKey = profileKey }
    let EmpCode = currentProfileKey;
    let personId = user.refId;
    let device = context.device
    let hashURL = await EncryptAll(
      `${year}:${month}:${personId}:${EmpCode}:${device}:${Date.now().toString()}:${process.env.DATA_ENCRYPT_KEY}`,
      'sha256'
    );
    let returnPath = await path.resolve(
      `${API_PREFIX_PATH}/${part}/`,
      year.toString(),
      month.toString(),
      personId,
      EmpCode,
      device,
      Date.now().toString(),
      hashURL
    )
    returnPath = await process.env.WEB_URL + returnPath
    return returnPath
}

URL structure

https://vtrcapi.redcross.or.th/readfile/prs/[year]/[month]/[pid]/[empcode]/[device]/[timestamp]/[hash]
Segmentค่าความหมาย
year2569ปี (คริสต์ศักราช — client แปลงเอง)
month07เดือน
piduuidรหัสบุคคล (user.refId)
empcode12345รหัสพนักงาน
deviceWEBประเภทอุปกรณ์
timestamp1752000000000เวลาที่ sign (ms)
hashabc123...SHA-256

Timeout 1 ชั่วโมง

js
// routes.js:55 (concept)
if (Date.now() - timestamp > 3600 * 1000) return res.sendStatus(404)

6. PDF retention — 5 วินาที

js
// routes.js:73-82 (concept)
res.sendFile(pdfPath)
setTimeout(() => deletePDF(pdfPath), 5000)

ปัญหา

ถ้า client download ช้ากว่า 5 วินาที → ไฟล์ถูกลบก่อน → 404

ดูรายละเอียดใน บท 11.11


7. ปีงบประมาญไทย (พศ.)

vtrc-api แปลงปี ค.ศ. → พศ. หลายจุด:

37:42:vtrc-api/api/src/lib/formatdate.js
// concept
const changeYear = (year) => year + 543
const changeFormatdate = (date) => {
  // ...
  const yyyy = (parseInt(moment(date).format('YYYY')) + 543).toString()
  return newdate
}

ข้อระวัง: client ต้องส่ง year เป็น คศ. เสมอ เพราะ cu-central-api ใช้ คศ. — การแปลงเป็น พศ. เกิดตอน render PDF เท่านั้น


8. Salary certificate — ไม่มี qpdf

248:298:vtrc-api/api/src/lib/repositories/slip/slip.js
const createUrlSalaryCertRoutes = async (context, documentType, profileKey) => {
  // concept — ไม่มี hash, ไม่มี qpdf
  // ใช้แค่ profileKey + documentType
  // เป็น PDF ธรรมดา
}

เพราะ salary certificate มักใช้กับบุคคลที่สาม (ธนาคาร, หน่วยงานราชการ) — การเข้ารหัสจะทำให้ใช้งานยาก

Working status check

js
// repositories/slip/slip.js:154 (concept)
if (workingStatus !== 'Working') {
  // disable การขอ salary cert
}

9. Cross-service calls

vtrc-api functioncu-central-api queryวัตถุประสงค์
fetchCDPayRollSlipInfopayrollSlipDetailสลิปเดือนเดียว
fetchCDPayRollSlipInfo6Month(ลูปยิง 1-6 หรือ 7-12)สลิป 6 เดือน / 1 ปี
salaryCertDetailsalaryCertDetailข้อมูลหนังสือรับรอง
fetchCDpwdVerificationpwdVerificationverify เลขบัตรประชาชน

Performance issue — 6 เดือน / 1 ปี

347:398:vtrc-api/api/src/lib/repositories/slip/slip.js
// concept — checkExistYearPayroll
for (let m = 1; m <= 6; m++) {
  await fetchCDPayRollSlipInfo(tokenCD, { profileKey, year, month: m })
}
// หรือ 7-12 หรือ 1-12

ยิง cu-central-api 6-12 ครั้งต่อ 1 request — ถ้า CD ช้า vtrc-api ก็ช้าตาม


10. Magic numbers / hardcoded values

Valueความหมายที่มา
3600 * 1000 mssigned URL timeout 1 ชม.routes.js:55
5000 msPDF retention (ลบหลัง 5 วิ)routes.js:82
+543แปลงปี คศ. → พศ.formatdate.js:37,42
qpdf --encrypt ... 40RC4-40 bit encryptionroutes.js:73
8 หลักpassword format DDMMYYYYcontrollers/slip/slip.js:41

11. PDF template — EJS + html-pdf (PhantomJS)

PDF ถูกสร้างผ่าน html-pdf ซึ่งใช้ PhantomJS ใต้ฝา:

1. ดึง template EJS (lib/createslip/templates/*.ejs)
2. render ด้วยข้อมูล payroll
3. html-pdf แปลง HTML → PDF
4. qpdf encrypt

ข้อจำกัดของ PhantomJS

  • เลิกพัฒนาแล้ว (abandoned) — ปัจจุบันเป็น security risk
  • ใช้ WebKit เก่า — CSS บางตัวไม่รองรับ
  • ช้ากว่า browser-based PDF generator สมัยใหม่ (Puppeteer, Playwright)

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

1. PDF retention 5 วินาทีอาจไม่พอ

  • ถ้า client อยู่ในเครือข่ายช้า → download เกิน 5 วินาที → 404
  • แนะนำให้เพิ่มเป็น 5 นาที หรือใช้ streaming

2. PhantomJS เลิกพัฒนาแล้ว

  • ควรเปลี่ยนเป็น Puppeteer หรือ Playwright

3. ยิง CD 6-12 ครั้งต่อ 1 request

  • สลิป 6 เดือน / 1 ปี ช้ามาก
  • ควรมี cache หรือ batch query

4. Password = วันเกิด (พศ.)

  • ถ้าผู้อื่นรู้วันเกิด → เปิด PDF ได้
  • ความปลอดภัยต่ำ

5. ไม่มี rate limit

  • ผู้ใช้สามารถขอ PDF รัว ๆ → ทำให้ server ทำงานหนัก

6. Salary certificate ไม่เข้ารหัส

  • ถ้า URL รั่ว → เปิดได้ทันที
  • แต่อาจตั้งใจ — เพราะต้องส่งให้บุคคลที่สาม

13. Summary — payroll domain

ด้านทำใน vtrc-apiส่งต่อ cu-central-api
ดึงข้อมูล payrollใช่ (ทั้งหมด)
คำนวณภาษีใช่ (ทั้งหมด)
สร้าง signed URLใช่
Render PDFใช่ (PhantomJS)
qpdf encryptionใช่
Password hintใช่
Working status checkใช่

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

ไป บท 11.5 Provident Fund