Skip to content

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:

EndpointDescription
POST /api/employees/{id}/clock-inHR records clock-in for any employee
POST /api/employees/{id}/clock-outHR records clock-out for any employee
POST /api/users/me/clock-inEmployee clocks in from the mobile app
POST /api/users/me/clock-outEmployee 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

  1. Weekend — date is in the employee's HolidayList.WeeklyOffDays
  2. Holiday — date is in the employee's HolidayList.Entries
  3. OnLeave(Phase 4d) approved leave application exists ← placeholder, falls through to step 4
  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 = 0
  • EarlyExit = 0
  • Overtime = 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))         → hours

All comparisons are done in UTC using shiftStart and shiftEnd applied on the record date.


Attendance Records

Read endpoints

EndpointDescription
GET /api/employees/{id}/attendance/{date}Single record by date — cached 5 min
GET /api/employees/{id}/attendancePaginated history (cursor-based)
GET /api/users/me/attendanceSelf-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

RuleDetail
BR-HR-008Request window expires 7 days after the attendance date
Duplicate preventionOnly one Open request per employee per date
Idempotency guardAlreadyDecided error if request is no longer Open

Endpoints

EndpointDescription
POST /api/attendance-requestsSubmit a correction request
GET /api/attendance-requestsManager queue (all employees)
GET /api/attendance-requests/{id}Single request detail
PUT /api/attendance-requests/{id}/approveApprove and recalculate the record
PUT /api/attendance-requests/{id}/rejectReject with a mandatory reason
GET /api/users/me/attendance-requestsSelf-service request history

Permissions

PermissionHolderDescription
attendance:clock-inEmployeeClock in/out via self-service
attendance:clock-in-anyHRClock in for any employee
attendance:clock-out-anyHRClock out for any employee
attendance:readEmployeeView own attendance
attendance:read-anyHRView attendance for any employee
attendance-requests:writeEmployeeSubmit correction requests
attendance-requests:readEmployeeView own requests
attendance-requests:manageManagerApprove / reject requests

Audit Trail

EventActionSeverity
Clock-in (mobile/biometric)personnel.clock_inInfo
Clock-in (manual)personnel.manual_checkinWarning
Clock-outpersonnel.clock_outInfo
Attendance regularizedpersonnel.attendance_regularizedWarning