Nine test files under tests/Feature/Artist/ exercising:
ArtistEngagementStateMachineTest 8 tests — terminal blocks, conditional
gates (Option/Contracted), full happy
path, cancel cascade
LaneCascadeServiceTest 5 tests — simple move, cascade-bump,
version mismatch, park, unpark
BumaVatCalculationTest 6 tests — D26 formula coverage:
Organisation/BookingAgency/NotApplicable,
VAT off, breakdown sum, zero fee
DemoteExpiredOptionsTest 4 tests — expired demote, future
untouched, non-Option untouched, run
twice → single option_expired entry
IdempotencyKey60sRedisTest 4 tests — missing header 400, first
cache, replay header, failed not cached
ArtistControllerTest 8 tests — index/create/destroy + cross-
tenant + duplicate detection + restore
StageControllerTest 7 tests — create + uniqueness, destroy
cascade-park, reorder permutation,
replaceDays orphan 409 + force_orphan
ArtistEngagementControllerTest 5 tests — index/create/update/destroy +
422 on invalid status transition
TimetableMoveControllerTest 3 tests — happy path with idempotency
header, missing header → 400, version
mismatch → 409
ArtistPolicyTest 6 tests — role checks, cross-tenant
denial, super_admin bypass, D27 active-
engagement gate
ActivityLogShapeTest 4 tests — performance.moved cascade
props, status_changed vs cancelled,
stage.day_added subject + props,
stage.reordered on Event subject
Bug fixes surfaced by Phase C:
Schema reality: events table uses `start_date`/`end_date` (date), not
`start_at`/`end_at`. Updated WithinEventBounds rule and the two stage_day
resolvers (LaneCascadeService + MoveTimetablePerformanceRequest) to
query the actual columns. ArtistResource.engagements_summary upcoming
filter likewise.
performances table has no organisation_id column (FK-chain via
engagement_id). Removed the org-id filter from the Rule::exists in
MoveTimetablePerformanceRequest; cross-tenant is caught by the policy
in TimetableMoveController.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
178 lines
5.7 KiB
PHP
178 lines
5.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Feature\Artist;
|
|
|
|
use App\Enums\Artist\ArtistEngagementStatus;
|
|
use App\Models\Artist;
|
|
use App\Models\ArtistEngagement;
|
|
use App\Models\Event;
|
|
use App\Models\Organisation;
|
|
use App\Models\Performance;
|
|
use App\Models\Stage;
|
|
use App\Models\StageDay;
|
|
use App\Services\Artist\ArtistEngagementService;
|
|
use App\Services\Artist\LaneCascadeService;
|
|
use App\Services\Artist\StageDayService;
|
|
use App\Services\Artist\StageService;
|
|
use Carbon\CarbonImmutable;
|
|
use Database\Seeders\RoleSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Spatie\Activitylog\Models\Activity;
|
|
use Tests\TestCase;
|
|
|
|
final class ActivityLogShapeTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private Organisation $org;
|
|
|
|
private Event $event;
|
|
|
|
private Stage $stage;
|
|
|
|
private ArtistEngagement $engagement;
|
|
|
|
private Performance $perf;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->seed(RoleSeeder::class);
|
|
|
|
$this->org = Organisation::factory()->create();
|
|
$this->event = Event::factory()->create([
|
|
'organisation_id' => $this->org->id,
|
|
'start_date' => CarbonImmutable::now()->subDay(),
|
|
'end_date' => CarbonImmutable::now()->addDays(30),
|
|
]);
|
|
$this->stage = Stage::factory()->create(['event_id' => $this->event->id]);
|
|
StageDay::query()->create(['stage_id' => $this->stage->id, 'event_id' => $this->event->id]);
|
|
|
|
$artist = Artist::factory()->create(['organisation_id' => $this->org->id]);
|
|
$this->engagement = ArtistEngagement::factory()->create([
|
|
'artist_id' => $artist->id,
|
|
'event_id' => $this->event->id,
|
|
'booking_status' => ArtistEngagementStatus::Draft,
|
|
'fee_amount' => 1500,
|
|
]);
|
|
|
|
$start = CarbonImmutable::now()->addDays(2)->setTime(20, 0);
|
|
$this->perf = 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,
|
|
]);
|
|
}
|
|
|
|
public function test_performance_moved_carries_cascade_props(): void
|
|
{
|
|
$other = Performance::factory()->create([
|
|
'engagement_id' => $this->engagement->id,
|
|
'event_id' => $this->event->id,
|
|
'stage_id' => $this->stage->id,
|
|
'lane' => 0,
|
|
'start_at' => CarbonImmutable::now()->addDays(2)->setTime(20, 30),
|
|
'end_at' => CarbonImmutable::now()->addDays(2)->setTime(21, 30),
|
|
'version' => 0,
|
|
]);
|
|
|
|
$start = CarbonImmutable::now()->addDays(2)->setTime(20, 0);
|
|
$this->app->make(LaneCascadeService::class)->move(
|
|
performance: $this->perf,
|
|
targetStage: $this->stage,
|
|
start: $start,
|
|
end: $start->addHour(),
|
|
targetLane: 0,
|
|
clientVersion: 0,
|
|
);
|
|
|
|
$entry = Activity::query()
|
|
->where('event', 'moved')
|
|
->where('subject_id', $this->perf->id)
|
|
->latest('id')
|
|
->first();
|
|
|
|
$this->assertNotNull($entry);
|
|
$props = $entry->properties->toArray();
|
|
$this->assertArrayHasKey('cascade_count', $props);
|
|
$this->assertArrayHasKey('cascaded_ids', $props);
|
|
$this->assertSame(1, $props['cascade_count']);
|
|
$this->assertContains((string) $other->id, $props['cascaded_ids']);
|
|
}
|
|
|
|
public function test_status_changed_distinct_from_cancelled(): void
|
|
{
|
|
$service = $this->app->make(ArtistEngagementService::class);
|
|
$service->transitionStatus($this->engagement, ArtistEngagementStatus::Requested);
|
|
|
|
$this->assertTrue(
|
|
Activity::query()
|
|
->where('event', 'status_changed')
|
|
->where('subject_id', $this->engagement->id)
|
|
->exists(),
|
|
);
|
|
$this->assertFalse(
|
|
Activity::query()
|
|
->where('event', 'cancelled')
|
|
->where('subject_id', $this->engagement->id)
|
|
->exists(),
|
|
);
|
|
|
|
$eng2 = ArtistEngagement::factory()->create([
|
|
'artist_id' => Artist::factory()->create(['organisation_id' => $this->org->id])->id,
|
|
'event_id' => $this->event->id,
|
|
'booking_status' => ArtistEngagementStatus::Confirmed,
|
|
'fee_amount' => 1000,
|
|
]);
|
|
$service->cancel($eng2);
|
|
|
|
$this->assertTrue(
|
|
Activity::query()
|
|
->where('event', 'cancelled')
|
|
->where('subject_id', $eng2->id)
|
|
->exists(),
|
|
);
|
|
}
|
|
|
|
public function test_stage_day_added_emitted(): void
|
|
{
|
|
$sub = Event::factory()->create([
|
|
'organisation_id' => $this->org->id,
|
|
'parent_event_id' => $this->event->id,
|
|
]);
|
|
|
|
$this->app->make(StageDayService::class)->replaceDays(
|
|
$this->stage,
|
|
[$this->event->id, $sub->id],
|
|
);
|
|
|
|
$this->assertTrue(
|
|
Activity::query()
|
|
->where('event', 'day_added')
|
|
->where('subject_id', $this->stage->id)
|
|
->whereJsonContains('properties->event_id', $sub->id)
|
|
->exists(),
|
|
);
|
|
}
|
|
|
|
public function test_stage_reordered_emitted_on_event_subject(): void
|
|
{
|
|
$other = Stage::factory()->create(['event_id' => $this->event->id]);
|
|
|
|
$this->app->make(StageService::class)->reorder($this->event, [$other->id, $this->stage->id]);
|
|
|
|
$this->assertTrue(
|
|
Activity::query()
|
|
->where('event', 'reordered')
|
|
->where('subject_id', $this->event->id)
|
|
->exists(),
|
|
);
|
|
}
|
|
}
|