isDirty('status')) { return; } $oldStatus = $declaration->getOriginal('status'); $newStatus = $declaration->status; // Handle both string and enum values $oldValue = $oldStatus instanceof DeclarationStatus ? $oldStatus->value : (string) $oldStatus; $newValue = $newStatus instanceof DeclarationStatus ? $newStatus->value : (string) $newStatus; $allowed = DeclarationStatus::allowedTransitions()[$oldValue] ?? []; if (! in_array($newValue, $allowed)) { throw ValidationException::withMessages([ 'status' => "Invalid status transition from '{$oldValue}' to '{$newValue}'.", ]); } // Auto-archive when status becomes "ferme" if ($newValue === DeclarationStatus::Ferme) { $declaration->archived_at = now(); } } /** * Handle the Declaration "updated" event. * * Dispatches side effects on status transitions: * - en_attente_client: queues client email via DeclarationFileRequestMail * - ferme: invalidates dashboard cache for all workspace users */ public function updated(Declaration $declaration): void { if (! $declaration->wasChanged('status')) { return; } $newStatus = $declaration->status instanceof DeclarationStatus ? $declaration->status->value : (string) $declaration->status; if ($newStatus === DeclarationStatus::EnAttenteClient && ! static::$suppressEmail) { $this->sendClientFileRequestEmail($declaration); } // Reset suppression flag after each update static::$suppressEmail = false; if ($newStatus === DeclarationStatus::Ferme) { $this->invalidateDashboardCache($declaration); } } private function sendClientFileRequestEmail(Declaration $declaration): void { $declaration->loadMissing('client'); $client = $declaration->client; if (! $client || ! $client->contact_email) { return; } $mailJob = DB::transaction(function () use ($declaration, $client) { $invitation = $declaration->invitations() ->whereNull('used_at') ->latest() ->first(); if ($invitation && $invitation->isValid()) { // Reuse valid unused invitation, but sync email with current client if ($invitation->email !== $client->contact_email) { $invitation->update(['email' => $client->contact_email]); $invitation->refresh(); } } elseif ($invitation && ! $invitation->isValid()) { // Expired but unused — renew it $invitation->update([ 'email' => $client->contact_email, 'expires_at' => now()->addDays(30), ]); $invitation->refresh(); } else { // No unused invitation exists (all used or none) — create fresh $invitation = $declaration->invitations()->create([ 'email' => $client->contact_email, 'expires_at' => now()->addDays(30), ]); } $body = 'Nous vous invitons à déposer les documents complémentaires pour votre déclaration "' . $declaration->title . '".'; return [ 'email' => $client->contact_email, 'mailable' => new DeclarationFileRequestMail($declaration, $invitation, $body), ]; }); Mail::to($mailJob['email'])->queue($mailJob['mailable']); } private function invalidateDashboardCache(Declaration $declaration): void { $declaration->loadMissing('workspace.users'); $workspace = $declaration->workspace; if (! $workspace) { return; } $workspace->users->each(function ($user) use ($workspace) { Cache::forget("dashboard:{$workspace->id}:{$user->id}"); }); } }