diff --git a/.env.example b/.env.local-example
similarity index 82%
rename from .env.example
rename to .env.local-example
index d07fde3bfc10bfd333798f632ff40852eddab5cb..004ebc7adb39a9800ad2126fb1989674b8ce6005 100644
--- a/.env.example
+++ b/.env.local-example
@@ -77,4 +77,13 @@ BUNNYCDN_PULL_ZONE=https://cdn.rann-graphic-design.fr
 BUNNYCDN_API_KEY="api-key"
 BUNNYCDN_REGION=de
 
-CDN_FILESYSTEM_DISK=bunnycdn
\ No newline at end of file
+CDN_FILESYSTEM_DISK=bunnycdn
+
+AI_PROVIDER=openai
+OPENAI_URL=https://api.openai.com/v1/chat/completions
+OPENAI_API_KEY=
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
\ No newline at end of file
diff --git a/.env.production b/.env.production
index 5801e3a773fce2ce7d46008656c0fa9d1f54b82b..31ad2117f53d2b40e76814c23386cfdab406ec38 100644
--- a/.env.production
+++ b/.env.production
@@ -77,4 +77,15 @@ BUNNYCDN_PULL_ZONE=https://cdn.rann-graphic-design.fr
 BUNNYCDN_API_KEY="api-key"
 BUNNYCDN_REGION=de
 
-CDN_FILESYSTEM_DISK=bunnycdn
\ No newline at end of file
+CDN_FILESYSTEM_DISK=bunnycdn
+
+AI_PROVIDER=openai
+OPENAI_URL=https://api.openai.com/v1/chat/completions
+OPENAI_API_KEY=
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://ip-api.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
+
+APP_PRIVATE_MODE_SECRET=secret
\ No newline at end of file
diff --git a/.env.testing b/.env.testing
index e17ecb8ba9cac602565c98671f78502ceb042507..7e40aa8641d219fb6f21e09ad7e1760f43b7c0fe 100644
--- a/.env.testing
+++ b/.env.testing
@@ -73,4 +73,15 @@ SENTRY_PROFILES_SAMPLE_RATE=1.0
 BUNNYCDN_STORAGE_ZONE=testing_storage_zone
 BUNNYCDN_PULL_ZONE=https://testing.b-cdn.net
 BUNNYCDN_API_KEY="api-key"
-BUNNYCDN_REGION=de
\ No newline at end of file
+BUNNYCDN_REGION=de
+
+AI_PROVIDER=openai
+OPENAI_URL=https://api.test-provider.com/v1/chat/completions
+OPENAI_API_KEY=fake-api-key
+OPENAI_MODEL=gpt-4o-mini
+
+IP_ADDRESS_RESOLVER_URL=http://api.test-provider.com/batch
+IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE=15
+IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL=100
+
+APP_PRIVATE_MODE_SECRET=secret
\ No newline at end of file
diff --git a/.idea/php.xml b/.idea/php.xml
index 832f137ea710891bc0a20ad3db1d8a42ac54bcf5..29277c40d287df16d06fa5bcf1acf8ce5595d3c3 100644
--- a/.idea/php.xml
+++ b/.idea/php.xml
@@ -3,7 +3,7 @@
   <component name="LaravelPint">
     <laravel_pint_settings>
       <LaravelPintConfiguration tool_path="$PROJECT_DIR$/vendor/bin/pint" />
-      <laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="41eb493c-2ba1-4c74-b9ea-41d9bb4c70c9" tool_path="/var/www/vendor/bin/pint">
+      <laravel_pint_by_interpreter asDefaultInterpreter="true" interpreter_id="41eb493c-2ba1-4c74-b9ea-41d9bb4c70c9" tool_path="/app/vendor/bin/pint">
         <option name="timeout" value="30000" />
       </laravel_pint_by_interpreter>
     </laravel_pint_settings>
@@ -176,6 +176,9 @@
       <path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
       <path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" />
       <path value="$PROJECT_DIR$/vendor/laravel/octane" />
+      <path value="$PROJECT_DIR$/vendor/_laravel_idea" />
+      <path value="$PROJECT_DIR$/vendor/mkocansey/bladewind" />
+      <path value="$PROJECT_DIR$/vendor/laravel/horizon" />
     </include_path>
   </component>
   <component name="PhpInterpreters">
diff --git a/Dockerfile b/Dockerfile
index 72ac0d50ee6945429c2b2ddf0b302173b49b2a9a..a9e02cb9ba3c62f16b3878e380c80522fcfca82e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,5 +20,9 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh && \
     bash nodesource_setup.sh && \
     apt-get install -y nodejs
 
+# Install supervisor
+RUN apt-get install -y supervisor
+COPY docker-init/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
+
 # Setting PHP Configuration
 COPY docker-init/php.ini $PHP_INI_DIR/php.ini
\ No newline at end of file
diff --git a/README.md b/README.md
index 3baed05eaefdb5e364186c5bead5128a60991f39..f60d50dcaa41906164541649d954ffbeb58c9f55 100644
--- a/README.md
+++ b/README.md
@@ -62,25 +62,6 @@ #### Laravel Idea
 
 #### Configuration
 
-##### Configuration des projets multiples
-
-Comme le projet est dépendant de quelques paquets développés en interne, il est nécessaire de configurer PHPStorm pour
-vous faciliter leur développement en simultané.
-
-1. Ouvrez les préférences de PHPStorm (Ctrl + Alt + S)
-2. Allez dans `Version Control`
-3. Allez dans `Directory Mappings`
-4. Cliquez sur `+` puis `Directory`
-5. Sélectionnez le dossier du projet sous-jacent
-6. Cliquez sur `OK`
-
-![Projets Git Multiples](a1readme-assets/multi-vcs.jpg)
-
-Vous devriez voir apparaître les multiples projets Git dans l'onglet vcs de PHPStorm. À présent, vous devriez pouvoir
-créer des branches sur les différents projets simultanément.
-
-![Menu VCS](a1readme-assets/vcs-menu.jpg)
-
 ##### Interpréteur PHP
 
 Il est nécessaire de renseigner le container Docker comme interpréteur PHP. Pour cela, suivez les étapes suivantes :
diff --git a/app/Console/Commands/ProcessIpAdressesCommand.php b/app/Console/Commands/ProcessIpAdressesCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..c9f8b9e07e5ca2c41dfaa11a1386b3115a4f2d3e
--- /dev/null
+++ b/app/Console/Commands/ProcessIpAdressesCommand.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Jobs\ProcessIpAddressesJob;
+use Illuminate\Console\Command;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class ProcessIpAdressesCommand extends Command
+{
+    protected $signature = 'process:ip-adresses';
+
+    protected $description = 'Process ip adresses to resolve their location';
+
+    public function handle(): void
+    {
+        $ipAddresses = IpAddress::leftJoin('ip_address_metadata', 'ip_addresses.id', '=', 'ip_address_metadata.ip_address_id')
+            ->whereNull('ip_address_metadata.id')
+            ->select('ip_addresses.id', 'ip_addresses.ip')
+            ->get();
+
+        if ($ipAddresses->isEmpty()) {
+            $this->info('No ip adresses to process');
+
+            return;
+        }
+
+        $this->info("Processing {$ipAddresses->count()} ip adresses");
+        ProcessIpAddressesJob::dispatch($ipAddresses);
+    }
+}
diff --git a/app/Console/Commands/ProcessUserAgentsCommand.php b/app/Console/Commands/ProcessUserAgentsCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..17cb7bc086f826fd13098b8ecb1520e3b6dd2b52
--- /dev/null
+++ b/app/Console/Commands/ProcessUserAgentsCommand.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Jobs\ProcessUserAgentJob;
+use Illuminate\Console\Command;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+
+class ProcessUserAgentsCommand extends Command
+{
+    protected $signature = 'process:user-agents';
+
+    protected $description = 'Process user agents to detect if they are bots';
+
+    public function handle(): void
+    {
+        $userAgents = UserAgent::leftJoin('user_agent_metadata', 'user_agents.id', '=', 'user_agent_metadata.user_agent_id')
+            ->whereNull('user_agent_metadata.id')
+            ->select('user_agents.id', 'user_agents.user_agent')
+            ->get();
+
+        if ($userAgents->isEmpty()) {
+            $this->info('No user agents to process.');
+
+            return;
+        }
+
+        foreach ($userAgents as $userAgent) {
+            ProcessUserAgentJob::dispatch($userAgent);
+        }
+
+        $this->info('Jobs dispatched for processing '.$userAgents->count().' user agents.');
+    }
+}
diff --git a/app/Http/Controllers/Admin/CreationController.php b/app/Http/Controllers/Admin/CreationController.php
index 93f4590310abaaf1fb1c4b4a49123722506eaa34..792b41cf0547811b75577778e475c6e0b8d6b052 100644
--- a/app/Http/Controllers/Admin/CreationController.php
+++ b/app/Http/Controllers/Admin/CreationController.php
@@ -3,6 +3,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Http\Controllers\Controller;
+use App\Jobs\TranslateCreationJob;
 use App\Models\Category;
 use App\Models\Creation;
 use App\Models\Translation;
@@ -350,4 +351,15 @@ public function removeAdditionalImage(int $creationId, int $uploadedPictureId):
             'success' => true,
         ]);
     }
+
+    public function translateWithAi(int $creationId): JsonResponse
+    {
+        $creation = Creation::findOrFail($creationId);
+        $job = new TranslateCreationJob($creation);
+        dispatch($job);
+
+        return response()->json([
+            'success' => true,
+        ]);
+    }
 }
diff --git a/app/Http/Controllers/Admin/HomeController.php b/app/Http/Controllers/Admin/HomeController.php
index ff5529d25461dc1a869bdc2c24dddf6b1a39ed02..9fc2501b928a6c5f56a393c1d723cf25395dbfce 100644
--- a/app/Http/Controllers/Admin/HomeController.php
+++ b/app/Http/Controllers/Admin/HomeController.php
@@ -19,24 +19,30 @@ public function index(Request $request): View
             'end_date' => 'nullable|date',
         ]);
 
-        $publicRoutes = collect(Route::getRoutes()->getRoutes())
-            ->filter(fn ($route) => ! Str::startsWith($route->uri, ['admin', 'api']) && ! empty($route->uri) && $route->uri !== '/')
-            ->pluck('uri');
-
-        $visits = LoggedRequest::selectRaw('
-                logged_requests.url_id,
-                logged_requests.ip_address_id,
-                logged_requests.country_code,
-                logged_requests.created_at,
-                urls.url
-            ')
+        $routes = Route::getRoutes()->getRoutes();
+
+        $individualExcludedRoutes = [];
+        foreach ($routes as $route) {
+            if (Str::startsWith($route->uri, ['login', 'register', 'password', 'admin', 'forgot-password'])) {
+                $individualExcludedRoutes[] = config('app.url').'/'.$route->uri;
+            }
+        }
+
+        $visits = LoggedRequest::select([
+            'logged_requests.url_id',
+            'logged_requests.ip_address_id',
+            'ip_address_metadata.country_code',
+            'logged_requests.created_at',
+            'urls.url'])
             ->distinct('logged_requests.url_id', 'logged_requests.ip_address_id')
             ->join('urls', 'logged_requests.url_id', '=', 'urls.id')
-            ->where(function ($query) use ($publicRoutes) {
-                foreach ($publicRoutes as $uri) {
-                    $query->orWhere('urls.url', 'like', "%$uri%");
-                }
-            })
+            ->join('user_agent_metadata', 'logged_requests.user_agent_id', '=', 'user_agent_metadata.user_agent_id')
+            ->join('ip_address_metadata', 'logged_requests.ip_address_id', '=', 'ip_address_metadata.ip_address_id')
+            ->whereLike('urls.url', config('app.url').'%')
+            ->whereNotIn('urls.url', $individualExcludedRoutes)
+            ->where('user_agent_metadata.is_bot', false)
+            ->where('status_code', 200)
+            ->whereNull('logged_requests.user_id')
             ->get();
 
         $now = now();
@@ -60,29 +66,18 @@ public function index(Request $request): View
         $selectedPeriod = $startDate;
 
         // Now, all the stats are calculated for the selected period
-
-        /*$visitsPerDay = $visits->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d'))
-            ->map(fn ($group) => ['date' => $group->first()->created_at->format('Y-m-d'), 'count' => $group->count()])
-            ->values();*/
         $visitsPerDay = $visits->where('created_at', '>=', $startDate)
             ->where('created_at', '<=', $dateEnd)
             ->groupBy(fn ($visit) => Carbon::parse($visit->created_at)->format('Y-m-d'))
             ->map(fn ($group) => ['date' => $group->first()->created_at->format('Y-m-d'), 'count' => $group->count()])
             ->values();
 
-        /*$visitsByCountry = $visits->groupBy('country_code')
-            ->map(fn ($group, $country) => ['country_code' => $country, 'count' => $group->count()])
-            ->values();*/
         $visitsByCountry = $visits->where('created_at', '>=', $startDate)
             ->where('created_at', '<=', $dateEnd)
             ->groupBy('country_code')
             ->map(fn ($group, $country) => ['country_code' => $country, 'count' => $group->count()])
             ->values();
 
-        /*$mostVisitedPages = $visits->groupBy('url')
-            ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
-            ->sortByDesc('count')
-            ->values();*/
         $mostVisitedPages = $visits->where('created_at', '>=', $startDate)
             ->where('created_at', '<=', $dateEnd)
             ->groupBy('url')
@@ -90,6 +85,24 @@ public function index(Request $request): View
             ->sortByDesc('count')
             ->values();
 
+        $mostVisitedPagesForPastTwentyFourHours = $visits->where('created_at', '>=', $now->copy()->subDay())
+            ->groupBy('url')
+            ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
+            ->sortByDesc('count')
+            ->values();
+
+        $mostVisitedPagesForPastSevenDays = $visits->where('created_at', '>=', $now->copy()->subDays(7))
+            ->groupBy('url')
+            ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
+            ->sortByDesc('count')
+            ->values();
+
+        $mostVisitedPagesForPastThirtyDays = $visits->where('created_at', '>=', $now->copy()->subDays(30))
+            ->groupBy('url')
+            ->map(fn ($group, $url) => ['url' => $url, 'count' => $group->count()])
+            ->sortByDesc('count')
+            ->values();
+
         return view('admin.home', [
             'totalVisitsPastTwentyFourHours' => $totalVisitsPastTwentyFourHours,
             'totalVisitsPastSevenDays' => $totalVisitsPastSevenDays,
@@ -100,6 +113,9 @@ public function index(Request $request): View
             'mostVisitedPages' => $mostVisitedPages,
             'periods' => $periods,
             'selectedPeriod' => $selectedPeriod,
+            'mostVisitedPagesForPastTwentyFourHours' => $mostVisitedPagesForPastTwentyFourHours,
+            'mostVisitedPagesForPastSevenDays' => $mostVisitedPagesForPastSevenDays,
+            'mostVisitedPagesForPastThirtyDays' => $mostVisitedPagesForPastThirtyDays,
         ]);
     }
 }
diff --git a/app/Http/Controllers/Public/LegalMentionsController.php b/app/Http/Controllers/Public/LegalMentionsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..4098697920ba33c78d7742a818a9902af53125af
--- /dev/null
+++ b/app/Http/Controllers/Public/LegalMentionsController.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Controllers\Public;
+
+use App\Http\Controllers\Controller;
+use App\Models\LegalMention;
+
+class LegalMentionsController extends Controller
+{
+    public function __invoke()
+    {
+        $activeLegalMentionsText = LegalMention::where('active', true)->first();
+
+        if (! $activeLegalMentionsText) {
+            abort(404);
+        }
+        $activeLegalMentionsText = $activeLegalMentionsText->contentTransKey->getTranslation();
+
+        return view('public.legal-mentions', compact('activeLegalMentionsText'));
+    }
+}
diff --git a/app/Http/Controllers/Public/TermsController.php b/app/Http/Controllers/Public/TermsController.php
new file mode 100644
index 0000000000000000000000000000000000000000..a20736b21e40f1970a758be6b82ebd599c7956b0
--- /dev/null
+++ b/app/Http/Controllers/Public/TermsController.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace App\Http\Controllers\Public;
+
+use App\Http\Controllers\Controller;
+use App\Models\TermsSection;
+use Illuminate\View\View;
+
+class TermsController extends Controller
+{
+    public function __invoke(): View
+    {
+        $activeTermsText = TermsSection::where('active', true)->first();
+
+        if (! $activeTermsText) {
+            abort(404);
+        }
+        $activeTermsText = $activeTermsText->contentTranslationKey->getTranslation();
+
+        return view('public.terms', compact('activeTermsText'));
+    }
+}
diff --git a/app/Http/Middleware/CheckPrivateModeMiddleware.php b/app/Http/Middleware/CheckPrivateModeMiddleware.php
index 2e9ba639dc3fd4fac8951d165f007a3aa89a07e7..7cf814038d6d7cc8d3516ce8e2c0f69652ecd3b1 100644
--- a/app/Http/Middleware/CheckPrivateModeMiddleware.php
+++ b/app/Http/Middleware/CheckPrivateModeMiddleware.php
@@ -9,9 +9,20 @@ class CheckPrivateModeMiddleware
 {
     public function handle(Request $request, Closure $next)
     {
-        if (config('app.private_mode') && ! $request->is('maintenance') && ! auth()->check()) {
-            return redirect()->route('maintenance');
-        } elseif (! config('app.private_mode') && $request->is('maintenance')) {
+        $privateModeEnabled = config('app.private_mode');
+        $privateModeSecret = config('app.private_mode_secret');
+        $userSecretInput = $request->input('secret');
+        $secretIsUsable = ! empty($privateModeSecret) && $privateModeSecret === $userSecretInput;
+
+        if ($privateModeEnabled) {
+            if (! $secretIsUsable && ! auth()->check()) {
+                if (! $request->is('maintenance')) {
+                    return redirect()->route('maintenance');
+                }
+            }
+        }
+
+        if (! $privateModeEnabled && $request->is('maintenance') && ! auth()->check()) {
             return redirect()->route('index');
         }
 
diff --git a/app/Http/Requests/UserAgentMetadataRequest.php b/app/Http/Requests/UserAgentMetadataRequest.php
new file mode 100644
index 0000000000000000000000000000000000000000..347e7e3dfc83c2ca937f4809732690d805933605
--- /dev/null
+++ b/app/Http/Requests/UserAgentMetadataRequest.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace App\Http\Requests;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class UserAgentMetadataRequest extends FormRequest
+{
+    public function rules(): array
+    {
+        return [
+            'user_agent_id' => ['required', 'exists:user_agents'],
+            'is_bot' => ['boolean'],
+        ];
+    }
+
+    public function authorize(): bool
+    {
+        return true;
+    }
+}
diff --git a/app/Jobs/ProcessIpAddressesJob.php b/app/Jobs/ProcessIpAddressesJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..d02f8cd33ecd23af984eb247bfdb10128ff18392
--- /dev/null
+++ b/app/Jobs/ProcessIpAddressesJob.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\IpAddressMetadata;
+use App\Services\IpAddressMetadataResolverService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Log;
+
+class ProcessIpAddressesJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(private readonly Collection $ipAddresses) {}
+
+    public function handle(IpAddressMetadataResolverService $ipAddressMetadataResolver): void
+    {
+        try {
+            $metadataObjects = $ipAddressMetadataResolver->resolve($this->ipAddresses);
+        } catch (Exception $exception) {
+            report($exception);
+            $this->fail($exception);
+
+            return;
+        }
+
+        foreach ($metadataObjects as $metadata) {
+            if ($metadata['status'] === 'fail') {
+                Log::warning('Failed to resolve IP address metadata.', [
+                    'query' => $metadata['query'],
+                    'message' => $metadata['message'],
+                ]);
+
+                continue;
+            }
+
+            IpAddressMetadata::create([
+                'ip_address_id' => $this->ipAddresses->where('ip', $metadata['query'])->first()->id,
+                'country_code' => $metadata['countryCode'],
+                'lat' => $metadata['lat'],
+                'lon' => $metadata['lon'],
+            ]);
+        }
+    }
+}
diff --git a/app/Jobs/ProcessUserAgentJob.php b/app/Jobs/ProcessUserAgentJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..fce0bbc7a1e6e9a8a71e09a82e2f541b030f2ee3
--- /dev/null
+++ b/app/Jobs/ProcessUserAgentJob.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\UserAgentMetadata;
+use App\Services\AiProviderService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Log;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+
+class ProcessUserAgentJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(private readonly UserAgent $userAgent) {}
+
+    public function handle(AiProviderService $aiProviderService): void
+    {
+        try {
+            $result = $aiProviderService->prompt(
+                'You are a robot detector designed to output JSON. ',
+                "Is this user agent a robot or a tool that is not a web browser? Please respond in the format {'is_bot': true/false}. The user agent is: {$this->userAgent->user_agent}"
+            );
+
+            $isBot = $result['is_bot'] ?? false;
+
+            UserAgentMetadata::create([
+                'user_agent_id' => $this->userAgent->id,
+                'is_bot' => $isBot,
+            ]);
+        } catch (Exception $e) {
+            Log::error("Failed to process UserAgent: {$this->userAgent->user_agent}", [
+                'exception' => $e->getMessage(),
+            ]);
+
+            $this->fail($e);
+        }
+    }
+}
diff --git a/app/Jobs/TranslateCreationJob.php b/app/Jobs/TranslateCreationJob.php
new file mode 100644
index 0000000000000000000000000000000000000000..152419f5cf4d53057709885073e673910ec8b018
--- /dev/null
+++ b/app/Jobs/TranslateCreationJob.php
@@ -0,0 +1,81 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Models\Creation;
+use App\Models\Translation;
+use App\Services\AiProviderService;
+use Exception;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Log;
+
+class TranslateCreationJob implements ShouldQueue
+{
+    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
+
+    public function __construct(private readonly Creation $creation) {}
+
+    public function handle(AiProviderService $aiProviderService): void
+    {
+        $frenchName = $this->creation->nameTranslationKey->getTranslation('fr');
+        $frenchShortDesc = $this->creation->shortDescriptionTranslationKey->getTranslation('fr');
+        $frenchDesc = $this->creation->descriptionTranslationKey->getTranslation('fr');
+
+        $shortDescriptionTranslationKeyId = $this->creation->shortDescriptionTranslationKey->id;
+        $descriptionTranslationKeyId = $this->creation->descriptionTranslationKey->id;
+
+        if (! empty($frenchName)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $this->creation->name_translation_key_id, 'locale' => 'en'],
+                ['text' => $frenchName]
+            );
+            Cache::forget("translation_key_{$this->creation->name_translation_key_id}_en");
+        }
+
+        if (! empty($frenchShortDesc)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $shortDescriptionTranslationKeyId, 'locale' => 'en'],
+                ['text' => $this->translate($frenchShortDesc, $aiProviderService)]
+            );
+            Cache::forget("translation_key_{$shortDescriptionTranslationKeyId}_en");
+        }
+
+        if (! empty($frenchDesc)) {
+            Translation::updateOrCreate(
+                ['translation_key_id' => $descriptionTranslationKeyId, 'locale' => 'en'],
+                ['text' => $this->translate($frenchDesc, $aiProviderService)]
+            );
+            Cache::forget("translation_key_{$descriptionTranslationKeyId}_en");
+        }
+    }
+
+    /**
+     * Translate the given text from French to English
+     *
+     * @param  string  $text  The text to translate
+     * @param  AiProviderService  $aiProviderService  The AI provider service
+     * @return string The translated text
+     */
+    private function translate(string $text, AiProviderService $aiProviderService): string
+    {
+        try {
+            $result = $aiProviderService->prompt(
+                'You are a helpful assistant that translates french markdown text in english and that outputs JSON in the format {message:string}. Markdown is supported.',
+                $text
+            );
+
+            return $result['message'] ?? '';
+        } catch (Exception $e) {
+            Log::error("Failed to translate text: {$text}", [
+                'exception' => $e->getMessage(),
+            ]);
+        }
+
+        return '';
+    }
+}
diff --git a/app/Models/IpAddressMetadata.php b/app/Models/IpAddressMetadata.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c0f61999c3b791d570c8b0ec353d43614b7c725
--- /dev/null
+++ b/app/Models/IpAddressMetadata.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadata extends Model
+{
+    use HasFactory;
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'ip_address_id',
+        'country_code',
+        'lat',
+        'lon',
+    ];
+
+    const COUNTRY_CODES = ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ'];
+
+    public function ipAddress(): BelongsTo
+    {
+        return $this->belongsTo(IpAddress::class);
+    }
+}
diff --git a/app/Models/UserAgentMetadata.php b/app/Models/UserAgentMetadata.php
new file mode 100644
index 0000000000000000000000000000000000000000..384ddf174d61873b99d57e76343c8521b8adfcdc
--- /dev/null
+++ b/app/Models/UserAgentMetadata.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+
+class UserAgentMetadata extends Model
+{
+    use HasFactory;
+
+    public $timestamps = false;
+
+    protected $fillable = [
+        'user_agent_id',
+        'is_bot',
+    ];
+
+    public function userAgent(): BelongsTo
+    {
+        return $this->belongsTo(UserAgent::class);
+    }
+
+    protected function casts(): array
+    {
+        return [
+            'is_bot' => 'boolean',
+        ];
+    }
+}
diff --git a/app/Providers/HorizonServiceProvider.php b/app/Providers/HorizonServiceProvider.php
new file mode 100644
index 0000000000000000000000000000000000000000..9811982b62bd5bc009df34beaba5a3c8e24fd987
--- /dev/null
+++ b/app/Providers/HorizonServiceProvider.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Providers;
+
+use Illuminate\Support\Facades\Gate;
+use Laravel\Horizon\Horizon;
+use Laravel\Horizon\HorizonApplicationServiceProvider;
+
+class HorizonServiceProvider extends HorizonApplicationServiceProvider
+{
+    /**
+     * Bootstrap any application services.
+     */
+    public function boot(): void
+    {
+        parent::boot();
+
+        // Horizon::routeSmsNotificationsTo('15556667777');
+        // Horizon::routeMailNotificationsTo('example@example.com');
+        // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
+    }
+
+    /**
+     * Register the Horizon gate.
+     *
+     * This gate determines who can access Horizon in non-local environments.
+     */
+    protected function gate(): void
+    {
+        Gate::define('viewHorizon', function ($user) {
+            return in_array($user->email, [
+                //
+            ]);
+        });
+    }
+}
diff --git a/app/Services/AiProviderService.php b/app/Services/AiProviderService.php
new file mode 100644
index 0000000000000000000000000000000000000000..89800c6f85507dbdd688893115a0fc4e8b8d587a
--- /dev/null
+++ b/app/Services/AiProviderService.php
@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Services;
+
+use App\Models\UploadedPicture;
+use Illuminate\Http\Client\ConnectionException;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Storage;
+use RuntimeException;
+
+class AiProviderService
+{
+    /**
+     * Prompt the AI provider with a text and pictures
+     *
+     * @param  string  $systemRole  The system role to send to the AI provider. E.g. "You are a helpful assistant."
+     * @param  string  $prompt  The prompt to send to the AI provider
+     * @param  UploadedPicture  ...$pictures  The pictures to send to the AI provider
+     * @return array The response from the AI provider.
+     */
+    public function promptWithPictures(string $systemRole, string $prompt, UploadedPicture ...$pictures): array
+    {
+        $transcodingService = app(ImageTranscodingService::class);
+
+        $transcodedPictures = [];
+        foreach ($pictures as $picture) {
+            $picturePath = Storage::disk('public')->get($picture->path_original);
+            $transcodedPicture = $transcodingService->transcode($picturePath, UploadedPicture::MEDIUM_SIZE, 'jpeg');
+
+            if (! $transcodedPicture) {
+                Log::error('Failed to transcode picture', [
+                    'picture' => $picture,
+                ]);
+                throw new RuntimeException('Failed to transcode picture');
+            }
+
+            $transcodedPictures[] = $transcodedPicture;
+        }
+
+        $selectedProvider = config('ai-provider.selected-provider');
+
+        $picturesArray = array_map(fn (string $transcodedPicture) => [
+            'type' => 'image_url',
+            'image_url' => [
+                'url' => 'data:image/jpeg;base64,'.base64_encode($transcodedPicture),
+            ],
+        ], $transcodedPictures);
+
+        $requestBody = [
+            'headers' => [
+                'Authorization' => 'Bearer '.config('ai-provider.providers.'.$selectedProvider.'.api-key'),
+                'Content-Type' => 'application/json',
+                'Accept' => 'application/json',
+            ],
+            'json' => [
+                'model' => config('ai-provider.providers.'.$selectedProvider.'.model'),
+                'messages' => [
+                    [
+                        'role' => 'system',
+                        'content' => [
+                            [
+                                'type' => 'text',
+                                'text' => $systemRole,
+                            ],
+                        ],
+                    ],
+                    [
+                        'role' => 'user',
+                        'content' => [
+                            [
+                                'type' => 'text',
+                                'text' => $prompt,
+                            ],
+                            ...$picturesArray,
+                        ],
+                    ],
+                ],
+                'max_tokens' => config('ai-provider.providers.'.$selectedProvider.'.max-tokens'),
+                'response_format' => [
+                    'type' => 'json_object',
+                ],
+            ],
+        ];
+
+        return $this->callApi(config('ai-provider.providers.'.$selectedProvider.'.url'), $requestBody);
+    }
+
+    /**
+     * Prompt the AI provider with a text
+     *
+     * @param  string  $systemRole  The system role to send to the AI provider. E.g. "You are a helpful assistant."
+     * @param  string  $prompt  The prompt to send to the AI provider
+     * @return array The response from the AI provider.
+     */
+    public function prompt(string $systemRole, string $prompt): array
+    {
+        $selectedProvider = config('ai-provider.selected-provider');
+
+        $requestBody = ['model' => config('ai-provider.providers.'.$selectedProvider.'.model'),
+            'messages' => [
+                [
+                    'role' => 'system',
+                    'content' => [
+                        [
+                            'type' => 'text',
+                            'text' => $systemRole,
+                        ],
+                    ],
+                ],
+                [
+                    'role' => 'user',
+                    'content' => [
+                        [
+                            'type' => 'text',
+                            'text' => $prompt,
+                        ],
+                    ],
+                ],
+            ],
+            'max_tokens' => config('ai-provider.providers.'.$selectedProvider.'.max-tokens'),
+            'response_format' => [
+                'type' => 'json_object',
+            ],
+        ];
+
+        return $this->callApi(config('ai-provider.providers.'.$selectedProvider.'.url'), $requestBody);
+    }
+
+    /**
+     * Call the AI provider API
+     *
+     * @param  string  $url  The URL of the AI provider API
+     * @param  array  $requestBody  The request body to send to the AI provider API
+     * @return array The response from the AI provider
+     */
+    private function callApi(string $url, array $requestBody): array
+    {
+        $selectedProvider = config('ai-provider.selected-provider');
+
+        try {
+            $response = Http::withHeaders([
+                'Authorization' => 'Bearer '.config('ai-provider.providers.'.$selectedProvider.'.api-key'),
+                'Content-Type' => 'application/json',
+                'Accept' => 'application/json',
+            ])->post($url, $requestBody);
+        } catch (ConnectionException $e) {
+            Log::error('Failed to call AI provider API', [
+                'exception' => $e,
+            ]);
+            throw new RuntimeException('Failed to call AI provider API');
+        }
+        $result = $response->json();
+
+        if (! isset($result['choices'][0]['message']['content'])) {
+            Log::error('Failed to get response from AI provider', [
+                'response' => $result,
+            ]);
+            throw new RuntimeException('Failed to get response from AI provider');
+        }
+
+        return json_decode($result['choices'][0]['message']['content'], true);
+    }
+}
diff --git a/app/Services/ImageTranscodingService.php b/app/Services/ImageTranscodingService.php
index e99468e1a3f59e2e81780c9fd7cdc1eddb0b00ce..3e5461e05bd15ab763e149c6c2ac7f7ccfeb4ec0 100644
--- a/app/Services/ImageTranscodingService.php
+++ b/app/Services/ImageTranscodingService.php
@@ -5,6 +5,9 @@
 use Imagick;
 use Intervention\Image\Drivers\Imagick\Driver as ImagickDriver;
 use Intervention\Image\Encoders\AvifEncoder;
+use Intervention\Image\Encoders\JpegEncoder;
+use Intervention\Image\Encoders\PngEncoder;
+use Intervention\Image\Encoders\WebpEncoder;
 use Intervention\Image\Exceptions\RuntimeException;
 use Intervention\Image\ImageManager;
 use Log;
@@ -23,9 +26,10 @@ public function __construct(ImagickDriver $driver)
      *
      * @param  string  $source  The source image path or content. Eg: /path/to/image.jpg or file_get_contents('/path/to/image.jpg')
      * @param  int|null  $resolution  The new resolution to transcode the image to
+     * @param  string  $codec  The codec to use for transcoding. Eg: jpeg, webp, png, avif
      * @return string|null The transcoded image content
      */
-    public function transcode(string $source, ?int $resolution = null): ?string
+    public function transcode(string $source, ?int $resolution = null, string $codec = 'avif'): ?string
     {
         $image = $this->imageManager->read($source);
         try {
@@ -53,7 +57,12 @@ public function transcode(string $source, ?int $resolution = null): ?string
                 $image->scale($resolution);
             }
 
-            return $image->encode(new AvifEncoder(quality: 85))->toString();
+            return match ($codec) {
+                'jpeg' => $image->encode(new JpegEncoder(quality: 85))->toString(),
+                'webp' => $image->encode(new WebpEncoder(quality: 85))->toString(),
+                'png' => $image->encode(new PngEncoder)->toString(),
+                default => $image->encode(new AvifEncoder(quality: 85))->toString(),
+            };
         } catch (RuntimeException $exception) {
             Log::error('Failed to transcode image', [
                 'exception' => $exception,
diff --git a/app/Services/IpAddressMetadataResolverService.php b/app/Services/IpAddressMetadataResolverService.php
new file mode 100644
index 0000000000000000000000000000000000000000..13f4eaec3cc317a5d6536bbe8ace6df74f42457a
--- /dev/null
+++ b/app/Services/IpAddressMetadataResolverService.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace App\Services;
+
+use Exception;
+use Illuminate\Http\Client\ConnectionException;
+use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Log;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadataResolverService
+{
+    /**
+     * Resolve metadata for the given IP addresses.
+     * This method has a fail tolerance. The only exception that is thrown is when the API server returns a 422 status code.
+     * Server errors are logged and the method returns without throwing an exception.
+     *
+     * @param  Collection  $ipAddresses  The IP addresses to resolve metadata for. Instances of IpAddress.
+     * @return array{array{status: 'success'|'fail', message?: string, countryCode?: string, lat?: float, lon?: float, query: string}} The resolved metadata for each IP address.
+     *
+     * @throws ConnectionException
+     */
+    public static function resolve(Collection $ipAddresses): array
+    {
+        $url = config('ip-address-resolver.url').'?fields=status,message,countryCode,lat,lon,query';
+        $maxIpPerCall = config('ip-address-resolver.max_ip_addresses_per_call');
+        $maxCallsPerMinute = config('ip-address-resolver.call_limit_per_minute');
+        $currentCallsCount = Cache::get('ip-address-resolver.calls_count', 0);
+
+        if ($currentCallsCount >= $maxCallsPerMinute) {
+            Log::info('Max calls per minute reached. Skipping metadata resolution.');
+
+            return [];
+        }
+
+        Cache::increment('ip-address-resolver.calls_count', 1, 60);
+
+        if ($ipAddresses->count() > $maxIpPerCall) {
+            $ipAddresses = $ipAddresses->take($maxIpPerCall);
+        }
+
+        $response = Http::post($url, $ipAddresses->pluck('ip')->toArray());
+
+        if ($response->failed()) {
+            $returnedError = [
+                'status' => $response->status(),
+                'message' => $response->body(),
+            ];
+
+            if ($response->unprocessableContent()) {
+                $apiResponse = $response->json();
+                Log::error('The API rejected the request with a 422 unprocessable entity status code. ', $returnedError);
+                throw new Exception('The API rejected the request with a 422 unprocessable entity status code. '.$apiResponse['message']);
+            }
+            if ($response->serverError()) {
+                Log::info('The API server encountered an error while processing the request. Skipping metadata resolution.', $returnedError);
+
+                return [];
+            }
+            Log::error('The API server returned an unexpected status code. Skipping metadata resolution.', $returnedError);
+
+            return [];
+        }
+
+        return $response->json();
+    }
+}
diff --git a/bootstrap/providers.php b/bootstrap/providers.php
index 0ad9c5732e62e505e0c80a86e584e6a1f6a78642..63da17d7f2f4d80e87b3aaeb99521018ed33d79f 100644
--- a/bootstrap/providers.php
+++ b/bootstrap/providers.php
@@ -3,4 +3,5 @@
 return [
     App\Providers\AppServiceProvider::class,
     App\Providers\FortifyServiceProvider::class,
+    App\Providers\HorizonServiceProvider::class,
 ];
diff --git a/composer.json b/composer.json
index 53d134543b9bdb7054973b3fa7312ee037733ff2..2147a7f861e665f181b2a1f66456c8ed9d257094 100644
--- a/composer.json
+++ b/composer.json
@@ -17,6 +17,7 @@
     "itsgoingd/clockwork": "^5.3",
     "laravel/fortify": "^1.24",
     "laravel/framework": "^11.9",
+    "laravel/horizon": "^5.30",
     "laravel/octane": "^2.6",
     "laravel/tinker": "^2.9",
     "platformcommunity/flysystem-bunnycdn": "*",
diff --git a/composer.lock b/composer.lock
index d74a19c34a0ef03a4ed7ada5c00d24754430d379..31c03844cc31ebc1a50c1657014fe21bba8ec132 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-  "content-hash": "0fe551a6979ff36ef2315ffbe54b9038",
+    "content-hash": "01fe61521ac3505612dd88366956b8b8",
     "packages": [
         {
             "name": "bacon/bacon-qr-code",
@@ -1074,16 +1074,16 @@
         },
         {
             "name": "guzzlehttp/uri-template",
-          "version": "v1.0.4",
+            "version": "v1.0.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/guzzle/uri-template.git",
-              "reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
+                "reference": "30e286560c137526eccd4ce21b2de477ab0676d2"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
-              "reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
+                "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2",
+                "reference": "30e286560c137526eccd4ce21b2de477ab0676d2",
                 "shasum": ""
             },
             "require": {
@@ -1140,7 +1140,7 @@
             ],
             "support": {
                 "issues": "https://github.com/guzzle/uri-template/issues",
-              "source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
+                "source": "https://github.com/guzzle/uri-template/tree/v1.0.4"
             },
             "funding": [
                 {
@@ -1156,7 +1156,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-02-03T10:55:03+00:00"
+            "time": "2025-02-03T10:55:03+00:00"
         },
         {
             "name": "intervention/gif",
@@ -1228,16 +1228,16 @@
         },
         {
             "name": "intervention/image",
-          "version": "3.11.1",
+            "version": "3.11.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Intervention/image.git",
-              "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af"
+                "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af",
-              "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af",
+                "url": "https://api.github.com/repos/Intervention/image/zipball/0f87254688e480fbb521e2a1ac6c11c784ca41af",
+                "reference": "0f87254688e480fbb521e2a1ac6c11c784ca41af",
                 "shasum": ""
             },
             "require": {
@@ -1284,7 +1284,7 @@
             ],
             "support": {
                 "issues": "https://github.com/Intervention/image/issues",
-              "source": "https://github.com/Intervention/image/tree/3.11.1"
+                "source": "https://github.com/Intervention/image/tree/3.11.1"
             },
             "funding": [
                 {
@@ -1300,30 +1300,30 @@
                     "type": "ko_fi"
                 }
             ],
-          "time": "2025-02-01T07:28:26+00:00"
+            "time": "2025-02-01T07:28:26+00:00"
         },
         {
             "name": "intervention/validation",
-            "version": "4.4.6",
+            "version": "4.5.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/Intervention/validation.git",
-                "reference": "10129adb07c0a57fffaf046828e7ec22a27797a3"
+                "reference": "9e3eba1a293438d72d5198744d800302a85fe4ae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/Intervention/validation/zipball/10129adb07c0a57fffaf046828e7ec22a27797a3",
-                "reference": "10129adb07c0a57fffaf046828e7ec22a27797a3",
+                "url": "https://api.github.com/repos/Intervention/validation/zipball/9e3eba1a293438d72d5198744d800302a85fe4ae",
+                "reference": "9e3eba1a293438d72d5198744d800302a85fe4ae",
                 "shasum": ""
             },
             "require": {
                 "ext-mbstring": "*",
-                "illuminate/validation": "^10|^11",
+                "illuminate/validation": "^10 || ^11 || ^12",
                 "php": "^8.1"
             },
             "require-dev": {
                 "phpstan/phpstan": "^2.1",
-                "phpunit/phpunit": "^10.0 || ^11.0",
+                "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
                 "slevomat/coding-standard": "~8.0",
                 "squizlabs/php_codesniffer": "^3.8",
                 "symfony/uid": "^5.1|^6.2"
@@ -1372,7 +1372,7 @@
             ],
             "support": {
                 "issues": "https://github.com/Intervention/validation/issues",
-                "source": "https://github.com/Intervention/validation/tree/4.4.6"
+                "source": "https://github.com/Intervention/validation/tree/4.5.0"
             },
             "funding": [
                 {
@@ -1388,20 +1388,20 @@
                     "type": "ko_fi"
                 }
             ],
-            "time": "2025-01-05T13:58:03+00:00"
+            "time": "2025-02-09T14:15:30+00:00"
         },
         {
             "name": "itsgoingd/clockwork",
-          "version": "v5.3.4",
+            "version": "v5.3.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/itsgoingd/clockwork.git",
-              "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5"
+                "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
-              "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
+                "url": "https://api.github.com/repos/itsgoingd/clockwork/zipball/c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
+                "reference": "c27ad77a08a9e58bf0049de46969fa4fe3b506e5",
                 "shasum": ""
             },
             "require": {
@@ -1456,7 +1456,7 @@
             ],
             "support": {
                 "issues": "https://github.com/itsgoingd/clockwork/issues",
-              "source": "https://github.com/itsgoingd/clockwork/tree/v5.3.4"
+                "source": "https://github.com/itsgoingd/clockwork/tree/v5.3.4"
             },
             "funding": [
                 {
@@ -1464,7 +1464,7 @@
                     "type": "github"
                 }
             ],
-          "time": "2025-02-09T15:57:21+00:00"
+            "time": "2025-02-09T15:57:21+00:00"
         },
         {
             "name": "jean85/pretty-package-versions",
@@ -1526,120 +1526,120 @@
             "time": "2024-11-18T16:19:46+00:00"
         },
         {
-          "name": "laminas/laminas-diactoros",
-          "version": "3.5.0",
-          "source": {
-            "type": "git",
-            "url": "https://github.com/laminas/laminas-diactoros.git",
-            "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2"
-          },
-          "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2",
-            "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2",
-            "shasum": ""
-          },
-          "require": {
-            "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
-            "psr/http-factory": "^1.1",
-            "psr/http-message": "^1.1 || ^2.0"
-          },
-          "conflict": {
-            "amphp/amp": "<2.6.4"
-          },
-          "provide": {
-            "psr/http-factory-implementation": "^1.0",
-            "psr/http-message-implementation": "^1.1 || ^2.0"
-          },
-          "require-dev": {
-            "ext-curl": "*",
-            "ext-dom": "*",
-            "ext-gd": "*",
-            "ext-libxml": "*",
-            "http-interop/http-factory-tests": "^2.2.0",
-            "laminas/laminas-coding-standard": "~2.5.0",
-            "php-http/psr7-integration-tests": "^1.4.0",
-            "phpunit/phpunit": "^10.5.36",
-            "psalm/plugin-phpunit": "^0.19.0",
-            "vimeo/psalm": "^5.26.1"
-          },
-          "type": "library",
-          "extra": {
-            "laminas": {
-              "module": "Laminas\\Diactoros",
-              "config-provider": "Laminas\\Diactoros\\ConfigProvider"
-            }
-          },
-          "autoload": {
-            "files": [
-              "src/functions/create_uploaded_file.php",
-              "src/functions/marshal_headers_from_sapi.php",
-              "src/functions/marshal_method_from_sapi.php",
-              "src/functions/marshal_protocol_version_from_sapi.php",
-              "src/functions/normalize_server.php",
-              "src/functions/normalize_uploaded_files.php",
-              "src/functions/parse_cookie_header.php"
-            ],
-            "psr-4": {
-              "Laminas\\Diactoros\\": "src/"
-            }
-          },
-          "notification-url": "https://packagist.org/downloads/",
-          "license": [
-            "BSD-3-Clause"
-          ],
-          "description": "PSR HTTP Message implementations",
-          "homepage": "https://laminas.dev",
-          "keywords": [
-            "http",
-            "laminas",
-            "psr",
-            "psr-17",
-            "psr-7"
-          ],
-          "support": {
-            "chat": "https://laminas.dev/chat",
-            "docs": "https://docs.laminas.dev/laminas-diactoros/",
-            "forum": "https://discourse.laminas.dev",
-            "issues": "https://github.com/laminas/laminas-diactoros/issues",
-            "rss": "https://github.com/laminas/laminas-diactoros/releases.atom",
-            "source": "https://github.com/laminas/laminas-diactoros"
-          },
-          "funding": [
-            {
-              "url": "https://funding.communitybridge.org/projects/laminas-project",
-              "type": "community_bridge"
-            }
-          ],
-          "time": "2024-10-14T11:59:49+00:00"
-        },
-      {
+            "name": "laminas/laminas-diactoros",
+            "version": "3.5.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-diactoros.git",
+                "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-diactoros/zipball/143a16306602ce56b8b092a7914fef03c37f9ed2",
+                "reference": "143a16306602ce56b8b092a7914fef03c37f9ed2",
+                "shasum": ""
+            },
+            "require": {
+                "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
+                "psr/http-factory": "^1.1",
+                "psr/http-message": "^1.1 || ^2.0"
+            },
+            "conflict": {
+                "amphp/amp": "<2.6.4"
+            },
+            "provide": {
+                "psr/http-factory-implementation": "^1.0",
+                "psr/http-message-implementation": "^1.1 || ^2.0"
+            },
+            "require-dev": {
+                "ext-curl": "*",
+                "ext-dom": "*",
+                "ext-gd": "*",
+                "ext-libxml": "*",
+                "http-interop/http-factory-tests": "^2.2.0",
+                "laminas/laminas-coding-standard": "~2.5.0",
+                "php-http/psr7-integration-tests": "^1.4.0",
+                "phpunit/phpunit": "^10.5.36",
+                "psalm/plugin-phpunit": "^0.19.0",
+                "vimeo/psalm": "^5.26.1"
+            },
+            "type": "library",
+            "extra": {
+                "laminas": {
+                    "module": "Laminas\\Diactoros",
+                    "config-provider": "Laminas\\Diactoros\\ConfigProvider"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/functions/create_uploaded_file.php",
+                    "src/functions/marshal_headers_from_sapi.php",
+                    "src/functions/marshal_method_from_sapi.php",
+                    "src/functions/marshal_protocol_version_from_sapi.php",
+                    "src/functions/normalize_server.php",
+                    "src/functions/normalize_uploaded_files.php",
+                    "src/functions/parse_cookie_header.php"
+                ],
+                "psr-4": {
+                    "Laminas\\Diactoros\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "PSR HTTP Message implementations",
+            "homepage": "https://laminas.dev",
+            "keywords": [
+                "http",
+                "laminas",
+                "psr",
+                "psr-17",
+                "psr-7"
+            ],
+            "support": {
+                "chat": "https://laminas.dev/chat",
+                "docs": "https://docs.laminas.dev/laminas-diactoros/",
+                "forum": "https://discourse.laminas.dev",
+                "issues": "https://github.com/laminas/laminas-diactoros/issues",
+                "rss": "https://github.com/laminas/laminas-diactoros/releases.atom",
+                "source": "https://github.com/laminas/laminas-diactoros"
+            },
+            "funding": [
+                {
+                    "url": "https://funding.communitybridge.org/projects/laminas-project",
+                    "type": "community_bridge"
+                }
+            ],
+            "time": "2024-10-14T11:59:49+00:00"
+        },
+        {
             "name": "laravel/fortify",
-        "version": "v1.25.4",
+            "version": "v1.25.4",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/fortify.git",
-              "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f"
+                "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f",
-              "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f",
+                "url": "https://api.github.com/repos/laravel/fortify/zipball/f185600e2d3a861834ad00ee3b7863f26ac25d3f",
+                "reference": "f185600e2d3a861834ad00ee3b7863f26ac25d3f",
                 "shasum": ""
             },
             "require": {
                 "bacon/bacon-qr-code": "^3.0",
                 "ext-json": "*",
-              "illuminate/support": "^10.0|^11.0|^12.0",
+                "illuminate/support": "^10.0|^11.0|^12.0",
                 "php": "^8.1",
                 "pragmarx/google2fa": "^8.0",
                 "symfony/console": "^6.0|^7.0"
             },
             "require-dev": {
                 "mockery/mockery": "^1.0",
-              "orchestra/testbench": "^8.16|^9.0|^10.0",
+                "orchestra/testbench": "^8.16|^9.0|^10.0",
                 "phpstan/phpstan": "^1.10",
-              "phpunit/phpunit": "^10.4|^11.3"
+                "phpunit/phpunit": "^10.4|^11.3"
             },
             "type": "library",
             "extra": {
@@ -1676,20 +1676,20 @@
                 "issues": "https://github.com/laravel/fortify/issues",
                 "source": "https://github.com/laravel/fortify"
             },
-        "time": "2025-01-26T19:34:46+00:00"
+            "time": "2025-01-26T19:34:46+00:00"
         },
         {
             "name": "laravel/framework",
-          "version": "v11.42.1",
+            "version": "v11.43.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/framework.git",
-              "reference": "ff392f42f6c55cc774ce75553a11c6b031da67f8"
+                "reference": "053f26afb699c845945e7380b407dd019a0a2c74"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/framework/zipball/ff392f42f6c55cc774ce75553a11c6b031da67f8",
-              "reference": "ff392f42f6c55cc774ce75553a11c6b031da67f8",
+                "url": "https://api.github.com/repos/laravel/framework/zipball/053f26afb699c845945e7380b407dd019a0a2c74",
+                "reference": "053f26afb699c845945e7380b407dd019a0a2c74",
                 "shasum": ""
             },
             "require": {
@@ -1797,11 +1797,11 @@
                 "league/flysystem-read-only": "^3.25.1",
                 "league/flysystem-sftp-v3": "^3.25.1",
                 "mockery/mockery": "^1.6.10",
-              "orchestra/testbench-core": "^9.9.4",
+                "orchestra/testbench-core": "^9.9.4",
                 "pda/pheanstalk": "^5.0.6",
                 "php-http/discovery": "^1.15",
-              "phpstan/phpstan": "^2.0",
-              "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1",
+                "phpstan/phpstan": "^2.0",
+                "phpunit/phpunit": "^10.5.35|^11.3.6|^12.0.1",
                 "predis/predis": "^2.3",
                 "resend/resend-php": "^0.10.0",
                 "symfony/cache": "^7.0.3",
@@ -1833,7 +1833,7 @@
                 "mockery/mockery": "Required to use mocking (^1.6).",
                 "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
                 "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
-              "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).",
+                "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.3.6|^12.0.1).",
                 "predis/predis": "Required to use the predis connector (^2.3).",
                 "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
                 "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
@@ -1891,110 +1891,190 @@
                 "issues": "https://github.com/laravel/framework/issues",
                 "source": "https://github.com/laravel/framework"
             },
-          "time": "2025-02-12T20:58:18+00:00"
-        },
-        {
-          "name": "laravel/octane",
-          "version": "v2.7.0",
-          "source": {
-            "type": "git",
-            "url": "https://github.com/laravel/octane.git",
-            "reference": "c9580d430fa8459823bd1dfbcb2bcfd591548cec"
-          },
-          "dist": {
-            "type": "zip",
-            "url": "https://api.github.com/repos/laravel/octane/zipball/c9580d430fa8459823bd1dfbcb2bcfd591548cec",
-            "reference": "c9580d430fa8459823bd1dfbcb2bcfd591548cec",
-            "shasum": ""
-          },
-          "require": {
-            "laminas/laminas-diactoros": "^3.0",
-            "laravel/framework": "^10.10.1|^11.0",
-            "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0",
-            "laravel/serializable-closure": "^1.3|^2.0",
-            "nesbot/carbon": "^2.66.0|^3.0",
-            "php": "^8.1.0",
-            "symfony/console": "^6.0|^7.0",
-            "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0"
-          },
-          "conflict": {
-            "spiral/roadrunner": "<2023.1.0",
-            "spiral/roadrunner-cli": "<2.6.0",
-            "spiral/roadrunner-http": "<3.3.0"
-          },
-          "require-dev": {
-            "guzzlehttp/guzzle": "^7.6.1",
-            "inertiajs/inertia-laravel": "^1.3.2|^2.0",
-            "laravel/scout": "^10.2.1",
-            "laravel/socialite": "^5.6.1",
-            "livewire/livewire": "^2.12.3|^3.0",
-            "mockery/mockery": "^1.5.1",
-            "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0",
-            "orchestra/testbench": "^8.21|^9.0",
-            "phpstan/phpstan": "^1.10.15",
-            "phpunit/phpunit": "^10.4",
-            "spiral/roadrunner-cli": "^2.6.0",
-            "spiral/roadrunner-http": "^3.3.0"
-          },
-          "bin": [
-            "bin/roadrunner-worker",
-            "bin/swoole-server"
-          ],
-          "type": "library",
-          "extra": {
-            "laravel": {
-              "aliases": {
-                "Octane": "Laravel\\Octane\\Facades\\Octane"
-              },
-              "providers": [
-                "Laravel\\Octane\\OctaneServiceProvider"
-              ]
-            },
-            "branch-alias": {
-              "dev-master": "2.x-dev"
-            }
-          },
-          "autoload": {
-            "psr-4": {
-              "Laravel\\Octane\\": "src"
-            }
-          },
-          "notification-url": "https://packagist.org/downloads/",
-          "license": [
-            "MIT"
-          ],
-          "authors": [
-            {
-              "name": "Taylor Otwell",
-              "email": "taylor@laravel.com"
-            }
-          ],
-          "description": "Supercharge your Laravel application's performance.",
-          "keywords": [
-            "frankenphp",
-            "laravel",
-            "octane",
-            "roadrunner",
-            "swoole"
-          ],
-          "support": {
-            "issues": "https://github.com/laravel/octane/issues",
-            "source": "https://github.com/laravel/octane"
-          },
-          "time": "2025-02-11T15:04:38+00:00"
-        },
-      {
+            "time": "2025-02-19T16:06:03+00:00"
+        },
+        {
+            "name": "laravel/horizon",
+            "version": "v5.30.3",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laravel/horizon.git",
+                "reference": "7b9ee870bf0e425b956fd0433f616f98fe951f72"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laravel/horizon/zipball/7b9ee870bf0e425b956fd0433f616f98fe951f72",
+                "reference": "7b9ee870bf0e425b956fd0433f616f98fe951f72",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "ext-pcntl": "*",
+                "ext-posix": "*",
+                "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0",
+                "illuminate/queue": "^9.21|^10.0|^11.0|^12.0",
+                "illuminate/support": "^9.21|^10.0|^11.0|^12.0",
+                "nesbot/carbon": "^2.17|^3.0",
+                "php": "^8.0",
+                "ramsey/uuid": "^4.0",
+                "symfony/console": "^6.0|^7.0",
+                "symfony/error-handler": "^6.0|^7.0",
+                "symfony/polyfill-php83": "^1.28",
+                "symfony/process": "^6.0|^7.0"
+            },
+            "require-dev": {
+                "mockery/mockery": "^1.0",
+                "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
+                "phpstan/phpstan": "^1.10",
+                "phpunit/phpunit": "^9.0|^10.4|^11.5",
+                "predis/predis": "^1.1|^2.0"
+            },
+            "suggest": {
+                "ext-redis": "Required to use the Redis PHP driver.",
+                "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0)."
+            },
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "aliases": {
+                        "Horizon": "Laravel\\Horizon\\Horizon"
+                    },
+                    "providers": [
+                        "Laravel\\Horizon\\HorizonServiceProvider"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-master": "5.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Laravel\\Horizon\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Taylor Otwell",
+                    "email": "taylor@laravel.com"
+                }
+            ],
+            "description": "Dashboard and code-driven configuration for Laravel queues.",
+            "keywords": [
+                "laravel",
+                "queue"
+            ],
+            "support": {
+                "issues": "https://github.com/laravel/horizon/issues",
+                "source": "https://github.com/laravel/horizon/tree/v5.30.3"
+            },
+            "time": "2025-02-11T13:52:50+00:00"
+        },
+        {
+            "name": "laravel/octane",
+            "version": "v2.8.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laravel/octane.git",
+                "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laravel/octane/zipball/1c5190cc5ad67eb4aadbf1816dcbfedc692851e7",
+                "reference": "1c5190cc5ad67eb4aadbf1816dcbfedc692851e7",
+                "shasum": ""
+            },
+            "require": {
+                "laminas/laminas-diactoros": "^3.0",
+                "laravel/framework": "^10.10.1|^11.0",
+                "laravel/prompts": "^0.1.24|^0.2.0|^0.3.0",
+                "laravel/serializable-closure": "^1.3|^2.0",
+                "nesbot/carbon": "^2.66.0|^3.0",
+                "php": "^8.1.0",
+                "symfony/console": "^6.0|^7.0",
+                "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0"
+            },
+            "conflict": {
+                "spiral/roadrunner": "<2023.1.0",
+                "spiral/roadrunner-cli": "<2.6.0",
+                "spiral/roadrunner-http": "<3.3.0"
+            },
+            "require-dev": {
+                "guzzlehttp/guzzle": "^7.6.1",
+                "inertiajs/inertia-laravel": "^1.3.2|^2.0",
+                "laravel/scout": "^10.2.1",
+                "laravel/socialite": "^5.6.1",
+                "livewire/livewire": "^2.12.3|^3.0",
+                "mockery/mockery": "^1.5.1",
+                "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0",
+                "orchestra/testbench": "^8.21|^9.0",
+                "phpstan/phpstan": "^1.10.15",
+                "phpunit/phpunit": "^10.4",
+                "spiral/roadrunner-cli": "^2.6.0",
+                "spiral/roadrunner-http": "^3.3.0"
+            },
+            "bin": [
+                "bin/roadrunner-worker",
+                "bin/swoole-server"
+            ],
+            "type": "library",
+            "extra": {
+                "laravel": {
+                    "aliases": {
+                        "Octane": "Laravel\\Octane\\Facades\\Octane"
+                    },
+                    "providers": [
+                        "Laravel\\Octane\\OctaneServiceProvider"
+                    ]
+                },
+                "branch-alias": {
+                    "dev-master": "2.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Laravel\\Octane\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Taylor Otwell",
+                    "email": "taylor@laravel.com"
+                }
+            ],
+            "description": "Supercharge your Laravel application's performance.",
+            "keywords": [
+                "frankenphp",
+                "laravel",
+                "octane",
+                "roadrunner",
+                "swoole"
+            ],
+            "support": {
+                "issues": "https://github.com/laravel/octane/issues",
+                "source": "https://github.com/laravel/octane"
+            },
+            "time": "2025-02-18T15:18:13+00:00"
+        },
+        {
             "name": "laravel/prompts",
-        "version": "v0.3.5",
+            "version": "v0.3.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/prompts.git",
-              "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
+                "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
-              "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
+                "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1",
+                "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1",
                 "shasum": ""
             },
             "require": {
@@ -2008,7 +2088,7 @@
                 "laravel/framework": ">=10.17.0 <10.25.0"
             },
             "require-dev": {
-              "illuminate/collections": "^10.0|^11.0|^12.0",
+                "illuminate/collections": "^10.0|^11.0|^12.0",
                 "mockery/mockery": "^1.5",
                 "pestphp/pest": "^2.3|^3.4",
                 "phpstan/phpstan": "^1.11",
@@ -2038,31 +2118,31 @@
             "description": "Add beautiful and user-friendly forms to your command-line applications.",
             "support": {
                 "issues": "https://github.com/laravel/prompts/issues",
-              "source": "https://github.com/laravel/prompts/tree/v0.3.5"
+                "source": "https://github.com/laravel/prompts/tree/v0.3.5"
             },
-        "time": "2025-02-11T13:34:40+00:00"
+            "time": "2025-02-11T13:34:40+00:00"
         },
         {
             "name": "laravel/serializable-closure",
-          "version": "v2.0.3",
+            "version": "v2.0.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/serializable-closure.git",
-              "reference": "f379c13663245f7aa4512a7869f62eb14095f23f"
+                "reference": "f379c13663245f7aa4512a7869f62eb14095f23f"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f",
-              "reference": "f379c13663245f7aa4512a7869f62eb14095f23f",
+                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/f379c13663245f7aa4512a7869f62eb14095f23f",
+                "reference": "f379c13663245f7aa4512a7869f62eb14095f23f",
                 "shasum": ""
             },
             "require": {
                 "php": "^8.1"
             },
             "require-dev": {
-              "illuminate/support": "^10.0|^11.0|^12.0",
+                "illuminate/support": "^10.0|^11.0|^12.0",
                 "nesbot/carbon": "^2.67|^3.0",
-              "pestphp/pest": "^2.36|^3.0",
+                "pestphp/pest": "^2.36|^3.0",
                 "phpstan/phpstan": "^2.0",
                 "symfony/var-dumper": "^6.2.0|^7.0.0"
             },
@@ -2101,26 +2181,26 @@
                 "issues": "https://github.com/laravel/serializable-closure/issues",
                 "source": "https://github.com/laravel/serializable-closure"
             },
-          "time": "2025-02-11T15:03:05+00:00"
+            "time": "2025-02-11T15:03:05+00:00"
         },
         {
             "name": "laravel/tinker",
-          "version": "v2.10.1",
+            "version": "v2.10.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/tinker.git",
-              "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3"
+                "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3",
-              "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3",
+                "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3",
+                "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3",
                 "shasum": ""
             },
             "require": {
-              "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
-              "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
-              "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+                "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+                "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+                "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
                 "php": "^7.2.5|^8.0",
                 "psy/psysh": "^0.11.1|^0.12.0",
                 "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0"
@@ -2128,10 +2208,10 @@
             "require-dev": {
                 "mockery/mockery": "~1.3.3|^1.4.2",
                 "phpstan/phpstan": "^1.10",
-              "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0"
+                "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0"
             },
             "suggest": {
-              "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)."
+                "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)."
             },
             "type": "library",
             "extra": {
@@ -2165,9 +2245,9 @@
             ],
             "support": {
                 "issues": "https://github.com/laravel/tinker/issues",
-              "source": "https://github.com/laravel/tinker/tree/v2.10.1"
+                "source": "https://github.com/laravel/tinker/tree/v2.10.1"
             },
-          "time": "2025-01-27T14:24:01+00:00"
+            "time": "2025-01-27T14:24:01+00:00"
         },
         {
             "name": "league/commonmark",
@@ -2825,16 +2905,16 @@
         },
         {
             "name": "nesbot/carbon",
-          "version": "3.8.5",
+            "version": "3.8.5",
             "source": {
                 "type": "git",
                 "url": "https://github.com/CarbonPHP/carbon.git",
-              "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4"
+                "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4",
-              "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4",
+                "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/b1a53a27898639579a67de42e8ced5d5386aa9a4",
+                "reference": "b1a53a27898639579a67de42e8ced5d5386aa9a4",
                 "shasum": ""
             },
             "require": {
@@ -2910,8 +2990,8 @@
             ],
             "support": {
                 "docs": "https://carbon.nesbot.com/docs",
-              "issues": "https://github.com/CarbonPHP/carbon/issues",
-              "source": "https://github.com/CarbonPHP/carbon"
+                "issues": "https://github.com/CarbonPHP/carbon/issues",
+                "source": "https://github.com/CarbonPHP/carbon"
             },
             "funding": [
                 {
@@ -2927,7 +3007,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-02-11T16:28:45+00:00"
+            "time": "2025-02-11T16:28:45+00:00"
         },
         {
             "name": "nette/schema",
@@ -4358,20 +4438,20 @@
         },
         {
             "name": "sentry/sentry-laravel",
-          "version": "4.12.0",
+            "version": "4.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/getsentry/sentry-laravel.git",
-              "reference": "da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0"
+                "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0",
-              "reference": "da1ee3417dfb3576a6aaa0f8b25892ebdb98fdb0",
+                "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/d232ac494258e0d50a77c575a5af5f1a426d3f87",
+                "reference": "d232ac494258e0d50a77c575a5af5f1a426d3f87",
                 "shasum": ""
             },
             "require": {
-                "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
+                "illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
                 "nyholm/psr7": "^1.0",
                 "php": "^7.2 | ^8.0",
                 "sentry/sentry": "^4.10",
@@ -4381,12 +4461,12 @@
                 "friendsofphp/php-cs-fixer": "^3.11",
                 "guzzlehttp/guzzle": "^7.2",
                 "laravel/folio": "^1.1",
-                "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0",
+                "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
                 "livewire/livewire": "^2.0 | ^3.0",
                 "mockery/mockery": "^1.3",
-                "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0",
+                "orchestra/testbench": "^4.7 | ^5.1 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
                 "phpstan/phpstan": "^1.10",
-                "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4"
+                "phpunit/phpunit": "^8.4 | ^9.3 | ^10.4 | ^11.5"
             },
             "type": "library",
             "extra": {
@@ -4431,7 +4511,7 @@
             ],
             "support": {
                 "issues": "https://github.com/getsentry/sentry-laravel/issues",
-              "source": "https://github.com/getsentry/sentry-laravel/tree/4.12.0"
+                "source": "https://github.com/getsentry/sentry-laravel/tree/4.13.0"
             },
             "funding": [
                 {
@@ -4443,20 +4523,20 @@
                     "type": "custom"
                 }
             ],
-          "time": "2025-02-05T13:13:03+00:00"
+            "time": "2025-02-18T10:09:29+00:00"
         },
         {
             "name": "sl-projects/laravel-request-logger",
-            "version": "v1.0.2",
+            "version": "v1.0.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/SofianeLasri/laravel-request-logger.git",
-                "reference": "e494b95ca6ac752f815df9875f8f3d25e58bfc36"
+                "reference": "e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/SofianeLasri/laravel-request-logger/zipball/e494b95ca6ac752f815df9875f8f3d25e58bfc36",
-                "reference": "e494b95ca6ac752f815df9875f8f3d25e58bfc36",
+                "url": "https://api.github.com/repos/SofianeLasri/laravel-request-logger/zipball/e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f",
+                "reference": "e42196f70e1c3b7b1ad2ea9a202b6d1e8c5a585f",
                 "shasum": ""
             },
             "require": {
@@ -4477,7 +4557,8 @@
             },
             "autoload": {
                 "psr-4": {
-                    "SlProjects\\LaravelRequestLogger\\": "src/"
+                    "SlProjects\\LaravelRequestLogger\\": "src/",
+                    "SlProjects\\LaravelRequestLogger\\Database\\Factories\\": "src/database/factories/"
                 }
             },
             "notification-url": "https://packagist.org/downloads/",
@@ -4495,9 +4576,9 @@
             "description": "A Laravel package to log all incoming HTTP requests",
             "support": {
                 "issues": "https://github.com/SofianeLasri/laravel-request-logger/issues",
-                "source": "https://github.com/SofianeLasri/laravel-request-logger/tree/v1.0.2"
+                "source": "https://github.com/SofianeLasri/laravel-request-logger/tree/v1.0.6"
             },
-            "time": "2024-12-15T14:29:11+00:00"
+            "time": "2025-02-19T16:59:10+00:00"
         },
         {
             "name": "spatie/commonmark-shiki-highlighter",
@@ -4561,16 +4642,16 @@
         },
         {
             "name": "spatie/db-dumper",
-            "version": "3.7.1",
+            "version": "3.8.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/db-dumper.git",
-                "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016"
+                "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/spatie/db-dumper/zipball/55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016",
-                "reference": "55d4d6710e1ab18c1e7ce2b22b8ad4bea2a30016",
+                "url": "https://api.github.com/repos/spatie/db-dumper/zipball/91e1fd4dc000aefc9753cda2da37069fc996baee",
+                "reference": "91e1fd4dc000aefc9753cda2da37069fc996baee",
                 "shasum": ""
             },
             "require": {
@@ -4608,7 +4689,7 @@
                 "spatie"
             ],
             "support": {
-                "source": "https://github.com/spatie/db-dumper/tree/3.7.1"
+                "source": "https://github.com/spatie/db-dumper/tree/3.8.0"
             },
             "funding": [
                 {
@@ -4620,7 +4701,7 @@
                     "type": "github"
                 }
             ],
-            "time": "2024-11-18T14:54:31+00:00"
+            "time": "2025-02-14T15:04:22+00:00"
         },
         {
             "name": "spatie/emoji",
@@ -4690,26 +4771,26 @@
         },
         {
             "name": "spatie/laravel-backup",
-          "version": "9.2.5",
+            "version": "9.2.7",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/laravel-backup.git",
-              "reference": "50effa86d6614282da747ae1194912e0ed273daf"
+                "reference": "0438eef46188e990cf6ddb34ce1eb4c94f3b3a05"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/50effa86d6614282da747ae1194912e0ed273daf",
-              "reference": "50effa86d6614282da747ae1194912e0ed273daf",
+                "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/0438eef46188e990cf6ddb34ce1eb4c94f3b3a05",
+                "reference": "0438eef46188e990cf6ddb34ce1eb4c94f3b3a05",
                 "shasum": ""
             },
             "require": {
                 "ext-zip": "^1.14.0",
-                "illuminate/console": "^10.10.0|^11.0",
-                "illuminate/contracts": "^10.10.0|^11.0",
-                "illuminate/events": "^10.10.0|^11.0",
-                "illuminate/filesystem": "^10.10.0|^11.0",
-                "illuminate/notifications": "^10.10.0|^11.0",
-                "illuminate/support": "^10.10.0|^11.0",
+                "illuminate/console": "^10.10.0|^11.0|^12.0",
+                "illuminate/contracts": "^10.10.0|^11.0|^12.0",
+                "illuminate/events": "^10.10.0|^11.0|^12.0",
+                "illuminate/filesystem": "^10.10.0|^11.0|^12.0",
+                "illuminate/notifications": "^10.10.0|^11.0|^12.0",
+                "illuminate/support": "^10.10.0|^11.0|^12.0",
                 "league/flysystem": "^3.0",
                 "php": "^8.2",
                 "spatie/db-dumper": "^3.7",
@@ -4722,12 +4803,12 @@
             "require-dev": {
                 "composer-runtime-api": "^2.0",
                 "ext-pcntl": "*",
-                "larastan/larastan": "^2.7.0",
+                "larastan/larastan": "^2.7.0|^3.0",
                 "laravel/slack-notification-channel": "^2.5|^3.0",
                 "league/flysystem-aws-s3-v3": "^2.0|^3.0",
                 "mockery/mockery": "^1.4",
-                "orchestra/testbench": "^8.0|^9.0",
-                "pestphp/pest": "^1.20|^2.0",
+                "orchestra/testbench": "^8.0|^9.0|^10.0",
+                "pestphp/pest": "^1.20|^2.0|^3.0",
                 "phpstan/extension-installer": "^1.1",
                 "phpstan/phpstan-deprecation-rules": "^1.0",
                 "phpstan/phpstan-phpunit": "^1.1",
@@ -4774,7 +4855,7 @@
             ],
             "support": {
                 "issues": "https://github.com/spatie/laravel-backup/issues",
-              "source": "https://github.com/spatie/laravel-backup/tree/9.2.5"
+                "source": "https://github.com/spatie/laravel-backup/tree/9.2.7"
             },
             "funding": [
                 {
@@ -4786,7 +4867,7 @@
                     "type": "other"
                 }
             ],
-          "time": "2025-01-31T11:14:27+00:00"
+            "time": "2025-02-16T16:59:01+00:00"
         },
         {
             "name": "spatie/laravel-markdown",
@@ -4866,27 +4947,27 @@
         },
         {
             "name": "spatie/laravel-package-tools",
-          "version": "1.19.0",
+            "version": "1.19.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/laravel-package-tools.git",
-              "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa"
+                "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
-              "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
+                "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
+                "reference": "1c9c30ac6a6576b8d15c6c37b6cf23d748df2faa",
                 "shasum": ""
             },
             "require": {
-              "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
+                "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0",
                 "php": "^8.0"
             },
             "require-dev": {
                 "mockery/mockery": "^1.5",
-              "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
-              "pestphp/pest": "^1.23|^2.1|^3.1",
-              "phpunit/phpunit": "^9.5.24|^10.5|^11.5",
+                "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0",
+                "pestphp/pest": "^1.23|^2.1|^3.1",
+                "phpunit/phpunit": "^9.5.24|^10.5|^11.5",
                 "spatie/pest-plugin-test-time": "^1.1|^2.2"
             },
             "type": "library",
@@ -4914,7 +4995,7 @@
             ],
             "support": {
                 "issues": "https://github.com/spatie/laravel-package-tools/issues",
-              "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0"
+                "source": "https://github.com/spatie/laravel-package-tools/tree/1.19.0"
             },
             "funding": [
                 {
@@ -4922,24 +5003,24 @@
                     "type": "github"
                 }
             ],
-          "time": "2025-02-06T14:58:20+00:00"
+            "time": "2025-02-06T14:58:20+00:00"
         },
         {
             "name": "spatie/laravel-signal-aware-command",
-          "version": "2.0.1",
+            "version": "2.1.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/laravel-signal-aware-command.git",
-              "reference": "5af15853cf593093e6b1abae3cca446ba59c30e8"
+                "reference": "8e8a226ed7fb45302294878ef339e75ffa9a878d"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/5af15853cf593093e6b1abae3cca446ba59c30e8",
-              "reference": "5af15853cf593093e6b1abae3cca446ba59c30e8",
+                "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/8e8a226ed7fb45302294878ef339e75ffa9a878d",
+                "reference": "8e8a226ed7fb45302294878ef339e75ffa9a878d",
                 "shasum": ""
             },
             "require": {
-                "illuminate/contracts": "^11.0",
+                "illuminate/contracts": "^11.0|^12.0",
                 "php": "^8.2",
                 "spatie/laravel-package-tools": "^1.4.3",
                 "symfony/console": "^7.0"
@@ -4948,8 +5029,8 @@
                 "brianium/paratest": "^6.2|^7.0",
                 "ext-pcntl": "*",
                 "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
-                "orchestra/testbench": "^9.0",
-                "pestphp/pest-plugin-laravel": "^1.3|^2.0",
+                "orchestra/testbench": "^9.0|^10.0",
+                "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0",
                 "phpunit/phpunit": "^9.5|^10|^11",
                 "spatie/laravel-ray": "^1.17"
             },
@@ -4989,7 +5070,7 @@
             ],
             "support": {
                 "issues": "https://github.com/spatie/laravel-signal-aware-command/issues",
-              "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.0.1"
+                "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.1.0"
             },
             "funding": [
                 {
@@ -4997,20 +5078,20 @@
                     "type": "github"
                 }
             ],
-          "time": "2025-02-05T08:24:50+00:00"
+            "time": "2025-02-14T09:55:51+00:00"
         },
         {
             "name": "spatie/shiki-php",
-          "version": "2.3.0",
+            "version": "2.3.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/spatie/shiki-php.git",
-              "reference": "50919178a6865f1165bf1a8f08430b88ef3a53de"
+                "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/spatie/shiki-php/zipball/50919178a6865f1165bf1a8f08430b88ef3a53de",
-              "reference": "50919178a6865f1165bf1a8f08430b88ef3a53de",
+                "url": "https://api.github.com/repos/spatie/shiki-php/zipball/24b4dcc161f37144180edbef49557edb96c1dc2d",
+                "reference": "24b4dcc161f37144180edbef49557edb96c1dc2d",
                 "shasum": ""
             },
             "require": {
@@ -5054,7 +5135,7 @@
                 "spatie"
             ],
             "support": {
-              "source": "https://github.com/spatie/shiki-php/tree/2.3.0"
+                "source": "https://github.com/spatie/shiki-php/tree/2.3.1"
             },
             "funding": [
                 {
@@ -5062,7 +5143,7 @@
                     "type": "github"
                 }
             ],
-          "time": "2025-02-10T15:41:13+00:00"
+            "time": "2025-02-18T13:18:46+00:00"
         },
         {
             "name": "spatie/temporary-directory",
@@ -5426,16 +5507,16 @@
         },
         {
             "name": "symfony/error-handler",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/error-handler.git",
-              "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49"
+                "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49",
-              "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49",
+                "url": "https://api.github.com/repos/symfony/error-handler/zipball/959a74d044a6db21f4caa6d695648dcb5584cb49",
+                "reference": "959a74d044a6db21f4caa6d695648dcb5584cb49",
                 "shasum": ""
             },
             "require": {
@@ -5481,7 +5562,7 @@
             "description": "Provides tools to manage errors and ease debugging PHP code",
             "homepage": "https://symfony.com",
             "support": {
-              "source": "https://github.com/symfony/error-handler/tree/v7.2.3"
+                "source": "https://github.com/symfony/error-handler/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -5497,7 +5578,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-07T09:39:55+00:00"
+            "time": "2025-01-07T09:39:55+00:00"
         },
         {
             "name": "symfony/event-dispatcher",
@@ -5721,16 +5802,16 @@
         },
         {
             "name": "symfony/http-foundation",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-foundation.git",
-              "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0"
+                "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0",
-              "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0",
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ee1b504b8926198be89d05e5b6fc4c3810c090f0",
+                "reference": "ee1b504b8926198be89d05e5b6fc4c3810c090f0",
                 "shasum": ""
             },
             "require": {
@@ -5779,7 +5860,7 @@
             "description": "Defines an object-oriented layer for the HTTP specification",
             "homepage": "https://symfony.com",
             "support": {
-              "source": "https://github.com/symfony/http-foundation/tree/v7.2.3"
+                "source": "https://github.com/symfony/http-foundation/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -5795,20 +5876,20 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-17T10:56:55+00:00"
+            "time": "2025-01-17T10:56:55+00:00"
         },
         {
             "name": "symfony/http-kernel",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/http-kernel.git",
-              "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b"
+                "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
-              "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
+                "reference": "caae9807f8e25a9b43ce8cc6fafab6cf91f0cc9b",
                 "shasum": ""
             },
             "require": {
@@ -5893,7 +5974,7 @@
             "description": "Provides a structured process for converting a Request into a Response",
             "homepage": "https://symfony.com",
             "support": {
-              "source": "https://github.com/symfony/http-kernel/tree/v7.2.3"
+                "source": "https://github.com/symfony/http-kernel/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -5909,20 +5990,20 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-29T07:40:13+00:00"
+            "time": "2025-01-29T07:40:13+00:00"
         },
         {
             "name": "symfony/mailer",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mailer.git",
-              "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3"
+                "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3",
-              "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3",
+                "url": "https://api.github.com/repos/symfony/mailer/zipball/f3871b182c44997cf039f3b462af4a48fb85f9d3",
+                "reference": "f3871b182c44997cf039f3b462af4a48fb85f9d3",
                 "shasum": ""
             },
             "require": {
@@ -5973,7 +6054,7 @@
             "description": "Helps sending emails",
             "homepage": "https://symfony.com",
             "support": {
-              "source": "https://github.com/symfony/mailer/tree/v7.2.3"
+                "source": "https://github.com/symfony/mailer/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -5989,20 +6070,20 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-27T11:08:17+00:00"
+            "time": "2025-01-27T11:08:17+00:00"
         },
         {
             "name": "symfony/mime",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/mime.git",
-              "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204"
+                "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204",
-              "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204",
+                "url": "https://api.github.com/repos/symfony/mime/zipball/2fc3b4bd67e4747e45195bc4c98bea4628476204",
+                "reference": "2fc3b4bd67e4747e45195bc4c98bea4628476204",
                 "shasum": ""
             },
             "require": {
@@ -6057,7 +6138,7 @@
                 "mime-type"
             ],
             "support": {
-              "source": "https://github.com/symfony/mime/tree/v7.2.3"
+                "source": "https://github.com/symfony/mime/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -6073,7 +6154,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-27T11:08:17+00:00"
+            "time": "2025-01-27T11:08:17+00:00"
         },
         {
             "name": "symfony/options-resolver",
@@ -6924,16 +7005,16 @@
         },
         {
             "name": "symfony/routing",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/routing.git",
-              "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996"
+                "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996",
-              "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996",
+                "url": "https://api.github.com/repos/symfony/routing/zipball/ee9a67edc6baa33e5fae662f94f91fd262930996",
+                "reference": "ee9a67edc6baa33e5fae662f94f91fd262930996",
                 "shasum": ""
             },
             "require": {
@@ -6985,7 +7066,7 @@
                 "url"
             ],
             "support": {
-              "source": "https://github.com/symfony/routing/tree/v7.2.3"
+                "source": "https://github.com/symfony/routing/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -7001,7 +7082,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-17T10:56:55+00:00"
+            "time": "2025-01-17T10:56:55+00:00"
         },
         {
             "name": "symfony/service-contracts",
@@ -7422,16 +7503,16 @@
         },
         {
             "name": "symfony/var-dumper",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/var-dumper.git",
-              "reference": "82b478c69745d8878eb60f9a049a4d584996f73a"
+                "reference": "82b478c69745d8878eb60f9a049a4d584996f73a"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a",
-              "reference": "82b478c69745d8878eb60f9a049a4d584996f73a",
+                "url": "https://api.github.com/repos/symfony/var-dumper/zipball/82b478c69745d8878eb60f9a049a4d584996f73a",
+                "reference": "82b478c69745d8878eb60f9a049a4d584996f73a",
                 "shasum": ""
             },
             "require": {
@@ -7485,7 +7566,7 @@
                 "dump"
             ],
             "support": {
-              "source": "https://github.com/symfony/var-dumper/tree/v7.2.3"
+                "source": "https://github.com/symfony/var-dumper/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -7501,7 +7582,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-17T11:39:41+00:00"
+            "time": "2025-01-17T11:39:41+00:00"
         },
         {
             "name": "tijsverkoyen/css-to-inline-styles",
@@ -8056,21 +8137,21 @@
         },
         {
             "name": "laravel/envoy",
-          "version": "v2.10.2",
+            "version": "v2.10.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/envoy.git",
-              "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd"
+                "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/envoy/zipball/819a519e3d86b056c7aa3bd5d0801952a6fc14fd",
-              "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd",
+                "url": "https://api.github.com/repos/laravel/envoy/zipball/819a519e3d86b056c7aa3bd5d0801952a6fc14fd",
+                "reference": "819a519e3d86b056c7aa3bd5d0801952a6fc14fd",
                 "shasum": ""
             },
             "require": {
                 "guzzlehttp/guzzle": "^6.0|^7.0",
-              "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
+                "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
                 "php": "^7.2|^8.0",
                 "symfony/console": "^4.3|^5.0|^6.0|^7.0",
                 "symfony/process": "^4.3|^5.0|^6.0|^7.0"
@@ -8113,41 +8194,41 @@
             ],
             "support": {
                 "issues": "https://github.com/laravel/envoy/issues",
-              "source": "https://github.com/laravel/envoy/tree/v2.10.2"
+                "source": "https://github.com/laravel/envoy/tree/v2.10.2"
             },
-          "time": "2025-01-28T15:47:18+00:00"
+            "time": "2025-01-28T15:47:18+00:00"
         },
         {
             "name": "laravel/pail",
-          "version": "v1.2.2",
+            "version": "v1.2.2",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/pail.git",
-              "reference": "f31f4980f52be17c4667f3eafe034e6826787db2"
+                "reference": "f31f4980f52be17c4667f3eafe034e6826787db2"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2",
-              "reference": "f31f4980f52be17c4667f3eafe034e6826787db2",
+                "url": "https://api.github.com/repos/laravel/pail/zipball/f31f4980f52be17c4667f3eafe034e6826787db2",
+                "reference": "f31f4980f52be17c4667f3eafe034e6826787db2",
                 "shasum": ""
             },
             "require": {
                 "ext-mbstring": "*",
-              "illuminate/console": "^10.24|^11.0|^12.0",
-              "illuminate/contracts": "^10.24|^11.0|^12.0",
-              "illuminate/log": "^10.24|^11.0|^12.0",
-              "illuminate/process": "^10.24|^11.0|^12.0",
-              "illuminate/support": "^10.24|^11.0|^12.0",
+                "illuminate/console": "^10.24|^11.0|^12.0",
+                "illuminate/contracts": "^10.24|^11.0|^12.0",
+                "illuminate/log": "^10.24|^11.0|^12.0",
+                "illuminate/process": "^10.24|^11.0|^12.0",
+                "illuminate/support": "^10.24|^11.0|^12.0",
                 "nunomaduro/termwind": "^1.15|^2.0",
                 "php": "^8.2",
                 "symfony/console": "^6.0|^7.0"
             },
             "require-dev": {
-              "laravel/framework": "^10.24|^11.0|^12.0",
+                "laravel/framework": "^10.24|^11.0|^12.0",
                 "laravel/pint": "^1.13",
-              "orchestra/testbench-core": "^8.13|^9.0|^10.0",
-              "pestphp/pest": "^2.20|^3.0",
-              "pestphp/pest-plugin-type-coverage": "^2.3|^3.0",
+                "orchestra/testbench-core": "^8.13|^9.0|^10.0",
+                "pestphp/pest": "^2.20|^3.0",
+                "pestphp/pest-plugin-type-coverage": "^2.3|^3.0",
                 "phpstan/phpstan": "^1.10",
                 "symfony/var-dumper": "^6.3|^7.0"
             },
@@ -8193,20 +8274,20 @@
                 "issues": "https://github.com/laravel/pail/issues",
                 "source": "https://github.com/laravel/pail"
             },
-          "time": "2025-01-28T15:15:15+00:00"
+            "time": "2025-01-28T15:15:15+00:00"
         },
         {
             "name": "laravel/pint",
-            "version": "v1.20.0",
+            "version": "v1.21.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/pint.git",
-                "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b"
+                "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/laravel/pint/zipball/53072e8ea22213a7ed168a8a15b96fbb8b82d44b",
-                "reference": "53072e8ea22213a7ed168a8a15b96fbb8b82d44b",
+                "url": "https://api.github.com/repos/laravel/pint/zipball/531fa0871fbde719c51b12afa3a443b8f4e4b425",
+                "reference": "531fa0871fbde719c51b12afa3a443b8f4e4b425",
                 "shasum": ""
             },
             "require": {
@@ -8214,15 +8295,15 @@
                 "ext-mbstring": "*",
                 "ext-tokenizer": "*",
                 "ext-xml": "*",
-                "php": "^8.1.0"
+                "php": "^8.2.0"
             },
             "require-dev": {
-                "friendsofphp/php-cs-fixer": "^3.66.0",
-                "illuminate/view": "^10.48.25",
-                "larastan/larastan": "^2.9.12",
-                "laravel-zero/framework": "^10.48.25",
+                "friendsofphp/php-cs-fixer": "^3.68.5",
+                "illuminate/view": "^11.42.0",
+                "larastan/larastan": "^3.0.4",
+                "laravel-zero/framework": "^11.36.1",
                 "mockery/mockery": "^1.6.12",
-                "nunomaduro/termwind": "^1.17.0",
+                "nunomaduro/termwind": "^2.3",
                 "pestphp/pest": "^2.36.0"
             },
             "bin": [
@@ -8259,32 +8340,32 @@
                 "issues": "https://github.com/laravel/pint/issues",
                 "source": "https://github.com/laravel/pint"
             },
-            "time": "2025-01-14T16:20:53+00:00"
+            "time": "2025-02-18T03:18:57+00:00"
         },
         {
             "name": "laravel/sail",
-          "version": "v1.41.0",
+            "version": "v1.41.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/laravel/sail.git",
-              "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec"
+                "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
-              "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
+                "url": "https://api.github.com/repos/laravel/sail/zipball/fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
+                "reference": "fe1a4ada0abb5e4bd99eb4e4b0d87906c00cdeec",
                 "shasum": ""
             },
             "require": {
-              "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0",
-              "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0",
-              "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0",
+                "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0",
+                "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0",
+                "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0",
                 "php": "^8.0",
                 "symfony/console": "^6.0|^7.0",
                 "symfony/yaml": "^6.0|^7.0"
             },
             "require-dev": {
-              "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
+                "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0",
                 "phpstan/phpstan": "^1.10"
             },
             "bin": [
@@ -8322,7 +8403,7 @@
                 "issues": "https://github.com/laravel/sail/issues",
                 "source": "https://github.com/laravel/sail"
             },
-          "time": "2025-01-24T15:45:36+00:00"
+            "time": "2025-01-24T15:45:36+00:00"
         },
         {
             "name": "mockery/mockery",
@@ -8409,16 +8490,16 @@
         },
         {
             "name": "myclabs/deep-copy",
-          "version": "1.13.0",
+            "version": "1.13.0",
             "source": {
                 "type": "git",
                 "url": "https://github.com/myclabs/DeepCopy.git",
-              "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
+                "reference": "024473a478be9df5fdaca2c793f2232fe788e414"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
-              "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
+                "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/024473a478be9df5fdaca2c793f2232fe788e414",
+                "reference": "024473a478be9df5fdaca2c793f2232fe788e414",
                 "shasum": ""
             },
             "require": {
@@ -8457,7 +8538,7 @@
             ],
             "support": {
                 "issues": "https://github.com/myclabs/DeepCopy/issues",
-              "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
+                "source": "https://github.com/myclabs/DeepCopy/tree/1.13.0"
             },
             "funding": [
                 {
@@ -8465,7 +8546,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-02-12T12:17:51+00:00"
+            "time": "2025-02-12T12:17:51+00:00"
         },
         {
             "name": "nunomaduro/collision",
@@ -8772,16 +8853,16 @@
         },
         {
             "name": "phpstan/phpstan",
-          "version": "2.1.5",
+            "version": "2.1.6",
             "source": {
                 "type": "git",
                 "url": "https://github.com/phpstan/phpstan.git",
-              "reference": "451b17f9665481ee502adc39be987cb71067ece2"
+                "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/phpstan/phpstan/zipball/451b17f9665481ee502adc39be987cb71067ece2",
-              "reference": "451b17f9665481ee502adc39be987cb71067ece2",
+                "url": "https://api.github.com/repos/phpstan/phpstan/zipball/6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
+                "reference": "6eaec7c6c9e90dcfe46ad1e1ffa5171e2dab641c",
                 "shasum": ""
             },
             "require": {
@@ -8826,7 +8907,7 @@
                     "type": "github"
                 }
             ],
-          "time": "2025-02-13T12:49:56+00:00"
+            "time": "2025-02-19T15:46:42+00:00"
         },
         {
             "name": "phpunit/php-code-coverage",
@@ -9153,16 +9234,16 @@
         },
         {
             "name": "phpunit/phpunit",
-          "version": "11.5.7",
+            "version": "11.5.8",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-              "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d"
+                "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e1cb706f019e2547039ca2c839898cd5f557ee5d",
-              "reference": "e1cb706f019e2547039ca2c839898cd5f557ee5d",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049",
+                "reference": "c9bd61aab12f0fc5e82ecfe621ff518a1d1f1049",
                 "shasum": ""
             },
             "require": {
@@ -9234,7 +9315,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-              "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.7"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.8"
             },
             "funding": [
                 {
@@ -9250,7 +9331,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-02-06T16:10:05+00:00"
+            "time": "2025-02-18T06:26:59+00:00"
         },
         {
             "name": "sebastian/cli-parser",
@@ -10232,16 +10313,16 @@
         },
         {
             "name": "symfony/yaml",
-          "version": "v7.2.3",
+            "version": "v7.2.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/yaml.git",
-              "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
+                "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec"
             },
             "dist": {
                 "type": "zip",
-              "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
-              "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
+                "url": "https://api.github.com/repos/symfony/yaml/zipball/ac238f173df0c9c1120f862d0f599e17535a87ec",
+                "reference": "ac238f173df0c9c1120f862d0f599e17535a87ec",
                 "shasum": ""
             },
             "require": {
@@ -10284,7 +10365,7 @@
             "description": "Loads and dumps YAML files",
             "homepage": "https://symfony.com",
             "support": {
-              "source": "https://github.com/symfony/yaml/tree/v7.2.3"
+                "source": "https://github.com/symfony/yaml/tree/v7.2.3"
             },
             "funding": [
                 {
@@ -10300,7 +10381,7 @@
                     "type": "tidelift"
                 }
             ],
-          "time": "2025-01-07T12:55:42+00:00"
+            "time": "2025-01-07T12:55:42+00:00"
         },
         {
             "name": "theseer/tokenizer",
@@ -10355,7 +10436,7 @@
     ],
     "aliases": [],
     "minimum-stability": "stable",
-  "stability-flags": [],
+    "stability-flags": {},
     "prefer-stable": true,
     "prefer-lowest": false,
     "platform": {
@@ -10364,6 +10445,6 @@
         "ext-pdo": "*",
         "ext-zip": "*"
     },
-  "platform-dev": [],
-  "plugin-api-version": "2.3.0"
+    "platform-dev": {},
+    "plugin-api-version": "2.6.0"
 }
diff --git a/config/ai-provider.php b/config/ai-provider.php
new file mode 100644
index 0000000000000000000000000000000000000000..4fa3c3971b8664bc7ec43255805b6edf47d75fec
--- /dev/null
+++ b/config/ai-provider.php
@@ -0,0 +1,13 @@
+<?php
+
+return [
+    'selected-provider' => env('AI_PROVIDER', 'openai'),
+    'providers' => [
+        'openai' => [
+            'url' => env('OPENAI_URL', 'https://api.openai.com/v1/chat/completions'),
+            'api-key' => env('OPENAI_API_KEY'),
+            'model' => env('OPENAI_MODEL', 'gpt-4o-mini'),
+            'max-tokens' => env('OPENAI_MAX_TOKENS', 256),
+        ],
+    ],
+];
diff --git a/config/app.php b/config/app.php
index b44d19f7b53e72e0c3756ca32a0d6d6540b17a67..9ea28a014eb03f08d0e32c6887094ee8780bf652 100644
--- a/config/app.php
+++ b/config/app.php
@@ -136,4 +136,6 @@
     ],
 
     'cdn_disk' => env('CDN_FILESYSTEM_DISK'),
+
+    'private_mode_secret' => env('APP_PRIVATE_MODE_SECRET'),
 ];
diff --git a/config/horizon.php b/config/horizon.php
new file mode 100644
index 0000000000000000000000000000000000000000..56d79c80d32d9fcd40acf27625d3a97d9abb6df3
--- /dev/null
+++ b/config/horizon.php
@@ -0,0 +1,213 @@
+<?php
+
+use Illuminate\Support\Str;
+
+return [
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Domain
+    |--------------------------------------------------------------------------
+    |
+    | This is the subdomain where Horizon will be accessible from. If this
+    | setting is null, Horizon will reside under the same domain as the
+    | application. Otherwise, this value will serve as the subdomain.
+    |
+    */
+
+    'domain' => env('HORIZON_DOMAIN'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Path
+    |--------------------------------------------------------------------------
+    |
+    | This is the URI path where Horizon will be accessible from. Feel free
+    | to change this path to anything you like. Note that the URI will not
+    | affect the paths of its internal API that aren't exposed to users.
+    |
+    */
+
+    'path' => env('HORIZON_PATH', 'horizon'),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Redis Connection
+    |--------------------------------------------------------------------------
+    |
+    | This is the name of the Redis connection where Horizon will store the
+    | meta information required for it to function. It includes the list
+    | of supervisors, failed jobs, job metrics, and other information.
+    |
+    */
+
+    'use' => 'default',
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Redis Prefix
+    |--------------------------------------------------------------------------
+    |
+    | This prefix will be used when storing all Horizon data in Redis. You
+    | may modify the prefix when you are running multiple installations
+    | of Horizon on the same server so that they don't have problems.
+    |
+    */
+
+    'prefix' => env(
+        'HORIZON_PREFIX',
+        Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
+    ),
+
+    /*
+    |--------------------------------------------------------------------------
+    | Horizon Route Middleware
+    |--------------------------------------------------------------------------
+    |
+    | These middleware will get attached onto each Horizon route, giving you
+    | the chance to add your own middleware to this list or change any of
+    | the existing middleware. Or, you can simply stick with this list.
+    |
+    */
+
+    'middleware' => ['web', 'auth'],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Queue Wait Time Thresholds
+    |--------------------------------------------------------------------------
+    |
+    | This option allows you to configure when the LongWaitDetected event
+    | will be fired. Every connection / queue combination may have its
+    | own, unique threshold (in seconds) before this event is fired.
+    |
+    */
+
+    'waits' => [
+        'redis:default' => 60,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Job Trimming Times
+    |--------------------------------------------------------------------------
+    |
+    | Here you can configure for how long (in minutes) you desire Horizon to
+    | persist the recent and failed jobs. Typically, recent jobs are kept
+    | for one hour while all failed jobs are stored for an entire week.
+    |
+    */
+
+    'trim' => [
+        'recent' => 60,
+        'pending' => 60,
+        'completed' => 60,
+        'recent_failed' => 10080,
+        'failed' => 10080,
+        'monitored' => 10080,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Silenced Jobs
+    |--------------------------------------------------------------------------
+    |
+    | Silencing a job will instruct Horizon to not place the job in the list
+    | of completed jobs within the Horizon dashboard. This setting may be
+    | used to fully remove any noisy jobs from the completed jobs list.
+    |
+    */
+
+    'silenced' => [
+        // App\Jobs\ExampleJob::class,
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Metrics
+    |--------------------------------------------------------------------------
+    |
+    | Here you can configure how many snapshots should be kept to display in
+    | the metrics graph. This will get used in combination with Horizon's
+    | `horizon:snapshot` schedule to define how long to retain metrics.
+    |
+    */
+
+    'metrics' => [
+        'trim_snapshots' => [
+            'job' => 24,
+            'queue' => 24,
+        ],
+    ],
+
+    /*
+    |--------------------------------------------------------------------------
+    | Fast Termination
+    |--------------------------------------------------------------------------
+    |
+    | When this option is enabled, Horizon's "terminate" command will not
+    | wait on all of the workers to terminate unless the --wait option
+    | is provided. Fast termination can shorten deployment delay by
+    | allowing a new instance of Horizon to start while the last
+    | instance will continue to terminate each of its workers.
+    |
+    */
+
+    'fast_termination' => false,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Memory Limit (MB)
+    |--------------------------------------------------------------------------
+    |
+    | This value describes the maximum amount of memory the Horizon master
+    | supervisor may consume before it is terminated and restarted. For
+    | configuring these limits on your workers, see the next section.
+    |
+    */
+
+    'memory_limit' => 64,
+
+    /*
+    |--------------------------------------------------------------------------
+    | Queue Worker Configuration
+    |--------------------------------------------------------------------------
+    |
+    | Here you may define the queue worker settings used by your application
+    | in all environments. These supervisors and settings handle all your
+    | queued jobs and will be provisioned by Horizon during deployment.
+    |
+    */
+
+    'defaults' => [
+        'supervisor-1' => [
+            'connection' => 'redis',
+            'queue' => ['default'],
+            'balance' => 'auto',
+            'autoScalingStrategy' => 'time',
+            'maxProcesses' => 1,
+            'maxTime' => 0,
+            'maxJobs' => 0,
+            'memory' => 128,
+            'tries' => 1,
+            'timeout' => 60,
+            'nice' => 0,
+        ],
+    ],
+
+    'environments' => [
+        'production' => [
+            'supervisor-1' => [
+                'maxProcesses' => 10,
+                'balanceMaxShift' => 1,
+                'balanceCooldown' => 3,
+            ],
+        ],
+
+        'local' => [
+            'supervisor-1' => [
+                'maxProcesses' => 3,
+            ],
+        ],
+    ],
+];
diff --git a/config/ip-address-resolver.php b/config/ip-address-resolver.php
new file mode 100644
index 0000000000000000000000000000000000000000..7dc71a84fda14b4dd12a32ae5f08c7452edd2b95
--- /dev/null
+++ b/config/ip-address-resolver.php
@@ -0,0 +1,7 @@
+<?php
+
+return [
+    'url' => env('IP_ADDRESS_RESOLVER_URL', 'http://ip-api.com/batch'),
+    'call_limit_per_minute' => env('IP_ADDRESS_RESOLVER_CALL_LIMIT_PER_MINUTE', 15),
+    'max_ip_addresses_per_call' => env('IP_ADDRESS_RESOLVER_MAX_IP_ADDRESSES_PER_CALL', 100),
+];
diff --git a/database/factories/IpAddressMetadataFactory.php b/database/factories/IpAddressMetadataFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..5581b8193b0a4a4f0f6a2edf952e61f5bcf10303
--- /dev/null
+++ b/database/factories/IpAddressMetadataFactory.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\IpAddressMetadata;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+
+class IpAddressMetadataFactory extends Factory
+{
+    protected $model = IpAddressMetadata::class;
+
+    public function definition(): array
+    {
+        return [
+            'country_code' => $this->faker->randomElement(IpAddressMetadata::COUNTRY_CODES),
+            'lat' => $this->faker->latitude(),
+            'lon' => $this->faker->longitude(),
+
+            'ip_address_id' => IpAddress::factory(),
+        ];
+    }
+}
diff --git a/database/factories/UserAgentMetadataFactory.php b/database/factories/UserAgentMetadataFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ed13a1dce9e8f751277b7cf91668d1098aae5e0
--- /dev/null
+++ b/database/factories/UserAgentMetadataFactory.php
@@ -0,0 +1,21 @@
+<?php
+
+namespace Database\Factories;
+
+use App\Models\UserAgentMetadata;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+
+class UserAgentMetadataFactory extends Factory
+{
+    protected $model = UserAgentMetadata::class;
+
+    public function definition(): array
+    {
+        return [
+            'is_bot' => $this->faker->boolean(),
+
+            'user_agent_id' => UserAgent::factory(),
+        ];
+    }
+}
diff --git a/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php b/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php
new file mode 100644
index 0000000000000000000000000000000000000000..cf525b8d2feb2fb07ae19f6c860fcb6e818464e8
--- /dev/null
+++ b/database/migrations/2025_02_16_212109_create_user_agent_metadata_table.php
@@ -0,0 +1,23 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('user_agent_metadata', function (Blueprint $table) {
+            $table->id();
+            $table->foreignIdFor(UserAgent::class);
+            $table->boolean('is_bot');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('user_agent_metadata');
+    }
+};
diff --git a/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php
new file mode 100644
index 0000000000000000000000000000000000000000..387e34bb37c327ce10231111387850c065c3f420
--- /dev/null
+++ b/database/migrations/2025_02_19_141925_create_ip_address_metadata_table.php
@@ -0,0 +1,24 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('ip_address_metadata', function (Blueprint $table) {
+            $table->id();
+            $table->foreignId('ip_address_id')->constrained('ip_addresses');
+            $table->enum('country_code', ['AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL', 'AM', 'AN', 'AO', 'AP', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AV', 'AW', 'AX', 'AY', 'AZ', 'BA', 'BB', 'BC', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BK', 'BL', 'BM', 'BN', 'BO', 'BP', 'BQ', 'BR', 'BS', 'BT', 'BU', 'BV', 'BW', 'BX', 'BY', 'BZ', 'CA', 'CB', 'CC', 'CD', 'CE', 'CF', 'CG', 'CH', 'CI', 'CJ', 'CK', 'CL', 'CM', 'CN', 'CO', 'CP', 'CQ', 'CR', 'CS', 'CT', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ', 'DA', 'DB', 'DC', 'DD', 'DE', 'DF', 'DG', 'DH', 'DI', 'DJ', 'DK', 'DL', 'DM', 'DN', 'DO', 'DP', 'DQ', 'DR', 'DS', 'DT', 'DU', 'DV', 'DW', 'DX', 'DY', 'DZ', 'EA', 'EB', 'EC', 'ED', 'EE', 'EF', 'EG', 'EH', 'EI', 'EJ', 'EK', 'EL', 'EM', 'EN', 'EO', 'EP', 'EQ', 'ER', 'ES', 'ET', 'EU', 'EV', 'EW', 'EX', 'EY', 'EZ', 'FA', 'FB', 'FC', 'FD', 'FE', 'FF', 'FG', 'FH', 'FI', 'FJ', 'FK', 'FL', 'FM', 'FN', 'FO', 'FP', 'FQ', 'FR', 'FS', 'FT', 'FU', 'FV', 'FW', 'FX', 'FY', 'FZ', 'GA', 'GB', 'GC', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GJ', 'GK', 'GL', 'GM', 'GN', 'GO', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GV', 'GW', 'GX', 'GY', 'GZ', 'HA', 'HB', 'HC', 'HD', 'HE', 'HF', 'HG', 'HH', 'HI', 'HJ', 'HK', 'HL', 'HM', 'HN', 'HO', 'HP', 'HQ', 'HR', 'HS', 'HT', 'HU', 'HV', 'HW', 'HX', 'HY', 'HZ', 'IA', 'IB', 'IC', 'ID', 'IE', 'IF', 'IG', 'IH', 'II', 'IJ', 'IK', 'IL', 'IM', 'IN', 'IO', 'IP', 'IQ', 'IR', 'IS', 'IT', 'IU', 'IV', 'IW', 'IX', 'IY', 'IZ', 'JA', 'JB', 'JC', 'JD', 'JE', 'JF', 'JG', 'JH', 'JI', 'JJ', 'JK', 'JL', 'JM', 'JN', 'JO', 'JP', 'JQ', 'JR', 'JS', 'JT', 'JU', 'JV', 'JW', 'JX', 'JY', 'JZ', 'KA', 'KB', 'KC', 'KD', 'KE', 'KF', 'KG', 'KH', 'KI', 'KJ', 'KK', 'KL', 'KM', 'KN', 'KO', 'KP', 'KQ', 'KR', 'KS', 'KT', 'KU', 'KV', 'KW', 'KX', 'KY', 'KZ', 'LA', 'LB', 'LC', 'LD', 'LE', 'LF', 'LG', 'LH', 'LI', 'LJ', 'LK', 'LL', 'LM', 'LN', 'LO', 'LP', 'LQ', 'LR', 'LS', 'LT', 'LU', 'LV', 'LW', 'LX', 'LY', 'LZ', 'MA', 'MB', 'MC', 'MD', 'ME', 'MF', 'MG', 'MH', 'MI', 'MJ', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU', 'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NB', 'NC', 'ND', 'NE', 'NF', 'NG', 'NH', 'NI', 'NJ', 'NK', 'NL', 'NM', 'NN', 'NO', 'NP', 'NQ', 'NR', 'NS', 'NT', 'NU', 'NV', 'NW', 'NX', 'NY', 'NZ', 'OA', 'OB', 'OC', 'OD', 'OE', 'OF', 'OG', 'OH', 'OI', 'OJ', 'OK', 'OL', 'OM', 'ON', 'OO', 'OP', 'OQ', 'OR', 'OS', 'OT', 'OU', 'OV', 'OW', 'OX', 'OY', 'OZ', 'PA', 'PB', 'PC', 'PD', 'PE', 'PF', 'PG', 'PH', 'PI', 'PJ', 'PK', 'PL', 'PM', 'PN', 'PO', 'PP', 'PQ', 'PR', 'PS', 'PT', 'PU', 'PV', 'PW', 'PX', 'PY', 'PZ', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', 'QH', 'QI', 'QJ', 'QK', 'QL', 'QM', 'QN', 'QO', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QU', 'QV', 'QW', 'QX', 'QY', 'QZ', 'RA', 'RB', 'RC', 'RD', 'RE', 'RF', 'RG', 'RH', 'RI', 'RJ', 'RK', 'RL', 'RM', 'RN', 'RO', 'RP', 'RQ', 'RR', 'RS', 'RT', 'RU', 'RV', 'RW', 'RX', 'RY', 'RZ', 'SA', 'SB', 'SC', 'SD', 'SE', 'SF', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SP', 'SQ', 'SR', 'SS', 'ST', 'SU', 'SV', 'SW', 'SX', 'SY', 'SZ', 'TA', 'TB', 'TC', 'TD', 'TE', 'TF', 'TG', 'TH', 'TI', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO', 'TP', 'TQ', 'TR', 'TS', 'TT', 'TU', 'TV', 'TW', 'TX', 'TY', 'TZ', 'UA', 'UB', 'UC', 'UD', 'UE', 'UF', 'UG', 'UH', 'UI', 'UJ', 'UK', 'UL', 'UM', 'UN', 'UO', 'UP', 'UQ', 'UR', 'US', 'UT', 'UU', 'UV', 'UW', 'UX', 'UY', 'UZ', 'VA', 'VB', 'VC', 'VD', 'VE', 'VF', 'VG', 'VH', 'VI', 'VJ', 'VK', 'VL', 'VM', 'VN', 'VO', 'VP', 'VQ', 'VR', 'VS', 'VT', 'VU', 'VV', 'VW', 'VX', 'VY', 'VZ', 'WA', 'WB', 'WC', 'WD', 'WE', 'WF', 'WG', 'WH', 'WI', 'WJ', 'WK', 'WL', 'WM', 'WN', 'WO', 'WP', 'WQ', 'WR', 'WS', 'WT', 'WU', 'WV', 'WW', 'WX', 'WY', 'WZ', 'XA', 'XB', 'XC', 'XD', 'XE', 'XF', 'XG', 'XH', 'XI', 'XJ', 'XK', 'XL', 'XM', 'XN', 'XO', 'XP', 'XQ', 'XR', 'XS', 'XT', 'XU', 'XV', 'XW', 'XX', 'XY', 'XZ', 'YA', 'YB', 'YC', 'YD', 'YE', 'YF', 'YG', 'YH', 'YI', 'YJ', 'YK', 'YL', 'YM', 'YN', 'YO', 'YP', 'YQ', 'YR', 'YS', 'YT', 'YU', 'YV', 'YW', 'YX', 'YY', 'YZ', 'ZA', 'ZB', 'ZC', 'ZD', 'ZE', 'ZF', 'ZG', 'ZH', 'ZI', 'ZJ', 'ZK', 'ZL', 'ZM', 'ZN', 'ZO', 'ZP', 'ZQ', 'ZR', 'ZS', 'ZT', 'ZU', 'ZV', 'ZW', 'ZX', 'ZY', 'ZZ']);
+            $table->float('lat')->nullable();
+            $table->float('lon')->nullable();
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('ip_address_metadata');
+    }
+};
diff --git a/docker-compose.yml b/docker-compose.yml
index 182619f84c0711bdc5892258207906240a1f97d8..1de12b7f98ba60bf0fb419f9245bb22dc297b694 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,7 +4,7 @@ services:
       context: .
     image: registry.gitlab.sl-projects.com/sofianelasri/rann-graphic-design-website:latest
     working_dir: /app
-    entrypoint: php artisan octane:frankenphp --workers=1 --max-requests=1
+    entrypoint: ["docker-init/entrypoint.sh"]
     ports:
       - '80:8000'
       - '5173:5173'
diff --git a/docker-init/000-default.conf b/docker-init/000-default.conf
deleted file mode 100644
index c83da7ba3251309bcaf10653213b971ed60d59fe..0000000000000000000000000000000000000000
--- a/docker-init/000-default.conf
+++ /dev/null
@@ -1,13 +0,0 @@
-<VirtualHost *:80>
-    ServerAdmin webmaster@localhost
-    DocumentRoot /var/www/public
-
-    <Directory /var/www/public>
-        Options Indexes FollowSymLinks MultiViews
-        AllowOverride All
-        Require all granted
-    </Directory>
-
-    ErrorLog ${APACHE_LOG_DIR}/error.log
-    CustomLog ${APACHE_LOG_DIR}/access.log combined
-</VirtualHost>
diff --git a/docker-init/entrypoint.sh b/docker-init/entrypoint.sh
index 20abb72bdd09e430be7b28adf27203c25f924260..c12663fd0e318205b8d48434248bfcc0850bce26 100644
--- a/docker-init/entrypoint.sh
+++ b/docker-init/entrypoint.sh
@@ -1,22 +1,15 @@
 #!/bin/bash
 
-# If var/wwww/composer.json exists...
-if [ ! -f /var/www/composer.json ]; then
-    echo "Probably a Gitlab CI environment, skipping..."
-    exec "$@"
-else
-    cd /var/www || exit
+echo "Installing dependencies..."
+composer install
+php artisan key:generate
 
-    echo "Installing dependencies..."
-    composer install
-    php artisan key:generate
+echo "Migrating database..."
+php artisan migrate
 
-    npm install
-    npm run build
+echo "Launching Supervisor..."
+service supervisor start
+supervisorctl start all
 
-    echo "Starting services..."
-    service apache2 start
-
-    echo "All good!"
-    exec "$@"
-fi
+echo "Running Octane..."
+php artisan octane:frankenphp --workers=1 --max-requests=1
\ No newline at end of file
diff --git a/docker-init/php.ini b/docker-init/php.ini
index dff9b622397b13220a49ba00dbe027fb9edfe47b..6a1056f71f7dc969935a8b86a9b0e46aa94518d7 100644
--- a/docker-init/php.ini
+++ b/docker-init/php.ini
@@ -1426,10 +1426,13 @@ session.use_trans_sid = 0
 ; Shorter length than default is supported only for compatibility reason.
 ; Users should use 32 or more chars.
 ; https://php.net/session.sid-length
+
+; NOTE: Deprecated since PHP 8.4
+
 ; Default Value: 32
 ; Development Value: 26
 ; Production Value: 26
-session.sid_length = 26
+; session.sid_length = 32
 
 ; The URL rewriter will look for URLs in a defined set of HTML tags.
 ; <form> is special; if you include them here, the rewriter will
@@ -1466,7 +1469,10 @@ session.trans_sid_tags = "a=href,area=href,frame=src,form="
 ; Development Value: 5
 ; Production Value: 5
 ; https://php.net/session.hash-bits-per-character
-session.sid_bits_per_character = 5
+
+; NOTE: Deprecated since PHP 8.4
+
+; session.sid_bits_per_character = 5
 
 ; Enable upload progress tracking in $_SESSION
 ; Default Value: On
@@ -1882,13 +1888,13 @@ ldap.max_links = -1
 ; List of headers files to preload, wildcard patterns allowed.
 ;ffi.preload=
 
-[xdebug]
-zend_extension=xdebug.so
-xdebug.mode=develop,coverage,debug,profile
-xdebug.start_with_request=trigger
-xdebug.discover_client_host=true
-xdebug.idekey=docker
-xdebug.log=/dev/stdout
-xdebug.log_level=0
-xdebug.client_port=9003
-xdebug.client_host=host.docker.internal
\ No newline at end of file
+;[xdebug]
+;zend_extension=xdebug.so
+;xdebug.mode=develop,coverage,debug,profile
+;xdebug.start_with_request=trigger
+;xdebug.discover_client_host=true
+;xdebug.idekey=docker
+;xdebug.log=/dev/stdout
+;xdebug.log_level=0
+;xdebug.client_port=9003
+;xdebug.client_host=host.docker.internal
\ No newline at end of file
diff --git a/docker-init/supervisord.conf b/docker-init/supervisord.conf
new file mode 100644
index 0000000000000000000000000000000000000000..c28a1573051ee3091e0fcc075e15f0d5d08d4fb0
--- /dev/null
+++ b/docker-init/supervisord.conf
@@ -0,0 +1,5 @@
+[program:laravel-worker]
+process_name=%(program_name)s_%(process_num)02d
+command=php /app/artisan queue:work --sleep=3 --tries=3
+autostart=true
+autorestart=true
\ No newline at end of file
diff --git a/lang/en/contact.php b/lang/en/contact.php
new file mode 100644
index 0000000000000000000000000000000000000000..87f2cd0a0374c6acae9a3c71e5e5fcb4dfb61cfe
--- /dev/null
+++ b/lang/en/contact.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'desc' => 'Do you have a project in mind or want to discuss your graphic design needs? Feel free to contact me! Whether it’s for a collaboration, a quote request, or just to exchange creative ideas, I’m here to help.',
+    'contact_me_on_one_of_my_social_media' => 'Contact me on one of my social media',
+];
diff --git a/lang/en/footer.php b/lang/en/footer.php
new file mode 100644
index 0000000000000000000000000000000000000000..8c46e83400e9f00b7abead2b34a3dd71c39d197e
--- /dev/null
+++ b/lang/en/footer.php
@@ -0,0 +1,14 @@
+<?php
+
+return [
+    'contact' => [
+        'title' => 'Excited to work with you!',
+        'desc' => 'Feel free to contact me if you need information, or if you just want to chat with me.',
+    ],
+    'links' => [
+        'title' => 'Stay in touch',
+    ],
+    'legals-infos' => [
+        'title' => 'Legal information',
+    ],
+];
diff --git a/lang/en/generic.php b/lang/en/generic.php
index 51de6fe9e666f20e6169dff894b319ce4b1416d9..a345a842c4e75dcc9747427837b12f5f10eb10f9 100644
--- a/lang/en/generic.php
+++ b/lang/en/generic.php
@@ -4,4 +4,6 @@
     'custom_needs' => 'A specific need?',
     'custom_needs_desc' => 'I am open to any proposal. Don’t hesitate to contact me!',
     'completed_in' => 'Completed in',
+    'legal_mentions' => 'Legal notices',
+    'terms' => 'Terms and conditions of sale',
 ];
diff --git a/lang/en/home.php b/lang/en/home.php
index 16cf7a51a14278df222b272557969e29d607dc6e..93efc9e769e02189d61ea81d70d4486e327e8d6d 100644
--- a/lang/en/home.php
+++ b/lang/en/home.php
@@ -17,4 +17,5 @@
         'waving_hand' => 'Waving hand emoji',
         'roxannas_photo' => 'Photo of Roxanna Valtre',
     ],
+    'meta_description' => 'Roxanna Valtre, Graphic Designer. Discover my services and my portfolio.',
 ];
diff --git a/lang/fr/contact.php b/lang/fr/contact.php
new file mode 100644
index 0000000000000000000000000000000000000000..67270fd422a5f39c002970a9ba9f6118848a3327
--- /dev/null
+++ b/lang/fr/contact.php
@@ -0,0 +1,6 @@
+<?php
+
+return [
+    'desc' => 'Vous avez un projet en tête ou souhaitez discuter de vos besoins en design graphique ? N\'hésitez pas à me contacter ! Que ce soit pour une collaboration, une demande de devis ou simplement pour échanger des idées créatives, je suis là pour vous aider.',
+    'contact_me_on_one_of_my_social_media' => 'Contactez moi sur l\'un de mes réseaux',
+];
diff --git a/lang/fr/footer.php b/lang/fr/footer.php
new file mode 100644
index 0000000000000000000000000000000000000000..0e7acdedfc7c68d0ae44642d3a24acba707dda0b
--- /dev/null
+++ b/lang/fr/footer.php
@@ -0,0 +1,14 @@
+<?php
+
+return [
+    'contact' => [
+        'title' => 'Hâte de travailler avec vous !',
+        'desc' => 'N’hésitez pas à me contacter si vous avez besoin de renseignements, ou que vous souhaitez simplement discuter avec moi.',
+    ],
+    'links' => [
+        'title' => 'Restons en contact',
+    ],
+    'legals-infos' => [
+        'title' => 'Informations légales',
+    ],
+];
diff --git a/lang/fr/generic.php b/lang/fr/generic.php
index 4b27032c33a73fb3c47194d9a0b8197c2d786f5d..0c369358497101f7f0bfffd75983290fdc6b7610 100644
--- a/lang/fr/generic.php
+++ b/lang/fr/generic.php
@@ -4,4 +4,6 @@
     'custom_needs' => 'Un besoin particulier ?',
     'custom_needs_desc' => 'Je suis ouverte à toute proposition. N’hésitez pas à me contacter !',
     'completed_in' => 'Réalisé en',
+    'legal_mentions' => 'Mentions légales',
+    'terms' => 'Conditions générales de vente',
 ];
diff --git a/lang/fr/home.php b/lang/fr/home.php
index c873aff786363813b37944ba5b8b5a907b465aef..b24c61701f0f400b4156847f875a2809446a9418 100644
--- a/lang/fr/home.php
+++ b/lang/fr/home.php
@@ -17,4 +17,5 @@
         'waving_hand' => 'Emoji de main qui salue',
         'roxannas_photo' => 'Photo de Roxana Valtre',
     ],
+    'meta_description' => 'Roxanna Valtre, Designer Graphique. Découvrez mes services et mon portfolio.',
 ];
diff --git a/package-lock.json b/package-lock.json
index 35b434f274705fccc39e6b057cb001ca6ef04b3c..081927838d9555f2dce206f17261fab01edda1ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,5 +1,5 @@
 {
-    "name": "rann-graphic-design-website",
+    "name": "app",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
diff --git a/resources/scss/public.scss b/resources/scss/public.scss
index 2d88a1389a6b99742d97199479c04e37fe0dbbda..6a7d3c8e7010d4a3502bf9c1c9fee0a8bd89d6db 100644
--- a/resources/scss/public.scss
+++ b/resources/scss/public.scss
@@ -3,6 +3,10 @@
 @tailwind utilities;
 @import "fonts";
 
+h1, h2, h3, h4, h5, h6 {
+    font-family: "Sniglet", serif;
+}
+
 .markdown {
     * {
         @apply mb-4;
diff --git a/resources/views/admin/creations/index.blade.php b/resources/views/admin/creations/index.blade.php
index e13f5b4f9fcb48b70d6605515bbc45addf9ca8a3..c5e81d17f85c53b0cce028eff3327f3618c4f197 100644
--- a/resources/views/admin/creations/index.blade.php
+++ b/resources/views/admin/creations/index.blade.php
@@ -28,6 +28,11 @@
                                  size="sm" tag="a" variant="danger">
                         Supprimer
                     </x-bs.button>
+                    <x-bs.button size="sm" variant="secondary" id="translate-{{ $creation->id }}"
+                                 onclick="translateWithAi({{ $creation->id }})">
+                        {{ \Spatie\Emoji\Emoji::flagsForFlagUnitedKingdom() }}
+                        Traduire
+                    </x-bs.button>
                 </x-slot:footer>
             </x-bs.card>
         @endforeach
@@ -38,3 +43,24 @@
         @endif
     </div>
 @endsection
+
+@pushonce('scripts')
+    <script type="text/javascript">
+        function translateWithAi(creationId) {
+            const button = document.querySelector(`#translate-${creationId}`);
+            button.disabled = true;
+            fetch(`/admin/creations/${creationId}/translate-with-ai`)
+                .then(response => response.json())
+                .then(data => {
+                    if (data.success) {
+                        button.classList.remove('btn-secondary');
+                        button.classList.add('btn-success');
+                    } else {
+                        button.disabled = false;
+                        button.classList.remove('btn-secondary');
+                        button.classList.add('btn-warning');
+                    }
+                });
+        }
+    </script>
+@endpushonce
diff --git a/resources/views/admin/home.blade.php b/resources/views/admin/home.blade.php
index 2ecf3e23d8592b7dd83e41a2ac5cabb1299f823e..46d75a745bb2a7d298febfa72b3cb2cecec24ebb 100644
--- a/resources/views/admin/home.blade.php
+++ b/resources/views/admin/home.blade.php
@@ -76,6 +76,90 @@
 
         <div class="row g-4">
             <!-- Pages les plus visitées -->
+            <div class="g-col-12 mb-4">
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">Pages les plus visitées (24H)</h5>
+                        <div class="table-responsive">
+                            <table class="table table-striped align-middle">
+                                <thead>
+                                <tr>
+                                    <th scope="col">#</th>
+                                    <th scope="col">Page</th>
+                                    <th scope="col">Nombre de visites</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach ($mostVisitedPagesForPastTwentyFourHours as $index => $page)
+                                    <tr>
+                                        <th scope="row">{{ $index + 1 }}</th>
+                                        <td>{{ $page['url'] }}</td>
+                                        <td>{{ $page['count'] }}</td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="g-col-12 mb-4">
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">Pages les plus visitées (7 jours)</h5>
+                        <div class="table-responsive">
+                            <table class="table table-striped align-middle">
+                                <thead>
+                                <tr>
+                                    <th scope="col">#</th>
+                                    <th scope="col">Page</th>
+                                    <th scope="col">Nombre de visites</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach ($mostVisitedPagesForPastSevenDays as $index => $page)
+                                    <tr>
+                                        <th scope="row">{{ $index + 1 }}</th>
+                                        <td>{{ $page['url'] }}</td>
+                                        <td>{{ $page['count'] }}</td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="g-col-12 mb-4">
+                <div class="card">
+                    <div class="card-body">
+                        <h5 class="card-title">Pages les plus visitées (30 jours)</h5>
+                        <div class="table-responsive">
+                            <table class="table table-striped align-middle">
+                                <thead>
+                                <tr>
+                                    <th scope="col">#</th>
+                                    <th scope="col">Page</th>
+                                    <th scope="col">Nombre de visites</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                @foreach ($mostVisitedPagesForPastThirtyDays as $index => $page)
+                                    <tr>
+                                        <th scope="row">{{ $index + 1 }}</th>
+                                        <td>{{ $page['url'] }}</td>
+                                        <td>{{ $page['count'] }}</td>
+                                    </tr>
+                                @endforeach
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
             <div class="g-col-12">
                 <div class="card">
                     <div class="card-body">
diff --git a/resources/views/components/public/footer.blade.php b/resources/views/components/public/footer.blade.php
index 05142fdf514118c6824e2c1982921cb4f779788b..de488c30d597cdcf66ee759c5d8e1a0adce70b94 100644
--- a/resources/views/components/public/footer.blade.php
+++ b/resources/views/components/public/footer.blade.php
@@ -8,20 +8,20 @@
                         <x-public.rann-logo height="4rem" role="image"/>
                     </div>
                     <div class="self-stretch flex-col justify-start items-start gap-2 flex">
-                        <div class="self-stretch text-2xl font-bold">Hâte de travailler avec vous !</div>
-                        <div class="self-stretch text-muted">N’hésitez pas à me contacter si vous avez besoin de
-                            renseignements, ou que vous souhaitez simplement discuter avec moi.
+                        <div class="self-stretch text-2xl font-bold">{{ __('footer.contact.title') }}</div>
+                        <div class="self-stretch text-muted">
+                            {{ __('footer.contact.desc') }}
                         </div>
                     </div>
-                    <x-public.button size="medium" tag="a" href="#">
-                        <div class="text-xl font-bold">Me contacter</div>
+                    <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
+                        <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
                     </x-public.button>
                 </div>
             </div>
 
             <div class="w-full lg:w-auto items-start flex flex-col lg:flex-row gap-8 lg:gap-24">
                 <div class="w-full lg:w-56 flex-col gap-8 flex">
-                    <div class="self-stretch text-2xl font-bold">Restons en contact</div>
+                    <div class="self-stretch text-2xl font-bold">{{ __('footer.links.title') }}</div>
                     <ul class="flex flex-col gap-2">
                         @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink)
                             <li>
@@ -33,16 +33,15 @@
                     </ul>
                 </div>
                 <div class="w-full lg:w-56 flex-col gap-8 flex">
-                    <div class="self-stretch text-2xl font-bold">Informations légales</div>
+                    <div class="self-stretch text-2xl font-bold">{{ __('footer.legals-infos.title') }}</div>
                     <ul class="flex flex-col gap-2">
                         <li>
-                            <a href="#" class="text-muted hover:text-primary">Mentions légales</a>
+                            <a href="{{ route('legal-mentions') }}"
+                               class="text-muted hover:text-primary">{{ __('generic.legal_mentions') }}</a>
                         </li>
                         <li>
-                            <a href="#" class="text-muted hover:text-primary">Conditions générales de vente</a>
-                        </li>
-                        <li>
-                            <a href="#" class="text-muted hover:text-primary">Politique de confidentialité</a>
+                            <a href="{{ route('terms') }}"
+                               class="text-muted hover:text-primary">{{ __('generic.terms') }}</a>
                         </li>
                     </ul>
                 </div>
diff --git a/resources/views/components/public/generic-page-header.blade.php b/resources/views/components/public/generic-page-header.blade.php
index 7883746c0253491d0b38ca139d379b74f96a19ac..defaac73b1c21652eacf74dd4be847ddcc368361 100644
--- a/resources/views/components/public/generic-page-header.blade.php
+++ b/resources/views/components/public/generic-page-header.blade.php
@@ -1,6 +1,6 @@
 @props([
     'title' => 'Title',
-    'description' => 'Description',
+    'description' => '',
 ])
 
 <div {{ $attributes->class(['relative flex flex-col max-w-2xl gap-3']) }}>
diff --git a/resources/views/components/public/navbar.blade.php b/resources/views/components/public/navbar.blade.php
index 0c0612b1bfd4171d88c7cc3d51e6daad738b95b4..f4d69177c983f15c502f55f274dc573c730ff346 100644
--- a/resources/views/components/public/navbar.blade.php
+++ b/resources/views/components/public/navbar.blade.php
@@ -17,7 +17,7 @@
                 <a href="{{ route('prestations') }}" class="text-xl font-bold">{{ __('navbar.services') }}</a>
                 <a href="{{ route('portfolio') }}" class="text-xl font-bold">{{ __('navbar.portfolio') }}</a>
                 <a href="{{ route('evenements') }}" class="text-xl font-bold">{{ __('navbar.events') }}</a>
-                <a href="#" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a>
+                <a href="{{ route('contact') }}" class="text-xl font-bold">{{ __('navbar.contact_me') }}</a>
                 <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/">
                     {{ __('navbar.shop') }}
                 </x-public.button>
@@ -41,7 +41,7 @@ class="fixed z-50 inset-0 bg-white bg-opacity-90 flex-col items-center justify-c
         <a href="{{ route('prestations') }}" class="text-2xl font-bold">{{ __('navbar.services') }}</a>
         <a href="{{ route('portfolio') }}" class="text-2xl font-bold">{{ __('navbar.portfolio') }}</a>
         <a href="{{ route('evenements') }}" class="text-2xl font-bold">{{ __('navbar.events') }}</a>
-        <a href="#" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a>
+        <a href="{{ route('contact') }}" class="text-2xl font-bold">{{ __('navbar.contact_me') }}</a>
         <x-public.button tag="a" href="https://dalnarabyrann.sumupstore.com/" class="text-xl">
             {{ __('navbar.shop') }}
         </x-public.button>
diff --git a/resources/views/layouts/public.blade.php b/resources/views/layouts/public.blade.php
index f2a8ee119fe0b91ff7bf298db03819fd2e8c710d..c465a550b7de938c2efd1668d78a98851c309d9a 100644
--- a/resources/views/layouts/public.blade.php
+++ b/resources/views/layouts/public.blade.php
@@ -5,8 +5,26 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ !empty($title) ? $title . ' - Rann Graphic Design' : 'Rann Graphic Design' }}</title>
     <link rel="icon" type="image/svg+xml" href="{{ asset('favicon.svg') }}"/>
+
+    <meta property="og:locale" content="{{ config('app.locale') }}">
+    <meta property="og:locale:alternate" content="{{ config('app.fallback_locale') }}">
+
+    <meta property="og:title" content="{{ !empty($title) ? $title : 'Rann Graphic Design' }}">
+    <meta property="og:type" content="website">
+    <meta property="og:url" content="{{ url()->current() }}">
+    @if(!empty($description))
+        <meta name="description" content="{{ $description }}">
+        <meta property="og:description" content="{{ $description }}">
+    @endif
+    @if(!empty($image) && is_a($image, \App\Models\UploadedPicture::class))
+        <meta property="og:image" content="{{ $image->getFullsizeUrl() }}">
+        <meta property="og:image:width" content="{{ $image->width }}">
+        <meta property="og:image:height" content="{{ $image->height }}">
+    @endif
+
     <link rel="preconnect" href="https://fonts.googleapis.com">
     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    <link href="https://fonts.googleapis.com/css2?family=Sniglet:wght@400;800&display=swap" rel="stylesheet">
     @vite(['resources/scss/public.scss', 'resources/js/app.js'])
 </head>
 <body>
diff --git a/resources/views/public/contact.blade.php b/resources/views/public/contact.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..74e56ac3a417a67230e323a6e33024e1ae361cfe
--- /dev/null
+++ b/resources/views/public/contact.blade.php
@@ -0,0 +1,30 @@
+@extends('layouts.public', ['title' => __('navbar.contact_me'), 'description' => __('contact.desc')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('navbar.contact_me') }}" :description="__('contact.desc')"/>
+
+        <div>
+            <div class="text-center text-2xl">
+                {{ __('contact.contact_me_on_one_of_my_social_media') }}
+            </div>
+
+            <div class="flex flex-wrap align-center justify-center gap-4 mt-4">
+                @foreach(\App\Models\SocialMediaLink::all() as $socialMediaLink)
+                    <x-public.button tag="a" :href="$socialMediaLink->url" class="flex items-center gap-2"
+                                     size="medium">
+                        <div>
+                            {{ $socialMediaLink->name }}
+                        </div>
+                        <div>
+                            <x-font-awesome :icon="$socialMediaLink->icon_name" type="brands" class="w-6 h-6"/>
+                        </div>
+                    </x-public.button>
+                @endforeach
+            </div>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/resources/views/public/index.blade.php b/resources/views/public/index.blade.php
index 72ec9b60bf89d33f28586896b118e7ec2be50ec5..ed2c352e43576e2d6270065ef23cbba0c12874b3 100644
--- a/resources/views/public/index.blade.php
+++ b/resources/views/public/index.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public')
+@extends('layouts.public', ['description' => __('home.meta_description')])
 
 @section('content')
     <section class="HeroSection position-relative w-100">
@@ -21,11 +21,12 @@ class="absolute hidden 2xl:block left-[-96px] top-[-26px] origin-top-left rotate
                                     </span>
                                     <x-public.arrow class="hidden xl:block w-28 h-9"/>
                                 </span>
-                                <span class="text-5xl xl:text-6xl font-bold">
+                                <span class="text-5xl xl:text-6xl">
                                     {{ __('home.hero.second_row') }}
                                 </span>
                             </h1>
-                            <x-public.button size="large" tag="a" href="#" class="flex items-center gap-8">
+                            <x-public.button size="large" tag="a" href="{{ route('contact') }}"
+                                             class="flex items-center gap-8">
                                 <img class="w-10 h-10" src="{{Vite::asset("resources/images/public/waving-hand.avif")}}"
                                      alt="{{ __('home.img_alt.waving_hand') }}" loading="eager"/>
                                 <div class="flex flex-col justify-center items-start">
@@ -71,18 +72,8 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/>
         <div class="w-full h-24 bg-gradient-to-b from-neutral-200 to-light"></div>
         <div class="container mx-auto px-4 flex flex-col justify-start items-center gap-2.5">
             <div class="flex flex-col justify-start items-start w-full gap-16">
-                <div class="flex flex-col justify-start items-start max-w-6xl">
-                    <div class="text-2xl font-bold">{{ __('home.about_me') }}</div>
-                    <h2 class="text-5xl xl:text-6xl font-normal">
-                        @if(!$aboutMeSection->isEmpty())
-                            {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }}
-                        @else
-                            Lorem ipsum dolor sit amet,
-                        @endif
-                    </h2>
-                </div>
-                <div class="flex flex-col xl:flex-row justify-start items-center w-full gap-4 xl:gap-0">
-                    <div class="flex shrink-0 basis-1/2 justify-center items-center p-4 xl:p-0 gap-2.5">
+                <div class="flex flex-col xl:flex-row justify-start w-full gap-8 xl:gap-16">
+                    <div class="flex shrink-0 justify-center items-center p-4 xl:p-0 gap-2.5">
                         <div class="overflow-hidden w-full xl:w-[25rem] aspect-square rounded-2xl shadow-[-.5rem_.5rem_0px_0px_rgba(0,0,0,1.00)] shadow-pink-normal">
                             @if(!$aboutMeSection->isEmpty())
                                 <img class="w-full h-full object-cover"
@@ -95,8 +86,17 @@ class="Creations relative hidden xl:grid grow w-full aspect-[885/622]"/>
                             @endif
                         </div>
                     </div>
-                    <div class="flex flex-col shrink-0 basis-1/2 py-2.5 justify-center items-start gap-4">
-                        <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-3xl font-normal">
+                    <div class="flex flex-col py-2.5 items-start gap-4">
+                        <div class="text-2xl font-bold">{{ __('home.about_me') }}</div>
+                        <h2 class="text-5xl xl:text-6xl font-normal mb-8">
+                            @if(!$aboutMeSection->isEmpty())
+                                {{ $aboutMeSection->first()->attractDescTransKey->getTranslation() }}
+                            @else
+                                Lorem ipsum dolor sit amet,
+                            @endif
+                        </h2>
+
+                        <x-markdown class="flex flex-col gap-3 text-muted text-xl xl:text-2xl font-normal">
                             @if(!$aboutMeSection->isEmpty())
                                 {{ $aboutMeSection->first()->contentTransKey->getTranslation() }}
                             @else
diff --git a/resources/views/public/legal-mentions.blade.php b/resources/views/public/legal-mentions.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..65d3c5cc42d7c0a94a6e7782337eb2535d96c795
--- /dev/null
+++ b/resources/views/public/legal-mentions.blade.php
@@ -0,0 +1,16 @@
+@extends('layouts.public', ['title' => __('generic.legal_mentions')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('generic.legal_mentions') }}"/>
+        <div>
+            <x-markdown class="markdown">
+                {{ $activeLegalMentionsText }}
+            </x-markdown>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/resources/views/public/maintenance.blade.php b/resources/views/public/maintenance.blade.php
index 0df958413fd3073599f2ed770dc9887ff542ceb7..15da0114fd74c35f542237191938e6c3cf5a107c 100644
--- a/resources/views/public/maintenance.blade.php
+++ b/resources/views/public/maintenance.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => 'Maintenance'])
+@extends('layouts.public', ['title' => 'Maintenance', 'description' => 'Site en construction...'])
 
 @section('content')
     <div class="inline-flex w-full min-h-screen flex-col justify-center items-center gap-2.5"
diff --git a/resources/views/public/portfolio-show.blade.php b/resources/views/public/portfolio-show.blade.php
index 9a8127f9722df173ee58f04ba33b39f773de5bca..bd3173a5e49d5173275150093e37c4031cea937a 100644
--- a/resources/views/public/portfolio-show.blade.php
+++ b/resources/views/public/portfolio-show.blade.php
@@ -1,4 +1,8 @@
-@extends('layouts.public', ['title' => $creation->nameTranslationKey->getTranslation()])
+@extends('layouts.public', [
+    'title' => $creation->nameTranslationKey->getTranslation(),
+    'description' => $creation->shortDescriptionTranslationKey->getTranslation(),
+    'image' => $creation->coverUploadedPicture
+])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
diff --git a/resources/views/public/portfolio.blade.php b/resources/views/public/portfolio.blade.php
index 11eb8b9f18bfd49d4f1d7269f0d2be519117f94a..5f001c4f7e15b224c42256fb73f56c73634e2ea6 100644
--- a/resources/views/public/portfolio.blade.php
+++ b/resources/views/public/portfolio.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => __('navbar.portfolio')])
+@extends('layouts.public', ['title' => __('navbar.portfolio'), 'description' => __('services.description')])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
diff --git a/resources/views/public/prestation.blade.php b/resources/views/public/prestation.blade.php
index 7fd1dc6cba32231a337c539297609523825db2db..e998491d84ac3f4faee9d8a030233cde6eca272c 100644
--- a/resources/views/public/prestation.blade.php
+++ b/resources/views/public/prestation.blade.php
@@ -1,4 +1,8 @@
-@extends('layouts.public', ['title' => $prestation->nameTransKey->getTranslation()])
+@extends('layouts.public', [
+    'title' => $prestation->nameTransKey->getTranslation(),
+    'description' => $prestation->attractDescTransKey->getTranslation(),
+    'image' => $prestation->uploadedPicture
+])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
@@ -45,7 +49,7 @@ class="w-full relative aspect-square"/>
                 {{ __('generic.custom_needs_desc') }}
             </div>
         </div>
-        <x-public.button size="medium" tag="a" href="#">
+        <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
             <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
         </x-public.button>
     </div>
diff --git a/resources/views/public/prestations.blade.php b/resources/views/public/prestations.blade.php
index 5d052fa588e10abe72c7c0f7580a09c50b906776..20b99d8a7fffe9978cb0085f02501ad9b0fc8614 100644
--- a/resources/views/public/prestations.blade.php
+++ b/resources/views/public/prestations.blade.php
@@ -1,4 +1,4 @@
-@extends('layouts.public', ['title' => __('navbar.services')])
+@extends('layouts.public', ['title' => __('navbar.services'), 'description' => __('services.attract_desc')])
 
 @section('content')
     <x-public.navbar class="container mx-auto px-4"/>
@@ -17,7 +17,7 @@
                 {{ __('generic.custom_needs_desc') }}
             </div>
         </div>
-        <x-public.button size="medium" tag="a" href="#">
+        <x-public.button size="medium" tag="a" href="{{ route('contact') }}">
             <div class="text-xl font-bold">{{ __('navbar.contact_me') }}</div>
         </x-public.button>
     </div>
diff --git a/resources/views/public/terms.blade.php b/resources/views/public/terms.blade.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7d919ab651ef56b0fc097ccca6d36e41e0cdc9d
--- /dev/null
+++ b/resources/views/public/terms.blade.php
@@ -0,0 +1,16 @@
+@extends('layouts.public', ['title' => __('generic.terms')])
+
+@section('content')
+    <x-public.navbar class="container mx-auto px-4"/>
+
+    <div class="container mx-auto px-4 py-24 flex flex-col gap-16">
+        <x-public.generic-page-header title="{{ __('generic.terms') }}"/>
+        <div>
+            <x-markdown class="markdown">
+                {{ $activeTermsText }}
+            </x-markdown>
+        </div>
+    </div>
+
+    <x-public.footer/>
+@endsection
\ No newline at end of file
diff --git a/routes/console.php b/routes/console.php
index eb1048cbc16399ebdcf4476a2d3fc9310096d8e0..37195099b5989fd118ad261513cef434bea7a23b 100644
--- a/routes/console.php
+++ b/routes/console.php
@@ -8,6 +8,8 @@
 })->purpose('Display an inspiring quote')->hourly();
 
 Schedule::command('save:requests')->everyMinute()->sentryMonitor();
+Schedule::command('process:user-agents')->hourly()->sentryMonitor();
+Schedule::command('process:ip-adresses')->everyFiveMinutes()->sentryMonitor();
 Schedule::command('flush:unused-uploaded-pictures')->daily()->sentryMonitor();
 
 Schedule::command('backup:clean --disable-notifications')->daily()->at('01:00');
diff --git a/routes/web.php b/routes/web.php
index aa637dc52583b1a3b5e1c9d22841990f31785ee2..d006a190a22dd7a0926f7202c57c29cd08fbcdd9 100644
--- a/routes/web.php
+++ b/routes/web.php
@@ -11,8 +11,10 @@
 use App\Http\Controllers\Admin\SocialMediaLinksController;
 use App\Http\Controllers\Admin\TermsSectionController;
 use App\Http\Controllers\Public\IndexController;
+use App\Http\Controllers\Public\LegalMentionsController;
 use App\Http\Controllers\Public\PortfolioController;
 use App\Http\Controllers\Public\PrestationController as PublicPrestationController;
+use App\Http\Controllers\Public\TermsController;
 use App\Http\Middleware\CheckPrivateModeMiddleware;
 use App\Http\Middleware\RedirectIfUserExistsMiddleware;
 use Illuminate\Support\Facades\Route;
@@ -29,6 +31,9 @@
     Route::get('/portfolio/api', [PortfolioController::class, 'api'])->name('portfolio.api');
     Route::get('/portfolio/{slug}', [PortfolioController::class, 'show'])->name('portfolio.show');
     Route::get('/prestations/{slug}', [PublicPrestationController::class, 'index'])->name('prestations.show');
+    Route::get('/mentions-legales', LegalMentionsController::class)->name('legal-mentions');
+    Route::get('/conditions-generales-de-vente', TermsController::class)->name('terms');
+    Route::view('/contact', 'public.contact')->name('contact');
 
     Route::view('/maintenance', 'public.maintenance')->name('maintenance');
 });
@@ -74,6 +79,8 @@
             ->name('delete');
         Route::delete('/{creation}/remove-addtionnal-image/{image}', [CreationController::class, 'removeAdditionalImage'])
             ->name('remove-additionnal-image');
+        Route::get('/{creation}/translate-with-ai', [CreationController::class, 'translateWithAi'])
+            ->name('translate-with-ai');
     });
 
     Route::prefix('social-media-links')->name('social-media-links.')->group(function () {
diff --git a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
similarity index 86%
rename from tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php
rename to tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
index 0d30022ee60d508e8dbaf02a80e4fb216bda598c..c362a64e7c121bee5b6d32811e0b114a02823342 100644
--- a/tests/Feature/Console/Command/FlushUnusedUploadedPicturesCommandTest.php
+++ b/tests/Feature/Console/Commands/FlushUnusedUploadedPicturesCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\FlushUnusedUploadedPicturesCommand;
 use App\Models\UploadedPicture;
@@ -9,7 +9,8 @@
 use PHPUnit\Framework\Attributes\CoversClass;
 use Tests\TestCase;
 
-#[CoversClass(FlushUnusedUploadedPicturesCommand::class)] class FlushUnusedUploadedPicturesCommandTest extends TestCase
+#[CoversClass(FlushUnusedUploadedPicturesCommand::class)]
+class FlushUnusedUploadedPicturesCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
similarity index 89%
rename from tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php
rename to tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
index f9f09eefc283cf17693a50f4373d9818e0c10848..8863523704bf1d11e5b1cb36f88ba5cbdf012954 100644
--- a/tests/Feature/Console/Command/OptimizeUploadedPicturesCommandTest.php
+++ b/tests/Feature/Console/Commands/OptimizeUploadedPicturesCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\OptimizeUploadedPicturesCommand;
 use App\Models\UploadedPicture;
@@ -12,7 +12,8 @@
 /**
  * Tests de la commande d'optimisation des images uploadées
  */
-#[CoversClass(OptimizeUploadedPicturesCommand::class)] class OptimizeUploadedPicturesCommandTest extends TestCase
+#[CoversClass(OptimizeUploadedPicturesCommand::class)]
+class OptimizeUploadedPicturesCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..87153e4c3ca3e11f8d6d9d78003014968bbadf11
--- /dev/null
+++ b/tests/Feature/Console/Commands/ProcessIpAdressesCommandTest.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Tests\Feature\Console\Commands;
+
+use App\Console\Commands\ProcessIpAdressesCommand;
+use App\Jobs\ProcessIpAddressesJob;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Queue;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(ProcessIpAdressesCommand::class)]
+class ProcessIpAdressesCommandTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_it_dispatches_job()
+    {
+        Queue::fake();
+        IpAddress::factory()->count(10)->create();
+
+        Artisan::call('process:ip-adresses');
+
+        Queue::assertPushed(ProcessIpAddressesJob::class);
+    }
+}
diff --git a/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..c6390dc1a8ee4b9cd4845a767bf1514ef6dbb4ae
--- /dev/null
+++ b/tests/Feature/Console/Commands/ProcessUserAgentsCommandTest.php
@@ -0,0 +1,27 @@
+<?php
+
+namespace Tests\Feature\Console\Commands;
+
+use App\Console\Commands\ProcessUserAgentsCommand;
+use App\Jobs\ProcessUserAgentJob;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Queue;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\UserAgent;
+use Tests\TestCase;
+
+#[CoversClass(ProcessUserAgentsCommand::class)]
+class ProcessUserAgentsCommandTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_it_dispatches_job()
+    {
+        Queue::fake();
+        UserAgent::factory()->count(10)->create();
+
+        $this->artisan('process:user-agents');
+
+        Queue::assertPushed(ProcessUserAgentJob::class, 10);
+    }
+}
diff --git a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
similarity index 92%
rename from tests/Feature/Console/Command/UpdateImageDimensionsTest.php
rename to tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
index df50701450a1af1058cbe8f37272250ab2420b43..99b45783e993c20d5a5d156556b09330626dc42c 100644
--- a/tests/Feature/Console/Command/UpdateImageDimensionsTest.php
+++ b/tests/Feature/Console/Commands/UpdateImageDimensionsCommandTest.php
@@ -1,6 +1,6 @@
 <?php
 
-namespace Tests\Feature\Console\Command;
+namespace Tests\Feature\Console\Commands;
 
 use App\Console\Commands\UpdateImageDimensionsCommand;
 use App\Models\UploadedPicture;
@@ -15,7 +15,8 @@
 /**
  * Tests de la commande de mise à jour des dimensions des images
  */
-#[CoversClass(UpdateImageDimensionsCommand::class)] class UpdateImageDimensionsTest extends TestCase
+#[CoversClass(UpdateImageDimensionsCommand::class)]
+class UpdateImageDimensionsCommandTest extends TestCase
 {
     use RefreshDatabase;
 
diff --git a/tests/Feature/Jobs/ProcessIpAddressesJobTest.php b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..005713708d5713d40fe35eb84d601cc9b826f14c
--- /dev/null
+++ b/tests/Feature/Jobs/ProcessIpAddressesJobTest.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace Tests\Feature\Jobs;
+
+use App\Jobs\ProcessIpAddressesJob;
+use App\Services\IpAddressMetadataResolverService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Http;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(ProcessIpAddressesJob::class)]
+class ProcessIpAddressesJobTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_processes_ip_addresses_successfully()
+    {
+        $ip1 = IpAddress::factory()->create(['ip' => '208.80.152.201']);
+        $ip2 = IpAddress::factory()->create(['ip' => '24.48.0.1']);
+
+        Http::fake([
+            '*' => Http::response([
+                [
+                    'status' => 'success',
+                    'countryCode' => 'US',
+                    'lat' => 37.7892,
+                    'lon' => -122.402,
+                    'query' => '208.80.152.201',
+                ],
+                [
+                    'status' => 'success',
+                    'countryCode' => 'CA',
+                    'lat' => 45.6085,
+                    'lon' => -73.5493,
+                    'query' => '24.48.0.1',
+                ],
+            ]),
+        ]);
+
+        $job = new ProcessIpAddressesJob(collect([$ip1, $ip2]));
+        $job->handle(app(IpAddressMetadataResolverService::class));
+
+        $this->assertDatabaseHas('ip_address_metadata', [
+            'ip_address_id' => $ip1->id,
+            'country_code' => 'US',
+            'lat' => 37.7892,
+            'lon' => -122.402,
+        ]);
+
+        $this->assertDatabaseHas('ip_address_metadata', [
+            'ip_address_id' => $ip2->id,
+            'country_code' => 'CA',
+            'lat' => 45.6085,
+            'lon' => -73.5493,
+        ]);
+    }
+}
diff --git a/tests/Feature/Service/AiProviderServiceTest.php b/tests/Feature/Service/AiProviderServiceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..1de43af3942ac979ba9f366f06300529db396eb9
--- /dev/null
+++ b/tests/Feature/Service/AiProviderServiceTest.php
@@ -0,0 +1,163 @@
+<?php
+
+namespace Tests\Feature\Service;
+
+use App\Models\UploadedPicture;
+use App\Services\AiProviderService;
+use App\Services\ImageTranscodingService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\App;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\Facades\Http;
+use Illuminate\Support\Facades\Storage;
+use Mockery;
+use PHPUnit\Framework\Attributes\CoversClass;
+use RuntimeException;
+use Tests\TestCase;
+
+#[CoversClass(AiProviderService::class)]
+class AiProviderServiceTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private array $sampleResponse = [
+        'id' => 'chat_1',
+        'object' => 'chat.completion',
+        'created' => 1739718124,
+        'model' => 'gpt-4o-mini',
+        'choices' => [
+            [
+                'index' => 0,
+                'message' => [
+                    'role' => 'assistant',
+                    'content' => "{\n  \"message\": \"Why don't scientists trust atoms? Because they make up everything!\"\n}",
+                    'refusal' => null,
+                ],
+                'logprobs' => null,
+                'finish_reason' => 'stop',
+            ],
+        ],
+        'usage' => [
+            'prompt_tokens' => 33,
+            'completion_tokens' => 20,
+            'total_tokens' => 53,
+            'prompt_tokens_details' => [
+                'cached_tokens' => 0,
+                'audio_tokens' => 0,
+            ],
+            'completion_tokens_details' => [
+                'reasoning_tokens' => 0,
+                'audio_tokens' => 0,
+                'accepted_prediction_tokens' => 0,
+                'rejected_prediction_tokens' => 0,
+            ],
+        ],
+        'service_tier' => 'default',
+        'system_fingerprint' => 'system-1',
+    ];
+
+    public function test_prompt_with_pictures_sends_correct_request()
+    {
+        Storage::fake('public');
+        Http::fake([
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
+        ]);
+
+        $mockTranscodingService = Mockery::mock(ImageTranscodingService::class);
+        $mockTranscodingService->shouldReceive('transcode')
+            ->andReturn('transcoded-image-content');
+        App::instance(ImageTranscodingService::class, $mockTranscodingService);
+
+        Config::set('ai-provider.selected-provider', 'test-provider');
+        Config::set('ai-provider.providers.test-provider', [
+            'api-key' => 'test-api-key',
+            'url' => 'https://api.test-provider.com',
+            'model' => 'test-model',
+            'max-tokens' => 100,
+        ]);
+
+        $uploadedPicture = UploadedPicture::factory()->create();
+        $service = new AiProviderService;
+
+        $response = $service->promptWithPictures(
+            'You are a helpful assistant.',
+            'Describe this image.',
+            $uploadedPicture
+        );
+
+        $this->assertEquals(['message' => "Why don't scientists trust atoms? Because they make up everything!"], $response);
+    }
+
+    public function test_prompt_sends_correct_request()
+    {
+        Http::fake([
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
+        ]);
+
+        Config::set('ai-provider.selected-provider', 'test-provider');
+        Config::set('ai-provider.providers.test-provider', [
+            'api-key' => 'test-api-key',
+            'url' => 'https://api.test-provider.com',
+            'model' => 'test-model',
+            'max-tokens' => 100,
+        ]);
+
+        $service = new AiProviderService;
+
+        $response = $service->prompt(
+            'You are a helpful assistant.',
+            'Tell me a joke.'
+        );
+
+        $this->assertEquals(['message' => "Why don't scientists trust atoms? Because they make up everything!"], $response);
+    }
+
+    public function test_prompt_with_pictures_handles_transcoding_failure()
+    {
+        Storage::fake('public');
+        Http::fake([
+            'https://api.test-provider.com' => Http::response($this->sampleResponse),
+        ]);
+
+        $mockTranscodingService = Mockery::mock(ImageTranscodingService::class);
+        $mockTranscodingService->shouldReceive('transcode')
+            ->andReturn(null);
+        App::instance(ImageTranscodingService::class, $mockTranscodingService);
+
+        $uploadedPicture = UploadedPicture::factory()->create();
+        $service = new AiProviderService;
+
+        $this->expectException(RuntimeException::class);
+        $this->expectExceptionMessage('Failed to transcode picture');
+
+        $service->promptWithPictures(
+            'You are a helpful assistant.',
+            'Describe this image.',
+            $uploadedPicture
+        );
+    }
+
+    public function test_prompt_handles_api_failure()
+    {
+        Http::fake([
+            'https://api.test-provider.com' => Http::response('Error', 500),
+        ]);
+
+        Config::set('ai-provider.selected-provider', 'test-provider');
+        Config::set('ai-provider.providers.test-provider', [
+            'api-key' => 'test-api-key',
+            'url' => 'https://api.test-provider.com',
+            'model' => 'test-model',
+            'max-tokens' => 100,
+        ]);
+
+        $service = new AiProviderService;
+
+        $this->expectException(RuntimeException::class);
+
+        $service->prompt(
+            'You are a helpful assistant.',
+            'Tell me a joke.'
+        );
+    }
+}
diff --git a/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..dcb6c86b358cbf6c93a836a1711927efbfc5e395
--- /dev/null
+++ b/tests/Feature/Service/IpAddressMetadataResolverServiceTest.php
@@ -0,0 +1,51 @@
+<?php
+
+namespace Tests\Feature\Service;
+
+use App\Services\IpAddressMetadataResolverService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Exceptions;
+use Illuminate\Support\Facades\Http;
+use PHPUnit\Framework\Attributes\CoversClass;
+use SlProjects\LaravelRequestLogger\app\Models\IpAddress;
+use Tests\TestCase;
+
+#[CoversClass(IpAddressMetadataResolverService::class)]
+class IpAddressMetadataResolverServiceTest extends TestCase
+{
+    use RefreshDatabase;
+
+    public function test_resolves_ip_addresses_successfully()
+    {
+        Exceptions::fake();
+
+        Http::fake([
+            '*' => Http::response([
+                [
+                    'status' => 'success',
+                    'countryCode' => 'US',
+                    'lat' => 37.7892,
+                    'lon' => -122.402,
+                    'query' => '208.80.152.201',
+                ],
+                [
+                    'status' => 'success',
+                    'countryCode' => 'CA',
+                    'lat' => 45.6085,
+                    'lon' => -73.5493,
+                    'query' => '24.48.0.1',
+                ],
+            ]),
+        ]);
+
+        $service = new IpAddressMetadataResolverService;
+
+        $ipAddresses = IpAddress::factory()->count(2)->create();
+        $result = $service->resolve($ipAddresses);
+
+        Exceptions::assertNothingReported();
+        $this->assertCount(2, $result);
+        $this->assertEquals('US', $result[0]['countryCode']);
+        $this->assertEquals('CA', $result[1]['countryCode']);
+    }
+}