assertCreationAllowed($user); $this->assertDailyLimit($user); $validated = $this->validator->validateUpload($file, $options); $source = $this->storage->storeUploadedSource($user, $file); $job = DB::transaction(function () use ($user, $validated, $source): EnhanceJob { $enhanceJob = EnhanceJob::query()->create($validated + $source + [ 'user_id' => (int) $user->id, 'status' => EnhanceJob::STATUS_PENDING, ]); $this->queue($enhanceJob); return $enhanceJob->fresh(); }); Log::info('enhance.job.created', [ 'enhance_job_id' => $job->id, 'user_id' => $user->id, 'type' => 'upload', 'engine' => $job->engine, ]); return $job; } public function createFromArtwork(User $user, Artwork $artwork, array $options): EnhanceJob { $this->assertCreationAllowed($user); $this->assertDailyLimit($user); $artworkSource = $this->storage->fetchArtworkSource($artwork); $validated = $this->validator->validateBinary( $artworkSource['binary'], $options, (int) ($artwork->file_size ?? strlen((string) $artworkSource['binary'])), ); $source = $this->storage->storeSourceBinary($user, (string) $artworkSource['binary'], (string) $artworkSource['extension']); $job = DB::transaction(function () use ($user, $artwork, $validated, $source): EnhanceJob { $enhanceJob = EnhanceJob::query()->create($validated + $source + [ 'user_id' => (int) $user->id, 'artwork_id' => (int) $artwork->id, 'status' => EnhanceJob::STATUS_PENDING, ]); $this->queue($enhanceJob); return $enhanceJob->fresh(); }); Log::info('enhance.job.created', [ 'enhance_job_id' => $job->id, 'user_id' => $user->id, 'artwork_id' => $artwork->id, 'type' => 'artwork', 'engine' => $job->engine, ]); return $job; } public function retry(EnhanceJob $job): EnhanceJob { Log::info('enhance.retry.started', [ 'enhance_job_id' => $job->id, 'user_id' => $job->user_id, 'status' => $job->status, ]); if (! $job->isFailed()) { throw ValidationException::withMessages([ 'job' => 'Only failed enhance jobs can be retried.', ]); } if (! $this->sourceExists($job)) { Log::warning('enhance.retry.failed_missing_source', [ 'enhance_job_id' => $job->id, 'user_id' => $job->user_id, ]); throw ValidationException::withMessages([ 'job' => 'This enhance job can no longer be retried because the original source file was cleaned up.', ]); } DB::transaction(function () use ($job): void { $this->storage->deleteGeneratedFiles($job); $metadata = is_array($job->metadata) ? $job->metadata : []; $retryCount = max(0, (int) ($metadata['retry_count'] ?? 0)) + 1; $job->forceFill([ 'status' => EnhanceJob::STATUS_QUEUED, 'output_disk' => null, 'output_path' => null, 'output_hash' => null, 'output_width' => null, 'output_height' => null, 'output_filesize' => null, 'output_mime' => null, 'preview_disk' => null, 'preview_path' => null, 'processing_seconds' => null, 'error_message' => null, 'started_at' => null, 'finished_at' => null, 'queued_at' => now(), 'metadata' => array_merge($metadata, [ 'retry_count' => $retryCount, 'last_retried_at' => now()->toIso8601String(), ]), ])->save(); ProcessEnhanceJob::dispatch((int) $job->id)->afterCommit(); }); Log::info('enhance.retry.dispatched', [ 'enhance_job_id' => $job->id, 'user_id' => $job->user_id, 'retry_count' => (int) (($job->fresh()?->metadata['retry_count'] ?? 0)), ]); return $job->fresh(); } public function markFailedByModerator(EnhanceJob $job, User $actor): EnhanceJob { if (! in_array($job->status, [EnhanceJob::STATUS_PENDING, EnhanceJob::STATUS_QUEUED, EnhanceJob::STATUS_PROCESSING], true)) { throw ValidationException::withMessages([ 'job' => 'Only pending, queued, or processing jobs can be marked as failed.', ]); } $metadata = is_array($job->metadata) ? $job->metadata : []; DB::transaction(function () use ($job, $actor, $metadata): void { $job->forceFill([ 'status' => EnhanceJob::STATUS_FAILED, 'error_message' => 'Marked as failed by moderator.', 'finished_at' => now(), 'processing_seconds' => $job->started_at ? max(0, now()->diffInSeconds($job->started_at)) : $job->processing_seconds, 'metadata' => array_merge($metadata, [ 'moderation' => [ 'marked_failed_at' => now()->toIso8601String(), 'marked_failed_by' => (int) $actor->id, ], ]), ])->save(); }); Log::info('enhance.moderation.mark_failed', [ 'enhance_job_id' => $job->id, 'moderator_id' => $actor->id, ]); return $job->fresh(); } public function delete(EnhanceJob $job): void { DB::transaction(function () use ($job): void { $this->storage->deleteFiles($job); $job->delete(); }); Log::info('enhance.job.deleted', [ 'enhance_job_id' => $job->id, 'user_id' => $job->user_id, ]); } private function assertCreationAllowed(User $user): void { if (method_exists($user, 'hasVerifiedEmail') && ! $user->hasVerifiedEmail()) { throw ValidationException::withMessages([ 'image' => 'Please verify your email address before using Skinbase Enhance.', ]); } } private function assertDailyLimit(User $user): void { $limit = max(0, (int) config('enhance.daily_limit', 10)); if ($limit === 0) { return; } $count = EnhanceJob::query() ->where('user_id', (int) $user->id) ->whereBetween('created_at', [now()->startOfDay(), now()->endOfDay()]) ->count(); if ($count >= $limit) { throw ValidationException::withMessages([ 'image' => 'You have reached your daily enhance limit. Please try again tomorrow.', ]); } } private function queue(EnhanceJob $job): void { $job->forceFill([ 'status' => EnhanceJob::STATUS_QUEUED, 'queued_at' => now(), ])->save(); ProcessEnhanceJob::dispatch((int) $job->id)->afterCommit(); } public function frontendConfig(): array { $engine = (string) config('enhance.default_engine', EnhanceJob::ENGINE_STUB); $showStubWarning = (bool) config('enhance.stub.show_warning', true) && $engine === EnhanceJob::ENGINE_STUB; return [ 'engine' => $engine, 'isStub' => $engine === EnhanceJob::ENGINE_STUB, 'showStubWarning' => $showStubWarning, 'maxUploadMb' => (int) config('enhance.max_upload_mb', 20), 'allowedModes' => array_values((array) config('enhance.allowed_modes', [])), 'allowedScales' => array_map('intval', (array) config('enhance.allowed_scales', [])), ]; } private function sourceExists(EnhanceJob $job): bool { $path = ltrim(trim((string) $job->source_path), '/'); if ($path === '') { return false; } return Storage::disk($job->source_disk ?: $this->storage->diskName())->exists($path); } }