diff --git a/app/Actions/Automation/Node/RunGenerateNode.php b/app/Actions/Automation/Node/RunGenerateNode.php index c4465256..41e7da30 100644 --- a/app/Actions/Automation/Node/RunGenerateNode.php +++ b/app/Actions/Automation/Node/RunGenerateNode.php @@ -12,6 +12,7 @@ use App\DataTransferObjects\Automation\NodeRunResult; use App\Enums\Ai\ContentStyle; use App\Enums\Ai\GeneratorFormat; +use App\Enums\Post\CreatedVia; use App\Enums\PostPlatform\ContentType; use App\Models\AutomationRun; use App\Models\SocialAccount; @@ -145,6 +146,7 @@ public function __invoke(AutomationRun $run, array $config): NodeRunResult 'content' => $generated->content, 'media' => $generated->media, 'platforms' => $platforms, + 'created_via' => CreatedVia::Automation, ]); $run->update(['generated_post_id' => $post->id]); diff --git a/app/Actions/Post/CreatePost.php b/app/Actions/Post/CreatePost.php index 215838b6..7950339d 100644 --- a/app/Actions/Post/CreatePost.php +++ b/app/Actions/Post/CreatePost.php @@ -4,6 +4,7 @@ namespace App\Actions\Post; +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Events\PostCreated; use App\Models\Post; @@ -26,11 +27,15 @@ class CreatePost * `label_ids[]` are attached after creation so the same set of UUIDs * works for REST, MCP, and web callers. * + * `created_via` records which entry point created the post (web, mcp, + * api, or automation). Callers must set it explicitly. + * * @param array{ * content?: ?string, * media?: array, * date?: ?string, * scheduled_at?: ?string, + * created_via: CreatedVia, * platforms?: array}>, * label_ids?: array * } $data @@ -45,6 +50,7 @@ public static function execute(Workspace $workspace, User $user, array $data): P 'content' => data_get($data, 'content', ''), 'media' => data_get($data, 'media', []), 'status' => PostStatus::Draft, + 'created_via' => data_get($data, 'created_via'), 'scheduled_at' => $scheduledAt, ]); diff --git a/app/Enums/Post/CreatedVia.php b/app/Enums/Post/CreatedVia.php new file mode 100644 index 00000000..bd714332 --- /dev/null +++ b/app/Enums/Post/CreatedVia.php @@ -0,0 +1,13 @@ +owner, $data); $post->load(['postPlatforms.socialAccount']); diff --git a/app/Http/Controllers/App/PostController.php b/app/Http/Controllers/App/PostController.php index 3b6e6ef7..7ef6f633 100644 --- a/app/Http/Controllers/App/PostController.php +++ b/app/Http/Controllers/App/PostController.php @@ -12,6 +12,7 @@ use App\Ai\Templates\AiContentTemplate; use App\Ai\Templates\AiTemplateRegistry; use App\Enums\Post\Action as PostAction; +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\SocialAccount\Platform; use App\Http\Requests\App\Post\StorePostRequest; @@ -189,6 +190,7 @@ public function store(StorePostRequest $request): RedirectResponse|\Symfony\Comp $post = CreatePost::execute($workspace, $request->user(), [ 'date' => $request->input('date'), 'media' => $request->input('media', []), + 'created_via' => CreatedVia::Web, ]); return Inertia::location(route('app.posts.edit', $post)); diff --git a/app/Http/Controllers/App/PostTemplateController.php b/app/Http/Controllers/App/PostTemplateController.php index c7889097..18ce4cdc 100644 --- a/app/Http/Controllers/App/PostTemplateController.php +++ b/app/Http/Controllers/App/PostTemplateController.php @@ -6,6 +6,7 @@ use App\Actions\Post\CreatePost; use App\Enums\Media\Type as MediaType; +use App\Enums\Post\CreatedVia; use App\Http\Requests\App\PostTemplate\ApplyPostTemplateRequest; use App\Http\Requests\App\PostTemplate\IndexPostTemplateRequest; use App\Http\Resources\App\PostTemplateResource; @@ -89,6 +90,7 @@ public function apply(ApplyPostTemplateRequest $request, string $slug, TemplateI 'content' => $content, 'media' => $media, 'date' => $request->input('date'), + 'created_via' => CreatedVia::Web, ]); return response()->json([ diff --git a/app/Jobs/Ai/StreamPostCreation.php b/app/Jobs/Ai/StreamPostCreation.php index f8c9eb14..ad8b8f26 100644 --- a/app/Jobs/Ai/StreamPostCreation.php +++ b/app/Jobs/Ai/StreamPostCreation.php @@ -14,6 +14,7 @@ use App\Enums\Ai\GeneratorFormat; use App\Enums\Notification\Channel as NotificationChannel; use App\Enums\Notification\Type as NotificationType; +use App\Enums\Post\CreatedVia; use App\Enums\PostPlatform\ContentType; use App\Events\Ai\PostCreationReady; use App\Jobs\SendNotification; @@ -195,6 +196,7 @@ private function createPostFromGenerated(Workspace $workspace, GeneratedPost $ge 'content' => $generated->content, 'media' => $generated->media, 'date' => $this->date, + 'created_via' => CreatedVia::Web, ]); if ($generated->contentType && $socialAccount) { diff --git a/app/Mcp/Tools/Post/CreatePostTool.php b/app/Mcp/Tools/Post/CreatePostTool.php index 80851f06..50c1ce08 100644 --- a/app/Mcp/Tools/Post/CreatePostTool.php +++ b/app/Mcp/Tools/Post/CreatePostTool.php @@ -5,6 +5,7 @@ namespace App\Mcp\Tools\Post; use App\Actions\Post\CreatePost; +use App\Enums\Post\CreatedVia; use App\Enums\PostPlatform\ContentType; use App\Http\Resources\Api\PostResource; use App\Rules\ContentTypeMatchesPlatform; @@ -41,6 +42,8 @@ public function handle(Request $request): ResponseFactory ...PostPlatformMetaRules::rules(), ]); + $validated['created_via'] = CreatedVia::Mcp; + $post = CreatePost::execute($workspace, $request->user(), $validated); $post->load(['postPlatforms.socialAccount', 'labels']); diff --git a/app/Models/Post.php b/app/Models/Post.php index 1fa41456..8d587d7f 100644 --- a/app/Models/Post.php +++ b/app/Models/Post.php @@ -6,6 +6,7 @@ use App\DataTransferObjects\MediaItem; use App\Enums\Media\Type; +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\SocialAccount\Platform; use App\Observers\PostObserver; @@ -34,6 +35,7 @@ class Post extends Model 'content', 'media', 'status', + 'created_via', 'scheduled_at', 'published_at', ]; @@ -42,6 +44,7 @@ protected function casts(): array { return [ 'status' => PostStatus::class, + 'created_via' => CreatedVia::class, 'media' => 'array', 'scheduled_at' => 'datetime', 'published_at' => 'datetime', diff --git a/database/migrations/2026_07_24_141454_add_created_via_to_posts_table.php b/database/migrations/2026_07_24_141454_add_created_via_to_posts_table.php new file mode 100644 index 00000000..17cd8c7d --- /dev/null +++ b/database/migrations/2026_07_24_141454_add_created_via_to_posts_table.php @@ -0,0 +1,28 @@ +string('created_via')->nullable()->after('status'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + $table->dropColumn('created_via'); + }); + } +}; diff --git a/tests/Feature/Actions/Post/CreatePostTest.php b/tests/Feature/Actions/Post/CreatePostTest.php index 4a1252e0..690f3bb9 100644 --- a/tests/Feature/Actions/Post/CreatePostTest.php +++ b/tests/Feature/Actions/Post/CreatePostTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Actions\Post\CreatePost; +use App\Enums\Post\CreatedVia; use App\Events\PostCreated; use App\Models\User; use App\Models\Workspace; @@ -16,6 +17,7 @@ $post = CreatePost::execute($workspace, $user, [ 'content' => 'Hello world', + 'created_via' => CreatedVia::Web, ]); Event::assertDispatched( @@ -24,3 +26,20 @@ && $event->post->workspace_id === $workspace->id, ); }); + +test('execute persists created_via for each entry point', function (CreatedVia $createdVia) { + $user = User::factory()->create(); + $workspace = Workspace::factory()->create(['user_id' => $user->id]); + + $post = CreatePost::execute($workspace, $user, [ + 'content' => 'Hello world', + 'created_via' => $createdVia, + ]); + + expect($post->fresh()->created_via)->toBe($createdVia); +})->with([ + 'web' => CreatedVia::Web, + 'mcp' => CreatedVia::Mcp, + 'api' => CreatedVia::Api, + 'automation' => CreatedVia::Automation, +]); diff --git a/tests/Feature/Api/PostApiTest.php b/tests/Feature/Api/PostApiTest.php index 5163af93..444bcbfd 100644 --- a/tests/Feature/Api/PostApiTest.php +++ b/tests/Feature/Api/PostApiTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\PostPlatform\ContentType; use App\Enums\SocialAccount\Platform; @@ -72,7 +73,9 @@ ->assertCreated() ->assertJsonPath('status', PostStatus::Draft->value); - expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(1); + $post = Post::where('workspace_id', $this->workspace->id)->first(); + expect($post)->not->toBeNull(); + expect($post->created_via)->toBe(CreatedVia::Api); }); it('creates a post with content, media, and labels', function () { diff --git a/tests/Feature/Mcp/PostToolTest.php b/tests/Feature/Mcp/PostToolTest.php index dfd631b4..ba60f010 100644 --- a/tests/Feature/Mcp/PostToolTest.php +++ b/tests/Feature/Mcp/PostToolTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\Post\CreatedVia; use App\Enums\SocialAccount\Platform; use App\Enums\UserWorkspace\Role; use App\Mcp\Servers\TryPostServer; @@ -109,7 +110,9 @@ ->etc(); }); - expect(Post::where('workspace_id', $this->workspace->id)->count())->toBe(1); + $post = Post::where('workspace_id', $this->workspace->id)->first(); + expect($post)->not->toBeNull(); + expect($post->created_via)->toBe(CreatedVia::Mcp); }); test('create post with platforms enables only those', function () { diff --git a/tests/Feature/PostControllerTest.php b/tests/Feature/PostControllerTest.php index a7da0b14..2ea4c55b 100644 --- a/tests/Feature/PostControllerTest.php +++ b/tests/Feature/PostControllerTest.php @@ -2,6 +2,7 @@ declare(strict_types=1); +use App\Enums\Post\CreatedVia; use App\Enums\Post\Status as PostStatus; use App\Enums\PostPlatform\ContentType; use App\Enums\PostPlatform\Status; @@ -257,6 +258,7 @@ $post = Post::where('workspace_id', $this->workspace->id)->first(); expect($post)->not->toBeNull(); expect($post->status)->toBe(PostStatus::Draft); + expect($post->created_via)->toBe(CreatedVia::Web); expect($post->postPlatforms)->toHaveCount(1); });