diff --git a/app/Services/ChurchToolsService.php b/app/Services/ChurchToolsService.php index ed0be51..5f9a28b 100644 --- a/app/Services/ChurchToolsService.php +++ b/app/Services/ChurchToolsService.php @@ -39,6 +39,7 @@ public function sync(): array 'songs_count' => $eventsSummary['songs_count'], 'matched_songs_count' => $eventsSummary['matched_songs_count'], 'unmatched_songs_count' => $eventsSummary['unmatched_songs_count'], + 'agenda_items_count' => $eventsSummary['agenda_items_count'], 'catalog_songs_count' => $songsCount, ]; @@ -51,6 +52,7 @@ public function sync(): array 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, + 'agenda_items_count' => 0, 'catalog_songs_count' => 0, ]; @@ -69,6 +71,7 @@ public function syncEvents(): array 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, + 'agenda_items_count' => 0, ]; foreach ($events as $event) { @@ -87,6 +90,7 @@ public function syncEvents(): array $summary['songs_count'] += $songSummary['songs_count']; $summary['matched_songs_count'] += $songSummary['matched_songs_count']; $summary['unmatched_songs_count'] += $songSummary['unmatched_songs_count']; + $summary['agenda_items_count'] += $songSummary['agenda_items_count'] ?? 0; } catch (\Throwable $e) { Log::warning('Sync-Fehler für Event', [ 'event_id' => $event->getId() ?? 'unknown', @@ -321,64 +325,129 @@ private function upsertService(mixed $event, array $serviceRoles): object ->firstOrFail(); } + /** @deprecated Use syncServiceAgendaItems() instead. */ private function syncServiceAgendaSongs(int $serviceId, int $eventId): array + { + return $this->syncServiceAgendaItems($serviceId, $eventId); + } + + private function syncServiceAgendaItems(int $serviceId, int $eventId): array { $agenda = $this->syncAgenda($eventId); - $agendaSongs = method_exists($agenda, 'getSongs') ? $agenda->getSongs() : []; + $agendaItems = method_exists($agenda, 'getItems') ? $agenda->getItems() : []; $summary = [ 'songs_count' => 0, 'matched_songs_count' => 0, 'unmatched_songs_count' => 0, + 'agenda_items_count' => 0, ]; $songMatchingService = app(SongMatchingService::class); + $processedSortOrders = []; - foreach ($agendaSongs as $index => $song) { - $ctsSongId = (string) ($song->getId() ?? ''); - if ($ctsSongId === '') { + foreach ($agendaItems as $index => $item) { + if (method_exists($item, 'getIsBeforeEvent') && $item->getIsBeforeEvent()) { continue; } - $ccliId = $this->normalizeCcli($song->getCcli() ?? null); + $sortOrder = $index + 1; + $processedSortOrders[] = $sortOrder; - DB::table('service_songs')->updateOrInsert( + $responsible = method_exists($item, 'getResponsible') ? $item->getResponsible() : null; + + DB::table('service_agenda_items')->updateOrInsert( [ 'service_id' => $serviceId, - 'order' => $index + 1, + 'sort_order' => $sortOrder, ], [ - 'cts_song_name' => (string) ($song->getName() ?? ''), - 'cts_ccli_id' => $ccliId, - 'cts_song_id' => $ctsSongId, + 'cts_agenda_item_id' => method_exists($item, 'getId') ? (string) ($item->getId() ?? '') : null, + 'position' => (string) (method_exists($item, 'getPosition') ? ($item->getPosition() ?? '') : ''), + 'title' => (string) (method_exists($item, 'getTitle') ? ($item->getTitle() ?? '') : ''), + 'type' => (string) (method_exists($item, 'getType') ? ($item->getType() ?? '') : ''), + 'note' => method_exists($item, 'getNote') ? $item->getNote() : null, + 'duration' => method_exists($item, 'getDuration') ? $item->getDuration() : null, + 'start' => method_exists($item, 'getStart') ? $item->getStart() : null, + 'is_before_event' => false, + 'responsible' => $responsible !== null ? json_encode($responsible, JSON_THROW_ON_ERROR) : null, 'updated_at' => Carbon::now(), 'created_at' => Carbon::now(), ] ); - $serviceSong = \App\Models\ServiceSong::where('service_id', $serviceId) - ->where('order', $index + 1) - ->first(); + $song = method_exists($item, 'getSong') ? $item->getSong() : null; - $matched = false; + if ($song !== null) { + $ctsSongId = (string) ($song->getId() ?? ''); + if ($ctsSongId !== '') { + $ccliId = $this->normalizeCcli($song->getCcli() ?? null); - 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; + DB::table('service_songs')->updateOrInsert( + [ + 'service_id' => $serviceId, + 'order' => $sortOrder, + ], + [ + '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', $sortOrder) + ->first(); + + if ($serviceSong) { + DB::table('service_agenda_items') + ->where('service_id', $serviceId) + ->where('sort_order', $sortOrder) + ->update(['service_song_id' => $serviceSong->id]); + } + + $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']++; + } + } } - $summary['songs_count']++; + $summary['agenda_items_count']++; + } - if ($matched) { - $summary['matched_songs_count']++; - } else { - $summary['unmatched_songs_count']++; - } + // Clean up orphaned agenda items: first null-ify slide FKs, then delete + $orphanedIds = DB::table('service_agenda_items') + ->where('service_id', $serviceId) + ->whereNotIn('sort_order', $processedSortOrders) + ->pluck('id'); + + if ($orphanedIds->isNotEmpty()) { + DB::table('slides') + ->whereIn('service_agenda_item_id', $orphanedIds) + ->update(['service_agenda_item_id' => null]); + + DB::table('service_agenda_items') + ->where('service_id', $serviceId) + ->whereNotIn('sort_order', $processedSortOrders) + ->delete(); } return $summary; diff --git a/tests/Feature/ChurchToolsSyncTest.php b/tests/Feature/ChurchToolsSyncTest.php index 3e42bad..1b0b643 100644 --- a/tests/Feature/ChurchToolsSyncTest.php +++ b/tests/Feature/ChurchToolsSyncTest.php @@ -36,8 +36,9 @@ ], songFetcher: fn () => [new FakeSong(id: 1, title: 'Way Maker', ccli: '7115744')], agendaFetcher: fn () => new FakeAgenda([ - new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744'), - new FakeSong(id: 5002, title: 'Unbekannt', ccli: '9999999'), + new FakeAgendaItem(id: 1, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 2, position: '2', title: 'Way Maker', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + new FakeAgendaItem(id: 3, position: '3', title: 'Unbekannt', type: 'Song', song: new FakeSong(id: 5002, title: 'Unbekannt', ccli: '9999999')), ]), eventServiceFetcher: fn (int $eventId) => [ new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')), @@ -55,12 +56,12 @@ expect($service->preacher_name)->toBe('Max Mustermann'); expect($service->beamer_tech_name)->toBe('Lisa Technik'); - $matchedSong = DB::table('service_songs')->where('order', 1)->first(); + $matchedSong = DB::table('service_songs')->where('order', 2)->first(); expect($matchedSong)->not->toBeNull(); expect((int) $matchedSong->song_id)->toBe($localSongId); expect($matchedSong->matched_at)->not->toBeNull(); - $unmatchedSong = DB::table('service_songs')->where('order', 2)->first(); + $unmatchedSong = DB::table('service_songs')->where('order', 3)->first(); expect($unmatchedSong)->not->toBeNull(); expect($unmatchedSong->song_id)->toBeNull(); expect($unmatchedSong->cts_ccli_id)->toBe('9999999'); @@ -72,6 +73,283 @@ expect((int) $syncLog->songs_count)->toBe(2); }); +test('sync speichert alle agenda items (songs und nicht-songs) in service_agenda_items', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 200, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 10, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 11, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + new FakeAgendaItem(id: 12, position: '3', title: 'Predigt', type: 'Default', note: 'Zum Thema Liebe'), + new FakeAgendaItem(id: 13, position: '4', title: 'Abschluss', type: 'Song', song: new FakeSong(id: 5002, title: 'Amazing Grace', ccli: '1234567')), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '200')->first(); + + $agendaItems = DB::table('service_agenda_items') + ->where('service_id', $service->id) + ->orderBy('sort_order') + ->get(); + + expect($agendaItems)->toHaveCount(4); + expect($agendaItems[0]->title)->toBe('Begrüssung'); + expect($agendaItems[0]->type)->toBe('Default'); + expect((int) $agendaItems[0]->sort_order)->toBe(1); + expect($agendaItems[1]->title)->toBe('Lobpreis'); + expect($agendaItems[1]->type)->toBe('Song'); + expect((int) $agendaItems[1]->sort_order)->toBe(2); + expect($agendaItems[2]->title)->toBe('Predigt'); + expect($agendaItems[2]->note)->toBe('Zum Thema Liebe'); + expect($agendaItems[3]->title)->toBe('Abschluss'); +}); + +test('song items erstellen service_song UND service_agenda_item mit korrekter service_song_id', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 300, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 20, position: '1', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '300')->first(); + + $serviceSong = DB::table('service_songs')->where('service_id', $service->id)->first(); + expect($serviceSong)->not->toBeNull(); + expect($serviceSong->cts_song_name)->toBe('Way Maker'); + + $agendaItem = DB::table('service_agenda_items')->where('service_id', $service->id)->first(); + expect($agendaItem)->not->toBeNull(); + expect($agendaItem->title)->toBe('Lobpreis'); + expect((int) $agendaItem->service_song_id)->toBe((int) $serviceSong->id); +}); + +test('nicht-song items erstellen keine service_song eintraege', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 400, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 30, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 31, position: '2', title: 'Predigt', type: 'Default'), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '400')->first(); + + $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get(); + expect($agendaItems)->toHaveCount(2); + + $serviceSongs = DB::table('service_songs')->where('service_id', $service->id)->get(); + expect($serviceSongs)->toHaveCount(0); +}); + +test('is_before_event items werden uebersprungen', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 500, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 40, position: '0', title: 'Soundcheck', type: 'Default', isBeforeEvent: true), + new FakeAgendaItem(id: 41, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 42, position: '2', title: 'Probe', type: 'Default', isBeforeEvent: true), + new FakeAgendaItem(id: 43, position: '3', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '500')->first(); + + $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get(); + expect($agendaItems)->toHaveCount(2); + + $titles = $agendaItems->pluck('title')->toArray(); + expect($titles)->not->toContain('Soundcheck'); + expect($titles)->not->toContain('Probe'); + expect($titles)->toContain('Begrüssung'); + expect($titles)->toContain('Lobpreis'); +}); + +test('verwaiste agenda items werden bei re-sync entfernt', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + // First sync with 3 items + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 600, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 50, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 51, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + new FakeAgendaItem(id: 52, position: '3', title: 'Predigt', type: 'Default'), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '600')->first(); + expect(DB::table('service_agenda_items')->where('service_id', $service->id)->count())->toBe(3); + + // Re-sync with only 1 item + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 600, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 50, position: '1', title: 'Begrüssung', type: 'Default'), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $agendaItems = DB::table('service_agenda_items')->where('service_id', $service->id)->get(); + expect($agendaItems)->toHaveCount(1); + expect($agendaItems[0]->title)->toBe('Begrüssung'); +}); + +test('slides werden erhalten (FK genullt) wenn verwaiste agenda items entfernt werden', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + // First sync with 2 items + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 700, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 60, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 61, position: '2', title: 'Predigt', type: 'Default'), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '700')->first(); + $agendaItem = DB::table('service_agenda_items') + ->where('service_id', $service->id) + ->where('sort_order', 2) + ->first(); + + // Create a slide linked to the second agenda item + $slideId = DB::table('slides')->insertGetId([ + 'service_id' => $service->id, + 'service_agenda_item_id' => $agendaItem->id, + 'type' => 'sermon', + 'original_filename' => 'predigt.jpg', + 'stored_filename' => 'predigt_stored.jpg', + 'thumbnail_filename' => 'predigt_thumb.jpg', + 'uploaded_at' => now(), + 'sort_order' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // Re-sync with only 1 item (second item removed) + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 700, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 60, position: '1', title: 'Begrüssung', type: 'Default'), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + // Slide still exists but FK is null + $slide = DB::table('slides')->where('id', $slideId)->first(); + expect($slide)->not->toBeNull(); + expect($slide->service_agenda_item_id)->toBeNull(); + + // Orphaned agenda item is gone + expect(DB::table('service_agenda_items')->where('service_id', $service->id)->count())->toBe(1); +}); + +test('sync summary enthaelt agenda_items_count', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + $service = new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 800, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem(id: 70, position: '1', title: 'Begrüssung', type: 'Default'), + new FakeAgendaItem(id: 71, position: '2', title: 'Lobpreis', type: 'Song', song: new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744')), + ]), + eventServiceFetcher: fn (int $eventId) => [], + ); + + $summary = $service->sync(); + + expect($summary)->toHaveKey('agenda_items_count'); + expect($summary['agenda_items_count'])->toBe(2); + expect($summary['songs_count'])->toBe(1); + expect($summary['services_count'])->toBe(1); +}); + +test('responsible feld wird als json gespeichert', function () { + Carbon::setTestNow('2026-03-01 09:00:00'); + + app()->instance(ChurchToolsService::class, new ChurchToolsService( + eventFetcher: fn () => [ + new FakeEvent(id: 900, title: 'Service', startDate: '2026-03-08T10:00:00+00:00'), + ], + songFetcher: fn () => [], + agendaFetcher: fn () => new FakeAgenda([ + new FakeAgendaItem( + id: 80, + position: '1', + title: 'Begrüssung', + type: 'Default', + responsible: ['name' => 'Max Mustermann'], + ), + ]), + eventServiceFetcher: fn (int $eventId) => [], + )); + + Artisan::call('cts:sync'); + + $service = DB::table('services')->where('cts_event_id', '900')->first(); + $agendaItem = DB::table('service_agenda_items')->where('service_id', $service->id)->first(); + + $responsible = json_decode($agendaItem->responsible, true); + expect($responsible)->toBe(['name' => 'Max Mustermann']); +}); + function ensureSyncTables(): void { if (! Schema::hasTable('services')) { @@ -93,6 +371,7 @@ function ensureSyncTables(): void $table->id(); $table->string('title'); $table->string('ccli_id')->nullable()->unique(); + $table->string('cts_song_id')->nullable()->index(); $table->timestamps(); }); } @@ -104,6 +383,7 @@ function ensureSyncTables(): void $table->foreignId('song_id')->nullable()->constrained('songs')->nullOnDelete(); $table->string('cts_song_name'); $table->string('cts_ccli_id')->nullable(); + $table->string('cts_song_id')->nullable(); $table->unsignedInteger('order')->default(0); $table->timestamp('matched_at')->nullable(); $table->timestamps(); @@ -111,6 +391,44 @@ function ensureSyncTables(): void }); } + if (! Schema::hasTable('service_agenda_items')) { + Schema::create('service_agenda_items', function (Blueprint $table) { + $table->id(); + $table->foreignId('service_id')->constrained('services')->cascadeOnDelete(); + $table->string('cts_agenda_item_id')->nullable()->index(); + $table->string('position'); + $table->string('title'); + $table->string('type'); + $table->text('note')->nullable(); + $table->string('duration')->nullable(); + $table->string('start')->nullable(); + $table->boolean('is_before_event')->default(false); + $table->json('responsible')->nullable(); + $table->foreignId('service_song_id')->nullable()->constrained('service_songs')->nullOnDelete(); + $table->unsignedInteger('sort_order'); + $table->timestamps(); + $table->unique(['service_id', 'sort_order']); + }); + } + + if (! Schema::hasTable('slides')) { + Schema::create('slides', function (Blueprint $table) { + $table->id(); + $table->enum('type', ['information', 'moderation', 'sermon']); + $table->foreignId('service_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('service_agenda_item_id')->nullable()->constrained('service_agenda_items')->nullOnDelete(); + $table->string('original_filename'); + $table->string('stored_filename'); + $table->string('thumbnail_filename'); + $table->date('expire_date')->nullable(); + $table->string('uploader_name')->nullable(); + $table->timestamp('uploaded_at'); + $table->unsignedInteger('sort_order')->default(0); + $table->softDeletes(); + $table->timestamps(); + }); + } + if (! Schema::hasTable('cts_sync_log')) { Schema::create('cts_sync_log', function (Blueprint $table) { $table->id(); @@ -198,11 +516,85 @@ public function getLastName(): string final class FakeAgenda { - public function __construct(private readonly array $songs) {} + public function __construct(private readonly array $items) {} + + public function getItems(): array + { + return $this->items; + } public function getSongs(): array { - return $this->songs; + return array_values(array_filter( + array_map(fn ($item) => $item->getSong(), $this->items), + fn ($song) => $song !== null, + )); + } +} + +final class FakeAgendaItem +{ + public function __construct( + private readonly int $id, + private readonly string $position, + private readonly string $title, + private readonly string $type, + private readonly ?FakeSong $song = null, + private readonly ?string $note = null, + private readonly ?string $duration = null, + private readonly ?string $start = null, + private readonly bool $isBeforeEvent = false, + private readonly ?array $responsible = null, + ) {} + + public function getId(): string + { + return (string) $this->id; + } + + public function getPosition(): string + { + return $this->position; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getType(): string + { + return $this->type; + } + + public function getSong(): ?FakeSong + { + return $this->song; + } + + public function getNote(): ?string + { + return $this->note; + } + + public function getDuration(): ?string + { + return $this->duration; + } + + public function getStart(): ?string + { + return $this->start; + } + + public function getIsBeforeEvent(): bool + { + return $this->isBeforeEvent; + } + + public function getResponsible(): ?array + { + return $this->responsible; } }