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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4293475ce1
commit
f746c5d6a0
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Post extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const TYPES = [
|
||||||
|
'twitter', 'instagram', 'facebook', 'reddit',
|
||||||
|
'tiktok', 'linkedin', 'youtube', 'blog', 'other',
|
||||||
|
];
|
||||||
|
|
||||||
|
public const STATUSES = ['draft', 'scheduled', 'published', 'failed'];
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'site_id',
|
||||||
|
'social_account_id',
|
||||||
|
'type',
|
||||||
|
'title',
|
||||||
|
'body',
|
||||||
|
'status',
|
||||||
|
'scheduled_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'scheduled_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function site(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Site::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function socialAccount(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SocialAccount::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Post;
|
||||||
|
use App\Models\Site;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class PostFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Post::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'site_id' => 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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('posts', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Post;
|
||||||
|
use App\Models\Site;
|
||||||
|
use App\Models\SocialAccount;
|
||||||
|
|
||||||
|
test('a post can be created with required fields only', function () {
|
||||||
|
$site = Site::factory()->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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user