12.4 · Payroll / Payslip / Tax
บทนี้เจาะลึก business logic ของสลิปเงินเดือนและภาษี — การสร้าง PDF, การเข้ารหัส qpdf, การสร้าง signed URL, และชนิดเอกสารทั้ง 6 ประเภท
ภาพรวม
| ด้าน | ค่า |
|---|---|
| ไฟล์ controller | lib/controllers/slip/slip.js (~336 บรรทัด) |
| Repository | lib/repositories/slip/slip.js (~399 บรรทัด — query CD + signed URL) |
| PDF builder | lib/createslip/index.js, lib/genSlip/index.js |
| Encryption | lib/encrypt/index.js |
| Routes | routes.js:51-300 |
| Forward controller | centralize/controllers/slip.js, salaryCert.js |
| ชนิดเอกสาร | 6 ประเภท |
| PDF timeout | 1 ชั่วโมง (signed URL) |
| PDF retention | 5 วินาที (หลัง generate แล้วลบ) |
| PDF encryption | qpdf (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 | ค่า | ความหมาย |
|---|---|---|
year | 2569 | ปี (คริสต์ศักราช — client แปลงเอง) |
month | 07 | เดือน |
pid | uuid | รหัสบุคคล (user.refId) |
empcode | 12345 | รหัสพนักงาน |
device | WEB | ประเภทอุปกรณ์ |
timestamp | 1752000000000 | เวลาที่ sign (ms) |
hash | abc123... | 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 function | cu-central-api query | วัตถุประสงค์ |
|---|---|---|
fetchCDPayRollSlipInfo | payrollSlipDetail | สลิปเดือนเดียว |
fetchCDPayRollSlipInfo6Month | (ลูปยิง 1-6 หรือ 7-12) | สลิป 6 เดือน / 1 ปี |
salaryCertDetail | salaryCertDetail | ข้อมูลหนังสือรับรอง |
fetchCDpwdVerification | pwdVerification | verify เลขบัตรประชาชน |
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 ms | signed URL timeout 1 ชม. | routes.js:55 |
5000 ms | PDF retention (ลบหลัง 5 วิ) | routes.js:82 |
+543 | แปลงปี คศ. → พศ. | formatdate.js:37,42 |
qpdf --encrypt ... 40 | RC4-40 bit encryption | routes.js:73 |
8 หลัก | password format DDMMYYYY | controllers/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 | — | ใช่ |