Merge pull request #203 from trypostit/feature/post-created-via
Track post creation origin with created_via
This commit is contained in:
commit
596ad491ef
26 changed files with 498 additions and 7 deletions
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
|
||||
namespace App\Actions\Post;
|
||||
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Events\PostCreated;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
|
|
@ -26,11 +26,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). Analytical only — null when omitted.
|
||||
*
|
||||
* @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 +49,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,
|
||||
]);
|
||||
|
||||
|
|
@ -85,8 +90,6 @@ public static function execute(Workspace $workspace, User $user, array $data): P
|
|||
return $post;
|
||||
});
|
||||
|
||||
PostCreated::dispatch($post);
|
||||
|
||||
return $post;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
namespace App\Actions\Post;
|
||||
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Enums\PostPlatform\Status as PostPlatformStatus;
|
||||
use App\Models\Post;
|
||||
|
|
@ -25,6 +26,7 @@ public static function execute(Post $original, User $user): Post
|
|||
'content' => $original->content,
|
||||
'media' => $original->media,
|
||||
'status' => PostStatus::Draft,
|
||||
'created_via' => CreatedVia::Web,
|
||||
'scheduled_at' => null,
|
||||
'published_at' => null,
|
||||
]);
|
||||
|
|
|
|||
13
app/Enums/Post/CreatedVia.php
Normal file
13
app/Enums/Post/CreatedVia.php
Normal 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';
|
||||
}
|
||||
10
app/Enums/PostHog/PostEvent.php
Normal file
10
app/Enums/PostHog/PostEvent.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums\PostHog;
|
||||
|
||||
enum PostEvent: string
|
||||
{
|
||||
case Created = 'post.created';
|
||||
}
|
||||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
67
app/Jobs/PostHog/TrackPost.php
Normal file
67
app/Jobs/PostHog/TrackPost.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Jobs\PostHog;
|
||||
|
||||
use App\Enums\PostHog\PostEvent;
|
||||
use App\Models\Post;
|
||||
use App\Services\PostHogService;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class TrackPost implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $timeout = 30;
|
||||
|
||||
public function __construct(public string $postId)
|
||||
{
|
||||
$this->onQueue('posthog');
|
||||
}
|
||||
|
||||
public function handle(PostHogService $postHog): void
|
||||
{
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$post = Post::query()
|
||||
->with(['workspace.account.plan'])
|
||||
->find($this->postId);
|
||||
|
||||
if (! $post) {
|
||||
return;
|
||||
}
|
||||
|
||||
$account = $post->workspace?->account;
|
||||
|
||||
if (! $account) {
|
||||
return;
|
||||
}
|
||||
|
||||
$distinctId = (string) ($post->user_id ?? $account->owner_id);
|
||||
|
||||
if ($distinctId === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$postHog->capture(
|
||||
$distinctId,
|
||||
PostEvent::Created->value,
|
||||
[
|
||||
'post_id' => (string) $post->id,
|
||||
'workspace_id' => (string) $post->workspace_id,
|
||||
'created_via' => $post->created_via?->value,
|
||||
'status' => $post->status->value,
|
||||
],
|
||||
$account,
|
||||
);
|
||||
}
|
||||
}
|
||||
21
app/Listeners/PostHog/TrackPostCreated.php
Normal file
21
app/Listeners/PostHog/TrackPostCreated.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Listeners\PostHog;
|
||||
|
||||
use App\Events\PostCreated;
|
||||
use App\Jobs\PostHog\TrackPost;
|
||||
use App\Services\PostHogService;
|
||||
|
||||
class TrackPostCreated
|
||||
{
|
||||
public function handle(PostCreated $event): void
|
||||
{
|
||||
if (! PostHogService::isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TrackPost::dispatch((string) $event->post->id);
|
||||
}
|
||||
}
|
||||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -6,11 +6,18 @@
|
|||
|
||||
use App\Enums\Automation\Trigger\Type as TriggerType;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Events\PostCreated;
|
||||
use App\Jobs\Automation\DispatchPostTriggerAutomationsJob;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class PostObserver
|
||||
{
|
||||
public function created(Post $post): void
|
||||
{
|
||||
DB::afterCommit(fn () => PostCreated::dispatch($post));
|
||||
}
|
||||
|
||||
public function saved(Post $post): void
|
||||
{
|
||||
if (! $post->wasChanged('status')) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
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');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -3,12 +3,13 @@
|
|||
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;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
test('execute dispatches PostCreated with the persisted post', function () {
|
||||
test('execute relies on the observer to dispatch PostCreated', function () {
|
||||
Event::fake([PostCreated::class]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
|
@ -16,6 +17,7 @@
|
|||
|
||||
$post = CreatePost::execute($workspace, $user, [
|
||||
'content' => 'Hello world',
|
||||
'created_via' => CreatedVia::Web,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(
|
||||
|
|
@ -24,3 +26,31 @@
|
|||
&& $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,
|
||||
]);
|
||||
|
||||
test('execute leaves created_via null when omitted', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$post = CreatePost::execute($workspace, $user, [
|
||||
'content' => 'Hello world',
|
||||
]);
|
||||
|
||||
expect($post->fresh()->created_via)->toBeNull();
|
||||
});
|
||||
|
|
|
|||
51
tests/Feature/Actions/Post/DuplicatePostTest.php
Normal file
51
tests/Feature/Actions/Post/DuplicatePostTest.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Actions\Post\DuplicatePost;
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Events\PostCreated;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
test('execute clones the post as a draft created via web', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
$original = Post::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $user->id,
|
||||
'content' => 'Original content',
|
||||
'created_via' => CreatedVia::Api,
|
||||
'status' => PostStatus::Published,
|
||||
]);
|
||||
|
||||
$copy = DuplicatePost::execute($original, $user);
|
||||
|
||||
expect($copy->id)->not->toBe($original->id)
|
||||
->and($copy->content)->toBe('Original content')
|
||||
->and($copy->status)->toBe(PostStatus::Draft)
|
||||
->and($copy->created_via)->toBe(CreatedVia::Web)
|
||||
->and($copy->scheduled_at)->toBeNull()
|
||||
->and($copy->published_at)->toBeNull();
|
||||
});
|
||||
|
||||
test('execute relies on the observer to dispatch PostCreated for the duplicate', function () {
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
$original = Post::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
Event::fake([PostCreated::class]);
|
||||
|
||||
$copy = DuplicatePost::execute($original, $user);
|
||||
|
||||
Event::assertDispatched(
|
||||
PostCreated::class,
|
||||
fn (PostCreated $event) => $event->post->id === $copy->id,
|
||||
);
|
||||
});
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Ai\Agents\PostContentGenerator;
|
||||
use App\Ai\Agents\PostContentHumanizer;
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Jobs\Ai\StreamPostCreation;
|
||||
|
|
@ -111,7 +112,8 @@ function runStreamPostCreation(string $format, SocialAccount $account, int $imag
|
|||
$post = $this->user->currentWorkspace->posts()->latest()->first();
|
||||
|
||||
expect($post->content)->toBe('Hello world\n\nSecond para.')
|
||||
->and($post->media)->toHaveCount(1);
|
||||
->and($post->media)->toHaveCount(1)
|
||||
->and($post->created_via)->toBe(CreatedVia::Web);
|
||||
|
||||
$platform = PostPlatform::where('social_account_id', $this->account->id)->firstOrFail();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,26 @@
|
|||
->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('ignores a client-supplied created_via and always records api', function () {
|
||||
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
|
||||
->postJson(route('api.posts.store'), [
|
||||
'created_via' => 'web',
|
||||
'platforms' => [
|
||||
[
|
||||
'social_account_id' => $this->socialAccount->id,
|
||||
'content_type' => 'linkedin_post',
|
||||
],
|
||||
],
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$post = Post::where('workspace_id', $this->workspace->id)->first();
|
||||
expect($post->created_via)->toBe(CreatedVia::Api);
|
||||
});
|
||||
|
||||
it('creates a post with content, media, and labels', function () {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Ai\Agents\PostContentHumanizer;
|
||||
use App\Enums\Ai\ContentStyle;
|
||||
use App\Enums\Ai\GeneratorFormat;
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\Post\Status as PostStatus;
|
||||
use App\Enums\PostPlatform\ContentType;
|
||||
use App\Models\Automation;
|
||||
|
|
@ -47,6 +48,7 @@
|
|||
$post = Post::find($run->generated_post_id);
|
||||
expect($post)->not->toBeNull();
|
||||
expect($post->status)->toBe(PostStatus::Draft);
|
||||
expect($post->created_via)->toBe(CreatedVia::Automation);
|
||||
});
|
||||
|
||||
it('applies brand voice by default', function () {
|
||||
|
|
|
|||
96
tests/Feature/Jobs/PostHog/TrackPostTest.php
Normal file
96
tests/Feature/Jobs/PostHog/TrackPostTest.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\PostHog\PostEvent;
|
||||
use App\Jobs\PostHog\SendEvent;
|
||||
use App\Jobs\PostHog\TrackPost;
|
||||
use App\Models\Account;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\PostHogService;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create();
|
||||
$this->user = User::factory()->create(['account_id' => $this->account->id]);
|
||||
$this->account->update(['owner_id' => $this->user->id]);
|
||||
$this->workspace = Workspace::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('job is queued on the posthog queue', function () {
|
||||
$job = new TrackPost((string) Str::uuid());
|
||||
|
||||
expect($job->queue)->toBe('posthog');
|
||||
});
|
||||
|
||||
test('handle captures post.created with created_via and account group', function () {
|
||||
Queue::fake();
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'created_via' => CreatedVia::Api,
|
||||
]);
|
||||
|
||||
(new TrackPost((string) $post->id))->handle(app(PostHogService::class));
|
||||
|
||||
Queue::assertPushed(SendEvent::class, function ($job) use ($post) {
|
||||
return $job->method === 'capture'
|
||||
&& $job->payload['event'] === PostEvent::Created->value
|
||||
&& $job->payload['distinctId'] === (string) $this->user->id
|
||||
&& $job->payload['properties']['post_id'] === (string) $post->id
|
||||
&& $job->payload['properties']['workspace_id'] === (string) $this->workspace->id
|
||||
&& $job->payload['properties']['created_via'] === CreatedVia::Api->value
|
||||
&& $job->payload['properties']['$groups']['account'] === (string) $this->account->id;
|
||||
});
|
||||
});
|
||||
|
||||
test('handle falls back to account owner when post has no user', function () {
|
||||
Queue::fake();
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => null,
|
||||
'created_via' => CreatedVia::Mcp,
|
||||
]);
|
||||
|
||||
(new TrackPost((string) $post->id))->handle(app(PostHogService::class));
|
||||
|
||||
Queue::assertPushed(
|
||||
SendEvent::class,
|
||||
fn ($job) => $job->payload['distinctId'] === (string) $this->user->id
|
||||
&& $job->payload['properties']['created_via'] === CreatedVia::Mcp->value,
|
||||
);
|
||||
});
|
||||
|
||||
test('handle returns silently when post does not exist', function () {
|
||||
Queue::fake();
|
||||
|
||||
(new TrackPost((string) Str::uuid()))->handle(app(PostHogService::class));
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('handle does not push when PostHog is disabled', function () {
|
||||
config(['services.posthog.api_key' => null]);
|
||||
Queue::fake();
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'created_via' => CreatedVia::Web,
|
||||
]);
|
||||
|
||||
(new TrackPost((string) $post->id))->handle(app(PostHogService::class));
|
||||
|
||||
Queue::assertNotPushed(SendEvent::class);
|
||||
});
|
||||
71
tests/Feature/Listeners/PostHog/TrackPostCreatedTest.php
Normal file
71
tests/Feature/Listeners/PostHog/TrackPostCreatedTest.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Events\PostCreated;
|
||||
use App\Jobs\PostHog\TrackPost;
|
||||
use App\Listeners\PostHog\TrackPostCreated;
|
||||
use App\Models\Account;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
beforeEach(function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test_key']);
|
||||
|
||||
$this->account = Account::factory()->create();
|
||||
$this->user = User::factory()->create(['account_id' => $this->account->id]);
|
||||
$this->account->update(['owner_id' => $this->user->id]);
|
||||
$this->workspace = Workspace::factory()->create([
|
||||
'account_id' => $this->account->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('listener dispatches TrackPost with the post id', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'created_via' => CreatedVia::Web,
|
||||
]);
|
||||
|
||||
Bus::fake();
|
||||
|
||||
(new TrackPostCreated)->handle(new PostCreated($post));
|
||||
|
||||
Bus::assertDispatched(
|
||||
TrackPost::class,
|
||||
fn ($job) => $job->postId === (string) $post->id,
|
||||
);
|
||||
});
|
||||
|
||||
test('listener is wired to the PostCreated event via auto-discovery', function () {
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
'created_via' => CreatedVia::Automation,
|
||||
]);
|
||||
|
||||
Bus::fake();
|
||||
|
||||
PostCreated::dispatch($post);
|
||||
|
||||
Bus::assertDispatched(TrackPost::class);
|
||||
});
|
||||
|
||||
test('listener does not dispatch when PostHog is disabled', function () {
|
||||
config(['services.posthog.enabled' => false]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $this->workspace->id,
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
Bus::fake();
|
||||
|
||||
(new TrackPostCreated)->handle(new PostCreated($post));
|
||||
|
||||
Bus::assertNotDispatched(TrackPost::class);
|
||||
});
|
||||
|
|
@ -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 () {
|
||||
|
|
|
|||
40
tests/Feature/Observers/PostObserverTest.php
Normal file
40
tests/Feature/Observers/PostObserverTest.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Events\PostCreated;
|
||||
use App\Models\Post;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
test('creating a post dispatches PostCreated via the observer', function () {
|
||||
Event::fake([PostCreated::class]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$post = Post::factory()->create([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
Event::assertDispatched(
|
||||
PostCreated::class,
|
||||
fn (PostCreated $event) => $event->post->id === $post->id,
|
||||
);
|
||||
});
|
||||
|
||||
test('creating a post quietly does not dispatch PostCreated', function () {
|
||||
Event::fake([PostCreated::class]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
Post::factory()->createQuietly([
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
|
||||
Event::assertNotDispatched(PostCreated::class);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Enums\Post\CreatedVia;
|
||||
use App\Enums\UserWorkspace\Role;
|
||||
use App\Models\Account;
|
||||
use App\Models\SocialAccount;
|
||||
|
|
@ -72,6 +73,9 @@
|
|||
|
||||
expect($response->json('post_id'))->toBeString();
|
||||
expect($response->json('redirect_url'))->toContain('edit');
|
||||
|
||||
$post = $this->workspace->posts()->find($response->json('post_id'));
|
||||
expect($post->created_via)->toBe(CreatedVia::Web);
|
||||
});
|
||||
|
||||
test('apply interpolates brand_name in content', function () {
|
||||
|
|
|
|||
Loading…
Reference in a new issue