From f746c5d6a0cdbb89636ca7bfe557501ede698183 Mon Sep 17 00:00:00 2001 From: Doar Mihai Date: Sat, 18 Jul 2026 17:07:48 +0300 Subject: [PATCH] feat: add Post model, migration, and factory Adds the Post model belonging to Site (required, cascade-deletes with the site) and SocialAccount (nullable, nulled out on account delete). Foundation for the Scheduled Posts feature; later tasks build the Livewire CRUD UI and API on top of this model. Co-Authored-By: Claude Sonnet 5 --- app/Models/Post.php | 43 ++++++++++ database/factories/PostFactory.php | 25 ++++++ .../2026_07_18_135721_create_posts_table.php | 37 ++++++++ tests/Feature/PostModelTest.php | 85 +++++++++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 app/Models/Post.php create mode 100644 database/factories/PostFactory.php create mode 100644 database/migrations/2026_07_18_135721_create_posts_table.php create mode 100644 tests/Feature/PostModelTest.php diff --git a/app/Models/Post.php b/app/Models/Post.php new file mode 100644 index 0000000..a1e9043 --- /dev/null +++ b/app/Models/Post.php @@ -0,0 +1,43 @@ + 'datetime', + ]; + + public function site(): BelongsTo + { + return $this->belongsTo(Site::class); + } + + public function socialAccount(): BelongsTo + { + return $this->belongsTo(SocialAccount::class); + } +} diff --git a/database/factories/PostFactory.php b/database/factories/PostFactory.php new file mode 100644 index 0000000..6690061 --- /dev/null +++ b/database/factories/PostFactory.php @@ -0,0 +1,25 @@ + Site::factory(), + 'social_account_id' => null, + 'type' => fake()->randomElement(Post::TYPES), + 'title' => fake()->optional()->sentence(), + 'body' => fake()->paragraph(), + 'status' => fake()->randomElement(Post::STATUSES), + 'scheduled_at' => fake()->optional()->dateTimeBetween('now', '+1 month'), + ]; + } +} diff --git a/database/migrations/2026_07_18_135721_create_posts_table.php b/database/migrations/2026_07_18_135721_create_posts_table.php new file mode 100644 index 0000000..52806ad --- /dev/null +++ b/database/migrations/2026_07_18_135721_create_posts_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('site_id')->constrained()->cascadeOnDelete(); + $table->foreignId('social_account_id')->nullable()->constrained()->nullOnDelete(); + $table->enum('type', [ + 'twitter', 'instagram', 'facebook', 'reddit', + 'tiktok', 'linkedin', 'youtube', 'blog', 'other', + ]); + $table->string('title')->nullable(); + $table->text('body'); + $table->enum('status', ['draft', 'scheduled', 'published', 'failed'])->default('draft'); + $table->timestamp('scheduled_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('posts'); + } +}; diff --git a/tests/Feature/PostModelTest.php b/tests/Feature/PostModelTest.php new file mode 100644 index 0000000..f473e3a --- /dev/null +++ b/tests/Feature/PostModelTest.php @@ -0,0 +1,85 @@ +create(); + + $post = Post::create([ + 'site_id' => $site->id, + 'type' => 'blog', + 'body' => 'Hello world', + ]); + + $fresh = $post->fresh(); + + expect($fresh) + ->site_id->toBe($site->id) + ->type->toBe('blog') + ->body->toBe('Hello world') + ->status->toBe('draft') + ->social_account_id->toBeNull() + ->title->toBeNull() + ->scheduled_at->toBeNull(); +}); + +test('a post can be created with an account, title, status, and schedule', function () { + $site = Site::factory()->create(); + $account = SocialAccount::factory()->create(); + + $post = Post::create([ + 'site_id' => $site->id, + 'social_account_id' => $account->id, + 'type' => 'twitter', + 'title' => 'Launch day', + 'body' => 'We just launched!', + 'status' => 'scheduled', + 'scheduled_at' => '2026-08-01 09:00:00', + ]); + + $fresh = $post->fresh(); + + expect($fresh) + ->social_account_id->toBe($account->id) + ->title->toBe('Launch day') + ->status->toBe('scheduled') + ->scheduled_at->not->toBeNull(); +}); + +test('a post belongs to a site', function () { + $site = Site::factory()->create(); + $post = Post::factory()->create(['site_id' => $site->id]); + + expect($post->site)->toBeInstanceOf(Site::class); + expect($post->site->id)->toBe($site->id); +}); + +test('a post can belong to a social account', function () { + $account = SocialAccount::factory()->create(); + $post = Post::factory()->create(['social_account_id' => $account->id]); + + expect($post->socialAccount)->toBeInstanceOf(SocialAccount::class); + expect($post->socialAccount->id)->toBe($account->id); +}); + +test('deleting a site cascades and deletes its posts', function () { + $site = Site::factory()->create(); + $post = Post::factory()->create(['site_id' => $site->id]); + + $site->delete(); + + expect(Post::find($post->id))->toBeNull(); +}); + +test('deleting a social account nulls out the post link but keeps the post', function () { + $account = SocialAccount::factory()->create(); + $post = Post::factory()->create(['social_account_id' => $account->id]); + + $account->delete(); + + expect($post->fresh()) + ->not->toBeNull() + ->social_account_id->toBeNull(); +});