# 05 — مواصفات API

**Base URL:** `/api/hr` (via Gateway port 3400)  
**Auth:** Bearer JWT + header `X-Company-Id`  
**Response format:** JSON camelCase  
**Errors:** `{ message: string, messageAr: string }`

---

## 1. الاتفاقيات العامة

### Headers

```http
Authorization: Bearer {token}
X-Company-Id: {companyId}
Content-Type: application/json
```

### Middleware Stack (mutations)

```
optionalAuth → requireAuth → requirePermission('hr.{feature}.write') → requireCompanyAccess
```

### List Response

```json
{
  "employees": [...],
  "companyId": 1,
  "total": 42
}
```

### Single Entity

```json
{
  "employee": { "id": 1, "nameAr": "...", ... }
}
```

---

## 2. Endpoints الحالية (Phase 0 ✅)

### Health & Meta

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/health` | — | Service health |
| GET | `/meta/saudi` | — | Regions, contract types, statuses |
| GET | `/` | — | Service descriptor |

### Employees

| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/employees` | hr.read | List (filters: branch, dept, status, search) |
| GET | `/employees/:id` | hr.read | Single employee + documents meta |
| POST | `/employees` | hr.write | Create |
| PUT | `/employees/:id` | hr.write | Update |
| PATCH | `/employees/:id/status` | hr.write | Change employment_status |
| DELETE | `/employees/:id` | hr.write | Soft delete (is_active=0) |

**Query params (GET /employees):**

```
?branchId=1&departmentId=2&status=active&search=أحمد&page=1&limit=50
```

### Employee Documents

| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/employees/:id/documents` | hr.read | List documents |
| POST | `/employees/:id/documents` | hr.write | Upload (multipart) |
| DELETE | `/employees/:id/documents/:docId` | hr.write | Delete |
| GET | `/employees/:id/documents/:docId/download` | hr.read | Download file |

**Upload body (multipart):**

```
file: binary
category: "iqama" | "passport" | "contract" | ...
notes: string (optional)
```

### Departments & Job Titles

| Method | Path | Permission |
|--------|------|------------|
| GET/POST | `/departments` | hr.read / hr.write |
| PUT/DELETE | `/departments/:id` | hr.write |
| GET/POST | `/job-titles` | hr.read / hr.write |
| PUT/DELETE | `/job-titles/:id` | hr.write |

### Stats

| Method | Path | Permission | Response |
|--------|------|------------|----------|
| GET | `/stats` | hr.read | Counts, expiring docs |

**Example `/stats` response:**

```json
{
  "totalEmployees": 120,
  "activeEmployees": 115,
  "onLeave": 3,
  "byDepartment": [{ "departmentId": 1, "count": 25 }],
  "expiringIqama": 5,
  "expiringContracts": 2,
  "expiringDocuments": 8
}
```

---

## 3. Endpoints المخططة — Phase 1

### User ↔ Employee Link

| Method | Path | Permission | Description |
|--------|------|------------|-------------|
| GET | `/employees/unlinked` | hr.read | Employees without user account |
| POST | `/employees/:id/link-user` | hr.write | Body: `{ userId }` |
| DELETE | `/employees/:id/link-user` | hr.write | Unlink |
| GET | `/me` | auth | Current user's employee profile |
| GET | `/me/leave-balance` | auth | Own leave balances |
| GET | `/me/attendance` | auth | Own attendance history |

### Leave Types

| Method | Path | Permission |
|--------|------|------------|
| GET | `/leave-types` | hr.read |
| POST | `/leave-types` | hr.leave.write |
| PUT | `/leave-types/:id` | hr.leave.write |
| DELETE | `/leave-types/:id` | hr.leave.write |

### Holiday Lists

| Method | Path | Permission |
|--------|------|------------|
| GET | `/holiday-lists` | hr.read |
| GET | `/holiday-lists/:id` | hr.read |
| POST | `/holiday-lists` | hr.leave.write |
| PUT | `/holiday-lists/:id` | hr.leave.write |
| POST | `/holiday-lists/:id/dates` | hr.leave.write |
| DELETE | `/holiday-lists/:id/dates/:dateId` | hr.leave.write |

### Leave Policies

| Method | Path | Permission |
|--------|------|------------|
| GET | `/leave-policies` | hr.read |
| POST | `/leave-policies` | hr.leave.write |
| PUT | `/leave-policies/:id` | hr.leave.write |
| POST | `/leave-policies/:id/assign` | hr.leave.write |

### Leave Allocations

| Method | Path | Permission |
|--------|------|------------|
| GET | `/leave-allocations` | hr.read |
| POST | `/leave-allocations` | hr.leave.write |
| POST | `/leave-allocations/bulk` | hr.leave.write |

**Bulk body:**

```json
{
  "leaveTypeId": 1,
  "periodStart": "2026-01-01",
  "periodEnd": "2026-12-31",
  "allocatedDays": 21,
  "employeeIds": [1, 2, 3],
  "departmentId": null
}
```

### Leave Applications

| Method | Path | Permission |
|--------|------|------------|
| GET | `/leave-applications` | hr.leave.read |
| GET | `/leave-applications/:id` | hr.leave.read |
| POST | `/leave-applications` | hr.leave.write / self |
| PUT | `/leave-applications/:id` | hr.leave.write |
| POST | `/leave-applications/:id/approve` | hr.leave.approve |
| POST | `/leave-applications/:id/reject` | hr.leave.approve |
| POST | `/leave-applications/:id/cancel` | hr.leave.write / self |

**Create body:**

```json
{
  "employeeId": 1,
  "leaveTypeId": 1,
  "fromDate": "2026-06-15",
  "toDate": "2026-06-20",
  "halfDay": false,
  "reason": "إجازة سنوية"
}
```

**Approve response:** updates ledger + optionally attendance

### Leave Balance Report

| Method | Path | Permission |
|--------|------|------------|
| GET | `/leave-balances` | hr.read |
| GET | `/leave-balances/:employeeId` | hr.read |
| GET | `/leave-ledger` | hr.read |

### Shifts

| Method | Path | Permission |
|--------|------|------------|
| GET/POST | `/shift-types` | hr.read / hr.attendance.write |
| PUT/DELETE | `/shift-types/:id` | hr.attendance.write |
| GET/POST | `/shift-assignments` | hr.read / hr.attendance.write |
| PUT/DELETE | `/shift-assignments/:id` | hr.attendance.write |

### Checkins & Attendance

| Method | Path | Permission |
|--------|------|------------|
| GET | `/checkins` | hr.attendance.read |
| POST | `/checkins` | hr.attendance.write / device API key |
| POST | `/checkins/import` | hr.attendance.write |
| POST | `/checkins/bulk` | hr.attendance.write |
| GET | `/attendance` | hr.attendance.read |
| GET | `/attendance/:employeeId/:date` | hr.attendance.read |
| POST | `/attendance/mark` | hr.attendance.write |
| POST | `/attendance/bulk` | hr.attendance.write |
| POST | `/attendance/process-auto` | hr.attendance.write |

**Checkin import (CSV/API):**

```json
{
  "records": [
    { "attendanceDeviceId": "EMP001", "logType": "IN", "checkinTime": "2026-06-01T08:02:00" },
    { "attendanceDeviceId": "EMP001", "logType": "OUT", "checkinTime": "2026-06-01T17:05:00" }
  ]
}
```

**Device API:** maps `attendance_device_id` on employee → employee_id

### Attendance Reports

| Method | Path | Permission |
|--------|------|------------|
| GET | `/attendance/report/daily` | hr.attendance.read |
| GET | `/attendance/report/monthly` | hr.attendance.read |
| GET | `/attendance/report/late` | hr.attendance.read |

---

## 4. Endpoints المخططة — Phase 2 (Payroll)

| Method | Path | Permission |
|--------|------|------------|
| GET/POST | `/salary-components` | hr.payroll.read / write |
| GET/POST | `/salary-structures` | hr.payroll.read / write |
| GET/POST | `/salary-assignments` | hr.payroll.read / write |
| GET/POST | `/payroll-runs` | hr.payroll.read / write |
| POST | `/payroll-runs/:id/process` | hr.payroll.write |
| POST | `/payroll-runs/:id/approve` | hr.payroll.approve |
| GET | `/payroll-runs/:id/slips` | hr.payroll.read |
| GET | `/salary-slips/:id` | hr.payroll.read |
| GET | `/salary-slips/:id/pdf` | hr.payroll.read |
| POST | `/payroll-runs/:id/post-accounting` | hr.payroll.approve |
| GET | `/payroll-runs/:id/wps-export` | hr.payroll.read |
| GET | `/payroll-runs/:id/gosi-report` | hr.payroll.read |
| POST | `/employees/:id/calculate-eos` | hr.payroll.read |

---

## 5. Endpoints المخططة — Phase 3–4

| Method | Path | Phase | Description |
|--------|------|-------|-------------|
| GET/POST | `/separations` | 3 | Employee separation |
| GET/POST | `/ff-settlements` | 3 | Full & final |
| GET/POST | `/onboarding-checklists` | 3 | Onboarding tasks |
| GET/POST | `/promotions` | 3 | Promotions |
| GET/POST | `/transfers` | 3 | Transfers |
| GET/POST | `/job-openings` | 4 | Recruitment |
| GET/POST | `/job-applicants` | 4 | Applicants |
| POST | `/job-applicants/:id/hire` | 4 | Create employee |

---

## 6. Permissions Matrix (مقترح)

| Code | Module | Action | Roles |
|------|--------|--------|-------|
| hr.read | hr | read | admin, hr_manager, branch_manager |
| hr.write | hr | write | admin, hr_manager |
| hr.leave.read | hr | read leave | + branch_manager |
| hr.leave.write | hr | manage leave | admin, hr_manager |
| hr.leave.approve | hr | approve leave | admin, hr_manager, branch_manager |
| hr.attendance.read | hr | read attendance | admin, hr_manager, branch_manager |
| hr.attendance.write | hr | manage attendance | admin, hr_manager |
| hr.payroll.read | hr | read payroll | admin, hr_manager, finance |
| hr.payroll.write | hr | run payroll | admin, hr_manager |
| hr.payroll.approve | hr | approve payroll | admin, finance |

---

## 7. Frontend API Client Pattern

أضف methods في `panel/src/api/client.js`:

```javascript
// Phase 1 example
hrLeaveApplications: (params) => api.get('/hr/leave-applications', { params }),
createHrLeaveApplication: (body) => api.post('/hr/leave-applications', body),
approveHrLeaveApplication: (id) => api.post(`/hr/leave-applications/${id}/approve`),
hrAttendanceDaily: (params) => api.get('/hr/attendance/report/daily', { params }),
importHrCheckins: (body) => api.post('/hr/checkins/import', body),
```

---

## 8. Validation Rules (Key)

### Leave Application

- `fromDate <= toDate`
- No overlap with approved leave
- Balance sufficient (unless unpaid)
- Cannot apply for past dates (configurable)
- Attachment required if leave type requires it

### Checkin Import

- `attendanceDeviceId` must match active employee
- Duplicate checkins within 1 minute → reject
- Unknown device → log warning, skip

### Payroll Run

- Cannot process if previous month unpaid
- Employee must have active salary assignment
- IBAN required for bank payment mode

---

## 9. Error Codes

| HTTP | message | messageAr |
|------|---------|-----------|
| 400 | Invalid payload | بيانات غير صالحة |
| 403 | Permission denied | غير مصرح |
| 404 | Employee not found | الموظف غير موجود |
| 409 | Leave balance insufficient | رصيد الإجازة غير كافٍ |
| 409 | Overlapping leave | تداخل في تواريخ الإجازة |
| 422 | IBAN invalid | رقم IBAN غير صالح |

---

## 10. Webhooks / Events (Future)

Publish via `@erp/shared` notifications:

| Event | Trigger |
|-------|---------|
| `hr.leave.pending` | Leave application created |
| `hr.leave.approved` | Leave approved |
| `hr.document.expiring` | Iqama/passport within 30 days |
| `hr.payroll.ready` | Payroll run processed |
| `hr.attendance.absent` | Unexcused absence |
