RFC-TIMETABLE v0.2 Session 2 — Backend API + business logic #16
@@ -74,9 +74,21 @@ final class LaneCascadeService
|
||||
if ($targetLane !== null) {
|
||||
$locked->lane = $targetLane;
|
||||
}
|
||||
$locked->disableLogging(); // suppress auto-log; park is captured in explicit entry below
|
||||
$locked->save();
|
||||
|
||||
return new MoveResult($locked->refresh(), []);
|
||||
$parked = $locked->refresh();
|
||||
|
||||
activity('performance')
|
||||
->performedOn($parked)
|
||||
->event('parked')
|
||||
->withProperties([
|
||||
'event_id' => $parked->event_id,
|
||||
'organisation_id' => $parked->engagement?->organisation_id,
|
||||
])
|
||||
->log('parked');
|
||||
|
||||
return new MoveResult($parked, []);
|
||||
}
|
||||
|
||||
if ($start === null || $end === null || $targetLane === null) {
|
||||
@@ -104,6 +116,7 @@ final class LaneCascadeService
|
||||
foreach ($existingOnLane as $other) {
|
||||
if ($this->overlaps($start, $end, $other->start_at, $other->end_at)) {
|
||||
$other->lane = (int) $other->lane + 1;
|
||||
$other->disableLogging(); // suppress auto-log; cascade is captured in parent entry
|
||||
$other->save();
|
||||
$cascaded[] = $other->refresh();
|
||||
}
|
||||
@@ -114,14 +127,15 @@ final class LaneCascadeService
|
||||
$locked->start_at = $start;
|
||||
$locked->end_at = $end;
|
||||
$locked->lane = $targetLane;
|
||||
$locked->disableLogging(); // suppress auto-log; move is captured in explicit entry below
|
||||
$locked->save();
|
||||
|
||||
$moved = $locked->refresh();
|
||||
|
||||
// RFC §8 — single parent activity entry summarising the
|
||||
// cascade. The per-row updates (lane bumps) still flow
|
||||
// through the model's auto-log; this entry is the audit
|
||||
// anchor for the whole transactional move.
|
||||
// RFC §8 — single parent activity entry summarising the cascade.
|
||||
// All saves inside this transaction call disableLogging() so the
|
||||
// auto-log trait does not write per-row 'updated' entries; this
|
||||
// explicit entry is the only audit record for the move.
|
||||
activity('performance')
|
||||
->performedOn($moved)
|
||||
->event('moved')
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Services\Artist\LaneCascadeService;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Tests\TestCase;
|
||||
|
||||
final class LaneCascadeServiceTest extends TestCase
|
||||
@@ -184,4 +185,85 @@ final class LaneCascadeServiceTest extends TestCase
|
||||
|
||||
$this->assertSame((string) $this->stage->id, (string) $result->moved->stage_id);
|
||||
}
|
||||
|
||||
public function test_move_with_cascade_writes_exactly_one_activity_entry_on_moved_subject_and_zero_on_peers(): void
|
||||
{
|
||||
$start = CarbonImmutable::now()->addDays(5)->setTime(20, 0);
|
||||
|
||||
$p2 = Performance::factory()->create([
|
||||
'engagement_id' => $this->engagement->id,
|
||||
'event_id' => $this->event->id,
|
||||
'stage_id' => $this->stage->id,
|
||||
'lane' => 0,
|
||||
'start_at' => $start,
|
||||
'end_at' => $start->addHour(),
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
$p1 = Performance::factory()->create([
|
||||
'engagement_id' => $this->engagement->id,
|
||||
'event_id' => $this->event->id,
|
||||
'stage_id' => $this->stage->id,
|
||||
'lane' => 1,
|
||||
'start_at' => $start,
|
||||
'end_at' => $start->addHour(),
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
// Clear setup-time activity rows so we measure only the move.
|
||||
Activity::query()->delete();
|
||||
|
||||
$this->service->move(
|
||||
performance: $p1,
|
||||
targetStage: $this->stage,
|
||||
start: $start,
|
||||
end: $start->addHour(),
|
||||
targetLane: 0,
|
||||
clientVersion: 0,
|
||||
);
|
||||
|
||||
$movedEntries = Activity::query()
|
||||
->where('subject_type', $p1->getMorphClass())
|
||||
->where('subject_id', $p1->id)
|
||||
->get();
|
||||
$this->assertCount(1, $movedEntries, 'Expected exactly one activity entry for moved performance');
|
||||
$this->assertSame('moved', $movedEntries->first()->event);
|
||||
$this->assertSame(1, $movedEntries->first()->properties['cascade_count']);
|
||||
$this->assertContains((string) $p2->id, $movedEntries->first()->properties['cascaded_ids']);
|
||||
|
||||
$cascadedEntries = Activity::query()
|
||||
->where('subject_type', $p2->getMorphClass())
|
||||
->where('subject_id', $p2->id)
|
||||
->get();
|
||||
$this->assertCount(0, $cascadedEntries, 'Expected zero activity entries on cascade-bumped peer');
|
||||
}
|
||||
|
||||
public function test_park_writes_single_parked_activity_entry(): void
|
||||
{
|
||||
$perf = Performance::factory()->create([
|
||||
'engagement_id' => $this->engagement->id,
|
||||
'event_id' => $this->event->id,
|
||||
'stage_id' => $this->stage->id,
|
||||
'lane' => 0,
|
||||
'version' => 0,
|
||||
]);
|
||||
|
||||
Activity::query()->delete();
|
||||
|
||||
$this->service->move(
|
||||
performance: $perf,
|
||||
targetStage: null,
|
||||
start: null,
|
||||
end: null,
|
||||
targetLane: null,
|
||||
clientVersion: 0,
|
||||
);
|
||||
|
||||
$entries = Activity::query()
|
||||
->where('subject_type', $perf->getMorphClass())
|
||||
->where('subject_id', $perf->id)
|
||||
->get();
|
||||
$this->assertCount(1, $entries, 'Expected exactly one parked entry');
|
||||
$this->assertSame('parked', $entries->first()->event);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user