syncEvents(); $songsCount = $this->syncSongs(); $summary = [ 'services_count' => $eventsSummary['services_count'], 'songs_count' => $eventsSummary['songs_count'], 'matched_songs_count' => $eventsSummary['matched_songs_count'], 'unmatched_songs_count' => $eventsSummary['unmatched_songs_count'], 'catalog_songs_count' => $songsCount, ]; $this->writeSyncLog('success', $summary, null, $startedAt, Carbon::now()); return $summary; } catch (Throwable $exception) { $summary = [ 'services_count' => 0, 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, 'catalog_songs_count' => 0, ]; $this->writeSyncLog('error', $summary, $exception->getMessage(), $startedAt, Carbon::now()); throw $exception; } } public function syncEvents(): array { $events = $this->fetchEvents(); $summary = [ 'services_count' => 0, 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, ]; foreach ($events as $event) { try { $eventId = (int) ($event->getId() ?? 0); if ($eventId === 0) { continue; } $serviceRoles = $this->extractServiceRoles($this->getEventServices($eventId)); $service = $this->upsertService($event, $serviceRoles); $songSummary = $this->syncServiceAgendaSongs((int) $service->id, $eventId); $summary['services_count']++; $summary['songs_count'] += $songSummary['songs_count']; $summary['matched_songs_count'] += $songSummary['matched_songs_count']; $summary['unmatched_songs_count'] += $songSummary['unmatched_songs_count']; } catch (\Throwable $e) { Log::warning('Sync-Fehler für Event', [ 'event_id' => $event->getId() ?? 'unknown', 'error' => $e->getMessage(), ]); continue; } } return $summary; } public function syncSongs(): int { $songs = $this->fetchSongs(); $count = 0; foreach ($songs as $song) { $ccliId = $this->normalizeCcli($song->getCcli() ?? null); $ctsSongId = (string) ($song->getId() ?? ''); if ($ccliId !== null) { DB::table('songs')->updateOrInsert( ['ccli_id' => $ccliId], [ 'cts_song_id' => $ctsSongId !== '' ? $ctsSongId : null, 'title' => (string) ($song->getName() ?? ''), 'updated_at' => Carbon::now(), 'created_at' => Carbon::now(), ] ); $count++; } if ($ctsSongId !== '' && $ccliId === null) { $existing = DB::table('songs')->where('cts_song_id', $ctsSongId)->first(); if (! $existing) { DB::table('songs')->insert([ 'cts_song_id' => $ctsSongId, 'title' => (string) ($song->getName() ?? ''), 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); } $count++; } } return $count; } public function syncAgenda(int $eventId): mixed { $fetcher = $this->agendaFetcher ?? function (int $id): mixed { $this->configureApi(); return EventAgendaRequest::fromEvent($id)->get(); }; return $this->logApiCall( 'syncAgenda', 'agenda', fn (): mixed => $fetcher($eventId), ['event_id' => $eventId] ); } public function getEventServices(int $eventId): array { $fetcher = $this->eventServiceFetcher ?? function (int $id): array { $this->configureApi(); $event = EventRequest::find($id); return $event?->getEventServices() ?? []; }; return $this->logApiCall( 'getEventServices', 'event_services', fn (): array => $fetcher($eventId), ['event_id' => $eventId] ); } private function fetchEvents(): array { $fetcher = $this->eventFetcher ?? function (): array { $this->configureApi(); $futureEvents = EventRequest::where('from', Carbon::now()->toDateString()) ->where('to', Carbon::now()->addMonths(3)->toDateString()) ->get(); $pastEvents = EventRequest::where('from', Carbon::now()->subMonths(3)->toDateString()) ->where('to', Carbon::now()->subDay()->toDateString()) ->get(); $future = array_slice($futureEvents, 0, 10); $past = array_slice(array_reverse($pastEvents), 0, 10); return array_merge($past, $future); }; return $this->logApiCall('fetchEvents', 'events', fn (): array => $fetcher()); } private function fetchSongs(): array { $fetcher = $this->songFetcher ?? function (): array { $this->configureApi(); return SongRequest::all(); }; return $this->logApiCall('fetchSongs', 'songs', fn (): array => $fetcher()); } private function logApiCall(string $method, string $endpoint, Closure $operation, ?array $context = null): mixed { $start = microtime(true); try { $result = $operation(); $duration = (int) ((microtime(true) - $start) * 1000); ApiRequestLog::create([ 'method' => $method, 'endpoint' => $endpoint, 'status' => 'success', 'request_context' => $context, 'response_summary' => $this->summarizeResponse($result), 'response_body' => $this->serializeResponseBody($result), 'duration_ms' => $duration, ]); return $result; } catch (\Throwable $e) { $duration = (int) ((microtime(true) - $start) * 1000); ApiRequestLog::create([ 'method' => $method, 'endpoint' => $endpoint, 'status' => 'error', 'request_context' => $context, 'error_message' => $e->getMessage(), 'duration_ms' => $duration, ]); throw $e; } } private function summarizeResponse(mixed $result): ?string { if ($result === null) { return null; } if (is_array($result)) { return 'Array mit '.count($result).' Eintraegen'; } if (is_scalar($result)) { return Str::limit((string) $result, 500); } if (is_object($result)) { return 'Objekt vom Typ '.$result::class; } return null; } private function serializeResponseBody(mixed $result): ?string { if ($result === null) { return null; } try { $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); return mb_strlen($json) > 512000 ? mb_substr($json, 0, 512000)."\n... (abgeschnitten)" : $json; } catch (\JsonException) { return null; } } private function configureApi(): void { if ($this->apiConfigured) { return; } $apiUrl = (string) Config::get('services.churchtools.url', ''); $apiToken = (string) Config::get('services.churchtools.api_token', ''); if ($apiUrl !== '') { CTConfig::setApiUrl(rtrim($apiUrl, '/')); } CTConfig::setApiKey($apiToken); $this->apiConfigured = true; } private function upsertService(mixed $event, array $serviceRoles): object { $ctsEventId = (string) $event->getId(); DB::table('services')->updateOrInsert( ['cts_event_id' => $ctsEventId], [ 'title' => (string) ($event->getName() ?? ''), 'date' => Carbon::parse((string) $event->getStartDate())->toDateString(), 'preacher_name' => $serviceRoles['preacher'], 'beamer_tech_name' => $serviceRoles['beamer_technician'], 'last_synced_at' => Carbon::now(), 'cts_data' => json_encode($this->extractRawData($event), JSON_THROW_ON_ERROR), 'updated_at' => Carbon::now(), 'created_at' => Carbon::now(), ] ); return DB::table('services') ->where('cts_event_id', $ctsEventId) ->firstOrFail(); } private function syncServiceAgendaSongs(int $serviceId, int $eventId): array { $agenda = $this->syncAgenda($eventId); $agendaSongs = method_exists($agenda, 'getSongs') ? $agenda->getSongs() : []; $summary = [ 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, ]; $songMatchingService = app(SongMatchingService::class); foreach ($agendaSongs as $index => $song) { $ctsSongId = (string) ($song->getId() ?? ''); if ($ctsSongId === '') { continue; } $ccliId = $this->normalizeCcli($song->getCcli() ?? null); DB::table('service_songs')->updateOrInsert( [ 'service_id' => $serviceId, 'order' => $index + 1, ], [ 'cts_song_name' => (string) ($song->getName() ?? ''), 'cts_ccli_id' => $ccliId, 'cts_song_id' => $ctsSongId, 'updated_at' => Carbon::now(), 'created_at' => Carbon::now(), ] ); $serviceSong = \App\Models\ServiceSong::where('service_id', $serviceId) ->where('order', $index + 1) ->first(); $matched = false; if ( $serviceSong !== null && ! $serviceSong->song_id && ($serviceSong->cts_ccli_id || $serviceSong->cts_song_id) ) { $matched = $songMatchingService->autoMatch($serviceSong); } elseif ($serviceSong !== null && $serviceSong->song_id) { $matched = true; } $summary['songs_count']++; if ($matched) { $summary['matched_songs_count']++; } else { $summary['unmatched_songs_count']++; } } return $summary; } private function extractServiceRoles(array $eventServices): array { $roles = [ 'preacher' => null, 'beamer_technician' => null, ]; foreach ($eventServices as $eventService) { $serviceName = Str::lower((string) ($eventService->getName() ?? '')); $personName = $this->extractPersonName($eventService); if ($personName === null) { continue; } if ($roles['preacher'] === null && (str_contains($serviceName, 'predigt') || str_contains($serviceName, 'preach'))) { $roles['preacher'] = $personName; } if ($roles['beamer_technician'] === null && str_contains($serviceName, 'beamer')) { $roles['beamer_technician'] = $personName; } } return $roles; } private function extractPersonName(mixed $eventService): ?string { if (! method_exists($eventService, 'getPerson')) { return null; } $person = $eventService->getPerson(); if ($person === null) { return null; } if (method_exists($person, 'getName')) { $name = trim((string) $person->getName()); if ($name !== '') { return $name; } } $firstName = method_exists($person, 'getFirstName') ? trim((string) $person->getFirstName()) : ''; $lastName = method_exists($person, 'getLastName') ? trim((string) $person->getLastName()) : ''; $fullName = trim($firstName.' '.$lastName); return $fullName === '' ? null : $fullName; } private function extractRawData(mixed $event): array { if (method_exists($event, 'extractData')) { return $event->extractData(); } return [ 'id' => $event->getId() ?? null, 'title' => $event->getName() ?? null, 'startDate' => $event->getStartDate() ?? null, 'note' => $event->getNote() ?? null, ]; } private function normalizeCcli(?string $ccli): ?string { if ($ccli === null) { return null; } $value = trim($ccli); return $value === '' ? null : $value; } private function writeSyncLog(string $status, array $summary, ?string $message, mixed $startedAt, mixed $finishedAt): void { DB::table('cts_sync_log')->insert([ 'status' => $status, 'synced_at' => $finishedAt, 'events_count' => $summary['services_count'] ?? 0, 'songs_count' => $summary['songs_count'] ?? 0, 'error' => $message, 'created_at' => Carbon::now(), 'updated_at' => Carbon::now(), ]); } }