# 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`, `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) 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.*