Appearance
Attendance Flow
Phase 4b of the Personnel module adds daily attendance tracking: clock-in/clock-out, automated nightly classification, and the attendance correction workflow.
Overview
Employee clock-in (mobile app / HR terminal)
↓
EmployeeCheckin saved in DB
↓
ProcessDailyAttendanceJob runs at 03:00 Madrid time
↓
AttendanceRecord created or updated for each active employee
↓
Employee or manager can submit / approve / reject AttendanceRequest
↓
Correction updates the AttendanceRecord (IsRegularized = true)Clock-in / Clock-out
Two endpoints register employee check events:
| Endpoint | Description |
|---|---|
POST /api/employees/{id}/clock-in | HR records clock-in for any employee |
POST /api/employees/{id}/clock-out | HR records clock-out for any employee |
POST /api/users/me/clock-in | Employee clocks in from the mobile app |
POST /api/users/me/clock-out | Employee clocks out from the mobile app |
Source codes: MobileApp · BiometricTerminal · Manual
When Source = Manual, the Note field is required. Manual clock-ins generate a Warning-level audit entry (personnel.manual_checkin) instead of the usual Info-level.
Idempotency: Calling clock-in while already clocked in returns 409 Checkin.AlreadyClockedIn. Calling clock-out without an open clock-in returns 409 Checkin.NoActiveCheckin.
ProcessDailyAttendanceJob
A Hangfire recurring job that runs every night at 03:00 Europe/Madrid time. It processes the previous calendar day for all active employees.
Classification priority
- Weekend — date is in the employee's
HolidayList.WeeklyOffDays - Holiday — date is in the employee's
HolidayList.Entries - OnLeave — (Phase 4d) approved leave application exists ← placeholder, falls through to step 4
- Checkin-based:
- Both In + Out checkins found → Present (HoursWorked calculated)
- Only In checkin found → Incomplete
- No checkins found → Absent
Upsert and idempotency
The job uses an upsert pattern on the (employee_id, date) unique constraint. Re-running the job for the same day does not duplicate records. Regularized records (IsRegularized = true) are skipped on re-run — their data comes from the approval workflow, not from raw checkins.
Phase 4b — soft dependency on ShiftType
ShiftType is not implemented until Phase 4c. In Phase 4b, shiftType = null is always passed to AttendanceCalculationService, which means:
HoursWorked= checkout − checkin (hours, decimal)LateEntry= 0EarlyExit= 0Overtime= 0
Phase 4c will wire up the shift assignment lookup to populate these fields accurately.
AttendanceCalculationService
A pure in-memory service (no DB calls) that computes time-based attendance metrics:
HoursWorked = checkOut - checkIn (hours, decimal)
When shiftType is not null:
LateEntry = max(0, checkIn − (shiftStart + lateEntryToleranceMinutes)) → minutes
EarlyExit = max(0, (shiftEnd − earlyExitToleranceMinutes) − checkOut) → minutes
Overtime = max(0, checkOut − (shiftEnd + overtimeAfterMinutes)) → hoursAll comparisons are done in UTC using shiftStart and shiftEnd applied on the record date.
Attendance Records
Read endpoints
| Endpoint | Description |
|---|---|
GET /api/employees/{id}/attendance/{date} | Single record by date — cached 5 min |
GET /api/employees/{id}/attendance | Paginated history (cursor-based) |
GET /api/users/me/attendance | Self-service history |
Query parameters (list): from, to, status, cursor, limit (1–100, default 20)
Cache key: personnel:employee:{id}:attendance:{date} — invalidated when a correction request is approved.
Attendance Request Workflow
When an employee missed a clock-in or has an incorrect record, they (or HR) can submit a correction request.
Submit → Open → Approve → (record recalculated, IsRegularized=true)
↘ Reject → (record unchanged, reason stored)Business rules
| Rule | Detail |
|---|---|
| BR-HR-008 | Request window expires 7 days after the attendance date |
| Duplicate prevention | Only one Open request per employee per date |
| Idempotency guard | AlreadyDecided error if request is no longer Open |
Endpoints
| Endpoint | Description |
|---|---|
POST /api/attendance-requests | Submit a correction request |
GET /api/attendance-requests | Manager queue (all employees) |
GET /api/attendance-requests/{id} | Single request detail |
PUT /api/attendance-requests/{id}/approve | Approve and recalculate the record |
PUT /api/attendance-requests/{id}/reject | Reject with a mandatory reason |
GET /api/users/me/attendance-requests | Self-service request history |
Permissions
| Permission | Holder | Description |
|---|---|---|
attendance:clock-in | Employee | Clock in/out via self-service |
attendance:clock-in-any | HR | Clock in for any employee |
attendance:clock-out-any | HR | Clock out for any employee |
attendance:read | Employee | View own attendance |
attendance:read-any | HR | View attendance for any employee |
attendance-requests:write | Employee | Submit correction requests |
attendance-requests:read | Employee | View own requests |
attendance-requests:manage | Manager | Approve / reject requests |
Audit Trail
| Event | Action | Severity |
|---|---|---|
| Clock-in (mobile/biometric) | personnel.clock_in | Info |
| Clock-in (manual) | personnel.manual_checkin | Warning |
| Clock-out | personnel.clock_out | Info |
| Attendance regularized | personnel.attendance_regularized | Warning |