develop full initial module
This commit is contained in:
+11
-6
@@ -20,6 +20,8 @@ A modular ERP built in phases. **Phase 1** delivers the **Inventory & Supply Cha
|
||||
|
||||
Full requirements live in the backend spec (see §7). This file does **not** duplicate them.
|
||||
|
||||
**Phase 2 (HRM)** is now underway alongside Phase 1 — see §7 routing to `12-BACKEND-HRM.md` / `13-BACKEND-HRM-API.md` / `21-FRONTEND-HRM.md`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Repository structure
|
||||
@@ -334,18 +336,21 @@ All frontend work is governed by `20-FRONTEND.md`.
|
||||
|
||||
| If you are working on… | Go to |
|
||||
|---|---|
|
||||
| Requirements, business rules, entities, ER model, data types, architecture detail | **`10-BACKEND-PHASE1.md`** |
|
||||
| API endpoints, request/response shapes, error catalog, enums | **`11-BACKEND-PHASE1.md`** |
|
||||
| Frontend user-flows, screen flow, architecture rules, validation posture | **`20-FRONTEND.md`** |
|
||||
| Requirements, business rules, entities, ER model, data types, architecture detail (Phase 1: Inventory & Supply Chain) | **`10-BACKEND-PHASE1.md`** |
|
||||
| API endpoints, request/response shapes, error catalog, enums (Phase 1) | **`11-BACKEND-PHASE1.md`** |
|
||||
| HRM requirements, business rules, entities, ER model (Phase 2) | **`12-BACKEND-HRM.md`** |
|
||||
| HRM API endpoints, request/response shapes, error catalog (Phase 2) | **`13-BACKEND-HRM-API.md`** |
|
||||
| Frontend user-flows, screen flow, architecture rules, validation posture (Phase 1) | **`20-FRONTEND.md`** |
|
||||
| HRM frontend user-flows (Phase 2) | **`21-FRONTEND-HRM.md`** |
|
||||
| Security risks per feature, accepted-risk register, pre-ship checklist | **`02-SECURITY.md`** |
|
||||
| Understanding the doc system, reading order, tracking conventions | **`01-DOC-GUIDE.md`** |
|
||||
| Recording backend changes made | **`Backend/PROGRESS.md`** |
|
||||
| Recording frontend changes made | **`Frontend/PROGRESS.md`** |
|
||||
|
||||
Quick resolver:
|
||||
- *"Where is the model / an entity defined?"* → `10-BACKEND-PHASE1.md` (schema is authoritative there).
|
||||
- *"What does this endpoint accept/return?"* → `11-BACKEND-PHASE1.md`.
|
||||
- *"How should the UI flow / what do I validate where?"* → `20-FRONTEND.md`.
|
||||
- *"Where is the model / an entity defined?"* → `10-BACKEND-PHASE1.md` (Phase 1) / `12-BACKEND-HRM.md` (HRM) — schema is authoritative there.
|
||||
- *"What does this endpoint accept/return?"* → `11-BACKEND-PHASE1.md` (Phase 1) / `13-BACKEND-HRM-API.md` (HRM).
|
||||
- *"How should the UI flow / what do I validate where?"* → `20-FRONTEND.md` (Phase 1) / `21-FRONTEND-HRM.md` (HRM).
|
||||
- *"What security risks / checks apply to this feature?"* → `02-SECURITY.md`.
|
||||
|
||||
---
|
||||
|
||||
@@ -34,9 +34,12 @@
|
||||
| `00-CORE.md` | High | **Hub.** Structure, tech stack, runnable backend init, routing. | First — every task. | Claude + humans |
|
||||
| `01-DOC-GUIDE.md` | High | **This file.** Doc map, reading order, tracking conventions. | To understand the doc system. | Claude + humans |
|
||||
| `02-SECURITY.md` | High (cross-cutting) | **Security review aid:** accepted-risks register + per-feature checklist. | Before ticking any feature in a PROGRESS.md; during security review. | Claude + humans |
|
||||
| `10-BACKEND-PHASE1.md` | High | Backend spec: **full SRS**, **ER model / 38-entity list**, tech stack detail, layer/architecture rules. Schema is **authoritative** here. | Any backend model / business-rule / requirement work. | Claude + humans |
|
||||
| `11-BACKEND-PHASE1.md` | High | Backend **API reference**: every endpoint with complete request/response, error catalog, enums. | Any API contract / controller / client work. | Claude + humans |
|
||||
| `20-FRONTEND.md` | High | Frontend **user-flows**, architecture rules to follow, **validation posture**. | Any frontend work. | Claude + humans |
|
||||
| `10-BACKEND-PHASE1.md` | High | Backend spec: **full SRS**, **ER model / 38-entity list**, tech stack detail, layer/architecture rules. Schema is **authoritative** here. | Any backend model / business-rule / requirement work (Phase 1). | Claude + humans |
|
||||
| `11-BACKEND-PHASE1.md` | High | Backend **API reference**: every endpoint with complete request/response, error catalog, enums. | Any API contract / controller / client work (Phase 1). | Claude + humans |
|
||||
| `12-BACKEND-HRM.md` | High | HRM backend spec: SRS, ER model, HRM-specific architecture notes. Schema is **authoritative** here. | Any HRM model / business-rule / requirement work (Phase 2). | Claude + humans |
|
||||
| `13-BACKEND-HRM-API.md` | High | HRM **API reference**: every endpoint, error catalog additions, enums. | Any HRM API contract / controller work. | Claude + humans |
|
||||
| `20-FRONTEND.md` | High | Frontend **user-flows**, architecture rules to follow, **validation posture**. | Any frontend work (Phase 1). | Claude + humans |
|
||||
| `21-FRONTEND-HRM.md` | High | HRM frontend **user-flows**, screens, validation specifics. | Any HRM frontend work. | Claude + humans |
|
||||
| `Backend/PROGRESS.md` | Low | Backend **change checklist**, git-shared. | After making backend changes. | **Claude** |
|
||||
| `Frontend/PROGRESS.md` | Low | Frontend **change checklist**, git-shared. | After making frontend changes. | **Claude** |
|
||||
|
||||
@@ -187,9 +190,9 @@ Spec: docs/20-FRONTEND.md (flows + rules) · docs/11-BACKEND-PHASE1.md (API cont
|
||||
|
||||
## 7. Adding future docs
|
||||
|
||||
When later phases arrive (Sales & CRM, Manufacturing, QC/QA, Accounting, HRM), follow the same scheme:
|
||||
- Backend spec/API for a phase → new `1x-` files (e.g. `30-BACKEND-PHASE2.md`), linked from the hub.
|
||||
- Frontend additions → extend `20-FRONTEND.md` or add `2x-` files.
|
||||
When later phases arrive (Sales & CRM, Manufacturing, QC/QA, Accounting), follow the same scheme HRM (Phase 2) established:
|
||||
- Backend spec/API for a phase → new `1x-` files continuing the backend decade (e.g. HRM used `12-BACKEND-HRM.md` + `13-BACKEND-HRM-API.md`, mirroring the `10`/`11` SRS+ER / API-reference split), linked from the hub.
|
||||
- Frontend additions → extend `20-FRONTEND.md` or add `2x-` files (e.g. HRM used `21-FRONTEND-HRM.md`).
|
||||
- Always register the new doc in `00-CORE.md` routing (§7) and in this index (§2).
|
||||
|
||||
---
|
||||
|
||||
+15
-1
@@ -20,6 +20,8 @@ These are known, deliberately-accepted Phase-1 exposures. Each has a compensatin
|
||||
| **AR-06** | **Localhost dev secrets** in `appsettings.Development.json`. | Local-dev convenience, current phase. | `.gitignore` + localhost only. | Before any shared/staging/prod → User Secrets / env vars; rotate. |
|
||||
| **AR-07** | **`getUserDetails` / `LogoutUser` callable without a bearer token** — `GET /api/v1/auth/users/{userId}` and `POST /api/v1/auth/logout` resolve the target user from the URL/payload, not the caller's session, so any anonymous caller can fetch a profile or log out an arbitrary user's sessions by GUID. | Carried over verbatim from AuthHex's own dispatcher contract (API_REFERENCE.md §3) — ERPCore's `AuthController` proxies it as-is rather than silently tightening a contract it doesn't own. | GUIDs are not enumerable; every call is written to `AuthEventLogs` upstream in AuthHex. | Revisit once AuthHex exposes a token-scoped variant, or add ERPCore-side rate limiting / auth requirement ahead of AuthHex. |
|
||||
| **AR-08** | **No rate limiting on `AuthController`'s anonymous endpoints** (login, register, refresh, recovery, OTP send/verify) — brute-force and account-enumeration exposure. | Not built in this pass (docs/11 §2.0, added 2026-07-16); AuthHex may rate-limit server-side but ERPCore does not add its own layer yet. | AuthHex's own lockout/backoff (per docs/10 NFR-03), immutable audit trail. | Add ASP.NET Core rate limiting middleware to `AuthController` before any non-local deployment. |
|
||||
| **AR-09** | **HRM inherits AR-01 for salary/PII data** — any door-admitted authenticated user can currently view/download any employee's salary figures, payslip, or uploaded documents (NIC scans, contracts). This is a **explicit, flagged decision, not a silent inheritance**: salary/PII is categorically more sensitive than Phase-1 inventory data, and this was called out to the business owner before HRM build started (see `12-BACKEND-HRM.md` A.1). | RBAC still deferred repo-wide; a coarse HR-role door-gate was not made a blocking prerequisite for HRM go-live. | Immutable audit trail (as AR-01); sidebar-visibility hiding of Employees/Attendance/Payroll sections for non-HR roles via the existing `NavItem`/`RolePermission` mechanism (UI-level only, not a server-enforced gate). | **HRM should be the forcing function that enables per-endpoint RBAC ahead of the rest of the system** (see Part D) — salary-data exposure is a materially worse blast radius than inventory data. |
|
||||
| **AR-10** | **`AuditLogsController` exposes salary figures** — a `PayrollLine`/`EmployeeSalaryStructure` mutation's `ChangeSet` contains salary amounts; `AuditLogsController` is not RBAC-gated, so any authenticated user can read another employee's salary history via `GET /audit-logs?entityType=PayrollLine&entityId=X`. | Consequence of AR-01/AR-09, specific enough to name on its own rather than leaving implicit. | None beyond authentication today. | Closed with RBAC (Part D), or an interim HRM-specific audit-log access filter. |
|
||||
|
||||
---
|
||||
|
||||
@@ -116,11 +118,23 @@ Auto-post + no approval + direct write-off = the primary theft/fraud surface. Re
|
||||
- [ ] System-qty snapshot immutable once the count is opened
|
||||
- [ ] Large variances flagged for review
|
||||
|
||||
### C.8 HRM (Employee / Documents / Attendance / Leave / Payroll) — **salary/PII data, see AR-09/AR-10**
|
||||
- [ ] Create/update DTOs exclude server-controlled fields (`status`, ids, `createdBy`, timestamps, computed payroll amounts)
|
||||
- [ ] Employee is **never hard-deleted** (deactivate via `EmployeeStatus` only), matching FR-MD-08's deactivate-not-delete pattern
|
||||
- [ ] `Employee.UserId` uniqueness (one User per Employee) enforced at the DB level (filtered unique index), not just service-level
|
||||
- [ ] File uploads (`EmployeeDocument`, attendance spreadsheets): extension allowlist + content-type cross-check + size cap enforced **server-side** (client checks are UX only); magic-byte sniffing / antivirus scanning explicitly **deferred**, not silently skipped — treat as an extension of AR-08's "not built in this pass" posture
|
||||
- [ ] `EmployeeDocument` download is never served via a static/guessable URL — authenticated controller action streaming through `IFileStorageService` only
|
||||
- [ ] Attendance batch lock (`Confirmed`/`UsedInPayroll`) genuinely blocks record edits server-side (`ATTENDANCE_BATCH_LOCKED`), not just hidden in the UI
|
||||
- [ ] Payroll figures (Gross/Net/Tax/EPF/ETF) are computed **server-side only**; the client never supplies or overrides them
|
||||
- [ ] **Payroll Unlock is the highest-risk action in this module** (parallel to C.5's framing of Adjustments) — mandatory reason, heavy audit; treat as the first HRM candidate for real per-endpoint RBAC
|
||||
- [ ] Untrusted spreadsheet parsing (`ClosedXML`/`CsvHelper`): packages pinned to current versions, no macro/external-entity execution path enabled
|
||||
- [ ] Review note: **AR-01/AR-09/AR-10** apply to every HRM endpoint until RBAC lands
|
||||
|
||||
---
|
||||
|
||||
## Part D — Post-Phase-1 controls to enable (in order)
|
||||
1. **Adjustment approval** (config flag already reserved) — closes the top fraud surface (AR-02, C.5).
|
||||
2. **RBAC enforcement** (role→permission) — closes AR-01, AR-03, AR-04.
|
||||
2. **RBAC enforcement** (role→permission) — closes AR-01, AR-03, AR-04, and **AR-09/AR-10 (HRM salary/PII)**. Given HRM's materially worse blast radius, consider bringing this forward ahead of item 3 once HRM ships (see AR-09's revisit trigger).
|
||||
3. **PO approval** (value thresholds) — closes remaining AR-02.
|
||||
4. **Monitoring reports** — stuck-transfer aging (AR-05) and large-variance/write-off review.
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ UI: responsive; count/pick screens handheld-friendly; status badges; mandatory-f
|
||||
| Sales & CRM | Reservation/allocation status distinguishing on-hand vs available (FR-STK-11). |
|
||||
| Manufacturing | Generic goods-issue/consumption movement type BOM will consume through (extends FR-STK-03). |
|
||||
| QC / QA | GRN inspection/quarantine hold (FR-GRN-05); hold blocks issue (FR-WH-07). |
|
||||
| HRM | User identity foundation (FR-X-01) reusable for employee-linked permissions. |
|
||||
| HRM | User identity foundation (FR-X-01) reusable for employee-linked permissions. **Now underway — see `12-BACKEND-HRM.md`.** |
|
||||
| RBAC & Approvals | Config flags + retained `PendingApproval`/role structures enable PO & adjustment approvals with no schema change. |
|
||||
|
||||
## B.8 Appendices
|
||||
|
||||
@@ -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`.*
|
||||
@@ -0,0 +1,156 @@
|
||||
# 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.*
|
||||
@@ -0,0 +1,50 @@
|
||||
# 21 · FRONTEND — Phase 2 (HRM)
|
||||
|
||||
> Follows `20-FRONTEND.md §1` architecture rules verbatim (Next.js App Router + TypeScript, Tailwind + shadcn/ui only, plain React hooks, dependency-free client validation, single `lib/api-client.ts`, types mirror API DTOs). Not repeated here. This file adds only HRM-specific screens/flows.
|
||||
|
||||
---
|
||||
|
||||
## 1. Screens (new, under `app/dashboard/hrm/`)
|
||||
|
||||
```
|
||||
app/dashboard/hrm/
|
||||
├── employees/ list + create/edit + detail (bank details, documents, salary history tabs)
|
||||
├── attendance/ batch list + upload wizard (upload → preview → confirm)
|
||||
├── leave/ leave request list + approval inbox
|
||||
├── payroll/ payroll run list + generate → review → approve → lock → payslips
|
||||
├── reports/ attendance summary / OT / late-arrival / payroll register / salary history / document expiry
|
||||
└── settings/ departments, designations, employment types, work shifts, document types,
|
||||
leave types, salary components, statutory settings, tax slabs
|
||||
```
|
||||
|
||||
Mirrors the existing `app/dashboard/settings/{roles,users}` admin-screen pattern for master-data CRUD; `components/auth/RolePermissionTree.tsx`-style list/detail layout is the closest existing analog for the Employees list+detail screen.
|
||||
|
||||
## 2. Flows → API mapping
|
||||
|
||||
- **Create Employee** — form posts `POST /employees`. On blur of the email field, call `GET /users/email-lookup?email=`; if a match is returned, show a non-blocking suggestion chip ("System user 'kasun.p' matches this email — link instead?") that sets `linkUserId` on submit if accepted. Never auto-link.
|
||||
- **Create User** (existing `/dashboard/settings/users` screen, extended) — same pattern in reverse: on email blur, call `GET /employees/email-lookup?email=`, offer `linkEmployeeId`.
|
||||
- **Attendance upload wizard** — three-step client flow over one `AttendanceUploadBatch`:
|
||||
1. Upload (file picker + "Download template" link) → `POST /attendance-batches`, batch created `Draft`, server-computed preview returned.
|
||||
2. Preview/Confirm — table of `AttendanceRecord`s with computed Working Hours/Late/Early/OT/Status columns; inline edit (`PUT .../records/{id}`) and duplicate-resolution actions; `POST .../validate` then `POST .../confirm`.
|
||||
3. Confirmed state is read-only in the UI (matches the server 409 on edit); an "Unlock" action is only shown to an HR Administrator.
|
||||
- **Payroll run** — `Generate` (`POST /payroll-runs`) shows a Preview list (`Employee | Basic | OT | Allowances | Deductions | Net`); clicking a row opens the detailed breakdown (`GET .../lines/{lineId}`) rendered as the exact Basic/Allowances/OT/Gross/Late/No-Pay/Loan/EPF/ETF/Tax/Net layout from the spec. `Approve`/`Lock`/`Unlock` buttons gated on current `Status`; `Generate Payslips` only enabled once `Locked`. Payslip view opens `GET /payslips/{id}/view` in a print-friendly page (browser print → PDF is the user's own path in this phase, per the confirmed "HTML first" decision).
|
||||
- **Leave approval inbox** — list of `Submitted` `LeaveRequest`s for the current approver's team; approve/reject inline.
|
||||
|
||||
## 3. Validation posture (per `20-FRONTEND.md §3`, applied to HRM specifics)
|
||||
|
||||
Client-side (format/required/range only, for UX):
|
||||
- Employee: required fields present, email format, date ranges sane (hire date not in future).
|
||||
- Attendance upload: file extension/size check before upload (fast feedback), mirrored server-side as authoritative.
|
||||
|
||||
Server-authoritative (never assumed client-side):
|
||||
- Employee code uniqueness, email-lookup match existence, one-User-per-Employee constraint.
|
||||
- Attendance duplicate detection (within-batch and cross-batch), employee-code resolution, batch lock state.
|
||||
- Payroll: attendance-confirmed precondition, salary-structure overlap, lock/unlock authorization, all calculated amounts (Gross/Net/Tax/EPF/ETF) — the client never recomputes or previews these independently of what the server returns.
|
||||
|
||||
## 4. Error & empty states
|
||||
|
||||
Same posture as `20-FRONTEND.md §4`: surface server `ProblemDetails.code` directly (e.g. a friendly message keyed off `ATTENDANCE_BATCH_LOCKED`/`PAYROLL_PERIOD_LOCKED`/`EMPLOYEE_CODE_DUPLICATE`), empty states for "no employees yet" / "no attendance batches this period" / "no payroll runs yet" following the existing master-data screen convention (§2.2 there).
|
||||
|
||||
---
|
||||
|
||||
*End of 21-FRONTEND-HRM.md.*
|
||||
Reference in New Issue
Block a user