feat: state, window and quiet rules
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BCrwHHnGCB5XH968Nxokqw
This commit is contained in:
@@ -11,7 +11,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
$this->app->singleton(\App\Scheduling\WindowResolver::class, fn () => new \App\Scheduling\WindowResolver(
|
||||||
|
config('hado.fallback_day_minutes'),
|
||||||
|
config('hado.fallback_hour_minutes'),
|
||||||
|
config('hado.fallback_hours'),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
36
core/app/Scheduling/QuietRule.php
Normal file
36
core/app/Scheduling/QuietRule.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Scheduling;
|
||||||
|
|
||||||
|
use App\Enums\EventState;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
/** «Помню»: до какого момента молчать, по текущему состоянию. */
|
||||||
|
final class QuietRule
|
||||||
|
{
|
||||||
|
public function __construct(private readonly DueResolver $due) {}
|
||||||
|
|
||||||
|
public function afterAck(Event $event, User $user, EventState $state, Due $due, CarbonImmutable $now): CarbonImmutable
|
||||||
|
{
|
||||||
|
return match ($state) {
|
||||||
|
EventState::Preparing => $this->nextCascadePoint($event, $user, $due, $now),
|
||||||
|
EventState::Today => $now->startOfHour()->addHours(2),
|
||||||
|
EventState::Overdue => $now->startOfDay()->addDays(2),
|
||||||
|
default => $now,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextCascadePoint(Event $event, User $user, Due $due, CarbonImmutable $now): CarbonImmutable
|
||||||
|
{
|
||||||
|
$today = $now->startOfDay();
|
||||||
|
foreach (Cascade::points($this->due->fireStart($event, $user), $due->day) as $point) {
|
||||||
|
if ($point->gt($today)) {
|
||||||
|
return $point;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->due->todayStart($due, $user);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
core/app/Scheduling/StateResolver.php
Normal file
39
core/app/Scheduling/StateResolver.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Scheduling;
|
||||||
|
|
||||||
|
use App\Enums\AfterDue;
|
||||||
|
use App\Enums\EventState;
|
||||||
|
use App\Models\Event;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Состояние, которое следует из дат события и «сейчас». Используется и кроном
|
||||||
|
* (переходы), и upsert-ом (пересчёт). Терминальные Done/Withdrawn ставятся
|
||||||
|
* только действиями, сюда не входят.
|
||||||
|
*/
|
||||||
|
final class StateResolver
|
||||||
|
{
|
||||||
|
public function __construct(private readonly DueResolver $due) {}
|
||||||
|
|
||||||
|
public function resolve(Event $event, User $user, CarbonImmutable $now): EventState
|
||||||
|
{
|
||||||
|
$due = $this->due->resolve($event, $user);
|
||||||
|
|
||||||
|
if ($now->gte($due->moment)) {
|
||||||
|
return $event->after_due === AfterDue::Keep ? EventState::Overdue : EventState::Expired;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($now->gte($this->due->todayStart($due, $user))) {
|
||||||
|
return EventState::Today;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fireStart = $this->due->fireStart($event, $user);
|
||||||
|
if ($fireStart->lt($due->day) && $now->gte($fireStart)) {
|
||||||
|
return EventState::Preparing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return EventState::Scheduled;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
core/app/Scheduling/Window.php
Normal file
18
core/app/Scheduling/Window.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Scheduling;
|
||||||
|
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
/** Окно доставки: внутри него ядро ищет момент, когда пользователь доступен. */
|
||||||
|
final class Window
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly CarbonImmutable $start,
|
||||||
|
public readonly CarbonImmutable $end,
|
||||||
|
/** Разрешена ли доставка «во все каналы наугад», если никто не present. */
|
||||||
|
public readonly bool $fallbackAllowed,
|
||||||
|
/** С какого момента fallback срабатывает (конец окна минус запас). */
|
||||||
|
public readonly CarbonImmutable $fallbackAt,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
55
core/app/Scheduling/WindowResolver.php
Normal file
55
core/app/Scheduling/WindowResolver.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Scheduling;
|
||||||
|
|
||||||
|
use App\Enums\EventState;
|
||||||
|
use App\Models\User;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
|
||||||
|
final class WindowResolver
|
||||||
|
{
|
||||||
|
/** @param list<int> $fallbackHours */
|
||||||
|
public function __construct(
|
||||||
|
private readonly int $dayMarginMinutes,
|
||||||
|
private readonly int $hourMarginMinutes,
|
||||||
|
private readonly array $fallbackHours,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Окно, открытое в момент $now, или null (тихие часы / срок прошёл / состояние не напоминает). */
|
||||||
|
public function current(EventState $state, User $user, Due $due, CarbonImmutable $now): ?Window
|
||||||
|
{
|
||||||
|
$activeStart = $user->activeStart($now);
|
||||||
|
$activeEnd = $user->activeEnd($now);
|
||||||
|
if ($now->lt($activeStart) || $now->gte($activeEnd)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($state) {
|
||||||
|
EventState::Preparing, EventState::Overdue => new Window(
|
||||||
|
$activeStart, $activeEnd, true, $activeEnd->subMinutes($this->dayMarginMinutes),
|
||||||
|
),
|
||||||
|
EventState::Today => $this->hourly($due, $now, $activeEnd),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function hourly(Due $due, CarbonImmutable $now, CarbonImmutable $activeEnd): ?Window
|
||||||
|
{
|
||||||
|
$start = $now->startOfHour();
|
||||||
|
$end = $start->addHour()->min($activeEnd);
|
||||||
|
|
||||||
|
$truncated = false;
|
||||||
|
if ($due->moment->gt($start) && $due->moment->lt($end)) {
|
||||||
|
$end = $due->moment;
|
||||||
|
$truncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($now->gte($end)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallbackAllowed = $truncated || in_array((int) $start->format('G'), $this->fallbackHours, true);
|
||||||
|
|
||||||
|
return new Window($start, $end, $fallbackAllowed, $end->subMinutes($this->hourMarginMinutes));
|
||||||
|
}
|
||||||
|
}
|
||||||
83
core/tests/Unit/Scheduling/QuietRuleTest.php
Normal file
83
core/tests/Unit/Scheduling/QuietRuleTest.php
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<?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 App\Scheduling\QuietRule;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Tests\TestCase; // Laravel TestCase: Eloquent date-casts need the connection grammar even in memory
|
||||||
|
|
||||||
|
class QuietRuleTest extends TestCase
|
||||||
|
{
|
||||||
|
private User $user;
|
||||||
|
private DueResolver $due;
|
||||||
|
private QuietRule $rule;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->user = new User(['login' => 'n', 'tz' => 'UTC', 'quiet_start' => '22:00:00', 'quiet_end' => '09:00:00']);
|
||||||
|
$this->due = new DueResolver;
|
||||||
|
$this->rule = new QuietRule($this->due);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function event(string $due, string $fireOn, string $time = '24:00:00'): Event
|
||||||
|
{
|
||||||
|
return new Event([
|
||||||
|
'due_mode' => DueMode::Local, 'due_date' => $due, 'due_time' => $time, 'fire_on' => $fireOn,
|
||||||
|
'after_due' => AfterDue::Keep, 'state' => EventState::Preparing, 'payload' => ['title' => 't'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function at(string $s): CarbonImmutable
|
||||||
|
{
|
||||||
|
return CarbonImmutable::parse($s, 'UTC');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_preparing_ack_is_quiet_until_next_cascade_point(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-30', '2026-08-31'); // точки: 30,15,7,3,1 → 31.08, 15.09, 23.09, 27.09, 29.09
|
||||||
|
$due = $this->due->resolve($e, $this->user);
|
||||||
|
|
||||||
|
$q = $this->rule->afterAck($e, $this->user, EventState::Preparing, $due, $this->at('2026-09-02 10:00'));
|
||||||
|
$this->assertSame('2026-09-15 00:00', $q->format('Y-m-d H:i'));
|
||||||
|
|
||||||
|
$q = $this->rule->afterAck($e, $this->user, EventState::Preparing, $due, $this->at('2026-09-15 10:00'));
|
||||||
|
$this->assertSame('2026-09-23 00:00', $q->format('Y-m-d H:i'), 'на самой точке — до следующей');
|
||||||
|
|
||||||
|
$q = $this->rule->afterAck($e, $this->user, EventState::Preparing, $due, $this->at('2026-09-29 10:00'));
|
||||||
|
$this->assertSame('2026-09-30 00:00', $q->format('Y-m-d H:i'), 'точек больше нет — до старта дня срока');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_today_ack_skips_the_next_hour(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-30', '2026-09-30');
|
||||||
|
$due = $this->due->resolve($e, $this->user);
|
||||||
|
|
||||||
|
$q = $this->rule->afterAck($e, $this->user, EventState::Today, $due, $this->at('2026-09-30 11:17'));
|
||||||
|
$this->assertSame('2026-09-30 13:00', $q->format('Y-m-d H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_overdue_ack_skips_tomorrow(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-30', '2026-09-30');
|
||||||
|
$due = $this->due->resolve($e, $this->user);
|
||||||
|
|
||||||
|
$q = $this->rule->afterAck($e, $this->user, EventState::Overdue, $due, $this->at('2026-10-05 15:00'));
|
||||||
|
$this->assertSame('2026-10-07 00:00', $q->format('Y-m-d H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_scheduled_ack_changes_nothing(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-30', '2026-09-30');
|
||||||
|
$due = $this->due->resolve($e, $this->user);
|
||||||
|
$now = $this->at('2026-09-01 15:00');
|
||||||
|
|
||||||
|
$this->assertTrue($this->rule->afterAck($e, $this->user, EventState::Scheduled, $due, $now)->eq($now));
|
||||||
|
}
|
||||||
|
}
|
||||||
89
core/tests/Unit/Scheduling/StateResolverTest.php
Normal file
89
core/tests/Unit/Scheduling/StateResolverTest.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?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 App\Scheduling\StateResolver;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Tests\TestCase; // Laravel TestCase: Eloquent date-casts need the connection grammar even in memory
|
||||||
|
|
||||||
|
class StateResolverTest extends TestCase
|
||||||
|
{
|
||||||
|
private User $user;
|
||||||
|
private StateResolver $resolver;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->user = new User(['login' => 'n', 'tz' => 'America/Montevideo', 'quiet_start' => '22:00:00', 'quiet_end' => '09:00:00']);
|
||||||
|
$this->resolver = new StateResolver(new DueResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function event(string $due, string $fireOn, AfterDue $afterDue = AfterDue::Keep, string $time = '24:00:00'): Event
|
||||||
|
{
|
||||||
|
return new Event([
|
||||||
|
'due_mode' => DueMode::Local, 'due_date' => $due, 'due_time' => $time, 'fire_on' => $fireOn,
|
||||||
|
'after_due' => $afterDue, 'state' => EventState::Scheduled, 'payload' => ['title' => 't'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function at(string $local): CarbonImmutable
|
||||||
|
{
|
||||||
|
return CarbonImmutable::parse($local, 'America/Montevideo');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_deadline_walks_through_states(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-30', '2026-08-31', AfterDue::Keep);
|
||||||
|
|
||||||
|
$this->assertSame(EventState::Scheduled, $this->resolver->resolve($e, $this->user, $this->at('2026-08-30 23:59')));
|
||||||
|
$this->assertSame(EventState::Preparing, $this->resolver->resolve($e, $this->user, $this->at('2026-08-31 00:00')));
|
||||||
|
$this->assertSame(EventState::Preparing, $this->resolver->resolve($e, $this->user, $this->at('2026-09-29 23:59')));
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-30 00:00')));
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-30 23:59')));
|
||||||
|
$this->assertSame(EventState::Overdue, $this->resolver->resolve($e, $this->user, $this->at('2026-10-01 00:00')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_day_of_never_prepares_and_expires(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-08-14', '2026-08-14', AfterDue::Expire);
|
||||||
|
|
||||||
|
$this->assertSame(EventState::Scheduled, $this->resolver->resolve($e, $this->user, $this->at('2026-08-13 23:59')));
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-08-14 00:00')));
|
||||||
|
$this->assertSame(EventState::Expired, $this->resolver->resolve($e, $this->user, $this->at('2026-08-15 00:00')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_timed_moment_ends_today_mid_day(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-10', '2026-09-03', AfterDue::Expire, '14:00:00');
|
||||||
|
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-10 13:59')));
|
||||||
|
$this->assertSame(EventState::Expired, $this->resolver->resolve($e, $this->user, $this->at('2026-09-10 14:00')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_early_moment_starts_today_the_evening_before(): void
|
||||||
|
{
|
||||||
|
$e = $this->event('2026-09-10', '2026-09-03', AfterDue::Expire, '07:00:00');
|
||||||
|
|
||||||
|
$this->assertSame(EventState::Preparing, $this->resolver->resolve($e, $this->user, $this->at('2026-09-09 19:59')));
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-09 20:00')));
|
||||||
|
$this->assertSame(EventState::Expired, $this->resolver->resolve($e, $this->user, $this->at('2026-09-10 07:00')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_fixed_flight_uses_user_timezone(): void
|
||||||
|
{
|
||||||
|
$e = new Event([
|
||||||
|
'due_mode' => DueMode::Fixed, 'due_instant' => CarbonImmutable::parse('2026-09-10T08:15:00+02:00'),
|
||||||
|
'fire_on' => '2026-09-03', 'after_due' => AfterDue::Expire, 'state' => EventState::Scheduled, 'payload' => ['title' => 't'],
|
||||||
|
]);
|
||||||
|
// 03:15 по Монтевидео → раньше активных часов → today с 20:00 накануне
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-09 20:00')));
|
||||||
|
$this->assertSame(EventState::Today, $this->resolver->resolve($e, $this->user, $this->at('2026-09-10 03:14')));
|
||||||
|
$this->assertSame(EventState::Expired, $this->resolver->resolve($e, $this->user, $this->at('2026-09-10 03:15')));
|
||||||
|
}
|
||||||
|
}
|
||||||
95
core/tests/Unit/Scheduling/WindowResolverTest.php
Normal file
95
core/tests/Unit/Scheduling/WindowResolverTest.php
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit\Scheduling;
|
||||||
|
|
||||||
|
use App\Enums\EventState;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Scheduling\Due;
|
||||||
|
use App\Scheduling\WindowResolver;
|
||||||
|
use Carbon\CarbonImmutable;
|
||||||
|
use Tests\TestCase; // Laravel TestCase: Eloquent date-casts need the connection grammar even in memory
|
||||||
|
|
||||||
|
class WindowResolverTest extends TestCase
|
||||||
|
{
|
||||||
|
private User $user;
|
||||||
|
private WindowResolver $windows;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->user = new User(['login' => 'n', 'tz' => 'UTC', 'quiet_start' => '22:00:00', 'quiet_end' => '09:00:00']);
|
||||||
|
$this->windows = new WindowResolver(30, 10, [9, 14, 20]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function at(string $s): CarbonImmutable
|
||||||
|
{
|
||||||
|
return CarbonImmutable::parse($s, 'UTC');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function wholeDay(string $ymd): Due
|
||||||
|
{
|
||||||
|
$day = $this->at($ymd);
|
||||||
|
return new Due($day, $day->addDay());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_outside_active_hours_there_is_no_window(): void
|
||||||
|
{
|
||||||
|
$due = $this->wholeDay('2026-09-30');
|
||||||
|
$this->assertNull($this->windows->current(EventState::Preparing, $this->user, $due, $this->at('2026-09-20 08:59')));
|
||||||
|
$this->assertNull($this->windows->current(EventState::Preparing, $this->user, $due, $this->at('2026-09-20 22:00')));
|
||||||
|
$this->assertNull($this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 23:00')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_preparing_and_overdue_have_one_daily_window(): void
|
||||||
|
{
|
||||||
|
$due = $this->wholeDay('2026-09-30');
|
||||||
|
foreach ([EventState::Preparing, EventState::Overdue] as $state) {
|
||||||
|
$w = $this->windows->current($state, $this->user, $due, $this->at('2026-09-20 12:34'));
|
||||||
|
$this->assertSame('2026-09-20 09:00', $w->start->format('Y-m-d H:i'));
|
||||||
|
$this->assertSame('2026-09-20 22:00', $w->end->format('Y-m-d H:i'));
|
||||||
|
$this->assertTrue($w->fallbackAllowed);
|
||||||
|
$this->assertSame('2026-09-20 21:30', $w->fallbackAt->format('Y-m-d H:i'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_today_has_hourly_windows_with_fallback_only_in_listed_hours(): void
|
||||||
|
{
|
||||||
|
$due = $this->wholeDay('2026-09-30');
|
||||||
|
|
||||||
|
$w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 09:20'));
|
||||||
|
$this->assertSame('2026-09-30 09:00', $w->start->format('Y-m-d H:i'));
|
||||||
|
$this->assertSame('2026-09-30 10:00', $w->end->format('Y-m-d H:i'));
|
||||||
|
$this->assertTrue($w->fallbackAllowed);
|
||||||
|
$this->assertSame('2026-09-30 09:50', $w->fallbackAt->format('Y-m-d H:i'));
|
||||||
|
|
||||||
|
$w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:05'));
|
||||||
|
$this->assertSame('2026-09-30 11:00', $w->start->format('Y-m-d H:i'));
|
||||||
|
$this->assertFalse($w->fallbackAllowed);
|
||||||
|
|
||||||
|
$w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 21:59'));
|
||||||
|
$this->assertSame('2026-09-30 22:00', $w->end->format('Y-m-d H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_today_window_is_truncated_by_due_moment_and_becomes_fallback(): void
|
||||||
|
{
|
||||||
|
$day = $this->at('2026-09-30');
|
||||||
|
|
||||||
|
$due = new Due($day, $day->setTime(9, 30));
|
||||||
|
$w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 09:05'));
|
||||||
|
$this->assertSame('2026-09-30 09:30', $w->end->format('Y-m-d H:i'));
|
||||||
|
$this->assertSame('2026-09-30 09:20', $w->fallbackAt->format('Y-m-d H:i'));
|
||||||
|
|
||||||
|
$due = new Due($day, $day->setTime(11, 15));
|
||||||
|
$w = $this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:02'));
|
||||||
|
$this->assertSame('2026-09-30 11:15', $w->end->format('Y-m-d H:i'));
|
||||||
|
$this->assertTrue($w->fallbackAllowed, 'обрезанное окно всегда fallback-окно');
|
||||||
|
$this->assertSame('2026-09-30 11:05', $w->fallbackAt->format('Y-m-d H:i'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_window_once_due_moment_passed(): void
|
||||||
|
{
|
||||||
|
$day = $this->at('2026-09-30');
|
||||||
|
$due = new Due($day, $day->setTime(11, 15));
|
||||||
|
$this->assertNull($this->windows->current(EventState::Today, $this->user, $due, $this->at('2026-09-30 11:20')));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user