* 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>
88 lines
2.3 KiB
PHP
88 lines
2.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Enums\Post\Status as PostStatus;
|
|
use App\Models\Post;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
/**
|
|
* Shared post status helpers — edit/delete gates plus the `scheduled_at`
|
|
* update contract used by web, API, and MCP so those entry points cannot drift.
|
|
*/
|
|
class PostStatusRules
|
|
{
|
|
private const EDIT_BLOCKED_MESSAGE_KEY = 'posts.cannot_edit_finalized';
|
|
|
|
/**
|
|
* Statuses where the post can no longer be edited.
|
|
*
|
|
* @var array<int, PostStatus>
|
|
*/
|
|
private const EDIT_BLOCKED_STATUSES = [
|
|
PostStatus::Published,
|
|
PostStatus::PartiallyPublished,
|
|
PostStatus::Failed,
|
|
PostStatus::Publishing,
|
|
];
|
|
|
|
/**
|
|
* Statuses where the post can no longer be deleted.
|
|
*
|
|
* @var array<int, PostStatus>
|
|
*/
|
|
private const DELETE_BLOCKED_STATUSES = [
|
|
PostStatus::Publishing,
|
|
PostStatus::Published,
|
|
PostStatus::PartiallyPublished,
|
|
];
|
|
|
|
public static function blocksEditing(Post $post): bool
|
|
{
|
|
return in_array($post->status, self::EDIT_BLOCKED_STATUSES, true);
|
|
}
|
|
|
|
public static function blocksDeletion(Post $post): bool
|
|
{
|
|
return in_array($post->status, self::DELETE_BLOCKED_STATUSES, true);
|
|
}
|
|
|
|
public static function editBlockedMessage(): string
|
|
{
|
|
return __(self::EDIT_BLOCKED_MESSAGE_KEY);
|
|
}
|
|
|
|
/**
|
|
* True when status is scheduled and the post has no future schedule to reuse.
|
|
*/
|
|
public static function requiresExplicitSchedule(?Post $post, mixed $status): bool
|
|
{
|
|
if ($status !== PostStatus::Scheduled->value) {
|
|
return false;
|
|
}
|
|
|
|
$existing = $post?->scheduled_at;
|
|
|
|
return $existing === null || $existing->isPast();
|
|
}
|
|
|
|
/**
|
|
* Validation rules for `scheduled_at` on post update (web, API, MCP).
|
|
*
|
|
* @return list<mixed>
|
|
*/
|
|
public static function scheduledAtRules(?Post $post, mixed $status): array
|
|
{
|
|
return [
|
|
Rule::requiredIf(fn (): bool => self::requiresExplicitSchedule($post, $status)),
|
|
'nullable',
|
|
'date',
|
|
Rule::when(
|
|
$status === PostStatus::Scheduled->value,
|
|
['after:now'],
|
|
),
|
|
];
|
|
}
|
|
}
|