develop full initial module

This commit is contained in:
Dhananjaya99
2026-07-23 19:54:56 +05:30
parent eacc21afad
commit 755df494fe
188 changed files with 13894 additions and 49 deletions
+198
View File
@@ -0,0 +1,198 @@
# 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<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`.*