Files
crewli/api/app/Http/Resources/Api/V1/Artist/ArtistResource.php
bert.hausmans 996dedc11d test(timetable): Phase C — 57 new tests covering session 2 surface
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>
2026-05-08 21:07:29 +02:00

70 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Http\Resources\Api\V1\Artist;
use App\Enums\Artist\ArtistEngagementStatus;
use App\Models\Artist;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
/**
* @mixin Artist
*/
final class ArtistResource extends JsonResource
{
/**
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
$lifetime = $this->engagements()
->whereNotIn('booking_status', [
ArtistEngagementStatus::Cancelled->value,
ArtistEngagementStatus::Rejected->value,
ArtistEngagementStatus::Declined->value,
])
->count();
$upcoming = $this->engagements()
->whereNotIn('booking_status', [
ArtistEngagementStatus::Cancelled->value,
ArtistEngagementStatus::Rejected->value,
ArtistEngagementStatus::Declined->value,
])
->whereHas('event', fn ($q) => $q->where('end_date', '>=', now()->toDateString()))
->count();
return [
'id' => $this->id,
'organisation_id' => $this->organisation_id,
'name' => $this->name,
'slug' => $this->slug,
'default_genre_id' => $this->default_genre_id,
'default_genre' => GenreResource::make($this->whenLoaded('defaultGenre')),
'default_draw' => $this->default_draw,
'star_rating' => $this->star_rating,
'home_base_country' => $this->home_base_country,
'agent_company_id' => $this->agent_company_id,
'agent_company' => $this->whenLoaded(
'agentCompany',
fn () => [
'id' => $this->agentCompany?->id,
'name' => $this->agentCompany?->name,
'handles_buma' => (bool) ($this->agentCompany?->handles_buma ?? false),
],
),
'notes' => $this->notes,
'contacts' => ArtistContactResource::collection($this->whenLoaded('contacts')),
'engagements_summary' => [
'lifetime_count' => $lifetime,
'upcoming_count' => $upcoming,
],
'created_at' => optional($this->created_at)->toIso8601String(),
'updated_at' => optional($this->updated_at)->toIso8601String(),
'deleted_at' => optional($this->deleted_at)->toIso8601String(),
];
}
}