Merge branch 'main' into fix/linkedin-drop-r_basicprofile

This commit is contained in:
Paulo Castellano 2026-06-10 16:07:52 -03:00 committed by GitHub
commit 8b8b59f52e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 236 additions and 7 deletions

View file

@ -62,6 +62,10 @@ public static function execute(Workspace $workspace, User $user, array $data): P
$updates['content_type'] = $contentType;
}
if (($meta = data_get($platformData, 'meta')) !== null) {
$updates['meta'] = $meta;
}
$post->postPlatforms()
->where('social_account_id', $accountId)
->update($updates);

View file

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Enums\PostPlatform;
enum AspectRatio: string
{
case Square = '1:1';
case Portrait = '4:5';
case Landscape = '16:9';
case Original = 'original';
/**
* Width-to-height ratio used to center-crop an image. `Original` means "no
* crop", so it resolves to a square (1.0) only as a safe fallback — callers
* should bypass cropping entirely for `Original`.
*/
public function toFloat(): float
{
return match ($this) {
self::Portrait => 4 / 5,
self::Landscape => 16 / 9,
self::Square, self::Original => 1.0,
};
}
}

View file

@ -75,7 +75,10 @@ public function update(UpdatePostRequest $request, Post $post): PostResource|Jso
);
}
return new PostResource(data_get($result, 'post'));
$updated = data_get($result, 'post');
$updated->load(['postPlatforms.socialAccount']);
return new PostResource($updated);
}
public function destroy(Request $request, Post $post): JsonResponse

View file

@ -4,6 +4,7 @@
namespace App\Http\Requests\Api\Post;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Models\SocialAccount;
@ -49,6 +50,8 @@ public function rules(): array
Rule::in(array_column(ContentType::cases(), 'value')),
new ContentTypeMatchesPlatform,
],
'platforms.*.meta' => ['sometimes', 'nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
'scheduled_at' => ['nullable', 'date', 'after:now'],
'label_ids' => ['sometimes', 'array'],
'label_ids.*' => [

View file

@ -5,6 +5,7 @@
namespace App\Http\Requests\Api\Post;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Models\Post;
@ -51,6 +52,7 @@ public function rules(): array
new ContentTypeMatchesPostPlatform,
],
'platforms.*.meta' => ['nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
'scheduled_at' => [
'nullable',
'date',

View file

@ -6,6 +6,7 @@
use App\Enums\Media\Source;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Enums\SocialAccount\Platform;
use App\Rules\ContentFitsPlatformLimits;
@ -70,7 +71,7 @@ public function rules(): array
Rule::when($enforcesMediaCompatibility, [new ContentTypeCompatibleWithMedia]),
],
'platforms.*.meta' => ['nullable', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::in(['1:1', '4:5', '16:9', 'original'])],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
'platforms.*.meta.privacy_level' => ['sometimes', 'nullable', 'string', Rule::in(['PUBLIC_TO_EVERYONE', 'MUTUAL_FOLLOW_FRIENDS', 'FOLLOWER_OF_CREATOR', 'SELF_ONLY'])],
'platforms.*.meta.auto_add_music' => ['sometimes', 'boolean'],
'platforms.*.meta.allow_comments' => ['sometimes', 'boolean'],

View file

@ -18,6 +18,7 @@ public function toArray(Request $request): array
'id' => $this->id,
'platform' => $this->platform?->value,
'content_type' => $this->content_type?->value,
'meta' => $this->meta,
'status' => $this->status?->value,
'enabled' => $this->enabled,
'platform_url' => $this->platform_url,

View file

@ -5,6 +5,7 @@
namespace App\Mcp\Tools\Post;
use App\Actions\Post\CreatePost;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Http\Resources\Api\PostResource;
use App\Rules\ContentTypeMatchesPlatform;
@ -37,6 +38,8 @@ public function handle(Request $request): ResponseFactory
->where('is_active', true),
],
'platforms.*.content_type' => ['required', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPlatform],
'platforms.*.meta' => ['sometimes', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
]);
$post = CreatePost::execute($workspace, $request->user(), $validated);
@ -58,6 +61,7 @@ public function schema(JsonSchema $schema): array
->items($schema->object(fn ($p) => [
'social_account_id' => $p->string()->required()->description('UUID of the connected social account.'),
'content_type' => $p->string()->required()->description('Format for this platform (e.g. linkedin_post, x_post, instagram_feed).'),
'meta' => $p->object()->description('Per-platform metadata (e.g. aspect_ratio: 1:1|4:5|16:9|original for Instagram/Facebook feed images).'),
]))
->description('Platforms to publish on. Accounts not listed remain available but disabled.'),
];

View file

@ -7,6 +7,7 @@
use App\Actions\Post\UpdatePost;
use App\Enums\Post\Action as PostAction;
use App\Enums\Post\Status;
use App\Enums\PostPlatform\AspectRatio;
use App\Enums\PostPlatform\ContentType;
use App\Http\Resources\Api\PostResource;
use App\Models\Post;
@ -49,6 +50,7 @@ public function handle(Request $request): Response|ResponseFactory
],
'platforms.*.content_type' => ['sometimes', 'string', Rule::in(array_column(ContentType::cases(), 'value')), new ContentTypeMatchesPostPlatform],
'platforms.*.meta' => ['sometimes', 'array'],
'platforms.*.meta.aspect_ratio' => ['sometimes', 'nullable', 'string', Rule::enum(AspectRatio::class)],
]);
$payload = collect($validated)->except('post_id')->all();

View file

@ -4,6 +4,7 @@
namespace App\Services\Social\Concerns;
use App\Enums\PostPlatform\AspectRatio;
use App\Exceptions\Social\SocialPublishException;
use App\Services\Media\MediaOptimizer;
use Illuminate\Support\Facades\Http;
@ -51,11 +52,7 @@ protected function cropImageForAspectRatio(string $imageUrl, ?string $aspectRati
protected function aspectRatioToFloat(string $ratio): float
{
return match ($ratio) {
'4:5' => 4 / 5,
'16:9' => 16 / 9,
default => 1.0,
};
return AspectRatio::tryFrom($ratio)?->toFloat() ?? 1.0;
}
/**

View file

@ -577,3 +577,89 @@
->assertOk()
->assertJsonStructure(['id', 'status', 'scheduled_at', 'published_at']);
});
it('creates a post with platform meta (aspect_ratio) and returns it', function () {
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
->postJson(route('api.posts.store'), [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
],
])
->assertCreated()
->assertJsonPath('platforms.0.meta.aspect_ratio', '4:5');
$platform = Post::where('workspace_id', $this->workspace->id)->first()
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
expect($platform->meta['aspect_ratio'])->toBe('4:5');
});
it('rejects creating a post with an invalid aspect_ratio', function () {
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
->postJson(route('api.posts.store'), [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
],
])
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
});
it('rejects updating a post with an invalid aspect_ratio', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
'status' => PostStatus::Draft,
]);
$postPlatform = PostPlatform::factory()->linkedin()->create([
'post_id' => $post->id,
'social_account_id' => $this->socialAccount->id,
'enabled' => true,
]);
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
->putJson(route('api.posts.update', $post), [
'status' => 'draft',
'platforms' => [
['id' => $postPlatform->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
],
])
->assertJsonValidationErrors(['platforms.0.meta.aspect_ratio']);
});
it('accepts a valid aspect_ratio on update and persists it', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
'status' => PostStatus::Draft,
]);
$postPlatform = PostPlatform::factory()->linkedin()->create([
'post_id' => $post->id,
'social_account_id' => $this->socialAccount->id,
'enabled' => true,
]);
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
->putJson(route('api.posts.update', $post), [
'status' => 'draft',
'platforms' => [
['id' => $postPlatform->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '16:9']],
],
])
->assertOk()
->assertJsonPath('platforms.0.meta.aspect_ratio', '16:9');
expect($postPlatform->fresh()->meta['aspect_ratio'])->toBe('16:9');
});
it('accepts the original aspect_ratio (no crop) on create', function () {
$this->withHeaders(['Authorization' => 'Bearer '.$this->plainToken])
->postJson(route('api.posts.store'), [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => 'original']],
],
])
->assertCreated();
$platform = Post::where('workspace_id', $this->workspace->id)->first()
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
expect($platform->meta['aspect_ratio'])->toBe('original');
});

View file

@ -276,3 +276,83 @@
$response->assertHasErrors();
});
test('create post persists platform meta (aspect_ratio)', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
],
]);
$response->assertOk();
$platform = Post::where('workspace_id', $this->workspace->id)->first()
->postPlatforms()->where('social_account_id', $this->socialAccount->id)->first();
expect($platform->meta['aspect_ratio'])->toBe('4:5');
});
test('create post rejects an invalid aspect_ratio', function () {
$response = TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '3:2']],
],
]);
$response->assertHasErrors();
});
test('update post rejects an invalid aspect_ratio', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$platform = PostPlatform::factory()->create([
'post_id' => $post->id,
'social_account_id' => $this->socialAccount->id,
]);
$response = TryPostServer::actingAs($this->user)
->tool(UpdatePostTool::class, [
'post_id' => $post->id,
'platforms' => [
['id' => $platform->id, 'meta' => ['aspect_ratio' => '3:2']],
],
]);
$response->assertHasErrors();
});
test('create post returns the platform meta in the response (read-back)', function () {
TryPostServer::actingAs($this->user)
->tool(CreatePostTool::class, [
'platforms' => [
['social_account_id' => $this->socialAccount->id, 'content_type' => 'linkedin_post', 'meta' => ['aspect_ratio' => '4:5']],
],
])
->assertOk()
->assertStructuredContent(fn (AssertableJson $json) => $json->where('platforms.0.meta.aspect_ratio', '4:5')->etc());
});
test('update post accepts a valid aspect_ratio and persists it', function () {
$post = Post::factory()->create([
'workspace_id' => $this->workspace->id,
'user_id' => $this->user->id,
]);
$platform = PostPlatform::factory()->create([
'post_id' => $post->id,
'social_account_id' => $this->socialAccount->id,
]);
TryPostServer::actingAs($this->user)
->tool(UpdatePostTool::class, [
'post_id' => $post->id,
'platforms' => [
['id' => $platform->id, 'meta' => ['aspect_ratio' => '16:9']],
],
])
->assertOk();
expect($platform->fresh()->meta['aspect_ratio'])->toBe('16:9');
});

View file

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
use App\Enums\PostPlatform\AspectRatio;
test('aspect ratio has the expected cases and values', function () {
expect(array_column(AspectRatio::cases(), 'value'))
->toEqualCanonicalizing(['1:1', '4:5', '16:9', 'original']);
});
test('aspect ratio maps to the correct crop float', function (string $value, float $expected) {
expect(AspectRatio::from($value)->toFloat())->toBe($expected);
})->with([
'1:1' => ['1:1', 1.0],
'4:5' => ['4:5', 4 / 5],
'16:9' => ['16:9', 16 / 9],
'original' => ['original', 1.0],
]);