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:
137
core/tests/Feature/EventUpserterTest.php
Normal file
137
core/tests/Feature/EventUpserterTest.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Enums\ChannelType;
|
||||
use App\Enums\EventState;
|
||||
use App\Ingest\EventUpserter;
|
||||
use App\Models\Event;
|
||||
use App\Models\Source;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EventUpserterTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Source $source;
|
||||
private EventUpserter $upserter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
config(['hado.default_tz' => '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_and_reports_missing(): void
|
||||
{
|
||||
$this->upserter->upsert($this->source, [$this->item()]);
|
||||
|
||||
$this->assertTrue($this->upserter->withdraw($this->source, 'document:918', 'expiry'));
|
||||
$this->assertSame(EventState::Withdrawn, Event::sole()->state);
|
||||
$this->assertFalse($this->upserter->withdraw($this->source, 'document:918', 'other'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user