insertGetId([ 'title' => 'Way Maker', 'ccli_id' => '7115744', 'created_at' => now(), 'updated_at' => now(), ]); app()->instance(ChurchToolsService::class, new ChurchToolsService( eventFetcher: fn () => [ new FakeEvent( id: 100, title: 'Gottesdienst Sonntag', startDate: '2026-03-08T10:00:00+00:00', note: 'Probe', eventServices: [ new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')), new FakeEventService('Beamer', new FakePerson('Lisa', 'Technik')), ], ), ], 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'), ]), eventServiceFetcher: fn (int $eventId) => [ new FakeEventService('Predigt', new FakePerson('Max', 'Mustermann')), new FakeEventService('Beamer', new FakePerson('Lisa', 'Technik')), ], )); Artisan::call('cts:sync'); expect(Artisan::output())->toContain('Daten wurden aktualisiert'); $service = DB::table('services')->where('cts_event_id', '100')->first(); expect($service)->not->toBeNull(); expect($service->title)->toBe('Gottesdienst Sonntag'); expect($service->preacher_name)->toBe('Max Mustermann'); expect($service->beamer_tech_name)->toBe('Lisa Technik'); $matchedSong = DB::table('service_songs')->where('order', 1)->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(); expect($unmatchedSong)->not->toBeNull(); expect($unmatchedSong->song_id)->toBeNull(); expect($unmatchedSong->cts_ccli_id)->toBe('9999999'); $syncLog = DB::table('cts_sync_log')->latest('id')->first(); expect($syncLog)->not->toBeNull(); expect($syncLog->status)->toBe('success'); expect((int) $syncLog->events_count)->toBe(1); expect((int) $syncLog->songs_count)->toBe(2); }); function ensureSyncTables(): void { if (! Schema::hasTable('services')) { Schema::create('services', function (Blueprint $table) { $table->id(); $table->string('cts_event_id')->unique(); $table->string('title'); $table->date('date')->nullable(); $table->string('preacher_name')->nullable(); $table->string('beamer_tech_name')->nullable(); $table->timestamp('last_synced_at')->nullable(); $table->json('cts_data')->nullable(); $table->timestamps(); }); } if (! Schema::hasTable('songs')) { Schema::create('songs', function (Blueprint $table) { $table->id(); $table->string('title'); $table->string('ccli_id')->nullable()->unique(); $table->timestamps(); }); } if (! Schema::hasTable('service_songs')) { Schema::create('service_songs', function (Blueprint $table) { $table->id(); $table->foreignId('service_id')->constrained('services')->cascadeOnDelete(); $table->foreignId('song_id')->nullable()->constrained('songs')->nullOnDelete(); $table->string('cts_song_name'); $table->string('cts_ccli_id')->nullable(); $table->unsignedInteger('order')->default(0); $table->timestamp('matched_at')->nullable(); $table->timestamps(); $table->unique(['service_id', 'order']); }); } if (! Schema::hasTable('cts_sync_log')) { Schema::create('cts_sync_log', function (Blueprint $table) { $table->id(); $table->timestamp('synced_at')->nullable(); $table->unsignedInteger('events_count')->default(0); $table->unsignedInteger('songs_count')->default(0); $table->string('status'); $table->text('error')->nullable(); $table->timestamps(); }); } } final class FakeEvent { public function __construct( private readonly int $id, private readonly string $title, private readonly string $startDate, private readonly ?string $note = null, private readonly array $eventServices = [], ) { } public function getId(): string { return (string) $this->id; } public function getName(): string { return $this->title; } public function getStartDate(): string { return $this->startDate; } public function getNote(): ?string { return $this->note; } public function getEventServices(): array { return $this->eventServices; } } final class FakeEventService { public function __construct( private readonly string $name, private readonly ?FakePerson $person, ) { } public function getName(): string { return $this->name; } public function getPerson(): ?FakePerson { return $this->person; } } final class FakePerson { public function __construct( private readonly string $firstName, private readonly string $lastName, ) { } public function getFirstName(): string { return $this->firstName; } public function getLastName(): string { return $this->lastName; } } final class FakeAgenda { public function __construct(private readonly array $songs) { } public function getSongs(): array { return $this->songs; } } final class FakeSong { public function __construct( private readonly int $id, private readonly string $title, private readonly ?string $ccli, ) { } public function getId(): string { return (string) $this->id; } public function getName(): string { return $this->title; } public function getCcli(): ?string { return $this->ccli; } }