header('X-API-Key') ?? $request->input('api_key'); // You should store a master API key in your .env like ZKTECO_SYNC_KEY=xxx $validKey = env('ZKTECO_SYNC_KEY', 'default_secure_sync_key_123'); if ($apiKey !== $validKey) { Log::warning('Unauthorized ZKTeco sync attempt', ['ip' => $request->ip()]); return response()->json(['error' => 'Unauthorized'], 401); } $branchId = $request->input('branch_id'); $zktecoIp = $request->input('zkteco_ip'); $attendances = $request->input('attendances', []); if (empty($attendances)) { return response()->json(['message' => 'No attendance records provided'], 400); } // 2. Find the branch $branchQuery = Branch::query(); if ($branchId) { $branchQuery->where('id', $branchId); } elseif ($zktecoIp) { $branchQuery->where('zkteco_ip', $zktecoIp); } else { return response()->json(['error' => 'Must provide branch_id or zkteco_ip'], 400); } $branch = $branchQuery->first(); if (!$branch) { return response()->json(['error' => 'Branch not found'], 404); } // 3. Process the logs (match BiometricAttendanceController logic) // Ensure we handle arrays with 'recordTime' or 'timestamp' $formattedAttendances = []; foreach ($attendances as $record) { $empCode = $record['deviceUserId'] ?? $record['id'] ?? null; $punchTime = $record['recordTime'] ?? $record['timestamp'] ?? null; $uid = $record['userSn'] ?? $record['uid'] ?? null; if ($empCode && $punchTime) { $formattedAttendances[] = [ 'id' => $uid, 'emp_code' => (string) $empCode, 'punch_time' => $punchTime, 'branch_id' => $branch->id ]; } } $groupedAttendances = collect($formattedAttendances)->groupBy(function ($item) { return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time'])); }); $processed = 0; $inserted = 0; foreach ($groupedAttendances as $key => $dayEntries) { $processed++; $sorted = $dayEntries->sortBy('punch_time'); $firstEntry = $sorted->first(); $lastEntry = $sorted->last(); // Note: Since this is an API, there is no Auth::user(). // We find the employee by branch_id instead of created_by to avoid Admin vs HR mismatch. $employee = \App\Models\Employee::with('user') ->where('branch_id', $branch->id) ->where('biometric_emp_id', $firstEntry['emp_code']) ->first(); if (!$employee) { // Try fallback: maybe the employee isn't assigned to the branch but belongs to the same company $employee = \App\Models\Employee::with('user') ->where('created_by', $branch->created_by) ->where('biometric_emp_id', $firstEntry['emp_code']) ->first(); } if ($employee) { if ($sorted->count() > 1) { $attedanceDate = date('Y-m-d', strtotime($firstEntry['punch_time'])); $clockInTime = date('H:i:s', strtotime($firstEntry['punch_time'])); $clockOutTime = date('H:i:s', strtotime($lastEntry['punch_time'])); $exists = \App\Models\AttendanceRecord::where('employee_id', $employee->user_id) ->where('date', $attedanceDate) ->exists(); if (!$exists) { $shift = \App\Models\Shift::where('id', $employee->shift_id)->where('status', 'active')->first() ?? \App\Models\Shift::where('created_by', $branch->created_by)->where('status', 'active')->first(); $policy = \App\Models\AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first() ?? \App\Models\AttendancePolicy::where('created_by', $branch->created_by)->where('status', 'active')->first(); $attendance = new \App\Models\AttendanceRecord(); $attendance->employee_id = $employee->user_id; $attendance->biometric_id = $firstEntry['id']; $attendance->shift_id = $shift?->id; $attendance->attendance_policy_id = $policy?->id; $attendance->date = $attedanceDate; $attendance->clock_in = $clockInTime; $attendance->clock_out = $clockOutTime; $attendance->created_by = $employee->created_by ?? $branch->created_by; $attendance->save(); $attendance->fresh(); $attendance->processAttendance(); $inserted++; } else { $skippedReasons[] = "Record already exists for Date: {$attedanceDate}"; } } else { $skippedReasons[] = "Only 1 punch found for {$firstEntry['emp_code']} on " . date('Y-m-d', strtotime($firstEntry['punch_time'])) . " (Need Clock In and Out)"; } } else { $skippedReasons[] = "Employee with Biometric ID {$firstEntry['emp_code']} not found in Branch {$branch->id}"; } } $debugMsg = empty($skippedReasons) ? "" : " | Skipped reasons: " . implode(', ', array_unique($skippedReasons)); return response()->json([ 'success' => true, 'message' => "Processed {$processed} unique daily records, Inserted {$inserted} new attendances." . $debugMsg, ]); } }