Files
hado/core/app/Ingest/ManualEvents.php

124 lines
4.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Ingest;
use App\Models\Event;
use App\Models\Source;
use App\Models\User;
use App\Presenters\EventPresenter;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
use LogicException;
/**
* События, которые пользователь заводит сам — кнопкой в инбоксе или через агента.
* Источник один: `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(), self::item($fields));
}
/**
* Частичная правка своего события: не переданные поля берутся из него. Новый `due_at` —
* это перенос (§5): состояние пересчитывается даже из терминального, а `fire_at`,
* если не задан явно, считается заново от нового срока.
*
* @param array<string,mixed> $fields
*/
public function update(Event $event, array $fields): Event
{
if (! self::owns($event)) {
throw new LogicException("Событие источника «{$event->source->name}» правит только его источник");
}
$current = EventPresenter::forClient($event);
$merged = [
'title' => $current['payload']['title'],
'subtitle' => $current['payload']['subtitle'] ?? null,
'done_label' => $current['payload']['done_label'] ?? null,
'deep_link' => $current['payload']['deep_link'] ?? null,
'due_at' => $current['due_at'],
'fire_at' => isset($fields['due_at']) ? null : $current['fire_at'],
'after_due' => $current['after_due'],
];
foreach ($fields as $key => $value) {
$merged[$key] = $value;
}
return $this->upsert($event->user, $event->source_ref, self::item($merged));
}
public function withdraw(Event $event): void
{
if (! self::owns($event)) {
throw new LogicException("Событие источника «{$event->source->name}» снимает только его источник");
}
$this->upserter->withdraw($event);
}
/**
* Плоские поля → upsert-элемент с дефолтами кнопки инбокса. Проверяется теми же
* правилами, что и события клиентов.
*
* @param array<string,mixed> $fields
* @return array<string,mixed>
*/
private static function item(array $fields): array
{
$dueAt = (string) ($fields['due_at'] ?? '');
$item = [
'due_at' => $dueAt,
'fire_at' => $fields['fire_at'] ?? self::defaultFireAt($dueAt),
'after_due' => $fields['after_due'] ?? 'keep',
'payload' => array_filter([
'title' => $fields['title'] ?? null,
'subtitle' => $fields['subtitle'] ?? null,
'done_label' => $fields['done_label'] ?? 'Сделано',
'deep_link' => $fields['deep_link'] ?? null,
], fn ($v) => $v !== null && $v !== ''),
];
EventRules::validate($item);
return $item;
}
/** @param array<string,mixed> $item */
private function upsert(User $user, string $sourceRef, array $item): Event
{
[$result] = $this->upserter->upsert($this->source(), [['user' => $user->login, 'source_ref' => $sourceRef, 'topic' => ''] + $item]);
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 берётся только дата. Кривой due_at отдаём правилам как есть. */
private static function defaultFireAt(string $dueAt): ?string
{
$day = CarbonImmutable::createFromFormat('Y-m-d', substr($dueAt, 0, 10));
return $day === false ? null : $day->subDays(self::DEFAULT_LEAD_DAYS)->format('Y-m-d');
}
}