2026-05-21 22:27:19 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-05-21 22:32:42 +00:00
|
|
|
namespace App\Support;
|
2026-05-21 22:27:19 +00:00
|
|
|
|
|
|
|
|
use App\Enums\Post\Status as PostStatus;
|
|
|
|
|
use App\Models\Post;
|
fix: keep post drafts unscheduled by default (#209)
* 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>
2026-08-01 20:39:18 +00:00
|
|
|
use Illuminate\Validation\Rule;
|
2026-05-21 22:27:19 +00:00
|
|
|
|
fix: keep post drafts unscheduled by default (#209)
* 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>
2026-08-01 20:39:18 +00:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2026-05-21 22:32:42 +00:00
|
|
|
class PostStatusRules
|
2026-05-21 22:27:19 +00:00
|
|
|
{
|
|
|
|
|
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);
|
|
|
|
|
}
|
fix: keep post drafts unscheduled by default (#209)
* 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>
2026-08-01 20:39:18 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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'],
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}
|
2026-05-21 22:27:19 +00:00
|
|
|
}
|