feat(sync): sync all CTS agenda items (not just songs)
This commit is contained in:
parent
0b671956d6
commit
88661c6bef
|
|
@ -39,6 +39,7 @@ public function sync(): array
|
||||||
'songs_count' => $eventsSummary['songs_count'],
|
'songs_count' => $eventsSummary['songs_count'],
|
||||||
'matched_songs_count' => $eventsSummary['matched_songs_count'],
|
'matched_songs_count' => $eventsSummary['matched_songs_count'],
|
||||||
'unmatched_songs_count' => $eventsSummary['unmatched_songs_count'],
|
'unmatched_songs_count' => $eventsSummary['unmatched_songs_count'],
|
||||||
|
'agenda_items_count' => $eventsSummary['agenda_items_count'],
|
||||||
'catalog_songs_count' => $songsCount,
|
'catalog_songs_count' => $songsCount,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -51,6 +52,7 @@ public function sync(): array
|
||||||
'songs_count' => 0,
|
'songs_count' => 0,
|
||||||
'matched_songs_count' => 0,
|
'matched_songs_count' => 0,
|
||||||
'unmatched_songs_count' => 0,
|
'unmatched_songs_count' => 0,
|
||||||
|
'agenda_items_count' => 0,
|
||||||
'catalog_songs_count' => 0,
|
'catalog_songs_count' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -69,6 +71,7 @@ public function syncEvents(): array
|
||||||
'songs_count' => 0,
|
'songs_count' => 0,
|
||||||
'matched_songs_count' => 0,
|
'matched_songs_count' => 0,
|
||||||
'unmatched_songs_count' => 0,
|
'unmatched_songs_count' => 0,
|
||||||
|
'agenda_items_count' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($events as $event) {
|
foreach ($events as $event) {
|
||||||
|
|
@ -87,6 +90,7 @@ public function syncEvents(): array
|
||||||
$summary['songs_count'] += $songSummary['songs_count'];
|
$summary['songs_count'] += $songSummary['songs_count'];
|
||||||
$summary['matched_songs_count'] += $songSummary['matched_songs_count'];
|
$summary['matched_songs_count'] += $songSummary['matched_songs_count'];
|
||||||
$summary['unmatched_songs_count'] += $songSummary['unmatched_songs_count'];
|
$summary['unmatched_songs_count'] += $songSummary['unmatched_songs_count'];
|
||||||
|
$summary['agenda_items_count'] += $songSummary['agenda_items_count'] ?? 0;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
Log::warning('Sync-Fehler für Event', [
|
Log::warning('Sync-Fehler für Event', [
|
||||||
'event_id' => $event->getId() ?? 'unknown',
|
'event_id' => $event->getId() ?? 'unknown',
|
||||||
|
|
@ -321,64 +325,129 @@ private function upsertService(mixed $event, array $serviceRoles): object
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use syncServiceAgendaItems() instead. */
|
||||||
private function syncServiceAgendaSongs(int $serviceId, int $eventId): array
|
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);
|
$agenda = $this->syncAgenda($eventId);
|
||||||
$agendaSongs = method_exists($agenda, 'getSongs') ? $agenda->getSongs() : [];
|
$agendaItems = method_exists($agenda, 'getItems') ? $agenda->getItems() : [];
|
||||||
|
|
||||||
$summary = [
|
$summary = [
|
||||||
'songs_count' => 0,
|
'songs_count' => 0,
|
||||||
'matched_songs_count' => 0,
|
'matched_songs_count' => 0,
|
||||||
'unmatched_songs_count' => 0,
|
'unmatched_songs_count' => 0,
|
||||||
|
'agenda_items_count' => 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
$songMatchingService = app(SongMatchingService::class);
|
$songMatchingService = app(SongMatchingService::class);
|
||||||
|
$processedSortOrders = [];
|
||||||
|
|
||||||
foreach ($agendaSongs as $index => $song) {
|
foreach ($agendaItems as $index => $item) {
|
||||||
$ctsSongId = (string) ($song->getId() ?? '');
|
if (method_exists($item, 'getIsBeforeEvent') && $item->getIsBeforeEvent()) {
|
||||||
if ($ctsSongId === '') {
|
|
||||||
continue;
|
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,
|
'service_id' => $serviceId,
|
||||||
'order' => $index + 1,
|
'sort_order' => $sortOrder,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'cts_song_name' => (string) ($song->getName() ?? ''),
|
'cts_agenda_item_id' => method_exists($item, 'getId') ? (string) ($item->getId() ?? '') : null,
|
||||||
'cts_ccli_id' => $ccliId,
|
'position' => (string) (method_exists($item, 'getPosition') ? ($item->getPosition() ?? '') : ''),
|
||||||
'cts_song_id' => $ctsSongId,
|
'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(),
|
'updated_at' => Carbon::now(),
|
||||||
'created_at' => Carbon::now(),
|
'created_at' => Carbon::now(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$serviceSong = \App\Models\ServiceSong::where('service_id', $serviceId)
|
$song = method_exists($item, 'getSong') ? $item->getSong() : null;
|
||||||
->where('order', $index + 1)
|
|
||||||
->first();
|
|
||||||
|
|
||||||
$matched = false;
|
if ($song !== null) {
|
||||||
|
$ctsSongId = (string) ($song->getId() ?? '');
|
||||||
|
if ($ctsSongId !== '') {
|
||||||
|
$ccliId = $this->normalizeCcli($song->getCcli() ?? null);
|
||||||
|
|
||||||
if (
|
DB::table('service_songs')->updateOrInsert(
|
||||||
$serviceSong !== null
|
[
|
||||||
&& ! $serviceSong->song_id
|
'service_id' => $serviceId,
|
||||||
&& ($serviceSong->cts_ccli_id || $serviceSong->cts_song_id)
|
'order' => $sortOrder,
|
||||||
) {
|
],
|
||||||
$matched = $songMatchingService->autoMatch($serviceSong);
|
[
|
||||||
} elseif ($serviceSong !== null && $serviceSong->song_id) {
|
'cts_song_name' => (string) ($song->getName() ?? ''),
|
||||||
$matched = true;
|
'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) {
|
// Clean up orphaned agenda items: first null-ify slide FKs, then delete
|
||||||
$summary['matched_songs_count']++;
|
$orphanedIds = DB::table('service_agenda_items')
|
||||||
} else {
|
->where('service_id', $serviceId)
|
||||||
$summary['unmatched_songs_count']++;
|
->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;
|
return $summary;
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,9 @@
|
||||||
],
|
],
|
||||||
songFetcher: fn () => [new FakeSong(id: 1, title: 'Way Maker', ccli: '7115744')],
|
songFetcher: fn () => [new FakeSong(id: 1, title: 'Way Maker', ccli: '7115744')],
|
||||||
agendaFetcher: fn () => new FakeAgenda([
|
agendaFetcher: fn () => new FakeAgenda([
|
||||||
new FakeSong(id: 5001, title: 'Way Maker', ccli: '7115744'),
|
new FakeAgendaItem(id: 1, position: '1', title: 'Begrüssung', type: 'Default'),
|
||||||
new FakeSong(id: 5002, title: 'Unbekannt', ccli: '9999999'),
|
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) => [
|
eventServiceFetcher: fn (int $eventId) => [
|
||||||
new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')),
|
new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')),
|
||||||
|
|
@ -55,12 +56,12 @@
|
||||||
expect($service->preacher_name)->toBe('Max Mustermann');
|
expect($service->preacher_name)->toBe('Max Mustermann');
|
||||||
expect($service->beamer_tech_name)->toBe('Lisa Technik');
|
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($matchedSong)->not->toBeNull();
|
||||||
expect((int) $matchedSong->song_id)->toBe($localSongId);
|
expect((int) $matchedSong->song_id)->toBe($localSongId);
|
||||||
expect($matchedSong->matched_at)->not->toBeNull();
|
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)->not->toBeNull();
|
||||||
expect($unmatchedSong->song_id)->toBeNull();
|
expect($unmatchedSong->song_id)->toBeNull();
|
||||||
expect($unmatchedSong->cts_ccli_id)->toBe('9999999');
|
expect($unmatchedSong->cts_ccli_id)->toBe('9999999');
|
||||||
|
|
@ -72,6 +73,283 @@
|
||||||
expect((int) $syncLog->songs_count)->toBe(2);
|
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
|
function ensureSyncTables(): void
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('services')) {
|
if (! Schema::hasTable('services')) {
|
||||||
|
|
@ -93,6 +371,7 @@ function ensureSyncTables(): void
|
||||||
$table->id();
|
$table->id();
|
||||||
$table->string('title');
|
$table->string('title');
|
||||||
$table->string('ccli_id')->nullable()->unique();
|
$table->string('ccli_id')->nullable()->unique();
|
||||||
|
$table->string('cts_song_id')->nullable()->index();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +383,7 @@ function ensureSyncTables(): void
|
||||||
$table->foreignId('song_id')->nullable()->constrained('songs')->nullOnDelete();
|
$table->foreignId('song_id')->nullable()->constrained('songs')->nullOnDelete();
|
||||||
$table->string('cts_song_name');
|
$table->string('cts_song_name');
|
||||||
$table->string('cts_ccli_id')->nullable();
|
$table->string('cts_ccli_id')->nullable();
|
||||||
|
$table->string('cts_song_id')->nullable();
|
||||||
$table->unsignedInteger('order')->default(0);
|
$table->unsignedInteger('order')->default(0);
|
||||||
$table->timestamp('matched_at')->nullable();
|
$table->timestamp('matched_at')->nullable();
|
||||||
$table->timestamps();
|
$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')) {
|
if (! Schema::hasTable('cts_sync_log')) {
|
||||||
Schema::create('cts_sync_log', function (Blueprint $table) {
|
Schema::create('cts_sync_log', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
|
@ -198,11 +516,85 @@ public function getLastName(): string
|
||||||
|
|
||||||
final class FakeAgenda
|
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
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue