Skip to main content

Attendance, Shifts & Time (ATT)

Module code ATT · Release R1 (MVP), device gateway R2 · Packaging Core · Requirements FR-ATT-*, BR-ATT-*, RULE-ATT-* in the PRD and BRD · Related Leave, Payroll, Mobile, Notifications & Workflow

1. Overview and scope​

ATT turns raw presence evidence into one attendance day per employee per date. The day carries a status, worked minutes, lates, early exits, overtime and a paid fraction. Payroll reads only these computed days, never raw punches. ATT is the first link in the attend-to-pay chain and the module with the most configuration, so its design goal is one typed, effective-dated Attendance Policy and one deterministic day-computation engine.

In scope (R1)

  • Punch capture: web, mobile selfie with geo-fence, biometric CSV/API import, Excel upload, manager entry. Kiosk and face verification are R2 (Mobile).
  • Shifts (fixed, flexible, night/cross-midnight), shift assignment, rotation sequences, week-off patterns.
  • The Attendance Policy: graces, half/full-day thresholds, late rules and late-to-leave deduction, missed-punch default out-time, breaks, overtime, extra-day pay and week-off penalty.
  • The day computer, which re-runs on any input change and is idempotent.
  • Requests: attendance request (WFH, WFO, on-duty, remote location), regularization, permission (short leave), shift change and swap, overtime claim. Every request goes through the approval engine.
  • Period lock aligned with the pay schedule, and post-lock adjustments.
  • Reports and dashboards for employees, managers, HR and owners.

Out of scope: drag-and-drop roster planning and coverage view (R3 candidate), anomaly detection (R4), Wi-Fi BSSID verification (R2), timesheets (TSM).

2. Learned from the reference product​

Source: Spryple analysis, §5, §16 and §21.3.

  • [Observed] Attendance comes from many channels: biometric mapping, Excel upload with a vendor template, mobile face check-in, a receptionist "front office" check-in and a CRM photo punch. Punch records carry a source ("Lite", "CRM", "MOBILEAPP"), an address and first/last check-in photos (SP-ATT-13, SP-ATT-14, SP-ATT-19, SP-ATT-20; A: Attendance, MOB).
  • [Observed] Shift-work features are rich: rotations, per-employee week-offs, shift-change requests where the manager can swap shifts between employees, one permission per day, and regularization that converts a day to leave (SP-ATT-06 to SP-ATT-12; M: Attendance).
  • [Observed] Rules are scattered: lates appear in the shift master and HR Policy rules 22–24; default shift in rules 19–21 and 25–26; OT is only "hours beyond the shift" with no approval (SP-ATT-01, SP-IAM-03; A: Employee Mgmt).
  • [Observed] The day-status legend is A, P, W, L, H, HD, WP, HP, HL, and counts accept halves (e.g. 14.5). Geo-fencing is a single radius per branch, e.g. 200 m (§21.3, screenshots).
  • [Observed] Manager "on behalf" attendance requests are auto-approved (SP-ATT-04; SP-X-04).
  • [Inferred] There is no documented attendance freeze per payroll period, so late corrections after payroll would silently change history (§5.4).

3. Our design decisions​

IDDecision [Proposed]Reason
D-ATT-01One Attendance Policy object, versioned and effective-dated, assigned by applicability (location, department, designation, employment type, grade). It holds graces, thresholds, late rules, OT, extra days and penalties.Removes the duplicated, contradictory rules seen in the reference product. Changes apply to existing employees from a chosen date.
D-ATT-02Shifts are a separate master; the policy references them. A shift defines time windows only, not deductions.Keeps "when do you work" apart from "how is it judged".
D-ATT-03A deterministic day computer: att_day = f(punches, shift, policy version, approved requests, leave, holidays). It stores an input_hash and recomputes on any input event.Late biometric data, approvals and policy edits are all just recomputes. It is idempotent and testable with golden cases.
D-ATT-04A day has two halves, each with its own status code. The day status is derived from the halves.Half-day leave, half-day WFH and half-day absence combine without special cases. The HL and HD legends fall out naturally.
D-ATT-05Period lock per pay period. After lock, changes create att_adjustment rows that flow to the next pay run as LOP reversal or arrears.Locked payroll must be reproducible. Corrections still reach the employee.
D-ATT-06Punches are append-only and deduplicated; edits are new punches with source = MANUAL and a reason.Audit evidence; the selfie and geo trail are never overwritten.
D-ATT-07Geo-fences are a list per location (circle with radius or polygon) with a policy action: BLOCK, FLAG or ALLOW.The reference product has one radius per branch. Large campuses and multi-gate sites need several.
D-ATT-08OT needs approval when the policy says so, and has a compensation mode: pay, comp-off or employee choice.OT is a cost line; unapproved "hours beyond shift" is not acceptable to finance.
D-ATT-09On-behalf requests by a manager are auto-approved for the levels the manager covers (see NTF).Parity with SP-X-04 without bypassing a second-level HR step where one is configured.
D-ATT-10Shift swaps require consent from the counterpart before the manager approves.The reference product swaps on approval without consent.

4. Domain model​

Notes:

  • att_punch is high volume. Partition it by month on punched_at. dedupe_key = hash of employee, minute bucket, direction and source family.
  • att_day has a unique key (tenant_id, employee_id, work_date). It is a projection: it can always be rebuilt.
  • Day status codes: P present, A absent, HD half day, W week-off, H holiday, L leave, HL half-day leave, WP week-off present, HP holiday present, OD on duty, WFH work from home, NJ not yet joined, SEP after separation. Half codes use the same set (P, A, L, OD, WFH, W, H). The first nine match the reference legend [Observed]; the rest are [Proposed].
  • Face templates and kiosk enrolment live in Mobile (mob_face_profile). Geo-fences live in ORG (org_geofence).

5. State machines​

5.1 Attendance request (all types)​

Withdrawn replaces the reference product's soft "Deleted" status. A shift-swap request adds a AwaitingCounterpart step between Submitted and InApproval.

5.2 Attendance day​

5.3 Period lock​

6. Key business logic​

6.1 Resolving the shift and policy for a date​

function resolveContext(employee, date):
assignment = shiftAssignment(employee) where valid_from <= date and (valid_to is null or valid_to >= date)
if assignment.rotation_id:
offset = days_between(assignment.valid_from, date) mod rotation.cycle_days
shift = rotation step covering offset
else:
shift = assignment.shift or policy.default_shift
weekoff = assignment.weekoff_pattern or policy.default_weekoff_pattern
policy = highest-priority att_policy_assignment whose applicability matches the
employee's job assignment on that date (location, department, designation,
employment type, grade); take the att_policy_version valid on that date
holiday = holiday on date in the employee's location calendar (see ORG)
return shift, weekoff, policy, holiday

6.2 Assigning punches to a work date (night shifts)​

window_start = date + shift.start_time - shift.early_window_minutes
window_end = date + shift.end_time + shift.late_window_minutes
if shift.crosses_midnight: window_end = window_end + 1 day
punches(date) = punches where window_start <= punched_at < window_end, after dedupe
# A punch falling in two windows (back-to-back shifts) goes to the window whose
# shift start is nearest.

Dedupe: punches by the same employee within dedupe_seconds (default 60) are one event; the earliest IN and the latest OUT win. When multiple sources are present on one day, break deductions are skipped, matching the reference rule [Observed §21.3].

6.3 The day computation​

function computeDay(employee, date):
ctx = resolveContext(employee, date)
if date < employee.joining_date: return day(status=NJ, paid=0)
if date > employee.last_working_date: return day(status=SEP, paid=0)

base = HOLIDAY if ctx.holiday else (WEEK_OFF if ctx.weekoff.matches(date) else WORKING)
p = punches(date)
approvedLeave = LVE.approvedHalves(employee, date) # e.g. {first: CL}
approvedReq = approved att_request halves (WFH, OD, remote) for date

# 1. Pair punches
first_in = earliest IN, or earliest punch if direction unknown
last_out = latest OUT after first_in
if first_in and not last_out:
if policy.missed_punch.mode == DEFAULT_OUT:
last_out = date + policy.missed_punch.default_out_time ; flag auto_out
else:
missed_punch = true
worked = minutes(first_in, last_out) - breakMinutes(policy, p)

# 2. Late and early
late = max(0, first_in - (shift.start + policy.grace_in_minutes))
early = max(0, (shift.end - policy.grace_out_minutes) - last_out)
if shift.is_flexible: late = 0 ; early = 0 ; judge by worked minutes only

# 3. Half statuses, highest precedence first
for half in [first, second]:
if approvedLeave[half]: status[half] = L
elif approvedReq[half]: status[half] = approvedReq[half].code # OD, WFH
elif base == HOLIDAY: status[half] = H
elif base == WEEK_OFF: status[half] = W
else: status[half] = pending
# 4. Judge worked time on working halves
if any half pending:
if missed_punch: mark pending halves A, needs_action
elif worked >= policy.full_day_minutes: pending halves -> P
elif worked >= policy.half_day_minutes: first pending half -> P, other -> A
else: pending halves -> A
# 5. Work on off days
if base in (HOLIDAY, WEEK_OFF) and worked >= policy.offday_min_minutes:
status = HP or WP ; eligible_for = policy.offday_compensation # PAY or COMP_OFF
# 6. Overtime
ot = worked - shift.duration if base == WORKING else worked
ot = 0 if ot < policy.ot.min_minutes else roundDown(ot, policy.ot.rounding_minutes)
ot = min(ot, policy.ot.daily_cap_minutes)
# 7. Paid fraction (input to payroll)
paid = sum over halves of 0.5 if half in (P, L_paid, OD, WFH, W, H) else 0
# L_paid excludes LOP; W and H paid unless the week-off penalty applies (6.5)
# 8. Persist only if input_hash changed; emit att.day.computed

Precedence order (highest first): separation/not-joined, approved leave, approved attendance request, holiday, week-off, punches. A holiday outranks a week-off for display; both are paid.

6.4 Late rule and late-to-leave deduction​

monthly at period close, and on each late day (to warn early):
lates = count of days in the pay period with late_minutes > 0
excluding days where an approved permission covers the late minutes
free = policy.late.free_lates_per_period # e.g. 3
if lates > free:
blocks = floor((lates - free) / policy.late.block_size) # e.g. every 3 lates
deduct = blocks * policy.late.deduct_days_per_block # e.g. 0.5 day
for leaveType in policy.late.deduct_from_order: # e.g. CL, EL, LOP
take = min(deduct, LVE.available(employee, leaveType))
LVE.post(LATE_DEDUCTION, leaveType, take) ; deduct -= take
if deduct > 0: LVE.post(LATE_DEDUCTION, LOP, deduct)
emit att.late.threshold_breached

This replaces both the shift-master late setting and HR Policy rules 22–24 in the reference product.

6.5 Week-off penalty (sandwich for absence)​

for each week-off or holiday day D in the period:
if policy.weekoff_penalty.enabled and employee designation in policy.weekoff_penalty.designations:
before = previous working day status ; after = next working day status
if before in (A, LOP) and after in (A, LOP): # "both" mode
D.paid_fraction = 0 ; D.penalty = true
# Mode "either" penalises when either side is absent. Mode "lop_threshold" applies a
# proportional week-off deduction once LOP days exceed N (payroll rule, see PAY).

6.6 Extra-day pay slabs​

worked_days = count of P plus half of HD in the period on working days
slab = policy.extra_days.slabs for the employee's designation where min <= worked_days <= max
extra_days = slab.eligible_extra_days limited to count of WP and HP days
emit as payroll input EXTRA_DAYS (quantity) at the pay-run snapshot

6.7 Geo-fence check at punch time​

fences = org_geofence for the employee's allowed locations (home location plus
any location granted by the policy, e.g. client sites for field staff)
inside = any fence where haversine(point, fence.center) <= fence.radius_m + min(accuracy_m, policy.geo.max_accuracy_slack_m)
or point within fence.polygon
if accuracy_m > policy.geo.max_accuracy_m: result = LOW_ACCURACY
result = INSIDE if inside else OUTSIDE
action = policy.geo.outside_action # BLOCK, FLAG or ALLOW
if action == BLOCK and result != INSIDE: reject punch with a clear reason
store geofence_result on the punch

6.8 Regularization and permission rules​

  • Regularization lookback: only dates within regularization.lookback_days and inside an open period. A regularization can set a missing out-time, a missing in-time, or convert the day to leave. Converting to leave creates an LVE request (LOP only when no balance, per [Observed] SP-ATT-11).
  • Permission: at most permission.max_per_day (default 1, as observed) and permission.max_per_period; permission.max_minutes each; paid or unpaid per permission.paid. An approved permission offsets late or early minutes on that day.

7. Configuration​

All settings live in the Attendance Policy version unless marked otherwise. Types are JSON Schema types in packages/policy.

SettingTypeDefaultDescription
default_shift_codestringtenant's first shiftShift used when no assignment exists
default_weekoff_patternstringSUNPattern code, e.g. SUN, SAT_SUN, 2ND_4TH_SAT_SUN
grace_in_minutesinteger10Minutes after shift start before a punch counts as late
grace_out_minutesinteger0Minutes before shift end before a punch counts as early exit
full_day_minutesinteger480Worked minutes for a full day
half_day_minutesinteger240Worked minutes for a half day
break.modeenumFIXEDFIXED deducts break_minutes from the shift; PUNCHED uses break punches; NONE
missed_punch.modeenumNEEDS_ACTIONNEEDS_ACTION or DEFAULT_OUT
missed_punch.default_out_timetimeshift endOut-time applied in DEFAULT_OUT mode
late.free_lates_per_periodinteger3Lates allowed before deduction
late.block_sizeinteger3Lates per deduction block
late.deduct_days_per_blocknumber0.5Leave days deducted per block
late.deduct_from_orderarray of string[CL, EL, LOP]Leave types tried in order
permission.enabledbooleantrueAllows short-leave requests
permission.max_per_dayinteger1Requests per day
permission.max_per_periodinteger2Requests per pay period
permission.max_minutesinteger120Minutes per request
permission.paidbooleantrueWhether permission minutes are paid
regularization.lookback_daysinteger7How far back an employee may regularize
regularization.max_per_periodinteger5Regularizations per period
regularization.allow_on_weekoffbooleanfalseAllows regularizing week-off days
geo.requiredbooleantrueMobile punches need a location
geo.outside_actionenumFLAGBLOCK, FLAG or ALLOW
geo.max_accuracy_minteger100Readings worse than this are LOW_ACCURACY
geo.max_accuracy_slack_minteger30Accuracy allowance added to the fence radius
selfie.requiredbooleantrueMobile punch needs a selfie
face_match.modeenumOFFOFF, FLAG or BLOCK (R2 add-on)
ot.enabledbooleanfalseOvertime is computed
ot.min_minutesinteger30Below this, OT is zero
ot.rounding_minutesinteger15Round OT down to this block
ot.daily_cap_minutesinteger240Daily OT cap
ot.period_cap_minutesinteger3000Cap per quarter; set it to the applicable statutory limit (example, verify per state)
ot.requires_approvalenumPOSTNONE, PRE or POST
ot.multipliernumber2.0Pay multiplier on the ordinary hourly rate (statutory example, verify)
offday_min_minutesinteger240Minimum minutes worked on an off day to count as WP or HP
offday_compensationenumEMPLOYEE_CHOICEPAY, COMP_OFF or EMPLOYEE_CHOICE
extra_days.enabledbooleanfalseExtra-day pay slabs apply
extra_days.slabsarray of object[]Per designation: min_days, max_days, eligible_extra_days
weekoff_penalty.enabledbooleanfalseWeek-off absence penalty applies
weekoff_penalty.modeenumBOTH_SIDESBOTH_SIDES or EITHER_SIDE
weekoff_penalty.designationsarray of uuidallDesignations the penalty applies to
dedupe_secondsinteger60Punches closer than this are merged
lock.blocking_checksarray of string[PENDING_REQUESTS, MISSED_PUNCH]Issues that block a period lock without override

8. Screens by persona​

PersonaWebMobile
Employee (employee)My calendar with legend, today's shift and next shift, punch (when web punch allowed), requests (attendance, regularization, permission, shift change, OT), alerts for days needing action, historyPunch with selfie and geo, offline queue indicator, calendar, requests, alerts
Field executiveAs employee; punch locations include client sitesPunch at client site with photo (shared with FFM)
Line managerTeam calendar (absentees, pending approvals), approval inbox with bulk actions, on-behalf requests, shift assignment, rotations, week-offs, swap approvals, team regularization (multi-select)Approvals, today's attendees, team calendar, on-behalf punch request
HR manager / adminAttendance Policy editor with preview ("who does this apply to"), shifts, rotations, week-off patterns, device mapping, Excel upload (dry-run), recompute tool, period lock console with blockers, adjustments queue—
Kiosk operator—Kiosk punch (R2, Mobile)
Org ownerAttendance dashboard: present/absent/late by location, work-type split, trendDashboard tiles

9. Events​

EventEmitted whenMain consumers
att.punch.recordedA punch is accepted (any source)ATT day computer, FFM (track session start/stop)
att.punch.rejectedA punch fails geo or face checks in BLOCK modeNTF (notify employee), RPT
att.day.computedAn att_day row is created or changedLVE (comp-off eligibility), PAY (input invalidation), RPT
att.day.needs_actionA day becomes missed-punch or unexplained absentNTF (employee alert, reminder)
att.request.submittedAny attendance request is submittedNTF (start workflow)
att.request.approvedWorkflow completes with approvalATT recompute, NTF
att.request.rejectedWorkflow rejectsNTF
att.request.cancelledA cancellation is acceptedATT recompute
att.overtime.approvedOT minutes are approvedPAY (OT input)
att.late.threshold_breachedLate count crosses the deduction thresholdLVE (post deduction), NTF
att.period.lockedA period is lockedPAY (snapshot inputs), NTF
att.period.unlockedA period is reopenedPAY (invalidate draft run)
att.adjustment.createdA post-lock change is recordedPAY (next-run input)

Consumed: lve.request.approved, lve.request.cancelled (recompute affected days), org.holiday.changed (recompute the location's future and open days), emp.job.changed (re-resolve policy), emp.employee.joined, ext.lwd.reached, pay.run.locked (close the period), ffm.visit.checked_in (optional auto-punch).

10. API surface​

Detailed in API endpoints.

ResourceMain operations
/v1/att/punchesPOST (mobile/web punch with selfie upload reference), GET (list by employee and date range)
/v1/att/punches/importPOST CSV or Excel batch with dry_run=true
/v1/att/devices and /v1/att/devices/:id/user-mapsCRUD; device ingest uses ingest.spyrl.app with a device secret
/v1/att/daysGET by employee, team or location and range; POST /v1/att/days/recompute (HR)
/v1/att/requestsPOST, GET, PATCH (while Submitted), POST /:id/withdraw, POST /:id/cancel
/v1/att/shifts, /v1/att/rotations, /v1/att/weekoff-patternsCRUD
/v1/att/shift-assignmentsPOST bulk assign with date range, GET
/v1/att/policies and /v1/att/policies/:id/versionsCRUD, publish version, GET /:id/applicability-preview
/v1/att/period-locksPOST (start lock with checks), POST /:id/unlock
/v1/att/adjustmentsGET

11. Permissions​

PermissionTypical scopesDefault roles
att.punch.createSELFemployee, field_executive
att.punch.create_for_othersLOCATIONkiosk_operator, hr_manager
att.punch.importORG, LOCATIONhr_manager, hr_admin
att.day.readSELF, TEAM, LOCATION, ORGemployee (SELF), line_manager (TEAM), hr_manager, org_owner, auditor
att.day.recomputeORGhr_admin
att.request.createSELFemployee
att.request.create_on_behalfTEAM, LOCATIONline_manager, hr_manager
att.request.approveTEAM, LOCATION, ORGline_manager, hr_manager
att.shift.manageORGhr_admin
att.shift.assignTEAM, LOCATIONline_manager, hr_manager
att.policy.manageORGhr_admin
att.device.manageORGhr_admin
att.period.lockORG, LOCATIONhr_manager, payroll_manager
att.period.unlockORGhr_admin (with reason; audited)
att.selfie.readTEAM, ORGline_manager, hr_manager (sensitive; audited)
att.report.exportTEAM, ORGline_manager, hr_manager, org_owner

12. Reports​

All reports use the RPT framework: one dataset, scoped by the viewer's permission, exported to Excel or PDF, schedulable.

ReportContentsReference
Monthly attendance registerGrid of status codes per day with present, absent, leave, late counts and halves[Observed] SP-ATT-16
Daily attendance (current day)Present, absent, late, on leave, not punched; check-in photo on demand[Observed] SP-ATT-16
Punch logEvery punch with source, location, geo result, device[Observed] SP-ATT-16
Attendance summaryFirst in, last out, worked, productive, OT and permission hours[Observed] SP-ATT-16
Late and early-exit reportLate minutes per day and deduction blocks[Observed] SP-ATT-16
Missed punch and regularizationDays needing action, regularizations raised and approved[Observed] SP-ATT-16
Shift roster and shift-wise headcountPresent over planned per shift per day[Observed] §21.3
Overtime reportComputed vs approved OT, cost estimate[Proposed]
Extra days and penaltyWP/HP days, eligible extra days, penalised week-offs[Observed] SP-ATT-16
Payable-days previewPaid fraction totals per employee for the open period[Proposed]
Geo exceptionsPunches outside fence or low accuracy, by location[Proposed]
Post-lock adjustmentsAdjustments waiting for or applied in the next run[Proposed]

13. Release and priority​

CapabilityReleaseMoSCoW
Web and mobile punch with selfie and geo-fence; shifts; week-offs; day computerR1Must
Rotations, shift change and swap, permissions, regularization, on-behalfR1Must
OT with approval, extra days, week-off penalty, late deductionR1Must
Biometric CSV/API import, device user mappingR1Must
Period lock and adjustmentsR1Must
Biometric push gateway (ADMS-style) and sync agentR2Should
Kiosk mode and face verification with livenessR2Should
Wi-Fi BSSID and QR fallback for punchesR2Could
Roster planner (drag and drop) and coverage viewR3Could
Attendance anomaly detectionR4Could

14. Open questions​

  1. Which biometric vendors and protocols do pilot customers use (ZKTeco/eSSL push, polling or file)? This decides whether the R2 gateway moves to R1.
  2. Should a holiday worked by a night-shift employee be judged on the shift-start date or the calendar date? The default is the shift-start date.
  3. Do we allow negative paid fractions (e.g. penalty days beyond the day itself), or keep penalties as LOP inputs to payroll? The default keeps them as LOP inputs.
  4. What retention for punch selfies satisfies customers' audit needs and DPDP minimisation? The architecture default is 180 days.
  5. Is "permission" a leave type (LVE) or an attendance request (ATT)? The decision here is ATT, because it offsets minutes rather than days.