Attendance Capture
End-to-End Flow
sequenceDiagram
participant E as Employee (mobile)
participant API as Laravel API
participant Q as Redis queue
participant P as AttendanceProcessor
participant DB as MySQL
E->>API: POST /attendance/punch (token + fingerprint + lat/lon)
API->>API: Validate (shift exists, window, device, geo policy)
API->>DB: INSERT punch_logs
API->>Q: dispatch ProcessAttendanceJob(punch_id)
API-->>E: 200 { accepted, attendance_id, flags }
Q->>P: handle(punch_id)
P->>DB: Upsert attendances
P->>DB: Diff + insert attendance_flags
P->>DB: Insert audit_logs
Validation Gates at the API
Before the raw punch is persisted the controller checks:
- The authenticated user is an employee with an active deployment to the unit.
- A
shift_assignmentsrow exists for(employee, assigned_for = today)— or for yesterday if the shift is overnight and current time is within the end window. - Current UTC time is within
[start - early_in_min, end + grace_out_min]. - If
device_bindingis enabled for the unit, the fingerprint matches an approved device. - If
geo_enabledis on, distance from unit centre ≤geo_radius_m, unless policy isallow_flag.
Any failure returns an error code from the errors page. Passing gates → persist punch → queue processor.
Processor Logic
The AttendanceProcessor runs idempotently for a (shift_assignment_id):
- Load all
punch_logsfor that employee on the attendance date (±12 hours for overnight). - Pick first
direction in (in, auto)asfirst_in_at. - Pick last
direction in (out, auto)aslast_out_at. - Compute
worked_minutes = diff(last_out - first_in). - Derive
status:presentifworked_minutes ≥ full_day_threshold.half_dayifworked_minutes ≥ half_day_threshold.absentiffirst_in_atis null.leave/holidayif those override.
- Evaluate red flags via
RedFlagEngine. - Upsert
attendances+ replaceattendance_flagsfor the row. - Emit audit and domain events.
Channels
| Channel | Notes |
|---|---|
| Mobile (Flutter) | Primary channel. Buffers punches offline in Hive; flushes on connect. |
| Web (React) | Used by managers to record on-behalf punches (always flagged MANUAL_PUNCH). |
| Telegram | /in / /out commands — lat/lon forwarded when Telegram has location. |
| Kiosk (future) | Shared tablet at the unit with PIN + face match. |
Offline Buffering
The Flutter app:
- Persists the punch to Hive with
sync_state = pending. - Shows immediate success to the employee.
- Retries in the background with exponential backoff.
- On server ack, updates
sync_state = synced. - If the server responds with
SHIFT_WINDOW_VIOLATIONfrom stale data, the punch becomes a regularization request automatically.