Skip to main content

Locking

Why

Downstream systems (payroll, billing) need a stable cut-off. Locking a month makes its attendance immutable except for an audited SuperAdmin override.

Triggers

  1. Auto-lock — a scheduled job on the 5th of the next month at 23:00 UTC locks every (organization, unit, previous month) tuple that isn't already locked.
  2. Manual lock — HR can lock a specific unit earlier via POST /api/v1/attendance/lock.
  3. Unlock — only SuperAdmin, via POST /api/v1/attendance/unlock with a required reason.

Semantics

When locked:

  • attendances.locked_at is set for every row in scope.
  • PATCH /attendance/{id} returns 423 ATTENDANCE_LOCKED.
  • New punches for that month go to punch_logs (never refused) but do not mutate attendances rows — they appear in a post_lock_punches view for investigation.
  • Regularizations against locked months are rejected unless SuperAdmin overrides first.

Unlock Flow

sequenceDiagram
participant SA as SuperAdmin
participant API as Laravel API
participant DB as MySQL

SA->>API: POST /attendance/unlock { scope, reason }
API->>DB: audit_logs(action=lock.unlock)
API->>DB: attendance_locks WHERE scope DELETE
API->>DB: attendances.locked_at = NULL for scope
API-->>SA: 200 { unlocked_count }

The unlock emits a domain event that notifies HR and SystemAdmin channels so no silent edits happen.

Re-Lock

After edits, HR (or SuperAdmin) re-locks. A single month can be locked and unlocked multiple times; each cycle is audited with its reason.

Reports

GET /attendance-locks?month=2026-04 lists the lock history for compliance.