'America/Montevideo']); CarbonImmutable::setTestNow('2026-09-01T12:00:00-03:00'); $this->source = Source::create(['name' => 'docs', 'token_hash' => hash('sha256', 'x')]); $this->upserter = app(EventUpserter::class); } protected function tearDown(): void { CarbonImmutable::setTestNow(); parent::tearDown(); } private function item(array $over = []): array { return array_replace([ 'user' => 'nikita', 'source_ref' => 'document:918', 'topic' => 'expiry', 'due_at' => '2026-09-30', 'fire_at' => '2026-08-31', 'after_due' => 'keep', 'payload' => ['title' => 'Седула протухает 30 сен'], ], $over); } public function test_creates_event_user_and_web_channel_with_computed_state(): void { $result = $this->upserter->upsert($this->source, [$this->item()]); $this->assertSame([['source_ref' => 'document:918', 'topic' => 'expiry', 'id' => 1, 'state' => 'preparing']], $result); $user = User::where('login', 'nikita')->firstOrFail(); $this->assertSame('America/Montevideo', $user->tz); $this->assertSame(ChannelType::Web, $user->channels()->sole()->type); $event = Event::sole(); $this->assertSame(EventState::Preparing, $event->state); $this->assertTrue($event->quiet_until->lt(CarbonImmutable::now())); } public function test_same_due_updates_fields_and_keeps_terminal_state(): void { $this->upserter->upsert($this->source, [$this->item()]); Event::sole()->update(['state' => EventState::Done, 'done_at' => now()]); $this->upserter->upsert($this->source, [$this->item(['payload' => ['title' => 'Новый заголовок'], 'fire_at' => '2026-09-15'])]); $event = Event::sole(); $this->assertSame(EventState::Done, $event->state); $this->assertSame('Новый заголовок', $event->payload['title']); $this->assertSame('2026-09-15', $event->fire_on->format('Y-m-d')); } public function test_same_due_recomputes_non_terminal_state(): void { $this->upserter->upsert($this->source, [$this->item()]); $this->assertSame(EventState::Preparing, Event::sole()->state); $this->upserter->upsert($this->source, [$this->item(['fire_at' => '2026-09-20'])]); $this->assertSame(EventState::Scheduled, Event::sole()->state, 'старт отодвинули в будущее'); } public function test_new_due_resets_even_from_terminal(): void { $this->upserter->upsert($this->source, [$this->item()]); Event::sole()->update(['state' => EventState::Done, 'done_at' => now(), 'quiet_until' => CarbonImmutable::now()->addDays(3)]); $this->upserter->upsert($this->source, [$this->item(['due_at' => '2026-10-15'])]); $event = Event::sole(); $this->assertSame(EventState::Preparing, $event->state); $this->assertNull($event->done_at); $this->assertTrue($event->quiet_until->lt(CarbonImmutable::now())); $this->assertSame('2026-10-15', $event->due_date->format('Y-m-d')); } public function test_reschedule_on_the_day_resets_today_event(): void { // Задокументированное поведение: перенос в день срока сбрасывает состояние. // Поэтому повторяющиеся события кладут период в topic, а не меняют due_at. CarbonImmutable::setTestNow('2026-09-30T10:00:00-03:00'); $this->upserter->upsert($this->source, [$this->item()]); $this->assertSame(EventState::Today, Event::sole()->state); $this->upserter->upsert($this->source, [$this->item(['due_at' => '2027-09-30', 'fire_at' => '2027-08-31'])]); $this->assertSame(EventState::Scheduled, Event::sole()->state); } public function test_occurrences_are_independent_rows(): void { CarbonImmutable::setTestNow('2026-10-05T10:00:00-03:00'); $this->upserter->upsert($this->source, [ $this->item(['source_ref' => 'rent', 'topic' => 'payment:2026-09', 'due_at' => '2026-09-10', 'fire_at' => '2026-09-10']), $this->item(['source_ref' => 'rent', 'topic' => 'payment:2026-10', 'due_at' => '2026-10-10', 'fire_at' => '2026-10-10']), ]); $states = Event::orderBy('topic')->pluck('state')->map->value->all(); $this->assertSame(['overdue', 'scheduled'], $states); } public function test_fixed_due_is_stored_as_instant(): void { $this->upserter->upsert($this->source, [$this->item(['due_at' => '2026-09-10T08:15+02:00', 'fire_at' => '2026-09-03'])]); $event = Event::sole(); $this->assertSame('fixed', $event->due_mode->value); $this->assertSame('2026-09-10T06:15:00+00:00', $event->due_instant->utc()->toIso8601String()); $this->assertNull($event->due_date); } public function test_withdraw_marks_withdrawn(): void { $this->upserter->upsert($this->source, [$this->item()]); $this->upserter->withdraw($this->upserter->find($this->source, 'document:918', 'expiry')); $this->assertSame(EventState::Withdrawn, Event::sole()->state); $this->assertNull($this->upserter->find($this->source, 'document:918', 'other')); } }