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. Save directly to biometric_attendances table $processed = 0; foreach ($attendances as $record) { $empCode = $record['deviceUserId'] ?? $record['id'] ?? null; $rawPunchTime = $record['recordTime'] ?? $record['timestamp'] ?? null; $punchTime = $this->parseDateSafe($rawPunchTime); $uid = $record['userSn'] ?? $record['uid'] ?? null; $type = $record['state'] ?? $record['type'] ?? 0; if ($empCode && $punchTime) { // Insert or ignore based on biometric_emp_id and punch_time to prevent duplicates $exists = \App\Models\BiometricAttendance::where('biometric_emp_id', $empCode) ->where('punch_time', $punchTime->toDateTimeString()) ->exists(); if (!$exists) { \App\Models\BiometricAttendance::create([ 'branch_id' => $branch->id, 'biometric_emp_id' => $empCode, 'punch_time' => $punchTime->toDateTimeString(), 'punch_type' => $type, 'terminal_alias' => $branch->name . ' (Agent)', 'sync_status' => 'pending', ]); $processed++; } } } return response()->json([ 'success' => true, 'message' => "Successfully pushed {$processed} new records to the Biometric Attendance database for Branch {$branch->id}.", ]); } private function parseDateSafe($dateStr) { if (!$dateStr) { return null; } // Handle numeric timestamps (Unix seconds or milliseconds) if (is_numeric($dateStr)) { if (strlen((string)$dateStr) > 10) { // It's likely in milliseconds return \Carbon\Carbon::createFromTimestampMs($dateStr)->setTimezone('Asia/Manila'); } return \Carbon\Carbon::createFromTimestamp($dateStr)->setTimezone('Asia/Manila'); } // Remove JS-specific timezone names like "(China Standard Time)" to prevent Carbon parse errors $cleanDateStr = trim(explode('(', $dateStr)[0]); try { return \Carbon\Carbon::parse($cleanDateStr, 'Asia/Manila')->setTimezone('Asia/Manila'); } catch (\Exception $e) { return null; } } }