refactor: single EventPresenter and ManualEvents shared by /api, /me, inbox, webhook; editable flag

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
nikita.hohlov
2026-09-05 06:01:55 -03:00
parent 2ec71ab710
commit 9163ff97f9
10 changed files with 297 additions and 75 deletions

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Ingest;
use App\Models\Event;
use App\Models\Source;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
/**
* События, которые пользователь заводит сам кнопкой в инбоксе или через агента.
* Источник один: `manual`. Только его события пользователь правит и снимает;
* чужие источники правят своё сами.
*/
final class ManualEvents
{
public const SOURCE = 'manual';
/** Старт подготовки по умолчанию: за столько дней до срока. */
public const DEFAULT_LEAD_DAYS = 30;
public function __construct(private readonly EventUpserter $upserter) {}
public static function owns(Event $event): bool
{
return $event->source->name === self::SOURCE;
}
/**
* @param array{title:string, due_at:string, subtitle?:?string, fire_at?:?string, after_due?:?string, done_label?:?string, deep_link?:?string} $fields
*/
public function create(User $user, array $fields): Event
{
return $this->upsert($user, 'manual:'.Str::ulid(), $fields);
}
private function upsert(User $user, string $sourceRef, array $fields): Event
{
[$result] = $this->upserter->upsert($this->source(), [[
'user' => $user->login,
'source_ref' => $sourceRef,
'topic' => '',
'due_at' => $fields['due_at'],
'fire_at' => $fields['fire_at'] ?? self::defaultFireAt($fields['due_at']),
'after_due' => $fields['after_due'] ?? 'keep',
'payload' => array_filter([
'title' => $fields['title'],
'subtitle' => $fields['subtitle'] ?? null,
'done_label' => $fields['done_label'] ?? 'Сделано',
'deep_link' => $fields['deep_link'] ?? null,
], fn ($v) => $v !== null && $v !== ''),
]]);
return Event::with('source')->findOrFail($result['id']);
}
private function source(): Source
{
return Source::firstOrCreate(['name' => self::SOURCE], ['token_hash' => hash('sha256', Str::random(64))]);
}
/** День срока минус DEFAULT_LEAD_DAYS; из due_at берётся только дата. */
private static function defaultFireAt(string $dueAt): string
{
return CarbonImmutable::createFromFormat('Y-m-d', substr($dueAt, 0, 10))->subDays(self::DEFAULT_LEAD_DAYS)->format('Y-m-d');
}
}