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

9.4 KiB

13 · BACKEND — Phase 2 API Reference (HRM)

Follows the exact conventions of 11-BACKEND-PHASE1.md §1 (base URL, list envelope, pagination, concurrency, error format). Not repeated verbatim here — see that file for the shared contract. This file adds only the HRM endpoint surface and the HRM-specific error codes/enums.


1. Conventions recap

  • Base URL: /api/v1. JSON, camelCase.
  • List envelope: { items: [...], pagination: { page, pageSize, total } }.
  • Concurrency: ETag/If-Match (backed by RowVersion) on every mutable aggregate's PUT/status-changing endpoints.
  • Errors: RFC 7807 ProblemDetails with a stable code (catalog in §7).

2. Org / Masters

GET/POST   /branches                 GET/PUT /branches/{id}                 PATCH /branches/{id}/status
GET/POST   /departments               GET/PUT /departments/{id}              PATCH /departments/{id}/status
GET/POST   /designations               GET/PUT /designations/{id}             PATCH /designations/{id}/status
GET/POST   /employment-types           GET/PUT /employment-types/{id}         PATCH /employment-types/{id}/status
GET/POST   /work-shifts                GET/PUT /work-shifts/{id}              PATCH /work-shifts/{id}/status
GET/POST   /hr-document-types          GET/PUT /hr-document-types/{id}        PATCH /hr-document-types/{id}/status
GET/POST   /leave-types                GET/PUT /leave-types/{id}              PATCH /leave-types/{id}/status
GET/POST   /salary-components          GET/PUT /salary-components/{id}        PATCH /salary-components/{id}/status

All follow the BrandsController pattern exactly: GET list (q, status, paging), GET {id} with ETag, POST201, PUT requires If-Match, PATCH .../status204, no hard DELETE. Department additionally accepts parentDepartmentId on create/update (DEPARTMENT_CYCLE_DETECTED 422 if it would create a cycle).


3. Employees

GET    /employees?q=&status=&departmentId=&designationId=&branchId=&page=&pageSize=
GET    /employees/{employeeId}                                              + ETag
POST   /employees        { employeeCode, fullName, hireDate, departmentId, designationId,
                            employmentTypeId, workShiftId, branchId?, reportingManagerId?,
                            email?, ..., linkUserId? }                       201; EMPLOYEE_CODE_DUPLICATE (400)
PUT    /employees/{employeeId}                                              requires If-Match
PATCH  /employees/{employeeId}/status      { status }                       204

GET    /employees/email-lookup?email=                                       { match: EmployeeSummary|null }
POST   /employees/{employeeId}/link-user   { userId }                       EMPLOYEE_ALREADY_LINKED / USER_ALREADY_LINKED (409)
DELETE /employees/{employeeId}/link-user                                    204

GET    /employees/{employeeId}/bank-details
PUT    /employees/{employeeId}/bank-details    { items: [...] }              replaces the set; exactly one isPrimary

GET    /employees/{employeeId}/documents
POST   /employees/{employeeId}/documents       multipart(file, hrDocumentTypeId, issueDate?, expiryDate?, notes?)
                                                                              201; FILE_TYPE_NOT_ALLOWED (422) / FILE_TOO_LARGE (413)
GET    /employees/{employeeId}/documents/{docId}/download                    streams via IFileStorageService, auth-gated
PATCH  /employees/{employeeId}/documents/{docId}/status  { status }          Active|Archived

Also on the Users surface (existing UsersController, extended):

POST   /users             { username, fullName, roleId, userTypeId, email, ..., linkEmployeeId? }
GET    /users/email-lookup?email=                                           { match: UserSummary|null }

4. Attendance

GET    /attendance-batches/template.xlsx | ?format=csv                       generated from parser's own column map
GET    /attendance-batches?status=&periodYear=&periodMonth=&page=&pageSize=
POST   /attendance-batches       multipart(file, periodStart, periodEnd)     201 Draft; row-level preview computed
GET    /attendance-batches/{id}                                              + summary counts
GET    /attendance-batches/{id}/records?status=
PUT    /attendance-batches/{id}/records/{recordId}    { checkIn?, checkOut?, attendanceStatus?, notes? }
                                                                              409 ATTENDANCE_BATCH_LOCKED once Confirmed/UsedInPayroll
POST   /attendance-batches/{id}/resolve-duplicate     { recordId, action: "keep"|"discard"|"supersede" }
POST   /attendance-batches/{id}/validate                                     Draft → Validated; 422 ATTENDANCE_DUPLICATE_UNRESOLVED
POST   /attendance-batches/{id}/confirm                                     Validated → Confirmed
POST   /attendance-batches/{id}/unlock          { reason }                  Confirmed → Validated; 409 if already UsedInPayroll

5. Leave

POST   /leave-requests           { employeeId, leaveTypeId, startDate, endDate, reason? }   201 Draft
GET    /leave-requests?employeeId=&status=&page=&pageSize=
GET    /leave-requests/{id}
POST   /leave-requests/{id}/submit | /approve | /reject { reason? } | /cancel

GET    /employees/{employeeId}/leave-balances?year=
PUT    /employees/{employeeId}/leave-balances       { items: [{ leaveTypeId, adjustmentDays }] }   HR manual adjustment

6. Payroll

GET    /employees/{employeeId}/salary-structure                              current + history
POST   /employees/{employeeId}/salary-structure     { effectiveFrom, basicSalary, lines: [{salaryComponentId, amount}] }
                                                                              409 SALARY_STRUCTURE_OVERLAP

GET    /employees/{employeeId}/loans
POST   /employees/{employeeId}/loans     { loanKind, principalAmount, installmentAmount, numberOfInstallments, startYear, startMonth }
GET    /employees/{employeeId}/loans/{loanId}                                + installment ledger

GET/PUT /payroll-statutory-settings      GET/PUT .../{id}
GET/POST /tax-slabs                       GET/PUT .../{id}                    422 TAX_SLAB_GAP_INVALID

POST   /payroll-runs        { periodYear, periodMonth, branchId? }           201 Draft; 422 ATTENDANCE_NOT_CONFIRMED
GET    /payroll-runs?periodYear=&periodMonth=&status=&branchId=
GET    /payroll-runs/{id}                                                    + lines summary
GET    /payroll-runs/{id}/lines                                              per-employee summary rows (the Payroll Preview table)
GET    /payroll-runs/{id}/lines/{lineId}                                     detailed breakdown (PayrollLineComponent[])
POST   /payroll-runs/{id}/approve                                            Draft → Approved
POST   /payroll-runs/{id}/lock                                               Approved → Locked; stamps loans/attendance
POST   /payroll-runs/{id}/unlock        { reason }                          Locked → Approved; 409 PAYROLL_PERIOD_LOCKED if unauthorized
POST   /payroll-runs/{id}/generate-payslips                                  Locked-only; idempotent
GET    /payslips/{payslipId}
GET    /payslips/{payslipId}/view                                            server-rendered HTML print view

7. Domain Error Catalog (additions)

code HTTP When
EMPLOYEE_CODE_DUPLICATE 400 Employee code already exists.
EMPLOYEE_ALREADY_LINKED 409 Target Employee already has a linked User.
USER_ALREADY_LINKED 409 Target User already backs a different Employee.
DEPARTMENT_CYCLE_DETECTED 422 Setting parentDepartmentId would create a cycle.
DOCUMENT_TYPE_IN_USE 409 Hard delete of a referenced HrDocumentType attempted (deactivate instead).
FILE_TYPE_NOT_ALLOWED 422 Upload extension/content-type outside the allowlist.
FILE_TOO_LARGE 413 Upload exceeds configured max size.
ATTENDANCE_BATCH_LOCKED 409 Edit attempted on a Confirmed/UsedInPayroll batch.
ATTENDANCE_DUPLICATE_UNRESOLVED 422 Validate/Confirm attempted with unresolved duplicates.
ATTENDANCE_NOT_CONFIRMED 422 Payroll generation attempted against a non-Confirmed batch for the period.
SALARY_STRUCTURE_OVERLAP 409 New effective-dated salary structure overlaps an existing open-ended one.
TAX_SLAB_GAP_INVALID 422 Tax slab bounds leave a gap or overlap with another slab.
PAYROLL_PERIOD_LOCKED 409 Edit or unauthorized unlock attempted on a Locked payroll run.

8. Enumerations (new)

EmployeeStatus, EmployeeDocument.Status (Active/Archived), HrDocumentType.Category, AttendanceUploadBatch.SourceType/Status, AttendanceRecord.AttendanceStatus/RowValidationStatus, LeaveRequest.Status, EmployeeLoan.LoanKind/Status, LoanInstallment.Status, PayrollRun.Status, PayrollLineComponent.ComponentCategory, SalaryComponent.ComponentType. Full field definitions: 12-BACKEND-HRM.md Part C.

9. Implementation notes

  • Excel/CSV parsing: ClosedXML (.xlsx) and CsvHelper (.csv), both new packages. Template generation reuses the same column-mapping constants as the parser.
  • File streaming: download endpoints stream via IFileStorageService.OpenReadAsync, never a static file path.
  • Payslip HTML view: server-rendered Razor/plain-HTML response from PayrollLine+PayrollLineComponent, no PDF library in this phase.

End of 13-BACKEND-HRM-API.md.