Source path: app/Jobs/GenerateImage.php
Extracted from supplied specification. Review before copying into application.

<?php
namespace App\Jobs;

use App\Models\Media;
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\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class GenerateImage implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 2;
    public int $timeout = 180;

    public function __construct(public Media $media, public string $size = '1024x1024') {}

    public function handle(): void
    {
        $this->media->update(['status' => 'processing']);

        $res = Http::withToken(config('services.openai.key'))->timeout(150)
            ->post('https://api.openai.com/v1/images/generations', [
                'model'  => config('services.openai.image_model'),
                'prompt' => $this->media->prompt,
                'size'   => $this->size,
                'n'      => 1,
            ])->throw();

        $item = $res->json('data.0');
        $bytes = isset($item['b64_json']) ? base64_decode($item['b64_json']) : Http::get($item['url'])->body();

        $path = 'media/' . $this->media->user_id . '/' . Str::uuid() . '.png';
        Storage::disk(config('filesystems.default'))->put($path, $bytes);

        $this->media->update(['path' => $path, 'status' => 'ready']);
    }

    public function failed(\Throwable $e): void
    {
        $this->media->update(['status' => 'failed', 'error' => substr($e->getMessage(), 0, 2000)]);
        $this->media->user()->increment('ai_credits', 1); // refund
    }
}
