user = User::factory()->create(); }); test('finalize ohne voraussetzungen gibt warnungen zurueck (legacy)', function () { $service = Service::factory()->create(['finalized_at' => null]); // Song without match or arrangement (legacy service_songs) ServiceSong::create([ 'service_id' => $service->id, 'song_id' => null, 'song_arrangement_id' => null, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Test Song', 'cts_ccli_id' => '12345', ]); $response = $this->actingAs($this->user) ->postJson(route('services.finalize', $service), ['confirmed' => false]); $response->assertOk() ->assertJson([ 'needs_confirmation' => true, ]); $data = $response->json(); expect($data['warnings'])->toHaveCount(3) ->and($data['warnings'][0])->toContain('Songs sind zugeordnet') ->and($data['warnings'][1])->toContain('Arrangement') ->and($data['warnings'][2])->toContain('Predigtfolien'); // Not finalized yet expect($service->fresh()->finalized_at)->toBeNull(); }); test('finalize mit confirmed=true trotz warnungen finalisiert service', function () { $service = Service::factory()->create(['finalized_at' => null]); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => null, 'song_arrangement_id' => null, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Test Song', 'cts_ccli_id' => '12345', ]); $response = $this->actingAs($this->user) ->postJson(route('services.finalize', $service), ['confirmed' => true]); $response->assertOk() ->assertJson([ 'needs_confirmation' => false, 'success' => 'Service wurde abgeschlossen.', ]); expect($service->fresh()->finalized_at)->not->toBeNull(); }); test('finalize ohne warnungen finalisiert direkt', function () { $service = Service::factory()->create(['finalized_at' => null]); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Test Song', 'cts_ccli_id' => '12345', ]); Slide::factory()->create([ 'service_id' => $service->id, 'type' => 'sermon', ]); $response = $this->actingAs($this->user) ->postJson(route('services.finalize', $service), ['confirmed' => false]); $response->assertOk() ->assertJson([ 'needs_confirmation' => false, 'success' => 'Service wurde abgeschlossen.', ]); expect($service->fresh()->finalized_at)->not->toBeNull(); }); test('finalize warnt bei fehlenden song-zuordnungen', function () { $service = Service::factory()->create(['finalized_at' => null]); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); // One matched, one unmatched ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Matched', 'cts_ccli_id' => '111', ]); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => null, 'song_arrangement_id' => null, 'use_translation' => false, 'order' => 2, 'cts_song_name' => 'Unmatched', 'cts_ccli_id' => '222', ]); Slide::factory()->create([ 'service_id' => $service->id, 'type' => 'sermon', ]); $response = $this->actingAs($this->user) ->postJson(route('services.finalize', $service), ['confirmed' => false]); $data = $response->json(); expect($data['needs_confirmation'])->toBeTrue() ->and($data['warnings'])->toContain('Nur 1 von 2 Songs sind zugeordnet.') ->and($data['warnings'])->toContain('Nur 1 von 2 Songs haben ein Arrangement.'); }); test('finalize warnt bei fehlenden predigtfolien', function () { $service = Service::factory()->create(['finalized_at' => null]); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Song', 'cts_ccli_id' => '111', ]); // No sermon slides $response = $this->actingAs($this->user) ->postJson(route('services.finalize', $service), ['confirmed' => false]); $data = $response->json(); expect($data['needs_confirmation'])->toBeTrue() ->and($data['warnings'])->toContain('Es wurden keine Predigtfolien hochgeladen.'); }); test('reopen setzt finalized_at zurueck', function () { $service = Service::factory()->create(['finalized_at' => now()]); $response = $this->actingAs($this->user) ->post(route('services.reopen', $service)); $response->assertRedirect(route('services.index')); expect($service->fresh()->finalized_at)->toBeNull(); }); test('download nicht-finalisierter service gibt 403 zurueck', function () { $service = Service::factory()->create(['finalized_at' => null]); $response = $this->actingAs($this->user) ->getJson(route('services.download', $service)); $response->assertForbidden(); }); test('download finalisierter service ohne songs gibt 422 zurueck', function () { $service = Service::factory()->create(['finalized_at' => now()]); $response = $this->actingAs($this->user) ->getJson(route('services.download', $service)); $response->assertUnprocessable() ->assertJson([ 'message' => 'Keine Songs mit Inhalt zum Exportieren gefunden.', ]); }); test('finalize erfordert authentifizierung', function () { $service = Service::factory()->create(); $response = $this->postJson(route('services.finalize', $service)); $response->assertUnauthorized(); }); test('download erfordert authentifizierung', function () { $service = Service::factory()->create(); $response = $this->getJson(route('services.download', $service)); $response->assertUnauthorized(); }); test('service model isReadyToFinalize accessor', function () { $service = Service::factory()->create(['finalized_at' => null]); // No songs, no sermon slides → only sermon warning expect($service->is_ready_to_finalize)->toBeFalse(); Slide::factory()->create([ 'service_id' => $service->id, 'type' => 'sermon', ]); // Refresh to clear cached relations $service->refresh(); // No songs, has sermon slides → ready (0/0 songs is not a warning) expect($service->is_ready_to_finalize)->toBeTrue(); }); test('finalization status mit service ohne songs warnt nur bei predigtfolien', function () { $service = Service::factory()->create(['finalized_at' => null]); $status = $service->finalizationStatus(); // No songs: no song warnings. Only sermon slides warning. expect($status['ready'])->toBeFalse() ->and($status['warnings'])->toHaveCount(1) ->and($status['warnings'][0])->toContain('Predigtfolien'); }); // Agenda-based finalization tests test('agenda finalization warnt bei unzugeordneten songs', function () { $service = Service::factory()->create(['finalized_at' => null]); // Create an unmatched song via agenda item $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => null, 'song_arrangement_id' => null, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Unzugeordneter Song', 'cts_ccli_id' => '12345', ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, ]); $status = $service->finalizationStatus(); expect($status['ready'])->toBeFalse() ->and($status['warnings'])->toContain('Unzugeordneter Song wurde noch nicht zugeordnet'); }); test('agenda finalization warnt bei songs ohne arrangement', function () { $service = Service::factory()->create(['finalized_at' => null]); $song = Song::factory()->create(); // Song is matched but has no arrangement $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => null, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Song ohne Arrangement', 'cts_ccli_id' => '12345', ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, ]); $status = $service->finalizationStatus(); expect($status['ready'])->toBeFalse() ->and($status['warnings'])->toContain('Song ohne Arrangement hat kein Arrangement ausgewählt'); }); test('agenda finalization bereit wenn alle songs zugeordnet und arrangement', function () { $service = Service::factory()->create(['finalized_at' => null]); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); // Song is matched with arrangement $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Zugeordneter Song', 'cts_ccli_id' => '12345', ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, ]); // Add sermon slides Slide::factory()->create([ 'service_id' => $service->id, 'type' => 'sermon', ]); $status = $service->finalizationStatus(); expect($status['ready'])->toBeTrue() ->and($status['warnings'])->toHaveCount(0); }); test('agenda finalization warnt wenn keine predigtfolien und sermon setting konfiguriert', function () { $service = Service::factory()->create(['finalized_at' => null]); // Configure sermon pattern Setting::set('agenda_sermon_matching', 'Predigt*'); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Zugeordneter Song', 'cts_ccli_id' => '12345', ]); // Create agenda item for sermon (matches pattern) ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Predigt', 'type' => 'Default', 'sort_order' => 1, ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '2', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 2, ]); // No sermon slides $status = $service->finalizationStatus(); expect($status['ready'])->toBeFalse() ->and($status['warnings'])->toContain('Keine Predigt-Folien hochgeladen'); }); test('agenda finalization ok wenn predigtfolien bei sermon item vorhanden', function () { $service = Service::factory()->create(['finalized_at' => null]); // Configure sermon pattern Setting::set('agenda_sermon_matching', 'Predigt*'); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Zugeordneter Song', 'cts_ccli_id' => '12345', ]); // Create sermon agenda item $sermonItem = ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Predigt', 'type' => 'Default', 'sort_order' => 1, ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '2', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 2, ]); // Add sermon slides to the sermon agenda item Slide::factory()->create([ 'service_id' => $service->id, 'service_agenda_item_id' => $sermonItem->id, ]); $status = $service->finalizationStatus(); expect($status['ready'])->toBeTrue() ->and($status['warnings'])->toHaveCount(0); }); test('agenda finalization warnt wenn keine predigtfolien und kein sermon setting', function () { $service = Service::factory()->create(['finalized_at' => null]); // No sermon pattern setting configured Setting::set('agenda_sermon_matching', ''); $song = Song::factory()->create(); $arrangement = SongArrangement::factory()->create(['song_id' => $song->id]); $serviceSong = ServiceSong::create([ 'service_id' => $service->id, 'song_id' => $song->id, 'song_arrangement_id' => $arrangement->id, 'use_translation' => false, 'order' => 1, 'cts_song_name' => 'Zugeordneter Song', 'cts_ccli_id' => '12345', ]); ServiceAgendaItem::create([ 'service_id' => $service->id, 'position' => '1', 'title' => 'Song 1', 'type' => 'Song', 'service_song_id' => $serviceSong->id, 'sort_order' => 1, ]); // No sermon slides - fallback should warn $status = $service->finalizationStatus(); expect($status['ready'])->toBeFalse() ->and($status['warnings'])->toContain('Keine Predigt-Folien hochgeladen'); });