'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')); } }