diff --git a/api/app/Models/Artist.php b/api/app/Models/Artist.php index f16e31a2..20eb7d1f 100644 --- a/api/app/Models/Artist.php +++ b/api/app/Models/Artist.php @@ -56,8 +56,17 @@ final class Artist extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logFillable() - ->dontLogEmptyChanges(); + ->logOnly(['name', 'slug', 'default_genre_id', 'default_draw', 'agent_company_id']) + ->logOnlyDirty() + ->dontLogIfAttributesChangedOnly(['updated_at']) + ->useLogName('artist'); + } + + public function tapActivity(Activity $activity, string $eventName): void + { + $properties = $activity->properties?->toArray() ?? []; + $properties['organisation_id'] = $this->organisation_id; + $activity->properties = collect($properties); } private function generateUniqueSlug(string $name): string diff --git a/api/app/Models/ArtistEngagement.php b/api/app/Models/ArtistEngagement.php index 7849de8e..bdde42e4 100644 --- a/api/app/Models/ArtistEngagement.php +++ b/api/app/Models/ArtistEngagement.php @@ -87,8 +87,26 @@ final class ArtistEngagement extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logFillable() - ->dontLogEmptyChanges(); + ->logOnly([ + 'booking_status', + 'fee_amount', 'fee_currency', 'fee_type', + 'buma_applicable', 'buma_percentage', 'buma_handled_by', + 'vat_applicable', 'vat_percentage', + 'project_leader_id', + 'option_expires_at', + 'payment_status', + ]) + ->logOnlyDirty() + ->dontLogIfAttributesChangedOnly(['updated_at']) + ->useLogName('artist_engagement'); + } + + public function tapActivity(Activity $activity, string $eventName): void + { + $properties = $activity->properties?->toArray() ?? []; + $properties['organisation_id'] = $this->organisation_id; + $properties['event_id'] = $this->event_id; + $activity->properties = collect($properties); } public function organisation(): BelongsTo diff --git a/api/app/Models/Genre.php b/api/app/Models/Genre.php index 076b27db..2e5757d7 100644 --- a/api/app/Models/Genre.php +++ b/api/app/Models/Genre.php @@ -43,8 +43,17 @@ final class Genre extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logFillable() - ->dontLogEmptyChanges(); + ->logOnly(['name', 'color', 'is_active', 'sort_order']) + ->logOnlyDirty() + ->dontLogIfAttributesChangedOnly(['updated_at']) + ->useLogName('genre'); + } + + public function tapActivity(Activity $activity, string $eventName): void + { + $properties = $activity->properties?->toArray() ?? []; + $properties['organisation_id'] = $this->organisation_id; + $activity->properties = collect($properties); } public function organisation(): BelongsTo diff --git a/api/app/Models/Performance.php b/api/app/Models/Performance.php index 5b16927e..36ac2153 100644 --- a/api/app/Models/Performance.php +++ b/api/app/Models/Performance.php @@ -55,8 +55,22 @@ final class Performance extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logFillable() - ->dontLogEmptyChanges(); + ->logOnly(['start_at', 'end_at', 'stage_id', 'lane', 'notes']) + ->logOnlyDirty() + // Performance.version is bumped by PerformanceObserver on every + // dirty save. Skip the auto-log when *only* updated_at + version + // moved — those rows correspond to bookkeeping touches, not + // user-meaningful changes (D14). + ->dontLogIfAttributesChangedOnly(['updated_at', 'version']) + ->useLogName('performance'); + } + + public function tapActivity(Activity $activity, string $eventName): void + { + $properties = $activity->properties?->toArray() ?? []; + $properties['event_id'] = $this->event_id; + $properties['organisation_id'] = $this->engagement?->organisation_id; + $activity->properties = collect($properties); } public function engagement(): BelongsTo diff --git a/api/app/Models/Stage.php b/api/app/Models/Stage.php index 539421e8..cbf8fdb5 100644 --- a/api/app/Models/Stage.php +++ b/api/app/Models/Stage.php @@ -50,8 +50,18 @@ final class Stage extends Model public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() - ->logFillable() - ->dontLogEmptyChanges(); + ->logOnly(['name', 'color', 'capacity', 'sort_order']) + ->logOnlyDirty() + ->dontLogIfAttributesChangedOnly(['updated_at']) + ->useLogName('stage'); + } + + public function tapActivity(Activity $activity, string $eventName): void + { + $properties = $activity->properties?->toArray() ?? []; + $properties['event_id'] = $this->event_id; + $properties['organisation_id'] = $this->event?->organisation_id; + $activity->properties = collect($properties); } public function event(): BelongsTo diff --git a/api/app/Services/Artist/ArtistEngagementService.php b/api/app/Services/Artist/ArtistEngagementService.php index f9215654..b1e15dca 100644 --- a/api/app/Services/Artist/ArtistEngagementService.php +++ b/api/app/Services/Artist/ArtistEngagementService.php @@ -103,11 +103,30 @@ final class ArtistEngagementService ): ArtistEngagement { $from = $this->coerceStatus($engagement->booking_status); + if ($from === $to) { + return $engagement; + } + $this->validateTransition($from, $to, $engagement); $engagement->booking_status = $to; $engagement->save(); + // RFC §8 — explicit `status_changed` audit event, separate from + // the auto-logged `updated` row that captures the diff. Pairs + // with the `cancelled` and `option_expired` events emitted by + // cancel() and the DemoteExpiredOptions command respectively. + activity('artist_engagement') + ->performedOn($engagement) + ->event($to === ArtistEngagementStatus::Cancelled ? 'cancelled' : 'status_changed') + ->withProperties([ + 'from' => $from->value, + 'to' => $to->value, + 'organisation_id' => $engagement->organisation_id, + 'event_id' => $engagement->event_id, + ]) + ->log($to === ArtistEngagementStatus::Cancelled ? 'cancelled' : 'status_changed'); + return $engagement; } diff --git a/api/app/Services/Artist/LaneCascadeService.php b/api/app/Services/Artist/LaneCascadeService.php index a55b14c4..d759325f 100644 --- a/api/app/Services/Artist/LaneCascadeService.php +++ b/api/app/Services/Artist/LaneCascadeService.php @@ -116,7 +116,24 @@ final class LaneCascadeService $locked->lane = $targetLane; $locked->save(); - return new MoveResult($locked->refresh(), $cascaded); + $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. + activity('performance') + ->performedOn($moved) + ->event('moved') + ->withProperties([ + 'cascade_count' => count($cascaded), + 'cascaded_ids' => array_map(fn ($p): string => (string) $p->id, $cascaded), + 'event_id' => $moved->event_id, + 'organisation_id' => $moved->engagement?->organisation_id, + ]) + ->log('moved'); + + return new MoveResult($moved, $cascaded); }); } diff --git a/api/app/Services/Artist/StageDayService.php b/api/app/Services/Artist/StageDayService.php index 92fdf4cf..7a5d2fb2 100644 --- a/api/app/Services/Artist/StageDayService.php +++ b/api/app/Services/Artist/StageDayService.php @@ -67,6 +67,32 @@ final class StageDayService ]); } + // RFC §8 — one activity entry per added/removed event_id, + // performed-on the parent stage so the audit log groups + // changes per stage rather than per pivot row. + $stage->loadMissing('event'); + $organisationId = $stage->event?->organisation_id; + foreach ($added as $eventId) { + activity('stage') + ->performedOn($stage) + ->event('day_added') + ->withProperties([ + 'event_id' => $eventId, + 'organisation_id' => $organisationId, + ]) + ->log('day_added'); + } + foreach ($removed as $eventId) { + activity('stage') + ->performedOn($stage) + ->event('day_removed') + ->withProperties([ + 'event_id' => $eventId, + 'organisation_id' => $organisationId, + ]) + ->log('day_removed'); + } + return ['added' => $added, 'removed' => $removed]; }); } diff --git a/api/app/Services/Artist/StageService.php b/api/app/Services/Artist/StageService.php index e96d8d15..f4649461 100644 --- a/api/app/Services/Artist/StageService.php +++ b/api/app/Services/Artist/StageService.php @@ -79,6 +79,16 @@ final class StageService ->where('id', $stageId) ->update(['sort_order' => $position]); } + + activity('stage') + ->on($event) + ->event('reordered') + ->withProperties([ + 'event_id' => $event->id, + 'organisation_id' => $event->organisation_id, + 'stage_ids' => $orderedStageIds, + ]) + ->log('reordered'); }); } }