Tìm Hiểu HRM Domain — Payroll, Workflow và Những Thứ Không Ai Dạy Bạn

Tôi build xong màn hình thêm/sửa/xóa nhân viên trong 2 ngày. Sếp gật đầu hài lòng.

Sau đó: “Tích hợp tính lương với BHXH, thuế TNCN, và overtime đi anh.”

2 tuần sau — tôi vẫn chưa xong. Không phải vì tôi code chậm. Mà vì tôi không hiểu domain.

HRM Không Phải CRUD — Và Bug $1M Chứng Minh Điều Đó

Đây là thực tế của HRM domain: developer hay nghĩ hệ thống nhân sự là Employee table, Leave table, vài form nhập liệu. Done.

Nhưng có một case study nổi tiếng: hai bộ phận — CFO (Kế toán) và COO (Nhân sự) — cùng dùng chung một function regularHours() để tính giờ làm việc bình thường. CFO yêu cầu thay đổi cách tính cho báo cáo tài chính. Developer sửa function. Báo cáo nhân sự của COO sai. Payroll tháng đó tính lương nhầm cho hàng trăm nhân viên.

Thiệt hại: hàng triệu đô la và mất niềm tin của toàn tổ chức.

Nguyên nhân gốc rễ: vi phạm Single Responsibility Principle ở tầng domain. Cùng một function nhưng phục vụ hai actor khác nhau — mỗi actor có quyền lợi và business rule riêng. Khi một actor thay đổi, actor kia bị ảnh hưởng.

Fix đúng: tách thành Payroll.regularHours()HR.regularHours() — cùng logic ban đầu, nhưng sống độc lập. Về cách chọn đúng domain để bắt đầu, xem thêm bài Tổng Quan Các Domain IT.

Payroll — Rule Engine Ẩn Sau 1 Dòng calculatePay()

Đây là nơi hầu hết developer intermediate bị bất ngờ.

Nhìn bề ngoài: calculatePay(employee, period) — đơn giản. Nhưng bên trong:

  • Overtime: giờ thứ 9 trở đi trong ngày = 150% lương. Làm ngày lễ = 200–300% tùy loại lễ.
  • BHXH/BHYT/BHTN: mỗi khoản có mức trần. Lương 50 triệu/tháng nhưng BHXH chỉ tính trên tối đa 29.8 triệu (20 lần lương tối thiểu vùng). Con số này thay đổi mỗi năm.
  • Thuế TNCN lũy tiến: 7 bậc thuế, khấu trừ gia cảnh, giảm trừ bản thân — mỗi nhân viên một con số khác nhau.
  • Pro-rata: nhân viên vào giữa tháng, tháng 2 có 28 hoặc 29 ngày — tính thế nào?

Developer junior viết gì? Một function 300 dòng if-else lồng nhau. Mỗi lần luật thay đổi, phải sửa code, test lại, deploy lại — rủi ro rất cao.

Developer senior xây gì? Table-Driven Rule Engine: các business rules được lưu trong database dưới dạng data, không phải code. Thêm quy tắc mới = insert row, không cần release.

PayrollRuleTable:
  rule_id | rule_type     | threshold   | rate  | effective_from
  001     | OVERTIME      | 8h/day      | 1.5x  | 2024-01-01
  002     | BHXH_CAP      | 29,800,000  | 0.08  | 2024-07-01
  003     | PERSONAL_TAX  | 0-5,000,000 | 0.05  | 2024-01-01

calculatePay() đọc rules từ table → apply theo thứ tự → trả kết quả. Luật thay đổi? Update table. Code không thay đổi.

Workflow State Machine — Tại Sao Leave Status Không Phải Boolean

Nhiều developer lưu trạng thái nghỉ phép bằng is_approved: boolean. Sau đó product manager hỏi: “Nhân viên có thể cancel đơn đã được approve không?”

Boolean không biểu diễn được điều này.

Leave request là một Finite State Machine:

DRAFT -> SUBMITTED -> APPROVED -> CANCELLED (chi khi ngay nghi chua bat dau)
                  -> REJECTED  -> SUBMITTED  (nhan vien chinh sua va nop lai)

Mỗi transition có điều kiện riêng: SUBMITTED -> APPROVED cần manager approve. APPROVED -> CANCELLED chỉ được thực hiện trước ngày nghỉ bắt đầu.

Tại sao FSM quan trọng? Không có FSM tường minh, business logic về transitions sẽ nằm rải rác trong service layer — một chỗ check is_approved, chỗ khác check status == 'approved', endpoint thứ ba không check gì cả. Kết quả: nhân viên có thể “cancel” nghỉ phép đã qua mà không có audit record.

Audit Trail — Khi Mỗi Dòng Data Đều Có Giá Trị Pháp Lý

HRM giống Fintech ở một điểm quan trọng: Integrity > Timeliness.

Lương trả sai có thể dẫn đến tranh chấp lao động. Hồ sơ nhân viên là tài liệu pháp lý. Vì vậy, rule cơ bản: không bao giờ UPDATE hay DELETE trực tiếp vào payroll records.

-- SAI: mat lich su
UPDATE payroll SET salary = 15000000 WHERE employee_id = 42;

-- DUNG: append event moi, giu toan bo lich su
INSERT INTO payroll_events (employee_id, event_type, old_value, new_value, changed_by, changed_at)
VALUES (42, 'SALARY_CHANGE', 12000000, 15000000, 'manager_01', NOW());

Current state = reconstruct từ event log. Ai thay đổi gì, khi nào, approved bởi ai — đều có thể trace lại đầy đủ.

Offboarding edge case: khi nhân viên nghỉ việc, luật bảo vệ dữ liệu yêu cầu xóa thông tin cá nhân (PII). Nhưng audit log payroll phải giữ lại. Solution: tách employee_pii table ra khỏi payroll_audit table. Anonymize PII, giữ nguyên audit trail. Xem thêm hướng dẫn compliance tại GDPR Right to Erasure.

Junior vs Senior HRM Developer — Ranh Giới Là “Policy Is Metadata”

Đây là insight quan trọng nhất trong bài.

Junior khi nhận yêu cầu “chỉ manager cấp 3 trở lên mới được approve tăng lương trên 20%”:

if employee.manager_level >= 3 and salary_increase > 0.2:
    approve()

3 tháng sau: “Anh ơi, sửa thành 25% đi.” → sửa code, test, deploy, rủi ro regression.

Senior xây một Policy Engine:

PolicyTable:
  action        | min_approver_level | max_increase
  SALARY_CHANGE | 3                  | 0.20

Thay đổi threshold = update database. Code không đổi. Không cần release. Không có regression risk.

Temporal Decomposition trap: developer junior tổ chức code theo thứ tự xử lýhandleInput(), processData(), saveResult(). Senior tổ chức theo business domainPayroll.calculateTax(), Leave.submitRequest(). Khi business rule thay đổi, temporal decomposition buộc bạn sửa 5 file; domain organization chỉ sửa 1. Nếu bạn đang gặp triệu chứng này, xem thêm Học Sai Cách.

Kết

HRM không phức tạp theo nghĩa kỹ thuật — không có sub-millisecond latency, không có distributed transaction phức tạp. Nó phức tạp theo nghĩa business rule density.

1 bug trong payroll = hàng trăm nhân viên nhận lương sai. 1 lỗ hổng trong leave approval = tranh chấp lao động. 1 thiếu sót trong audit trail = rủi ro pháp lý cho cả công ty.

Developer hiểu HRM domain biết rằng “chạy được” chưa bao giờ là đủ ở đây. Tiêu chuẩn thực sự là: đúng theo luật lao động, đúng theo business rule hiện tại, và có thể trace lại khi cần.

Bước tiếp theo: ngồi với HR manager và hỏi: “Trường hợp nào tính lương phức tạp nhất trong công ty mình?” Câu trả lời của họ là domain knowledge không tìm thấy trong bất kỳ documentation nào.


Comments

Gửi phản hồi

Khám phá thêm từ Hiểu Code

Đăng ký ngay để tiếp tục đọc và truy cập kho lưu trữ đầy đủ.

Tiếp tục đọc