indexer->index($artwork); $this->userStats->incrementUploads($artwork->user_id); $this->userStats->setLastUploadAt($artwork->user_id, $artwork->created_at); } /** Artwork updated — covers publish, approval, metadata changes. */ public function updated(Artwork $artwork): void { // When soft-deleted, remove from index immediately. if ($artwork->isDirty('deleted_at') && $artwork->deleted_at !== null) { $this->indexer->delete($artwork->id); return; } $this->indexer->update($artwork); // §7.5 On-demand: recompute similarity when tags/categories could have changed. // The pivot sync happens outside this observer, so we dispatch on every // meaningful update and let the job be idempotent (cheap if nothing changed). if ($artwork->is_public && $artwork->published_at) { RecComputeSimilarByTagsJob::dispatch($artwork->id)->delay(now()->addSeconds(30)); RecComputeSimilarHybridJob::dispatch($artwork->id)->delay(now()->addMinutes(1)); } } /** Soft delete — remove from search and decrement uploads_count. */ public function deleted(Artwork $artwork): void { $this->indexer->delete($artwork->id); $this->userStats->decrementUploads($artwork->user_id); } /** Force delete — ensure removal from index; only decrement if NOT already soft-deleted. */ public function forceDeleted(Artwork $artwork): void { $this->indexer->delete($artwork->id); // If deleted_at was null the artwork was not soft-deleted before; // the deleted() event did NOT fire, so we decrement here. if ($artwork->deleted_at === null) { $this->userStats->decrementUploads($artwork->user_id); } } /** Restored from soft-delete — re-index and re-increment uploads_count. */ public function restored(Artwork $artwork): void { $this->indexer->index($artwork); $this->userStats->incrementUploads($artwork->user_id); } }