From 73b7afcc2ffe9d12ba318ff206c0fa1af109ed67 Mon Sep 17 00:00:00 2001 From: Thorsten Bus Date: Sun, 10 May 2026 18:33:24 +0200 Subject: [PATCH] feat(songs): track CCLI import metadata on songs table --- app/Models/Song.php | 3 ++ database/factories/SongFactory.php | 8 +++ ..._add_imported_from_ccli_to_songs_table.php | 22 ++++++++ tests/Feature/SongCcliMetadataTest.php | 54 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 database/migrations/2026_05_10_120000_add_imported_from_ccli_to_songs_table.php create mode 100644 tests/Feature/SongCcliMetadataTest.php diff --git a/app/Models/Song.php b/app/Models/Song.php index 571e5f1..a532b5b 100644 --- a/app/Models/Song.php +++ b/app/Models/Song.php @@ -16,6 +16,8 @@ class Song extends Model protected $fillable = [ 'ccli_id', 'cts_song_id', + 'imported_from_ccli_at', + 'ccli_source_url', 'title', 'author', 'copyright_text', @@ -30,6 +32,7 @@ protected function casts(): array return [ 'has_translation' => 'boolean', 'last_used_at' => 'datetime', + 'imported_from_ccli_at' => 'datetime', ]; } diff --git a/database/factories/SongFactory.php b/database/factories/SongFactory.php index 3a24313..1ac2c38 100644 --- a/database/factories/SongFactory.php +++ b/database/factories/SongFactory.php @@ -22,4 +22,12 @@ public function definition(): array 'last_used_at' => $this->faker->optional()->dateTimeBetween('-6 months', 'now'), ]; } + + public function fromCcli(): self + { + return $this->state(fn (): array => [ + 'imported_from_ccli_at' => now(), + 'ccli_source_url' => 'https://songselect.ccli.com/Songs/9999001', + ]); + } } diff --git a/database/migrations/2026_05_10_120000_add_imported_from_ccli_to_songs_table.php b/database/migrations/2026_05_10_120000_add_imported_from_ccli_to_songs_table.php new file mode 100644 index 0000000..1bbd553 --- /dev/null +++ b/database/migrations/2026_05_10_120000_add_imported_from_ccli_to_songs_table.php @@ -0,0 +1,22 @@ +timestamp('imported_from_ccli_at')->nullable()->after('last_used_at'); + $table->string('ccli_source_url', 500)->nullable()->after('imported_from_ccli_at'); + }); + } + + public function down(): void + { + Schema::table('songs', function (Blueprint $table): void { + $table->dropColumn(['imported_from_ccli_at', 'ccli_source_url']); + }); + } +}; diff --git a/tests/Feature/SongCcliMetadataTest.php b/tests/Feature/SongCcliMetadataTest.php new file mode 100644 index 0000000..804bf53 --- /dev/null +++ b/tests/Feature/SongCcliMetadataTest.php @@ -0,0 +1,54 @@ +toBeTrue(); + expect(Schema::hasColumn('songs', 'ccli_source_url'))->toBeTrue(); +}); + +test('imported_from_ccli_at defaults to null', function (): void { + $song = Song::factory()->create(); + + expect($song->fresh()->imported_from_ccli_at)->toBeNull(); +}); + +test('ccli_source_url defaults to null', function (): void { + $song = Song::factory()->create(); + + expect($song->fresh()->ccli_source_url)->toBeNull(); +}); + +test('imported_from_ccli_at casts to Carbon instance', function (): void { + $song = Song::factory()->create(['imported_from_ccli_at' => '2026-05-10 12:00:00']); + + expect($song->fresh()->imported_from_ccli_at)->toBeInstanceOf(Carbon::class); +}); + +test('fromCcli factory state populates both fields', function (): void { + $song = Song::factory()->fromCcli()->create(); + $fresh = $song->fresh(); + + expect($fresh->imported_from_ccli_at)->not->toBeNull(); + expect($fresh->imported_from_ccli_at)->toBeInstanceOf(Carbon::class); + expect($fresh->ccli_source_url)->not->toBeNull(); + expect($fresh->ccli_source_url)->toContain('songselect.ccli.com'); +}); + +test('migration rolls back cleanly', function (): void { + Artisan::call('migrate:rollback', ['--step' => 1]); + + expect(Schema::hasColumn('songs', 'imported_from_ccli_at'))->toBeFalse(); + expect(Schema::hasColumn('songs', 'ccli_source_url'))->toBeFalse(); + + Artisan::call('migrate'); + + expect(Schema::hasColumn('songs', 'imported_from_ccli_at'))->toBeTrue(); + expect(Schema::hasColumn('songs', 'ccli_source_url'))->toBeTrue(); +});