Files
ERP-core/docs/12-BACKEND-HRM.md
2026-07-23 19:54:56 +05:30

23 KiB
Raw Permalink Blame History

12 · BACKEND — Phase 2 Spec (HRM)

Companion to 10-BACKEND-PHASE1.md. Same rules apply unless noted otherwise here — this file adds HRM-specific requirements, decisions, and the ER model; it does not repeat Part A's layering rules verbatim (see 00-CORE.md §4 and 10-BACKEND-PHASE1.md Part A, which remain in force for every module).


Part A — HRM-specific architecture notes

A.1 What's new vs. Phase 1

  • New cross-cutting fix: the local shadow User entity (Domain/Entities/User.cs) gains a nullable Email column + case-insensitive filtered unique index. This is additive; no existing Phase-1 behavior changes.
  • New infra layer: Infra/Storage/IFileStorageService (+ LocalFileStorageService) — the first file-upload/attachment mechanism in the codebase. No other module currently has one; HRM introduces the abstraction, other modules may adopt it later.
  • New service namespace: Services/Hrm/* — mirrors Services/Stock/* (domain calculation services distinct from CRUD services): AttendanceComputationService (analog of FifoCostingService), PayrollCalculationService, EmployeeUserLinkService.
  • New packages: ClosedXML (.xlsx read/write), CsvHelper (.csv read/write) — both added for Attendance upload/template generation only.
  • Decisions carried into this phase (confirmed with the business owner, not re-litigated per module):
    Decision Value
    Document storage Local disk, behind IFileStorageService; cloud swap is a future DI change, not a schema change
    Leave Management In scope now (lightweight) — feeds Attendance's OnLeave status and Payroll's No-Pay calc
    Payslip output Server-rendered HTML/print view; PDF export explicitly deferred
    Statutory rules Sri Lanka (EPF 8%/12%, ETF 3% employer-only); tax via a configurable TaxSlab table, not hardcoded
    RBAC Still deferred repo-wide (per Phase 1 AR-01), but flagged in 02-SECURITY.md §C.8 as a decision to make explicitly for HRM, not silently inherited

A.2 Employee.EmployeeCode numbering (deviation from NumberSequence)

Unlike PurchaseOrder/Grn/other Phase-1 documents, Employee.EmployeeCode is user-entered, validated for uniqueness server-side (EMPLOYEE_CODE_DUPLICATE), not generated via NumberSequenceService. Rationale: NumberSequence is year-scoped ({DocType}-{Year}-{00000}), the wrong shape for an identifier that must never look "reset" across years; HR departments also typically already have a legacy numbering scheme to preserve at go-live. All other new HRM transactional documents (AttendanceUploadBatch, LeaveRequest, EmployeeLoan, PayrollRun) do use NumberSequenceService.NextAsync(docType) as-is (ATT-, LV-, LOAN-, PAY-).

A.3 Snapshot-at-ingestion rule (attendance integrity)

AttendanceRecord.WorkShiftId is captured from Employee.WorkShiftId at the moment a record is ingested, not read live from Employee at report time. If an employee's shift assignment changes later, historical Late/OT/Working-Hours figures for already-recorded dates must not silently change. Any service reading AttendanceRecord for computation always joins to the record's own WorkShiftId, never the employee's current one.

A.4 Lock-boundary rule (payroll integrity)

LoanInstallment.PayrollRunId is stamped, and Loan.OutstandingBalance decremented, only when a PayrollRun transitions to Locked — not at Generate/Draft and not at Approve. This is deliberate: a Draft payroll run may be regenerated or discarded; consuming a loan installment before the run is truly final would make regeneration lossy. The same boundary applies to AttendanceUploadBatch.Status flipping Confirmed → UsedInPayroll.


Part B — Software Requirements Specification

B.1 Scope

Phase 2 (HRM) covers: Employee/Staff Management (incl. the Employee↔User login cross-link and staff document attachments), Attendance (Excel/CSV upload → validate → confirm), a lightweight Leave module (feeds Attendance/Payroll), and Payroll (calculate → review → approve → lock → payslips), plus read-only Reports over all of the above. Out of scope for this phase (see §B.7): PDF payslip generation, employee self-service portal, notification/email infrastructure, biometric device integration (schema seam reserved only), tiered late/OT policy, multi-emergency-contact and structured education-history child tables, exit/clearance workflow.

B.2 User classes

  • HR Administrator — full CRUD on Employees, masters, Attendance batches, Payroll runs; the only role that can Unlock a payroll run or supersede a confirmed attendance duplicate.
  • HR Staff / Payroll Clerk — day-to-day upload/validate/confirm attendance, generate payroll drafts, upload staff documents.
  • Approver (Department Head / Finance) — approves Leave Requests, approves/locks Payroll runs.
  • System User (non-HR) — no HRM access; sidebar sections hidden via existing NavItem/RolePermission mechanism (see 02-SECURITY.md §C.8).

Per-endpoint RBAC enforcement is still deferred (mirrors Phase 1 AR-01); the role split above is currently a UI-visibility convention only, not a server-enforced one, except where explicitly noted (Unlock actions).

B.3 Functional Requirements

B.3.1 Employee & Org Masters (FR-HR-MD)

  • FR-HR-MD-01 Maintain Branch, Department (unlimited self-nesting, cycle-guarded), Designation, EmploymentType, WorkShift masters — standard deactivate-not-delete CRUD.
  • FR-HR-MD-02 Maintain Employee records with the full field set in Part C.2; EmployeeCode unique, user-entered; Status lifecycle Active→Suspended/Resigned/Terminated/Retired, never hard-deleted.
  • FR-HR-MD-03 Maintain one or more EmployeeBankDetail rows per employee, exactly one marked IsPrimary.
  • FR-HR-MD-04 Bidirectional email-based cross-check between Employee and User (see A.5/§B.3.2) — advisory only, human-confirmed link.
  • FR-HR-LINK-01 GET /employees/email-lookup?email= and GET /users/email-lookup?email= return the matching record (or null) for the other side, given an email.
  • FR-HR-LINK-02 Employee.UserId is nullable; when set, unique (one User backs at most one Employee), enforced by a filtered unique index plus a friendly 409 EMPLOYEE_ALREADY_LINKED/USER_ALREADY_LINKED service check.
  • FR-HR-LINK-03 Linking is only ever explicit (via linkEmployeeId/linkUserId on create, or POST/DELETE /employees/{id}/link-user) — never automatic, even on an exact email match.

B.3.3 Staff Documents (FR-HR-DOC)

  • FR-HR-DOC-01 Maintain HrDocumentType catalog (deactivate-not-delete; 409 DOCUMENT_TYPE_IN_USE if referenced).
  • FR-HR-DOC-02 Upload an EmployeeDocument against a document type; server validates extension allowlist + content-type match + max size (422 FILE_TYPE_NOT_ALLOWED / 413 FILE_TOO_LARGE).
  • FR-HR-DOC-03 Download requires authentication; files are never served via a static/guessable URL.
  • FR-HR-DOC-04 Archive (not delete) a document row; the on-disk file may be retained or purged per retention policy (not specified further in this phase).
  • FR-HR-DOC-05 HrDocumentType.ExpiryTracked + EmployeeDocument.ExpiryDate support a document-expiry report (§B.3.7).

B.3.4 Attendance (FR-HR-ATT)

  • FR-HR-ATT-01 Upload Excel (.xlsx) or CSV attendance file: columns Employee Code | Date | Check In | Check Out. Creates an AttendanceUploadBatch in Draft.
  • FR-HR-ATT-02 Downloadable template (GET /attendance-batches/template.xlsx/?format=csv) generated from the same column-mapping the parser uses.
  • FR-HR-ATT-03 Validation on upload: employee-code resolution (must exist & be Active), date/time parse + sanity (not future-dated, not before hire date), within-batch duplicate detection, cross-batch-already-confirmed duplicate detection (flagged separately, requires an explicit "supersede" action).
  • FR-HR-ATT-04 Preview computes, per record, against the employee's WorkShift: WorkingMinutes, LateMinutes, EarlyLeaveMinutes, OvertimeMinutes, and derives AttendanceStatus (Present/Absent/HalfDay/OnLeave/Holiday/WeekOff) — OnLeave derived from an overlapping Approved LeaveRequest (§B.3.5).
  • FR-HR-ATT-05 Confirmation flow: manual record edit, duplicate resolution, then validate (Draft→Validated, blocks if unresolved errors/duplicates remain — 422 ATTENDANCE_DUPLICATE_UNRESOLVED), then confirm (Validated→Confirmed, locks records against further edit — 409 ATTENDANCE_BATCH_LOCKED).
  • FR-HR-ATT-06 unlock (Confirmed→Validated) requires a mandatory reason, is authorized-user only, and is itself blocked once the batch reaches UsedInPayroll.
  • FR-HR-ATT-07 A Confirmed batch is the only valid input to Payroll generation for its period (§B.3.7).

B.3.5 Leave (FR-HR-LV)

  • FR-HR-LV-01 Maintain LeaveType master (IsPaid, CountsAsNoPay, AccrualPerYear, CarryForwardAllowed).
  • FR-HR-LV-02 Submit/approve/reject/cancel a LeaveRequest (Draft→Submitted→Approved/Rejected, or →Cancelled).
  • FR-HR-LV-03 Maintain per-employee/type/year LeaveBalance (Entitled/Taken/CarriedForward/Adjustment); HR can manually adjust.
  • FR-HR-LV-04 An Approved leave request overlapping an attendance date with no punch classifies that date OnLeave (not Absent) during Attendance validation.

B.3.6 Payroll (FR-HR-PAY)

  • FR-HR-PAY-01 Maintain SalaryComponent master (Earning/Deduction, IsTaxable, IsEpfEtfApplicable) — for Allowances and ad hoc Other Deductions only.
  • FR-HR-PAY-02 Maintain effective-dated EmployeeSalaryStructure (+ lines) per employee; exactly one open-ended (EffectiveTo IS NULL) row at a time (409 SALARY_STRUCTURE_OVERLAP otherwise).
  • FR-HR-PAY-03 Maintain EmployeeLoan (Loan or Advance) + LoanInstallment schedule/ledger.
  • FR-HR-PAY-04 Maintain effective-dated PayrollStatutorySetting (EPF/ETF rates) and TaxSlab (marginal tax bounds/rate, effective-dated).
  • FR-HR-PAY-05 Generate a PayrollRun for a period (+ optional branch scope): blocked (422 ATTENDANCE_NOT_CONFIRMED) unless every relevant attendance batch for the period is Confirmed. Computes one PayrollLine + PayrollLineComponent set per employee per the formula in §B.4.
  • FR-HR-PAY-06 Approval workflow: Draft (Generate/regenerate freely) → Approved (freeze from recompute) → Locked (immutable; stamps loan installments as Deducted and attendance batches as UsedInPayroll — see A.4). Unlock (mandatory reason) reverses those stamps and reverts to Approved; blocked without authorization.
  • FR-HR-PAY-07 POST /payroll-runs/{id}/generate-payslips (Locked-only) creates one Payslip marker per PayrollLine; GET /payslips/{id}/view renders an HTML print view.

B.3.7 Reports (FR-HR-RPT)

  • FR-HR-RPT-01 Attendance summary (Present/Absent/Leave/OT/Late totals per employee/period).
  • FR-HR-RPT-02 Overtime report, Late-arrival report (both derived from AttendanceRecord).
  • FR-HR-RPT-03 Payroll register (per PayrollRun, exportable).
  • FR-HR-RPT-04 Employee salary history (EmployeeSalaryStructure revisions over time).
  • FR-HR-RPT-05 Leave balance report.
  • FR-HR-RPT-06 Document expiry report (EmployeeDocument.ExpiryDate within N days / already expired).

B.4 Payroll calculation (authoritative formula)

GrossSalary   = BasicSalary + Σ(SalaryStructureLine WHERE ComponentType=Earning) + OvertimeAmount
NetSalary     = GrossSalary
               LateDeductionAmount  NoPayAmount  LoanDeductionAmount
               EpfEmployeeAmount  TaxAmount  OtherDeductionsAmount

EpfEmployerAmount and EtfEmployerAmount are informational/liability lines only — never subtracted from Net (they represent the company's own contribution, not an employee deduction). OvertimeAmount = per-minute rate (derived from Basic ÷ WorkShift.StandardWorkingMinutes, × WorkShift.OtMultiplier or PayrollStatutorySetting.OtMultiplierDefault) × total OT minutes for the period. LateDeductionAmount/NoPayAmount use a flat per-minute/per-day rate in this phase (tiered policies are a future improvement, not built now — see §B.7). Tax is looked up from TaxSlab via standard marginal-slab computation over taxable earnings (per SalaryComponent.IsTaxable).

Open compliance question, not silently assumed: the exact APIT taxable-income base (whether EPF-employee reduces taxable income before slab lookup, whether OT is taxable) requires finance/statutory sign-off before go-live. The TaxSlab mechanism is built to be configurable; no specific formula is hardcoded beyond "marginal rate over taxable earnings."

B.5 Non-Functional Requirements

  • Salary/PII data is more sensitive than Phase-1 inventory data — see 02-SECURITY.md §C.8 for the explicit (not inherited) risk decision required before go-live.
  • File uploads (attendance spreadsheets, staff documents) are untrusted input: size-capped, extension/content-type-validated server-side; true magic-byte sniffing and antivirus scanning are explicitly deferred (new accepted-risk entry, not silently skipped).
  • All HRM transactional documents (AttendanceUploadBatch, LeaveRequest, EmployeeLoan, PayrollRun) follow the same audit/concurrency posture as Phase 1: uint RowVersion ETag, automatic AuditLog capture via AuditScribe (no per-entity wiring needed).

B.6 Constraints & assumptions

  • Single currency (LKR) for Phase 2; multi-currency payroll is out of scope.
  • Statutory rules assume Sri Lanka (EPF/ETF/APIT-style tax); the TaxSlab/PayrollStatutorySetting tables exist specifically so this isn't hardcoded, but no other country's scheme is modeled.
  • No employee self-service session model exists; all HRM screens are HR/admin-facing only in this phase.

B.7 Future Modules & Integration Seams (Phase 2 → later)

Seam Note
Biometric device attendance AttendanceUploadBatch.SourceType reserves a BiometricDevice enum value now; a device feed can write AttendanceRecords directly later with no schema change.
PDF payslips Payslip/PayrollLine/PayrollLineComponent already carry everything a PDF renderer needs; only a rendering step (e.g. QuestPDF) is deferred.
Employee self-service portal Requires a new employee-facing session model, not present in this phase.
Notification hooks Document expiry, payslip release, leave approval — no email/notification infra exists anywhere in this repo yet; a genuine future subsystem.
Tiered late/OT policy MVP uses a flat per-minute/per-day rate; policies like "3 lates = 1 day" are a future enhancement to PayrollCalculationService.
Bulk employee import Mirror the Attendance upload UX (ClosedXML/CsvHelper + preview + validate) for onboarding an existing workforce at go-live.
RBAC for HR data Recommend HRM be the forcing function that finally turns on per-endpoint RBAC (02-SECURITY.md Part D), ahead of inventory adjustments as currently ordered.

Part C — ER Model

C.1 Org / Masters

  • BranchBranchId PK, Code, Name, Address?, Status(EntityStatus), CreatedAt, UpdatedAt, RowVersion.
  • DepartmentDepartmentId PK, Code, Name, ParentDepartmentId? (self-FK), HeadEmployeeId? FK→Employee, BranchId? FK→Branch, Status, CreatedAt, UpdatedAt, RowVersion. Unlimited self-nesting (unlike the two-level-capped Category); service-level cycle guard on write.
  • DesignationDesignationId PK, Code, Name, Status, CreatedAt, UpdatedAt, RowVersion.
  • EmploymentTypeEmploymentTypeId PK, Code, Name, Status, CreatedAt, UpdatedAt, RowVersion. Master (mirrors Brand), not an enum.
  • WorkShiftWorkShiftId PK, Code, Name, StartTime, EndTime, IsOvernight(bool), GraceMinutes(int, default 15), BreakMinutes(int, default 60), StandardWorkingMinutes(int, default 480), OtMultiplier(decimal, default 1.5), WorkingDaysMask(int), Status, CreatedAt, UpdatedAt, RowVersion.

C.2 Employee core

  • EmployeeEmployeeId PK, EmployeeCode(unique, user-entered), FullName, Nic?, DateOfBirth?, Gender?(enum), Nationality?, ProfilePhotoPath?, Email?, PersonalMobile?, AddressLine1?, AddressLine2?, City?, PostalCode?, Country?, EmergencyContactName?, EmergencyContactRelationship?, EmergencyContactPhone?, HireDate, ConfirmationDate?, LastWorkingDate?, DepartmentId FK, DesignationId FK, EmploymentTypeId FK, BranchId? FK, WorkShiftId FK, ReportingManagerId? (self-FK), EpfNumber?, EtfNumber?, TaxIdentificationNumber?, UserId? FK→User (unique, filtered), Status(EmployeeStatus enum: Active/Suspended/Resigned/Terminated/Retired), CreatedBy FK→User, CreatedAt, UpdatedBy? FK→User, UpdatedAt?, RowVersion.
  • EmployeeBankDetailEmployeeBankDetailId PK, EmployeeId FK, BankName, BranchName, AccountNumber, AccountHolderName, SwiftCode?, IsPrimary(bool), Status, CreatedAt, UpdatedAt, RowVersion.

C.3 Documents

  • HrDocumentTypeHrDocumentTypeId PK, Code, Name, Category(enum: Identity/Educational/Contract/Certification/Statutory/Other), RequiredAtOnboarding(bool), ExpiryTracked(bool), Status, CreatedAt, UpdatedAt, RowVersion.
  • EmployeeDocumentEmployeeDocumentId PK, EmployeeId FK, HrDocumentTypeId FK, OriginalFileName, StoredFileName, RelativePath, ContentType, SizeBytes, IssueDate?, ExpiryDate?, Notes?, UploadedBy FK→User, UploadedAt, VerifiedBy? FK→User, VerifiedAt?, Status(enum: Active, Archived), RowVersion.

C.4 Attendance

  • AttendanceUploadBatchAttendanceUploadBatchId PK, DocNo(NumberSequence "ATT"), PeriodStart, PeriodEnd, SourceType(enum: Excel, Csv, Manual, BiometricDevice[reserved]), OriginalFileName?, UploadedBy FK→User, UploadedAt, Status(enum: Draft, Validated, Confirmed, UsedInPayroll), ConfirmedBy? FK→User, ConfirmedAt?, RowCountTotal, RowCountDuplicate, RowCountError, RowVersion.
  • AttendanceRecordAttendanceRecordId PK, AttendanceUploadBatchId? FK, EmployeeId FK, AttendanceDate, CheckIn?, CheckOut?, WorkShiftId FK (snapshotted, see A.3), WorkingMinutes(computed), LateMinutes(computed), EarlyLeaveMinutes(computed), OvertimeMinutes(computed), AttendanceStatus(enum: Present/Absent/HalfDay/OnLeave/Holiday/WeekOff), RowValidationStatus(enum: Valid/DuplicateWithinBatch/DuplicateConfirmed/EmployeeNotFound/InvalidDateTime/Error), DuplicateOfAttendanceRecordId? (self-FK), Notes?, IsManualOverride(bool), EditedBy? FK→User, EditedAt?, RowVersion.

C.5 Leave

  • LeaveTypeLeaveTypeId PK, Code, Name, IsPaid(bool), CountsAsNoPay(bool), AccrualPerYear(decimal), CarryForwardAllowed(bool), MaxCarryForwardDays?, RequiresApproval(bool, default true), Status, CreatedAt, UpdatedAt, RowVersion.
  • LeaveRequestLeaveRequestId PK, DocNo(NumberSequence "LV"), EmployeeId FK, LeaveTypeId FK, StartDate, EndDate, DaysCount(decimal), Reason?, Status(enum: Draft/Submitted/Approved/Rejected/Cancelled), ApprovedBy? FK→User, ApprovedAt?, RejectionReason?, CreatedBy FK→User, CreatedAt, RowVersion.
  • LeaveBalanceLeaveBalanceId PK, EmployeeId FK, LeaveTypeId FK, Year, EntitledDays, TakenDays, CarriedForwardDays, AdjustmentDays, RowVersion, UpdatedAt. Unique (EmployeeId, LeaveTypeId, Year).

C.6 Payroll

  • SalaryComponentSalaryComponentId PK, Code, Name, ComponentType(enum: Earning, Deduction), IsTaxable(bool), IsEpfEtfApplicable(bool), Status, CreatedAt, UpdatedAt, RowVersion.
  • EmployeeSalaryStructureEmployeeSalaryStructureId PK, EmployeeId FK, EffectiveFrom, EffectiveTo?, BasicSalary(decimal), Currency(default "LKR"), Status(enum: Active, Superseded), ApprovedBy FK→User, ApprovedAt, CreatedBy FK→User, CreatedAt, RowVersion.
  • EmployeeSalaryStructureLineEmployeeSalaryStructureLineId PK, EmployeeSalaryStructureId FK, SalaryComponentId FK, Amount(decimal).
  • EmployeeLoanEmployeeLoanId PK, DocNo(NumberSequence "LOAN"), EmployeeId FK, LoanKind(enum: Loan, Advance), PrincipalAmount, InterestRate(decimal, default 0), InstallmentAmount, NumberOfInstallments, StartYear, StartMonth, OutstandingBalance(decimal, denormalized), Status(enum: Active, Closed, Cancelled), ApprovedBy FK→User, ApprovedAt, CreatedBy FK→User, CreatedAt, RowVersion.
  • LoanInstallmentLoanInstallmentId PK, EmployeeLoanId FK, InstallmentNumber, DueYear, DueMonth, ScheduledAmount, PaidAmount?, PayrollRunId? FK (stamped only at Lock), Status(enum: Pending, Deducted, Skipped), RowVersion.
  • PayrollStatutorySettingPayrollStatutorySettingId PK, EpfEmployeeRate(decimal, default 0.08), EpfEmployerRate(decimal, default 0.12), EtfEmployerRate(decimal, default 0.03), OtMultiplierDefault(decimal, default 1.5), EffectiveFrom, EffectiveTo?, CreatedBy FK→User, CreatedAt, RowVersion.
  • TaxSlabTaxSlabId PK, EffectiveFrom, EffectiveTo?, LowerBound(decimal), UpperBound(decimal?, null = "and above"), Rate(decimal), RowVersion, CreatedAt.
  • PayrollRunPayrollRunId PK, DocNo(NumberSequence "PAY"), PeriodYear, PeriodMonth, BranchId? (null = company-wide), Status(enum: Draft, Approved, Locked), GeneratedBy FK→User, GeneratedAt, ApprovedBy? FK→User, ApprovedAt?, LockedBy? FK→User, LockedAt?, UnlockedBy? FK→User, UnlockedAt?, UnlockReason?, RowVersion.
  • PayrollLinePayrollLineId PK, PayrollRunId FK, EmployeeId FK, BasicSalary, TotalAllowances, OvertimeAmount, GrossSalary(computed), LateDeductionAmount, NoPayAmount, LoanDeductionAmount, EpfEmployeeAmount, EpfEmployerAmount(informational), EtfEmployerAmount(informational), TaxAmount, OtherDeductionsAmount, NetSalary(computed), WorkingDays, PresentDays, AbsentDays, LeaveDays, OtMinutesTotal, LateMinutesTotal, RowVersion.
  • PayrollLineComponentPayrollLineComponentId PK, PayrollLineId FK, ComponentCategory(enum: Earning, Deduction, EmployerContribution), SalaryComponentId? FK (null for system-computed lines), Label, Amount, SortOrder.
  • PayslipPayslipId PK, PayrollLineId FK (unique), GeneratedAt, ReleasedAt?, ReleasedBy? FK→User.

C.7 Cross-cutting change

  • User (existing entity, Domain/Entities/User.cs) — add Email? (nullable string), case-insensitive filtered unique index. No other column changes.

C.10 Entity → implementation mapping

Same convention as Phase 1 (10-BACKEND-PHASE1.md C.10): entities → Domain/Entities/*.cs; enums → Domain/Enums/*.cs; EF configurations (IEntityTypeConfiguration<T>, one per entity) → Infra/Persistence/Configurations/*Configuration.cs; DTOs grouped by module → Dtos/Hrm/*.cs; services → Services/Hrm/*Service.cs (CRUD) + Services/Hrm/{AttendanceComputationService,PayrollCalculationService,EmployeeUserLinkService}.cs (domain calculation, analog of Services/Stock/FifoCostingService.cs); controllers → Controllers/Hrm/*Controller.cs; new file-storage abstraction → Infra/Storage/{IFileStorageService,LocalFileStorageService}.cs.


End of 12-BACKEND-HRM.md. API contract: 13-BACKEND-HRM-API.md. Frontend flows: 21-FRONTEND-HRM.md.