# ZKTeco biometric device setup (KiyoHR SaaS)

## Overview

ZKTeco devices push attendance punches to KiyoHR via HTTP POST (Api Settings on the device). Each terminal is registered per company with Basic Auth credentials. Punches are stored in an audit log, then applied as multi-session `WorkDuration` rows (Check In / Check Out).

## HRMS setup

1. Ensure the subscription plan allows biometric attendance (`attendance_biometric` plan feature when features are configured).
2. Open **Settings → Biometric Devices → Add device**.
3. Enter the device **Terminal SN**, choose **Punch mode**, and optional alias / location / IP allowlist.
4. On the setup screen, copy:
   - **API URL**
   - **User Name**
   - **Password** (shown once; regenerate if lost)
   - **Data Template**
5. Ensure each device user ID matches the employee **Employee ID** (`users.employee_number`) in that company.

## Punch modes (login / logout / both)

| Mode | Use when | Behaviour |
|------|----------|-----------|
| **Login only** | Entry gate device | Every punch is check-in (opens a session), even if device sends Check Out |
| **Logout only** | Exit gate device | Every punch is check-out (closes open session), even if device sends Check In |
| **Both** | Single device for in and out | Session-aware: uses device `punch_state` when it matches session state; if state conflicts (e.g. always Check In) or is empty/unknown, **auto-toggles** (no open session → in, open session → out) |

Examples:

- Two terminals: register Entry SN as **Login only**, Exit SN as **Logout only**.
- One terminal: register as **Both**. Alternating Check In/Out from the device is ideal; if the device always sends the same state, HRMS still toggles based on whether a session is open.


## Device Api Settings

| Field | Value |
|-------|--------|
| User Name | From device setup screen |
| Password | From device setup screen |
| API URL | `https://{your-domain}/api/integrations/zkteco/attendance` |
| Data Template | JSON array from setup screen (placeholders like `{emp_code}`) |
| Interval Time | `1` or higher |
| Enable | Checked |

Use **HTTPS** in production. Basic Auth credentials identify the tenant; do not put company IDs in the URL.

## Recommended data template

```json
[{"company_name":"{company_name}","emp_code":"{emp_code}","first_name":"{first_name}","last_name":"{last_name}","punch_datetime":"{punch_datetime}","punch_date":"{punch_date}","punch_time":"{punch_time}","punch_state":"{punch_state}","verify_type":"{verify_type}","terminal_sn":"{terminal_sn}","terminal_alias":"{terminal_alias}","longitude":"{longitude}","latitude":"{latitude}","upload_time":"{upload_time}"}]
```

## Behaviour

- Device **punch mode** controls whether punches are treated as login, logout, or both (see above).
- **Check In** opens a `WorkDuration` session (multiple sessions per day supported).
- **Check Out** closes the open session and updates attendance totals.
- Duplicate pushes (same emp/SN/datetime/state) are accepted as duplicates and not re-applied.
- Payload `terminal_sn` must match the registered SN when present.
- Review **Settings → Attendance Punches** for pending/error/unmapped codes; use **Retry** after fixing Employee IDs.

## Troubleshooting

| Symptom | Cause | Fix |
|---------|--------|-----|
| Device API log shows OK but Punch Log empty | Body not parsed / wrong keys / date format | Use lowercase template from Setup screen; UPPERCASE keys are also accepted now |
| Punch Log status `pending` forever | Punch apply failed or interrupted | Run `php artisan zkteco:reprocess-pending`. Punches are applied inline on ingest (no queue). |
| Punch status `error`: No active employee… | Device User ID ≠ HRMS Employee ID | Set device user ID to match `employee_number` (e.g. `1`, `KIYO2`) |
| Punch `ignored`: Unknown punch_state | Both-mode device sent unmapped state *and* auto-toggle failed (should be rare) | Use Check In/Out, or set device to Login/Logout only |
| Punch `ignored`: Active session already open | Login-only device punched while already checked in | Clock out first, or use logout device |
| Punch `ignored`: No open session to close | Logout-only device with no prior check-in | Check in first on login/both device |
| 401 Unauthorized | Bad username/password or inactive device | Regenerate credentials on Setup screen and paste into device |
| 202 with `rejected` > 0 | SN mismatch / missing fields | Ensure `terminal_sn` matches registered SN; include `emp_code` + `punch_datetime` |

Reprocess stuck punches:

```bash
php artisan zkteco:reprocess-pending
```

## Security notes

- Passwords are stored hashed; plaintext is only shown on create/regenerate.
- Optional IP allowlist per device.
- Webhook is rate-limited (`zkteco-webhook`).
- Tenant isolation is by device credentials + `company_id`; never by payload `company_name`.
