# 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. ### B.3.2 Employee ↔ User cross-link (FR-HR-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 `AttendanceRecord`s 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 - **Branch** — `BranchId PK, Code, Name, Address?, Status(EntityStatus), CreatedAt, UpdatedAt, RowVersion`. - **Department** — `DepartmentId 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. - **Designation** — `DesignationId PK, Code, Name, Status, CreatedAt, UpdatedAt, RowVersion`. - **EmploymentType** — `EmploymentTypeId PK, Code, Name, Status, CreatedAt, UpdatedAt, RowVersion`. Master (mirrors `Brand`), not an enum. - **WorkShift** — `WorkShiftId 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 - **Employee** — `EmployeeId 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`. - **EmployeeBankDetail** — `EmployeeBankDetailId PK, EmployeeId FK, BankName, BranchName, AccountNumber, AccountHolderName, SwiftCode?, IsPrimary(bool), Status, CreatedAt, UpdatedAt, RowVersion`. ## C.3 Documents - **HrDocumentType** — `HrDocumentTypeId PK, Code, Name, Category(enum: Identity/Educational/Contract/Certification/Statutory/Other), RequiredAtOnboarding(bool), ExpiryTracked(bool), Status, CreatedAt, UpdatedAt, RowVersion`. - **EmployeeDocument** — `EmployeeDocumentId 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 - **AttendanceUploadBatch** — `AttendanceUploadBatchId 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`. - **AttendanceRecord** — `AttendanceRecordId 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 - **LeaveType** — `LeaveTypeId PK, Code, Name, IsPaid(bool), CountsAsNoPay(bool), AccrualPerYear(decimal), CarryForwardAllowed(bool), MaxCarryForwardDays?, RequiresApproval(bool, default true), Status, CreatedAt, UpdatedAt, RowVersion`. - **LeaveRequest** — `LeaveRequestId 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`. - **LeaveBalance** — `LeaveBalanceId PK, EmployeeId FK, LeaveTypeId FK, Year, EntitledDays, TakenDays, CarriedForwardDays, AdjustmentDays, RowVersion, UpdatedAt`. Unique `(EmployeeId, LeaveTypeId, Year)`. ## C.6 Payroll - **SalaryComponent** — `SalaryComponentId PK, Code, Name, ComponentType(enum: Earning, Deduction), IsTaxable(bool), IsEpfEtfApplicable(bool), Status, CreatedAt, UpdatedAt, RowVersion`. - **EmployeeSalaryStructure** — `EmployeeSalaryStructureId PK, EmployeeId FK, EffectiveFrom, EffectiveTo?, BasicSalary(decimal), Currency(default "LKR"), Status(enum: Active, Superseded), ApprovedBy FK→User, ApprovedAt, CreatedBy FK→User, CreatedAt, RowVersion`. - **EmployeeSalaryStructureLine** — `EmployeeSalaryStructureLineId PK, EmployeeSalaryStructureId FK, SalaryComponentId FK, Amount(decimal)`. - **EmployeeLoan** — `EmployeeLoanId 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`. - **LoanInstallment** — `LoanInstallmentId PK, EmployeeLoanId FK, InstallmentNumber, DueYear, DueMonth, ScheduledAmount, PaidAmount?, PayrollRunId? FK (stamped only at Lock), Status(enum: Pending, Deducted, Skipped), RowVersion`. - **PayrollStatutorySetting** — `PayrollStatutorySettingId 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`. - **TaxSlab** — `TaxSlabId PK, EffectiveFrom, EffectiveTo?, LowerBound(decimal), UpperBound(decimal?, null = "and above"), Rate(decimal), RowVersion, CreatedAt`. - **PayrollRun** — `PayrollRunId 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`. - **PayrollLine** — `PayrollLineId 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`. - **PayrollLineComponent** — `PayrollLineComponentId PK, PayrollLineId FK, ComponentCategory(enum: Earning, Deduction, EmployerContribution), SalaryComponentId? FK (null for system-computed lines), Label, Amount, SortOrder`. - **Payslip** — `PayslipId 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`, 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`.*