Track post creation origin via created_via.

Persist whether a post was created through web, MCP, API, or automation so we can attribute entry points without guessing from request context.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Paulo Castellano 2026-07-24 11:19:01 -03:00
parent e2c43339e2
commit 06f83a6571
14 changed files with 93 additions and 2 deletions

View file

@ -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]);

View file

@ -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<int, mixed>,
* date?: ?string,
* scheduled_at?: ?string,
* created_via: CreatedVia,
* platforms?: array<int, array{social_account_id: string, content_type?: string, meta?: array<string, mixed>}>,
* label_ids?: array<int, string>
* } $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,
]);

View file

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Enums\Post;
enum CreatedVia: string
{
case Web = 'web';
case Mcp = 'mcp';
case Api = 'api';
case Automation = 'automation';
}

View file

@ -10,6 +10,7 @@
use App\Actions\Post\UpdatePost;
use App\Enums\Media\Type as MediaType;
use App\Enums\Post\Action as PostAction;
use App\Enums\Post\CreatedVia;
use App\Http\Requests\Api\Post\AttachMediaFromUrlRequest;
use App\Http\Requests\Api\Post\StoreMediaRequest;
use App\Http\Requests\Api\Post\StorePostRequest;
@ -61,6 +62,8 @@ public function store(StorePostRequest $request): JsonResponse
);
}
$data['created_via'] = CreatedVia::Api;
$post = CreatePost::execute($workspace, $workspace->owner, $data);
$post->load(['postPlatforms.socialAccount']);

View file

@ -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));

View file

@ -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([

View file

@ -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) {

View file

@ -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']);

View file

@ -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',

View file

@ -0,0 +1,28 @@
<?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::table('posts', function (Blueprint $table) {
$table->string('created_via')->nullable()->after('status');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('created_via');
});
}
};

View file

@ -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,
]);

View file

@ -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 () {

View file

@ -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 () {

View file

@ -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);
});