Skip to main content

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:

  1. The authenticated user is an employee with an active deployment to the unit.
  2. A shift_assignments row exists for (employee, assigned_for = today) — or for yesterday if the shift is overnight and current time is within the end window.
  3. Current UTC time is within [start - early_in_min, end + grace_out_min].
  4. If device_binding is enabled for the unit, the fingerprint matches an approved device.
  5. If geo_enabled is on, distance from unit centre ≤ geo_radius_m, unless policy is allow_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):

  1. Load all punch_logs for that employee on the attendance date (±12 hours for overnight).
  2. Pick first direction in (in, auto) as first_in_at.
  3. Pick last direction in (out, auto) as last_out_at.
  4. Compute worked_minutes = diff(last_out - first_in).
  5. Derive status:
    • present if worked_minutes ≥ full_day_threshold.
    • half_day if worked_minutes ≥ half_day_threshold.
    • absent if first_in_at is null.
    • leave / holiday if those override.
  6. Evaluate red flags via RedFlagEngine.
  7. Upsert attendances + replace attendance_flags for the row.
  8. Emit audit and domain events.

Channels

ChannelNotes
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:

  1. Persists the punch to Hive with sync_state = pending.
  2. Shows immediate success to the employee.
  3. Retries in the background with exponential backoff.
  4. On server ack, updates sync_state = synced.
  5. If the server responds with SHIFT_WINDOW_VIOLATION from stale data, the punch becomes a regularization request automatically.