* fix: keep post drafts unscheduled by default * Align schedule validation and keep drafts unscheduled. Require scheduled_at only when status is scheduled and the post has no usable future schedule. Share that rule across web, API, and MCP, keep create without a date as null, and preserve the legacy date → 09:00 UTC fallback. * Polish schedule validation typing and tests. Type requiresExplicitSchedule status as ?string, reuse a local status variable in request/tool validation, tighten the web reject assertion, and collapse overlapping MCP unscheduled-create cases. * Centralize status helper in post update validation. Reuse the typed status() helper across FormRequests and the already-parsed $status in UpdatePostTool so schedule checks stay consistent and less noisy. * Share scheduled_at update rules across web, API, and MCP. Centralize schedule validation in PostStatusRules, normalize status parsing in one place, and align past-schedule coverage across entry points. * Cover the full unscheduled-draft checklist in Pest. Add feature coverage for null/past schedule rejection, explicit scheduling, draft saves, publish-now without a schedule, calendar exclusion, and 09:00 UTC date defaults across web, API, and MCP. * Remove normalizeStatus helper. Keep the inline is_string check at the few call sites that read raw request status before validation — no shared wrapper needed. Co-authored-by: Cursor <cursoragent@cursor.com> * Drop is_string status guards from schedule validation. Accept mixed status in PostStatusRules and rely on strict comparisons with Rule::requiredIf / Rule::when — malformed input simply does not match. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Paulo Castellano <paulo@castellanos.llc>
113 lines
3.6 KiB
PHP
113 lines
3.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Actions\Post;
|
|
|
|
use App\Enums\Post\CreatedVia;
|
|
use App\Enums\Post\Status as PostStatus;
|
|
use App\Models\Post;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class CreatePost
|
|
{
|
|
/**
|
|
* Create a Post with optional platform selection.
|
|
*
|
|
* `platforms[]` enables specific social accounts. Each entry takes
|
|
* `social_account_id` and an optional `content_type` (defaults to the
|
|
* platform's default). Accounts not listed remain disabled but are still
|
|
* created via SyncPostPlatforms so the user can toggle them later in the
|
|
* editor.
|
|
*
|
|
* `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
|
|
*/
|
|
public static function execute(Workspace $workspace, User $user, array $data): Post
|
|
{
|
|
$scheduledAt = self::resolveScheduledAt($data);
|
|
|
|
$post = DB::transaction(function () use ($workspace, $user, $data, $scheduledAt): Post {
|
|
$post = $workspace->posts()->create([
|
|
'user_id' => $user->id,
|
|
'content' => data_get($data, 'content', ''),
|
|
'media' => data_get($data, 'media', []),
|
|
'status' => PostStatus::Draft,
|
|
'created_via' => data_get($data, 'created_via'),
|
|
'scheduled_at' => $scheduledAt,
|
|
]);
|
|
|
|
SyncPostPlatforms::execute($post);
|
|
|
|
foreach (data_get($data, 'platforms', []) as $platformData) {
|
|
$accountId = data_get($platformData, 'social_account_id');
|
|
if (! $accountId) {
|
|
continue;
|
|
}
|
|
|
|
$updates = ['enabled' => true];
|
|
|
|
if ($contentType = data_get($platformData, 'content_type')) {
|
|
$updates['content_type'] = $contentType;
|
|
}
|
|
|
|
$meta = data_get($platformData, 'meta');
|
|
if (is_array($meta) && $meta !== []) {
|
|
$existing = $post->postPlatforms()
|
|
->where('social_account_id', $accountId)
|
|
->first();
|
|
|
|
if ($existing) {
|
|
$updates['meta'] = array_merge($existing->meta ?? [], $meta);
|
|
}
|
|
}
|
|
|
|
$post->postPlatforms()
|
|
->where('social_account_id', $accountId)
|
|
->update($updates);
|
|
}
|
|
|
|
if ($labelIds = data_get($data, 'label_ids')) {
|
|
$post->labels()->sync($labelIds);
|
|
}
|
|
|
|
return $post;
|
|
});
|
|
|
|
return $post;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
private static function resolveScheduledAt(array $data): ?Carbon
|
|
{
|
|
if ($scheduledAt = data_get($data, 'scheduled_at')) {
|
|
return Carbon::parse($scheduledAt)->utc();
|
|
}
|
|
|
|
$date = data_get($data, 'date');
|
|
|
|
if (blank($date)) {
|
|
return null;
|
|
}
|
|
|
|
return Carbon::parse($date, 'UTC')->setTime(9, 0)->utc();
|
|
}
|
|
}
|