feat: due resolution and cascade math
Also: StoresUtc trait normalizes datetime casts to UTC (controller ruling). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
99
core/tests/Unit/Scheduling/DueResolverTest.php
Normal file
99
core/tests/Unit/Scheduling/DueResolverTest.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Scheduling;
|
||||
|
||||
use App\Enums\AfterDue;
|
||||
use App\Enums\DueMode;
|
||||
use App\Enums\EventState;
|
||||
use App\Models\Event;
|
||||
use App\Models\User;
|
||||
use App\Scheduling\DueResolver;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Tests\TestCase; // Laravel TestCase: Eloquent date-casts need the connection grammar even in memory
|
||||
|
||||
class DueResolverTest extends TestCase
|
||||
{
|
||||
private function user(string $tz = 'America/Montevideo'): User
|
||||
{
|
||||
return new User(['login' => 'n', 'tz' => $tz, 'quiet_start' => '22:00:00', 'quiet_end' => '09:00:00']);
|
||||
}
|
||||
|
||||
private function local(string $date, string $time = '24:00:00', ?string $fireOn = null): Event
|
||||
{
|
||||
return new Event([
|
||||
'due_mode' => DueMode::Local, 'due_date' => $date, 'due_time' => $time,
|
||||
'fire_on' => $fireOn ?? $date, 'after_due' => AfterDue::Expire,
|
||||
'state' => EventState::Scheduled, 'payload' => ['title' => 't'],
|
||||
]);
|
||||
}
|
||||
|
||||
private function fixed(string $instant, string $fireOn): Event
|
||||
{
|
||||
return new Event([
|
||||
'due_mode' => DueMode::Fixed, 'due_instant' => CarbonImmutable::parse($instant),
|
||||
'fire_on' => $fireOn, 'after_due' => AfterDue::Expire,
|
||||
'state' => EventState::Scheduled, 'payload' => ['title' => 't'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_whole_day_local_event(): void
|
||||
{
|
||||
$due = (new DueResolver)->resolve($this->local('2026-08-14'), $this->user());
|
||||
|
||||
$this->assertSame('2026-08-14 00:00 America/Montevideo', $due->day->format('Y-m-d H:i e'));
|
||||
$this->assertSame('2026-08-15 00:00 America/Montevideo', $due->moment->format('Y-m-d H:i e'));
|
||||
}
|
||||
|
||||
public function test_timed_local_event(): void
|
||||
{
|
||||
$due = (new DueResolver)->resolve($this->local('2026-09-10', '14:00:00'), $this->user());
|
||||
|
||||
$this->assertSame('2026-09-10 00:00', $due->day->format('Y-m-d H:i'));
|
||||
$this->assertSame('2026-09-10 14:00', $due->moment->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function test_local_event_follows_user_timezone(): void
|
||||
{
|
||||
$event = $this->local('2026-09-10', '14:00:00');
|
||||
|
||||
$uy = (new DueResolver)->resolve($event, $this->user('America/Montevideo'));
|
||||
$cn = (new DueResolver)->resolve($event, $this->user('Asia/Shanghai'));
|
||||
|
||||
$this->assertSame('2026-09-10 14:00', $uy->moment->format('Y-m-d H:i'));
|
||||
$this->assertSame('2026-09-10 14:00', $cn->moment->format('Y-m-d H:i'));
|
||||
$this->assertNotEquals($uy->moment->utc()->toIso8601String(), $cn->moment->utc()->toIso8601String());
|
||||
}
|
||||
|
||||
public function test_fixed_event_is_converted_into_user_timezone(): void
|
||||
{
|
||||
// Вылет из Мадрида 08:15+02:00 = 06:15Z = 03:15 в Монтевидео (-03:00)
|
||||
$due = (new DueResolver)->resolve($this->fixed('2026-09-10T08:15:00+02:00', '2026-09-03'), $this->user());
|
||||
|
||||
$this->assertSame('2026-09-10 03:15 America/Montevideo', $due->moment->format('Y-m-d H:i e'));
|
||||
$this->assertSame('2026-09-10 00:00', $due->day->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function test_fire_start_is_local_midnight(): void
|
||||
{
|
||||
$start = (new DueResolver)->fireStart($this->local('2026-09-30', '24:00:00', '2026-08-31'), $this->user());
|
||||
$this->assertSame('2026-08-31 00:00 America/Montevideo', $start->format('Y-m-d H:i e'));
|
||||
}
|
||||
|
||||
public function test_today_starts_at_midnight_for_normal_moment(): void
|
||||
{
|
||||
$r = new DueResolver;
|
||||
$due = $r->resolve($this->local('2026-09-10', '14:00:00'), $this->user());
|
||||
$this->assertSame('2026-09-10 00:00', $r->todayStart($due, $this->user())->format('Y-m-d H:i'));
|
||||
}
|
||||
|
||||
public function test_today_starts_the_evening_before_for_early_moment(): void
|
||||
{
|
||||
$r = new DueResolver;
|
||||
$due = $r->resolve($this->local('2026-09-10', '07:00:00'), $this->user());
|
||||
// quiet_start 22:00 − 2h = 20:00 накануне
|
||||
$this->assertSame('2026-09-09 20:00', $r->todayStart($due, $this->user())->format('Y-m-d H:i'));
|
||||
|
||||
$exactlyAtActiveStart = $r->resolve($this->local('2026-09-10', '09:00:00'), $this->user());
|
||||
$this->assertSame('2026-09-09 20:00', $r->todayStart($exactlyAtActiveStart, $this->user())->format('Y-m-d H:i'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user