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 byRowVersion) on every mutable aggregate'sPUT/status-changing endpoints. - Errors: RFC 7807
ProblemDetailswith a stablecode(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, POST → 201, PUT requires If-Match, PATCH .../status → 204, 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) andCsvHelper(.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.