# HR Module — Architecture Map

> **Last updated:** 2026-07-28
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The HR module is the **human resources management system** of the ERP. It manages:
- Employee HR profiles (personal info, employment details, salary, insurance)
- Department and job title management
- Employment contracts (lifecycle, renewal, termination)
- Attendance tracking (check-in/out, hours calculation, overtime, violations)
- Leave management (requests, approvals, balances, calendar)
- Payroll processing (full pipeline: calculate, approve, pay, close)
- Social insurance calculations (Egyptian Law 148/2019)
- Income tax calculations (progressive brackets, YTD-aware)
- Employee loans (request, approve, disburse, deduct from payroll)
- Disciplinary actions (investigation, penalties, appeals)
- End of service settlements (severance, leave payout, Egyptian Labour Law)
- Performance reviews (cycles, evaluations)
- Employee document management
- Work schedules, shifts, and shift violations
- Overtime requests and approvals
- Permission requests (hourly leaves)
- Official holidays management
- HR reports (headcount, payroll, attendance, leaves, insurance, tax, loans, contracts)

It does **NOT** manage:
- Employee system accounts/login (Users module)
- Role/permission assignment (Users/Roles modules)
- Accounting journal entries from payroll (Accounting module listens to events)

---

## 2. Directory & File Structure

```
app/Modules/HR/
├── bootstrap.php                          # 48 permissions + sidebar menu + event listeners
├── Routes.php                             # Web routes (120+ routes)
├── Controllers/
│   ├── AttendanceController.php          # Daily/monthly attendance, bulk record, approve
│   ├── ContractController.php            # CRUD, renew, terminate
│   ├── DepartmentController.php          # CRUD for departments
│   ├── DisciplinaryController.php        # CRUD, decide penalty, appeal
│   ├── EmployeeDocumentController.php    # Upload, verify, archive documents
│   ├── EmployeeProfileController.php     # CRUD + salary management + JSON search
│   ├── EndOfServiceController.php        # CRUD, calculate, approve, mark paid
│   ├── HolidayController.php            # CRUD for official holidays
│   ├── HrReportController.php           # 8 report endpoints
│   ├── BiometricController.php          # Device management, punch reception, processing
│   ├── InsuranceController.php          # View records, Form1, Form2, Form6 (Egyptian forms)
│   ├── JobTitleController.php           # CRUD for job titles
│   ├── LeaveController.php             # Request, approve/reject, balances, calendar
│   ├── LoanController.php             # CRUD, approve/reject, disburse
│   ├── OvertimeController.php          # CRUD, approve/reject, manage types
│   ├── PayrollController.php           # Periods, calculate, approve, pay, close, slips
│   ├── PerformanceController.php       # Cycles, reviews
│   ├── PermissionRequestController.php # Hourly permission requests
│   ├── SalaryStructureController.php   # CRUD + component management
│   ├── ShiftController.php            # CRUD, assignments, violations
│   ├── TaxController.php             # Tax records viewer
│   └── WorkScheduleController.php     # CRUD for work schedules
├── Models/
│   ├── HrAttendance.php
│   ├── HrContract.php
│   ├── HrDepartment.php
│   ├── HrDepartmentPosition.php         # Department staffing positions (authorized headcount per job title)
│   ├── HrDisciplinaryAction.php
│   ├── HrEmployeeDocument.php
│   ├── HrEmployeeLoan.php
│   ├── HrEmployeeProfile.php            # Core HR profile model
│   ├── HrEmployeeSalaryDetail.php
│   ├── HrEndOfService.php
│   ├── HrHoliday.php
│   ├── HrInsuranceRecord.php
│   ├── HrJobTitle.php
│   ├── HrLeaveBalance.php
│   ├── HrLeaveRequest.php
│   ├── HrLeaveType.php
│   ├── HrLoanInstallment.php
│   ├── HrPayrollPeriod.php
│   ├── HrPayrollRun.php
│   ├── HrPerformanceCycle.php
│   ├── HrPerformanceReview.php
│   ├── HrSalaryAdjustment.php
│   ├── HrSalaryComponent.php
│   ├── HrSalaryStructure.php
│   ├── HrTaxRecord.php
│   └── HrWorkSchedule.php
├── Services/
│   ├── AttendanceService.php             # Check-in/out, hours calc, Ramadan detection, breastfeeding tolerance
│   ├── BiometricService.php             # Device punch processing, employee matching
│   ├── AttendanceViolationService.php    # Auto-detect late/early/absence violations
│   ├── ContractService.php               # Renew, terminate contracts
│   ├── DisciplinaryService.php           # Penalty calculation for payroll deduction
│   ├── EndOfServiceService.php           # Egyptian Labour Law severance calc
│   ├── HrNumberGenerator.php            # Employee number sequencing
│   ├── IncomeTaxService.php             # Progressive tax brackets (Egyptian law)
│   ├── InsuranceCalculationService.php   # Insurance form calculations
│   ├── LeaveService.php                 # Leave request lifecycle, configurable entitlement, breastfeeding eligibility
│   ├── LoanService.php                  # Loan lifecycle + installment processing
│   ├── OvertimeService.php              # Overtime amount calculation
│   ├── PayrollCalculationService.php    # Full payroll pipeline (CRITICAL)
│   ├── SalarySlipService.php            # Generate printable salary slip data
│   ├── SocialInsuranceService.php       # Egyptian Insurance Law 148/2019
│   └── TaxBracketService.php            # Tax bracket lookup
└── Views/
    ├── attendance/ (4 views)
    ├── contracts/ (4 views: index, form, show, work_receipt)
    ├── departments/ (4 views: index, form, show, positions)
    ├── disciplinary/ (3 views)
    ├── documents/ (2 views)
    ├── employees/ (4 views: index, form, show, salary)
    ├── end_of_service/ (3 views)
    ├── holidays/ (2 views)
    ├── biometric/ (3 views: devices, punches, device_form)
    ├── insurance/ (6 views: index, period, employee, form1, form2, form6)
    ├── job_titles/ (2 views)
    ├── leaves/ (6 views: index, request_form, show, balances, employee_balance, calendar)
    ├── loans/ (3 views)
    ├── overtime/ (2 views)
    ├── payroll/ (5 views: periods, show_period, show_run, salary_slip, my_slips)
    ├── performance/ (5 views)
    ├── reports/ (9 views)
    ├── salary_structures/ (3 views)
    ├── schedules/ (2 views)
    ├── shifts/ (2 views)
    └── tax/ (2 views)
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `hr_employee_profiles` Table (core HR entity)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | UNI | FK to employees (system user) |
| employee_number | varchar(20) | NO | UNI | HR employee code |
| fingerprint_code | varchar(50) | YES | | Biometric device matching |
| first_name_ar | varchar(100) | NO | | |
| last_name_ar | varchar(100) | NO | | |
| first_name_en | varchar(100) | YES | | |
| last_name_en | varchar(100) | YES | | |
| phone | varchar(20) | YES | | |
| email | varchar(200) | YES | | |
| national_id | varchar(14) | YES | MUL | Egyptian 14-digit NID |
| date_of_birth | date | YES | | |
| gender | varchar(10) | YES | | |
| marital_status | varchar(20) | YES | | |
| religion | varchar(20) | YES | | Affects Ramadan schedule |
| nationality | varchar(50) | NO | | Default: 'egyptian' |
| education_qualification | varchar(255) | YES | | Degree/diploma name |
| education_specialization | varchar(255) | YES | | Field of study |
| education_graduation_year | year | YES | | |
| education_grade | varchar(100) | YES | | Grade/GPA |
| education_university | varchar(255) | YES | | Institution name |
| address | text | YES | | |
| emergency_contact_name | varchar(200) | YES | | |
| emergency_contact_phone | varchar(20) | YES | | |
| department_id | bigint unsigned | YES | MUL | FK to hr_departments |
| job_title_id | bigint unsigned | YES | MUL | FK to hr_job_titles |
| direct_manager_id | bigint unsigned | YES | MUL | FK to hr_employee_profiles |
| hire_date | date | NO | MUL | Employment start date |
| probation_end_date | date | YES | | |
| probation_status | varchar(20) | YES | | Default: 'in_probation' |
| employment_type | varchar(20) | NO | | Default: 'full_time' |
| workforce_type | varchar(30) | YES | | insured / part_time_freelance |
| employment_status | varchar(20) | NO | MUL | Default: 'active' |
| salary_structure_id | bigint unsigned | YES | MUL | FK to hr_salary_structures |
| basic_salary | decimal(15,2) | NO | | Default: 0.00 |
| insurable_salary | decimal(15,2) | NO | | Default: 0.00 |
| bank_name | varchar(200) | YES | | |
| bank_account_number | varchar(50) | YES | | |
| bank_iban | varchar(34) | YES | | |
| insurance_number | varchar(20) | YES | MUL | Social insurance number |
| insurance_start_date | date | YES | | |
| tax_card_number | varchar(20) | YES | | |
| military_status | varchar(20) | YES | | |
| has_disability | tinyint(1) | NO | | Default: 0 |
| disability_details | text | YES | | |
| notes | text | YES | | |
| photo_path | varchar(500) | YES | | |
| termination_date | date | YES | | |
| termination_type | varchar(30) | YES | | |
| termination_reason | text | YES | | |
| last_working_day | date | YES | | |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |
| is_archived | tinyint(1) | NO | MUL | Soft delete |

### 3.2 `hr_departments` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| parent_id | bigint unsigned | YES | MUL | Hierarchical departments |
| department_code | varchar(20) | NO | UNI | |
| name_ar | varchar(200) | NO | | |
| name_en | varchar(200) | YES | | |
| description_ar | text | YES | | |
| manager_employee_id | bigint unsigned | YES | MUL | FK to employees |
| branch_id | bigint unsigned | YES | MUL | FK to branches |
| is_active | tinyint(1) | NO | MUL | |
| sort_order | int unsigned | NO | | |
| staffing_capacity | int unsigned | YES | | Overall authorized headcount |
| is_archived | tinyint(1) | NO | MUL | |

### 3.2b `hr_department_positions` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| department_id | bigint unsigned | NO | MUL+UNI | FK to hr_departments |
| job_title_id | bigint unsigned | NO | MUL+UNI | FK to hr_job_titles |
| authorized_count | int unsigned | NO | | Default: 1 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

### 3.3 `hr_contracts` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | FK to hr_employee_profiles |
| contract_number | varchar(30) | NO | UNI | |
| contract_type | varchar(20) | NO | | fixed/indefinite/temporary |
| start_date | date | NO | MUL | |
| end_date | date | YES | | null for indefinite |
| probation_months | int unsigned | NO | | Default: 3 |
| basic_salary | decimal(15,2) | NO | | |
| total_package | decimal(15,2) | NO | | |
| working_hours_per_day | decimal(3,1) | NO | | Default: 8.0 |
| working_days_per_week | int unsigned | NO | | Default: 6 |
| notice_period_months | int unsigned | NO | | Default: 2 |
| terms_json | json | YES | | Contract terms |
| status | varchar(20) | NO | MUL | draft/active/expired/terminated/renewed |
| renewal_of_contract_id | bigint unsigned | YES | MUL | Previous contract FK |
| workflow_instance_id | bigint unsigned | YES | MUL | |
| signed_date | date | YES | | |
| document_path | varchar(500) | YES | | |
| is_archived | tinyint(1) | NO | MUL | |

### 3.4 `hr_attendance` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | FK to hr_employee_profiles |
| attendance_date | date | NO | MUL | |
| check_in_time | time | YES | | |
| check_out_time | time | YES | | |
| check_in_method | varchar(20) | NO | | Default: 'manual' |
| check_out_method | varchar(20) | YES | | |
| actual_hours | decimal(4,2) | YES | | Calculated on check-out |
| overtime_hours | decimal(4,2) | NO | | Default: 0.00 |
| overtime_type | varchar(10) | NO | | Default: 'none' |
| late_minutes | int unsigned | NO | | Default: 0 |
| early_leave_minutes | int unsigned | NO | | Default: 0 |
| status | varchar(20) | NO | MUL | present/late/absent/holiday/rest_day/leave |
| is_ramadan_schedule | tinyint(1) | NO | | Default: 0 |
| is_approved | tinyint(1) | NO | | Default: 0 |
| approved_by | bigint unsigned | YES | | |
| is_archived | tinyint(1) | NO | | |

### 3.5 `hr_leave_requests` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | |
| leave_type_id | bigint unsigned | NO | MUL | FK to hr_leave_types |
| start_date | date | NO | MUL | |
| end_date | date | NO | | |
| total_days | decimal(5,1) | NO | | Half-days supported |
| half_day | tinyint(1) | NO | | |
| half_day_type | varchar(20) | YES | | first_half/second_half |
| reason | text | YES | | |
| status | varchar(20) | NO | MUL | pending/approved/rejected/cancelled |
| workflow_instance_id | bigint unsigned | YES | MUL | |
| approved_by | bigint unsigned | YES | MUL | |
| rejection_reason | text | YES | | |
| document_path | varchar(500) | YES | | |
| is_archived | tinyint(1) | NO | MUL | |

### 3.6 `hr_payroll_periods` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| period_code | varchar(20) | NO | UNI | e.g., PAY-2026-06 |
| year | year | NO | MUL | |
| month | int unsigned | NO | | |
| start_date | date | NO | | |
| end_date | date | NO | | |
| status | varchar(20) | NO | MUL | open/processing/calculated/approved/paid/closed |
| is_locked | tinyint(1) | NO | | |
| approved_by | bigint unsigned | YES | | |
| approved_at | timestamp | YES | | |
| paid_date | date | YES | | |
| total_gross | decimal(15,2) | YES | | |
| total_deductions | decimal(15,2) | YES | | |
| total_net | decimal(15,2) | YES | | |
| total_employer_cost | decimal(15,2) | YES | | |
| is_archived | tinyint(1) | NO | | |

### 3.7 `hr_payroll_runs` Table (per-employee per-period)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| period_id | bigint unsigned | NO | MUL | FK to hr_payroll_periods |
| employee_profile_id | bigint unsigned | NO | MUL | FK to hr_employee_profiles |
| employee_number | varchar(20) | NO | | |
| basic_salary | decimal(15,2) | NO | | |
| gross_earnings | decimal(15,2) | NO | | |
| total_allowances | decimal(15,2) | NO | | |
| overtime_amount | decimal(15,2) | NO | | |
| total_deductions | decimal(15,2) | NO | | |
| insurance_employee | decimal(15,2) | NO | | |
| insurance_employer | decimal(15,2) | NO | | |
| tax_amount | decimal(15,2) | NO | | |
| loan_deduction | decimal(15,2) | NO | | |
| penalty_deduction | decimal(15,2) | NO | | |
| absence_deduction | decimal(15,2) | NO | | |
| other_deductions | decimal(15,2) | NO | | |
| other_earnings | decimal(15,2) | NO | | |
| net_salary | decimal(15,2) | NO | | |
| working_days | int unsigned | NO | | |
| present_days | int unsigned | NO | | |
| absent_days | int unsigned | NO | | |
| leave_days | decimal(5,1) | NO | | |
| overtime_hours | decimal(6,2) | NO | | |
| calculation_json | json | YES | | Full calculation breakdown |
| status | varchar(20) | NO | MUL | calculated/approved/paid |
| paid_at | timestamp | YES | | |
| is_archived | tinyint(1) | NO | | |

### 3.8 `hr_employee_loans` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | |
| loan_number | varchar(30) | NO | UNI | |
| loan_type | varchar(30) | NO | | advance/emergency/personal/financing_company/bank_loan |
| loan_amount | decimal(15,2) | NO | | |
| installment_amount | decimal(15,2) | NO | | Monthly deduction |
| number_of_installments | int unsigned | NO | | |
| paid_installments | int unsigned | NO | | Default: 0 |
| remaining_amount | decimal(15,2) | NO | | |
| interest_rate | decimal(5,2) | NO | | Default: 0.00 |
| start_deduction_date | date | NO | | |
| external_entity_name | varchar(200) | YES | | Bank/financing company name |
| external_reference | varchar(100) | YES | | Contract/reference number |
| monthly_salary_impact | decimal(15,2) | YES | | Monthly salary deduction amount |
| status | varchar(20) | NO | MUL | pending/approved/active/completed/rejected |
| is_archived | tinyint(1) | NO | MUL | |

### 3.9 `hr_insurance_records` Table (per-employee per-period)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | |
| period_id | bigint unsigned | NO | MUL | |
| payroll_run_id | bigint unsigned | YES | MUL | |
| basic_insurable_salary | decimal(15,2) | NO | | |
| variable_insurable_salary | decimal(15,2) | NO | | |
| employee_basic_share | decimal(15,2) | NO | | |
| employee_variable_share | decimal(15,2) | NO | | |
| total_employee_share | decimal(15,2) | NO | | |
| employer_basic_share | decimal(15,2) | NO | | |
| employer_variable_share | decimal(15,2) | NO | | |
| total_employer_share | decimal(15,2) | NO | | |
| total_contribution | decimal(15,2) | NO | | |
| is_archived | tinyint(1) | NO | | |

### 3.10 `hr_tax_records` Table (per-employee per-period)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_id | bigint unsigned | NO | MUL | FK to employees (NOT profile) |
| period_id | bigint unsigned | NO | MUL | |
| payroll_run_id | bigint unsigned | YES | MUL | |
| month | int unsigned | YES | | |
| gross_taxable_monthly | decimal(15,2) | NO | | |
| annual_taxable_income | decimal(15,2) | NO | | |
| personal_exemption | decimal(15,2) | NO | | |
| insurance_exemption | decimal(15,2) | NO | | |
| net_taxable_income | decimal(15,2) | NO | | |
| annual_tax | decimal(15,2) | NO | | |
| monthly_tax | decimal(15,2) | NO | | |
| ytd_tax | decimal(15,2) | NO | | Year-to-date cumulative |
| ytd_taxable_income | decimal(15,2) | NO | | |
| calculation_json | json | YES | | Bracket breakdown |
| is_archived | tinyint(1) | NO | | |

### 3.11 `hr_disciplinary_actions` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | |
| action_number | varchar(30) | NO | UNI | |
| incident_date | date | NO | MUL | |
| incident_description | text | NO | | |
| penalty_type | varchar(30) | NO | MUL | warning/fine/suspension/termination |
| wage_deduction_days | int unsigned | YES | | Days to deduct |
| wage_deduction_amount | decimal(15,2) | YES | | Calculated amount |
| suspension_days | int unsigned | YES | | |
| effective_date | date | YES | | |
| status | varchar(30) | NO | MUL | investigation/pending_decision/decided/appealed/final |
| investigation_result | text | YES | | Investigation result / applied penalty |
| appeal_text | text | YES | | |
| appeal_result | varchar(20) | YES | | accepted/rejected |
| is_archived | tinyint(1) | NO | MUL | |

### 3.12 `hr_end_of_service` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| employee_profile_id | bigint unsigned | NO | MUL | |
| termination_type | varchar(30) | NO | MUL | resignation/termination/retirement/death/end_of_contract |
| request_date | date | NO | | |
| effective_date | date | NO | | |
| years_of_service | decimal(5,2) | NO | | |
| last_basic_salary | decimal(15,2) | NO | | |
| last_total_salary | decimal(15,2) | NO | | |
| severance_first_5y | decimal(15,2) | NO | | 0.5 month/year |
| severance_after_5y | decimal(15,2) | NO | | 1.0 month/year |
| total_severance | decimal(15,2) | NO | | |
| leave_compensation | decimal(15,2) | NO | | |
| notice_period_compensation | decimal(15,2) | NO | | |
| total_entitlement | decimal(15,2) | NO | | |
| total_deductions | decimal(15,2) | NO | | Outstanding loans etc |
| net_settlement | decimal(15,2) | NO | | |
| status | varchar(20) | NO | MUL | draft/calculated/approved/paid |
| deductions_json | json | YES | | Breakdown of deductions |
| calculation_json | json | NO | | Full calculation audit trail |
| is_archived | tinyint(1) | NO | MUL | |

### 3.13 `hr_work_schedules` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(30) | NO | UNI | e.g., STD_6DAY |
| name_ar | varchar(200) | NO | | |
| schedule_type | varchar(20) | NO | | fixed/flexible/shift |
| working_days_per_week | int unsigned | NO | | Default: 6 |
| hours_per_day | decimal(4,2) | NO | | Default: 8.00 |
| hours_per_week | decimal(5,2) | NO | | Default: 48.00 |
| ramadan_hours_per_day | decimal(4,2) | NO | | Default: 7.00 |
| is_night_shift | tinyint(1) | NO | | |
| start_time | time | YES | | |
| end_time | time | YES | | |
| break_duration_minutes | int unsigned | NO | | Default: 60 |
| late_tolerance_minutes | int unsigned | NO | | Default: 15 |
| early_leave_tolerance | int unsigned | NO | | Default: 15 |
| days_config_json | json | NO | | Per-day working/non-working config |
| is_active | tinyint(1) | NO | MUL | |

### 3.14 `hr_salary_structures` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(30) | NO | UNI | |
| name_ar | varchar(200) | NO | | |
| payment_basis | varchar(20) | NO | | Default: 'monthly' |
| is_default | tinyint(1) | NO | | |
| is_active | tinyint(1) | NO | MUL | |

### 3.15 Other HR Tables (not fully described)

| Table | Purpose |
|-------|---------|
| hr_job_titles | Job title definitions (includes job_description_ar, job_description_en for formal job descriptions) |
| hr_leave_types | Leave type definitions (annual, sick, etc.) |
| hr_leave_balances | Per-employee per-year leave balance tracking |
| hr_loan_installments | Individual loan installment records |
| hr_payroll_components_log | Detailed component breakdown per payroll run |
| hr_employee_salary_details | Salary components assigned to employee |
| hr_salary_components | Component definitions (code, type, percentage) |
| hr_employee_schedules | Employee-to-schedule assignment (effective dates) |
| hr_shifts | Shift definitions |
| hr_shift_assignments | Employee-to-shift assignments |
| hr_attendance_violations | Auto-detected attendance violations |
| hr_overtime_requests | Overtime request records |
| hr_overtime_types | Overtime type definitions (day/night rates) |
| hr_permission_requests | Hourly permission requests |
| hr_holidays | Official holiday dates |
| hr_performance_cycles | Performance review cycles |
| hr_performance_reviews | Individual performance reviews |
| hr_employee_documents | Uploaded employee documents (with validity_status: valid/expiring_soon/expired) |
| hr_biometric_devices | Biometric hardware devices (fingerprint/face/card readers) |
| hr_biometric_punches | Raw punch records from devices (processed → attendance records) |

---

## 4. Payroll Lifecycle (Critical Flow)

```
1. CREATE PERIOD (POST /hr/payroll/periods)
   - Status: 'open'
   - Unique per year+month

2. CALCULATE (POST /hr/payroll/periods/{id}/calculate)
   - Status: open/calculated → 'processing' → 'calculated'
   - For EACH active employee:
     a. Load salary structure + components
     b. Get attendance summary (present, absent, overtime hours)
     c. Calculate gross: basic + allowances + overtime
     d. Calculate social insurance (Egyptian Law 148/2019 rates)
     e. Calculate income tax (progressive brackets, YTD-aware)
     f. Get loan installments due
     g. Get disciplinary deductions
     h. Calculate absence deductions (basic/30 * absent_days)
     h2. Martyrs fund deduction (from system_config: hr.deduction.martyrs_fund_amount)
     h3. Regular stamp duty (from system_config: hr.deduction.regular_stamp_duty)
     h4. Additional stamp duty (from system_config: hr.deduction.additional_stamp_duty)
     h5. External loan deductions (financing_company/bank_loan monthly_salary_impact)
     i. Net = gross - all deductions
     j. Persist: hr_payroll_runs + components_log + insurance_records + tax_records
     k. Process loan installments
   - All per-employee in transaction (atomic per employee)
   - Recalculation deletes previous runs

3. APPROVE (POST /hr/payroll/periods/{id}/approve)
   - Status: 'calculated' → 'approved'
   - Records approver + timestamp

4. PAY (POST /hr/payroll/periods/{id}/pay)
   - Status: 'approved' → 'paid'
   - Marks all runs as 'paid' with payment date
   - Dispatches 'hr.payroll.paid' for EACH run (triggers Accounting)
   - Wrapped in transaction

5. CLOSE (POST /hr/payroll/periods/{id}/close)
   - Status: 'paid' → 'closed'
   - Prevents any further modifications
```

---

## 5. Social Insurance Calculation (Egyptian Law 148/2019)

```
Rates:
  Employee: 9% basic + 2% variable = 11%
  Employer: 12% basic + 6.75% variable = 18.75%

Subject to ceiling/floor caps (from system_config):
  basic_floor, basic_ceiling
  variable_floor, variable_ceiling

All monetary arithmetic: bcmath scale=2
```

---

## 6. Income Tax Calculation (Egyptian Progressive Brackets)

- Annual taxable = (monthly gross - insurance exemption) * 12
- Personal exemption applied
- Progressive brackets applied to net taxable income
- Monthly tax = (annual_tax / 12) adjusted for YTD payments
- All YTD-aware to prevent over/under-taxation

---

## 7. End of Service (Egyptian Labour Law)

```
Severance:
  First 5 years: 0.5 month salary per year
  After 5 years: 1.0 month salary per year
  Based on last total salary

Notice period:
  < 10 years: 2 months compensation
  >= 10 years: 3 months compensation
  Waived if served, or if resignation (employee's obligation)

Additional:
  + Remaining leave days payout
  - Outstanding loan balance deduction
  - Any other deductions (JSON breakdown)
  = Net settlement

Maternity protection: Cannot terminate during maternity or 1 year after (Art.91)
```

---

## 8. Attendance & Schedule System

- **Work Schedules**: Define hours_per_day, start/end times, break duration, tolerances, days_config (per-day)
- **Employee Schedules**: Link employees to schedules with effective_from/to dates
- **Ramadan Detection**: System config `hr.ramadan.start_date` / `hr.ramadan.end_date` — Muslim employees get reduced hours
- **Holiday Detection**: `hr_holidays` table checked for each date
- **Rest Day Detection**: From schedule's `days_config_json` per weekday
- **Violation Auto-Detection**: `AttendanceViolationService::detectViolations()` triggered by `hr.attendance.recorded` event

---

## 9. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `hr.payroll.paid` | `{payroll_run_id, period_id, payment_date}` | PayrollController::markPaid |
| `hr.leave.submitted` | `{employee_id, leave_type, days}` | LeaveService |
| `hr.leave.approved` | `{employee_id, leave_id, ...}` | LeaveService |
| `hr.leave.rejected` | `{employee_id, leave_id, ...}` | LeaveService |
| `hr.loan.approved` | `{employee_id, loan_id, amount}` | LoanService |
| `hr.contract.renewed` | `{employee_id, contract_id}` | ContractService |
| `hr.contract.terminated` | `{employee_id, contract_id}` | ContractService |
| `attendance.violations_detected` | `{date, violations}` | AttendanceViolationService |

---

## 10. Events Consumed

| Event | Listener | Action |
|-------|----------|--------|
| `employee.created` | HR bootstrap | Log that HR profile is needed |
| `hr.leave.approved` | HR bootstrap | Send SMS notification |
| `hr.leave.rejected` | HR bootstrap | Send SMS notification |
| `hr.payroll.paid` | HR bootstrap | Send salary slip SMS |
| `hr.payroll.paid` | Accounting bootstrap | Auto-post journal entry for salaries |
| `hr.loan.approved` | HR bootstrap | Send SMS notification |
| `hr.attendance.recorded` | HR bootstrap | Auto-detect attendance violations |

---

## 11. Cross-Module Dependencies

### 11.1 HR module IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Users | Employee model (referenced via employee_id FK in profiles) |
| Core | EventBus, Logger, Model, QueryBuilder |

### 11.2 Other modules that IMPORT FROM HR:

| Module | What it imports |
|--------|----------------|
| Accounting | Listens to `hr.payroll.paid` event to auto-post journal entries |

---

## 12. Routes (120+ total, grouped by subsystem)

| Subsystem | Count | Prefix | Key Operations |
|-----------|-------|--------|----------------|
| Departments | 10 | /hr/departments | CRUD + archive + positions management |
| Job Titles | 6 | /hr/job-titles | CRUD + archive |
| Employee Profiles | 10 | /hr/employees | CRUD + archive + salary + search-json |
| Contracts | 8 | /hr/contracts | CRUD + renew + terminate |
| Salary Structures | 7 | /hr/salary-structures | CRUD + component management |
| Attendance | 7 | /hr/attendance | Daily/monthly, bulk record, approve |
| Leaves | 11 | /hr/leaves | Request, approve/reject, cancel, balances, calendar |
| Payroll | 11 | /hr/payroll | Periods, calculate, approve, pay, close, runs, slips |
| Insurance | 5 | /hr/insurance | View, period, employee, Form1, Form6 |
| Tax | 2 | /hr/tax | View, employee history |
| Disciplinary | 8 | /hr/disciplinary | CRUD + decide + appeal |
| Loans | 7 | /hr/loans | CRUD + approve/reject + disburse |
| End of Service | 7 | /hr/end-of-service | CRUD + calculate + approve + pay |
| Performance | 7 | /hr/performance | Cycles, reviews |
| Documents | 5 | /hr/documents | List, by-employee, upload, verify, archive |
| Holidays | 6 | /hr/holidays | CRUD |
| Work Schedules | 5 | /hr/schedules | CRUD |
| Overtime | 7 | /hr/overtime | CRUD + approve/reject + types |
| Shifts | 6 | /hr/shifts | CRUD + assignments + violations |
| Permission Requests | 5 | /hr/permissions | CRUD + approve/reject |
| Reports | 9 | /hr/reports | 8 report types + index |

---

## 13. Permissions (48 total)

| Key | Description |
|-----|-------------|
| hr.department.view | View departments |
| hr.department.manage | Manage departments |
| hr.job_title.view | View job titles |
| hr.job_title.manage | Manage job titles |
| hr.employee.view | View employee profiles |
| hr.employee.manage | Manage employee profiles |
| hr.employee.view_salary | View employee salaries |
| hr.employee.manage_salary | Manage employee salaries |
| hr.contract.view | View contracts |
| hr.contract.manage | Manage contracts |
| hr.attendance.view | View attendance |
| hr.attendance.manage | Manage attendance |
| hr.attendance.approve | Approve attendance records |
| hr.leave.view | View leaves |
| hr.leave.request | Request leave |
| hr.leave.approve | Approve/reject leaves |
| hr.leave.manage | Manage leaves and balances |
| hr.payroll.view | View payroll |
| hr.payroll.process | Run payroll calculation |
| hr.payroll.approve | Approve/pay/close payroll |
| hr.payslip.view_own | View own salary slips |
| hr.insurance.view | View insurance records |
| hr.insurance.manage | Manage insurance records |
| hr.tax.view | View tax records |
| hr.disciplinary.view | View disciplinary actions |
| hr.disciplinary.manage | Manage disciplinary actions |
| hr.loan.view | View loans |
| hr.loan.manage | Manage loans |
| hr.loan.approve | Approve loans |
| hr.eos.view | View end of service |
| hr.eos.manage | Manage end of service |
| hr.eos.approve | Approve end of service |
| hr.performance.view | View performance reviews |
| hr.performance.manage | Manage performance reviews |
| hr.document.view | View employee documents |
| hr.document.manage | Manage employee documents |
| hr.report.view | HR reports access |
| hr.holiday.manage | Manage holidays |
| hr.schedule.manage | Manage work schedules |
| hr.overtime.view | View overtime |
| hr.overtime.create | Submit overtime request |
| hr.overtime.approve | Approve overtime |
| hr.overtime.manage | Manage overtime types |
| hr.shifts.view | View shifts |
| hr.shifts.manage | Manage shifts and assignments |
| hr.permissions.view | View permission requests |
| hr.permissions.create | Submit permission request |
| hr.permissions.approve | Approve permission requests |

---

## 14. Configuration Dependencies

| Config Key (system_config) | Purpose |
|---------------------------|---------|
| hr.ramadan.start_date | Ramadan period start (for reduced hours) |
| hr.ramadan.end_date | Ramadan period end |
| hr.insurance.basic_floor | Social insurance basic salary floor |
| hr.insurance.basic_ceiling | Social insurance basic salary ceiling |
| hr.insurance.variable_floor | Social insurance variable salary floor |
| hr.insurance.variable_ceiling | Social insurance variable salary ceiling |
| hr.insurance.employee_basic_pct | Employee basic share % (9%) |
| hr.insurance.employee_variable_pct | Employee variable share % (2%) |
| hr.insurance.employer_basic_pct | Employer basic share % (12%) |
| hr.insurance.employer_variable_pct | Employer variable share % (6.75%) |
| hr.tax.brackets | Progressive tax bracket definitions |
| hr.tax.personal_exemption | Personal tax exemption amount |
| hr.eos.first_5y_rate | End of service first 5 years rate (0.5) |
| hr.eos.after_5y_rate | End of service after 5 years rate (1.0) |
| hr.eos.notice_under_10y | Notice period months < 10 years (2) |
| hr.eos.notice_over_10y | Notice period months >= 10 years (3) |
| hr.deduction.martyrs_fund_amount | Martyrs fund monthly deduction (fixed amount) |
| hr.deduction.regular_stamp_duty | Regular stamp duty monthly deduction |
| hr.deduction.additional_stamp_duty | Additional stamp duty monthly deduction |
| hr.leave.annual_days_under_10y | Annual leave days for < 10 years service (default: 21) |
| hr.leave.annual_days_over_10y | Annual leave days for >= 10 years or age 50+ (default: 30) |
| hr.leave.maternity_days | Maternity leave days (default: 90) |
| hr.leave.maternity_max_career | Max maternity leaves in career (default: 3) |
| hr.leave.breastfeeding_daily_hours | Breastfeeding tolerance hours/day (default: 1) |
| hr.leave.unpaid_max_days | Max unpaid leave days (0 = unlimited) |

---

## 15. High-Risk Areas

### 15.1 PayrollCalculationService (CRITICAL)
- Orchestrates 10+ steps per employee with multiple service calls
- Uses bcmath throughout — type errors can cause calculation failures
- Deletes previous runs before recalculating (destructive recalculation)
- Per-employee transaction but period-level updates are outside transaction
- If ANY service (Insurance, Tax, Attendance) returns incorrect data, payroll is wrong

### 15.2 Payroll Payment → Accounting Event
- `hr.payroll.paid` dispatches per-run, triggers Accounting journal entry
- If Accounting listener fails, payroll is marked paid but no journal entry exists
- No retry mechanism for failed event handlers

### 15.3 Attendance Hours Calculation
- Break-across-midnight detection (if check-out < check-in, add 1 day)
- Ramadan detection depends on system_config dates being set correctly
- Schedule fallback to STD_6DAY if no assignment found — could calculate wrong hours

### 15.4 Income Tax YTD Accumulation
- Tax records reference `employee_id` (NOT profile_id) — inconsistent with other HR tables
- YTD calculation assumes continuous employment within the year
- Mid-year hires may have incorrect tax accumulation

### 15.5 End of Service Settlement
- Net settlement can be negative if deductions exceed entitlement
- Maternity protection check only applies to 'termination' type
- No validation that effective_date > last_working_day

### 15.6 Loan Deduction in Payroll
- `getDueInstallments()` determines what to deduct — if loan dates are misconfigured, deductions may be wrong
- Processed installments are marked in the same transaction as payroll run

### 15.7 Employee Profile ↔ Employee Account Link
- `hr_employee_profiles.employee_id` is a 1:1 FK to `employees.id`
- If an employee has no HR profile, payroll skips them silently
- If profile exists without valid salary, it returns an error (not fatal)

---

## 15.8 Biometric Integration

The biometric subsystem provides integration with physical biometric devices (fingerprint readers, face scanners, card readers) to automatically record attendance.

### Database Tables

**`hr_biometric_devices`** — Registered biometric hardware

| Column | Type | Notes |
|--------|------|-------|
| id | bigint unsigned | PRI, auto_increment |
| device_name | varchar(100) | NOT NULL |
| device_type | varchar(20) | NOT NULL, default 'fingerprint' (fingerprint/face/card) |
| ip_address | varchar(45) | nullable |
| port | int unsigned | nullable |
| serial_number | varchar(100) | nullable, used for device authentication |
| location_description | varchar(255) | nullable |
| branch_id | bigint unsigned | nullable, FK to branches |
| is_active | tinyint(1) | NOT NULL, default 1 |
| last_sync_at | datetime | nullable, updated on each punch received |
| created_at | timestamp | |
| updated_at | timestamp | |

**`hr_biometric_punches`** — Individual punch records from devices

| Column | Type | Notes |
|--------|------|-------|
| id | bigint unsigned | PRI, auto_increment |
| device_id | bigint unsigned | NOT NULL, FK to hr_biometric_devices |
| fingerprint_code | varchar(50) | NOT NULL, links to hr_employee_profiles.fingerprint_code |
| punch_time | datetime | NOT NULL |
| punch_type | varchar(10) | NOT NULL, default 'unknown' (in/out/unknown) |
| processed | tinyint(1) | NOT NULL, default 0 |
| attendance_id | bigint unsigned | nullable, FK to hr_attendance (after processing) |
| error_message | varchar(500) | nullable |
| created_at | timestamp | |

### Key Components

- **Controller**: `BiometricController` — CRUD for devices, punch listing, API endpoint for receiving punches, manual process-all trigger
- **Service**: `BiometricService` — processPunch(), processAllPending(), resolveEmployee(), determinePunchType()
- **Views**: `biometric/devices.php`, `biometric/punches.php`, `biometric/device_form.php`

### Integration Flow

```
Biometric Device → POST /hr/biometric/receive-punch (no auth, device serial auth)
    → BiometricService::processPunch()
        → Validate device by serial_number
        → Insert punch record
        → Resolve employee via fingerprint_code
        → Determine punch type (in/out) from current attendance state
        → Call AttendanceService::recordCheckIn() or recordCheckOut()
        → Mark punch as processed, link to attendance_id
```

### Permissions

| Key | Description |
|-----|-------------|
| hr.biometric.view | View biometric devices and punch records |
| hr.biometric.manage | Manage devices, trigger bulk processing |

### Routes (9 total)

| Method | Path | Handler | Auth | Permission |
|--------|------|---------|------|------------|
| GET | /hr/biometric/devices | devices | auth | hr.biometric.view |
| GET | /hr/biometric/devices/create | createDevice | auth | hr.biometric.manage |
| POST | /hr/biometric/devices | storeDevice | auth, csrf | hr.biometric.manage |
| GET | /hr/biometric/devices/{id}/edit | editDevice | auth | hr.biometric.manage |
| POST | /hr/biometric/devices/{id} | updateDevice | auth, csrf | hr.biometric.manage |
| POST | /hr/biometric/devices/{id}/delete | deleteDevice | auth, csrf | hr.biometric.manage |
| GET | /hr/biometric/punches | punches | auth | hr.biometric.view |
| POST | /hr/biometric/receive-punch | receivePunch | none | none (device serial auth) |
| POST | /hr/biometric/process-all | processAll | auth, csrf | hr.biometric.manage |

### Risk: API Endpoint Security

The `receivePunch` endpoint has NO authentication middleware. It relies solely on the device serial_number being valid and active. This is intentional for IoT device integration but means:
- Any entity knowing a valid serial can inject punches
- Network-level security (firewall, VPN) should protect this endpoint in production
- Rate limiting is not currently implemented

---

## 16. Known Patterns & Gotchas

1. **Two employee entities**: `employees` (system user for login) and `hr_employee_profiles` (HR data for payroll). Linked by `employee_id` FK
2. **Profile ID vs Employee ID**: Most HR operations use `employee_profile_id`. Tax records use `employee_id`. Be careful which you pass
3. **Payroll recalculation**: Deletes all previous runs for the period — safe for open/calculated periods, dangerous for paid periods (guarded by status check)
4. **Payroll period lifecycle**: open → processing → calculated → approved → paid → closed (strictly sequential)
5. **Self-referencing managers**: `hr_employee_profiles.direct_manager_id` references the same table
6. **Contract renewal chain**: `renewal_of_contract_id` creates a linked list of contract history
7. **Salary slip access**: `hr.payslip.view_own` allows employees to see only their own slips via `/hr/payroll/my-slips`
8. **Attendance bulk recording**: Daily attendance page supports bulk check-in/out for all employees on a date
9. **Insurance forms**: Form 1 (new employee registration) and Form 6 (monthly contributions) are Egyptian government format reports
10. **hr_payroll_runs.paid_at uses timestamp**: Unlike `hr_payroll_periods.paid_date` which is a date — the run stores exact payment datetime
