feat: due_at parser and event upserter

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-04 06:45:33 -03:00
parent c2fa8aa00d
commit 7d1df7828a
5 changed files with 427 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Ingest;
use App\Enums\DueMode;
use App\Models\Event;
use Carbon\CarbonImmutable;
/** Разобранный due_at: tagged union по mode. */
final class ParsedDue
{
private function __construct(
public readonly DueMode $mode,
public readonly ?string $date,
public readonly ?string $time,
public readonly ?CarbonImmutable $instant,
) {}
public static function local(string $date, string $time): self
{
return new self(DueMode::Local, $date, $time, null);
}
public static function fixed(CarbonImmutable $instant): self
{
return new self(DueMode::Fixed, null, null, $instant->utc());
}
/** Календарный день срока в данном поясе, Y-m-d. */
public function dayFor(string $tz): string
{
return match ($this->mode) {
DueMode::Local => $this->date,
DueMode::Fixed => $this->instant->setTimezone($tz)->format('Y-m-d'),
};
}
public function equals(Event $event): bool
{
if ($event->due_mode !== $this->mode) {
return false;
}
return match ($this->mode) {
DueMode::Local => $event->due_date->format('Y-m-d') === $this->date && $event->due_time === $this->time,
DueMode::Fixed => $event->due_instant->eq($this->instant),
};
}
/** @return array<string, mixed> колонки events */
public function columns(): array
{
return [
'due_mode' => $this->mode,
'due_date' => $this->date,
'due_time' => $this->time,
'due_instant' => $this->instant,
];
}
}