sed -n '1,260p' app/Http/Controllers/Api/Admin/V1/ShippingRateController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Enums\ShippingRateType;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\ShippingRateResource;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class ShippingRateController extends Controller
{
    public function store(Request $request, Store $store, ShippingZone $shippingZone): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessZoneBelongsToStore($shippingZone, $store);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'type' => ['required', Rule::in(array_map(fn (ShippingRateType $type): string => $type->value, ShippingRateType::cases()))],
            'config_json' => ['required', 'array'],
            'config_json.price_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.amount' => ['nullable', 'integer', 'min:0'],
            'config_json.currency' => ['nullable', 'string', 'size:3'],
            'config_json.tiers' => ['nullable', 'array'],
            'config_json.tiers.*.min_weight_g' => ['nullable', 'integer', 'min:0'],
            'config_json.tiers.*.max_weight_g' => ['nullable', 'integer', 'min:1'],
            'config_json.tiers.*.min_order_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.tiers.*.max_order_amount' => ['nullable', 'integer', 'min:1'],
            'config_json.tiers.*.price_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.ranges' => ['nullable', 'array'],
            'is_active' => ['sometimes', 'boolean'],
        ]);

        $rate = ShippingRate::withoutGlobalScopes()->create([
            'zone_id' => $shippingZone->getKey(),
            'name' => $validated['name'],
            'type' => ShippingRateType::from($validated['type']),
            'config_json' => $this->normalizeConfig($validated['type'], $request->input('config_json', []), $store),
            'is_active' => (bool) ($validated['is_active'] ?? true),
        ]);

        return ShippingRateResource::make($rate->load('zone.store'))
            ->response()
            ->setStatusCode(201);
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function abortUnlessZoneBelongsToStore(ShippingZone $zone, Store $store): void
    {
        abort_unless((int) $zone->store_id === $store->getKey(), 404);
    }

    /**
     * @param  array<string, mixed>  $config
     * @return array<string, mixed>
     */
    private function normalizeConfig(string $type, array $config, Store $store): array
    {
        if (in_array($type, [ShippingRateType::Flat->value, ShippingRateType::Carrier->value], true)) {
            return [
                'amount' => (int) ($config['price_amount'] ?? $config['amount'] ?? 0),
                'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
            ];
        }

        if ($type === ShippingRateType::Weight->value) {
            return [
                'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
                'ranges' => collect($config['tiers'] ?? $config['ranges'] ?? [])
                    ->map(fn (array $tier): array => [
                        'min_g' => (int) ($tier['min_weight_g'] ?? $tier['min_g'] ?? 0),
                        'max_g' => array_key_exists('max_weight_g', $tier) ? $tier['max_weight_g'] : ($tier['max_g'] ?? null),
                        'amount' => (int) ($tier['price_amount'] ?? $tier['amount'] ?? 0),
                    ])
                    ->values()
                    ->all(),
            ];
        }

        return [
            'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
            'ranges' => collect($config['tiers'] ?? $config['ranges'] ?? [])
                ->map(fn (array $tier): array => [
                    'min_amount' => (int) ($tier['min_order_amount'] ?? $tier['min_amount'] ?? 0),
                    'max_amount' => array_key_exists('max_order_amount', $tier) ? $tier['max_order_amount'] : ($tier['max_amount'] ?? null),
                    'amount' => (int) ($tier['price_amount'] ?? $tier['amount'] ?? 0),
                ])
                ->values()
                ->all(),
        ];
    }
}


Output:
Chunk ID: 8b8488
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1178
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Enums\ShippingRateType;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\ShippingRateResource;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class ShippingRateController extends Controller
{
    public function store(Request $request, Store $store, ShippingZone $shippingZone): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessZoneBelongsToStore($shippingZone, $store);

        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'type' => ['required', Rule::in(array_map(fn (ShippingRateType $type): string => $type->value, ShippingRateType::cases()))],
            'config_json' => ['required', 'array'],
            'config_json.price_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.amount' => ['nullable', 'integer', 'min:0'],
            'config_json.currency' => ['nullable', 'string', 'size:3'],
            'config_json.tiers' => ['nullable', 'array'],
            'config_json.tiers.*.min_weight_g' => ['nullable', 'integer', 'min:0'],
            'config_json.tiers.*.max_weight_g' => ['nullable', 'integer', 'min:1'],
            'config_json.tiers.*.min_order_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.tiers.*.max_order_amount' => ['nullable', 'integer', 'min:1'],
            'config_json.tiers.*.price_amount' => ['nullable', 'integer', 'min:0'],
            'config_json.ranges' => ['nullable', 'array'],
            'is_active' => ['sometimes', 'boolean'],
        ]);

        $rate = ShippingRate::withoutGlobalScopes()->create([
            'zone_id' => $shippingZone->getKey(),
            'name' => $validated['name'],
            'type' => ShippingRateType::from($validated['type']),
            'config_json' => $this->normalizeConfig($validated['type'], $request->input('config_json', []), $store),
            'is_active' => (bool) ($validated['is_active'] ?? true),
        ]);

        return ShippingRateResource::make($rate->load('zone.store'))
            ->response()
            ->setStatusCode(201);
    }

    private function authorizeStore(Request $request, Store $store): void
    {
        if (! $request->attributes->has('admin_api_oauth_token')) {
            abort_unless($request->user()?->stores()->whereKey($store->getKey())->exists(), 403);
        }

        app()->instance('current_store', $store);
    }

    private function abortUnlessZoneBelongsToStore(ShippingZone $zone, Store $store): void
    {
        abort_unless((int) $zone->store_id === $store->getKey(), 404);
    }

    /**
     * @param  array<string, mixed>  $config
     * @return array<string, mixed>
     */
    private function normalizeConfig(string $type, array $config, Store $store): array
    {
        if (in_array($type, [ShippingRateType::Flat->value, ShippingRateType::Carrier->value], true)) {
            return [
                'amount' => (int) ($config['price_amount'] ?? $config['amount'] ?? 0),
                'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
            ];
        }

        if ($type === ShippingRateType::Weight->value) {
            return [
                'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
                'ranges' => collect($config['tiers'] ?? $config['ranges'] ?? [])
                    ->map(fn (array $tier): array => [
                        'min_g' => (int) ($tier['min_weight_g'] ?? $tier['min_g'] ?? 0),
                        'max_g' => array_key_exists('max_weight_g', $tier) ? $tier['max_weight_g'] : ($tier['max_g'] ?? null),
                        'amount' => (int) ($tier['price_amount'] ?? $tier['amount'] ?? 0),
                    ])
                    ->values()
                    ->all(),
            ];
        }

        return [
            'currency' => strtoupper((string) ($config['currency'] ?? $store->default_currency)),
            'ranges' => collect($config['tiers'] ?? $config['ranges'] ?? [])
                ->map(fn (array $tier): array => [
                    'min_amount' => (int) ($tier['min_order_amount'] ?? $tier['min_amount'] ?? 0),
                    'max_amount' => array_key_exists('max_order_amount', $tier) ? $tier['max_order_amount'] : ($tier['max_amount'] ?? null),
                    'amount' => (int) ($tier['price_amount'] ?? $tier['amount'] ?? 0),
                ])
                ->values()
                ->all(),
        ];
    }
}


sed -n '1,220p' database/migrations/*create_themes* database/migrations/*create_theme* 2>/dev/null

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->nullable();
            $table->enum('status', ['draft', 'published'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('themes');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->nullable();
            $table->enum('status', ['draft', 'published'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('themes');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('theme_files', function (Blueprint $table) {
            $table->id();
            $table->foreignId('theme_id')->constrained()->cascadeOnDelete();
            $table->string('path');
            $table->string('storage_key');
            $table->string('sha256');
            $table->unsignedInteger('byte_size')->default(0);

            $table->unique(['theme_id', 'path']);
            $table->index('theme_id');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('theme_files');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('theme_settings', function (Blueprint $table) {
            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('theme_settings');
    }
};


Output:
Chunk ID: 00659b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 846
Output:
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->nullable();
            $table->enum('status', ['draft', 'published'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('themes');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('themes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->string('name');
            $table->string('version')->nullable();
            $table->enum('status', ['draft', 'published'])->default('draft');
            $table->timestamp('published_at')->nullable();
            $table->timestamps();

            $table->index('store_id');
            $table->index(['store_id', 'status']);
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('themes');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('theme_files', function (Blueprint $table) {
            $table->id();
            $table->foreignId('theme_id')->constrained()->cascadeOnDelete();
            $table->string('path');
            $table->string('storage_key');
            $table->string('sha256');
            $table->unsignedInteger('byte_size')->default(0);

            $table->unique(['theme_id', 'path']);
            $table->index('theme_id');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('theme_files');
    }
};
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('theme_settings', function (Blueprint $table) {
            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
            $table->text('settings_json')->default('{}');
            $table->timestamp('updated_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('theme_settings');
    }
};


sed -n '1,220p' app/Models/ThemeFile.php


sed -n '1,180p' database/factories/ThemeFileFactory.php


rg -n "ThemeFile::|theme files|themes/.+blade|sections/hero" database app tests


sed -n '1,220p' database/seeders/ThemeFileSeeder.php 2>/dev/null || true


find database/seeders -maxdepth 1 -type f | sort | rg 'Theme|Content|Storefront'


sed -n '1,180p' database/factories/ThemeFileFactory.php

<?php

namespace Database\Factories;

use App\Models\Theme;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeFile>
 */
class ThemeFileFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $path = 'sections/'.fake()->unique()->slug().'.blade.php';

        return [
            'theme_id' => Theme::factory(),
            'path' => $path,
            'storage_key' => 'themes/'.Str::random(12).'/'.$path,
            'sha256' => hash('sha256', $path.fake()->uuid()),
            'byte_size' => fake()->numberBetween(128, 4096),
        ];
    }
}


sed -n '1,220p' app/Models/ThemeFile.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ThemeFile extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFileFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'theme_id',
        'path',
        'storage_key',
        'sha256',
        'byte_size',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'byte_size' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('theme', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<Theme, $this>
     */
    public function theme(): BelongsTo
    {
        return $this->belongsTo(Theme::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'byte_size' => 'integer',
        ];
    }
}


Output:
Chunk ID: e0cf16
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 397
Output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class ThemeFile extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFileFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'theme_id',
        'path',
        'storage_key',
        'sha256',
        'byte_size',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'byte_size' => 0,
    ];

    protected static function booted(): void
    {
        static::addGlobalScope('current_store', function (Builder $builder): void {
            if (! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if (! $store instanceof Store) {
                return;
            }

            $builder->whereHas('theme', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<Theme, $this>
     */
    public function theme(): BelongsTo
    {
        return $this->belongsTo(Theme::class);
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'byte_size' => 'integer',
        ];
    }
}


Output:
Chunk ID: ef0614
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 197
Output:
<?php

namespace Database\Factories;

use App\Models\Theme;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeFile>
 */
class ThemeFileFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        $path = 'sections/'.fake()->unique()->slug().'.blade.php';

        return [
            'theme_id' => Theme::factory(),
            'path' => $path,
            'storage_key' => 'themes/'.Str::random(12).'/'.$path,
            'sha256' => hash('sha256', $path.fake()->uuid()),
            'byte_size' => fake()->numberBetween(128, 4096),
        ];
    }
}


sed -n '1,220p' database/seeders/ThemeFileSeeder.php 2>/dev/null || true

<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeFile;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;

class ThemeFileSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()->get()->each(function (Theme $theme): void {
            foreach ([
                'layouts/storefront.blade.php',
                'sections/hero.blade.php',
                'sections/featured-products.blade.php',
            ] as $path) {
                $contents = $this->contents($theme, $path);
                $storageKey = "themes/{$theme->getKey()}/{$path}";

                Storage::disk('local')->put($storageKey, $contents);

                ThemeFile::withoutGlobalScopes()->updateOrCreate(
                    [
                        'theme_id' => $theme->getKey(),
                        'path' => $path,
                    ],
                    [
                        'storage_key' => $storageKey,
                        'sha256' => hash('sha256', $contents),
                        'byte_size' => strlen($contents),
                    ],
                );
            }
        });
    }

    private function contents(Theme $theme, string $path): string
    {
        return match ($path) {
            'layouts/storefront.blade.php' => "<x-layouts.storefront :title=\"\$title ?? '{$theme->name}'\">\n    {{ \$slot }}\n</x-layouts.storefront>\n",
            'sections/hero.blade.php' => "
\n
{{ data_get(\$settings, 'home.hero.heading') }}
\n
\n",
            'sections/featured-products.blade.php' => "
\n    @foreach(\$products as \$product)\n
{{ \$product->title }}
\n    @endforeach\n
\n",
            default => '',
        };
    }
}


find database/seeders -maxdepth 1 -type f | sort | rg 'Theme|Content|Storefront'

Question: Theme|Content|Storefront


rg -n "ThemeFile::|theme files|themes/.+blade|sections/hero" database app tests

rg -n 'ThemeFile::|theme files|themes/.+blade|sections/hero' database app tests

Question: ThemeFile::|theme files|themes/.+blade|sections/hero

tests/Feature/Admin/ContentManagementTest.php:238:    $file = $theme->files()->withoutGlobalScopes()->where('path', 'sections/hero.blade.php')->firstOrFail();
app/Livewire/Admin/Themes/Editor.php:209:        return ThemeFile::withoutGlobalScopes()
app/Livewire/Admin/Themes/Editor.php:226:        return ThemeFile::withoutGlobalScopes()
app/Livewire/Admin/Themes/Index.php:88:                    ThemeFile::withoutGlobalScopes()->create([
app/Models/Theme.php:49:        return $this->hasMany(ThemeFile::class)->orderBy('path');
tests/Feature/Storefront/ThemeDataTest.php:34:        ->and(ThemeFile::withoutGlobalScopes()->count())->toBe(6)
tests/Feature/Storefront/ThemeDataTest.php:67:    ThemeFile::factory()->create(['theme_id' => $firstTheme->getKey(), 'path' => 'first.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:68:    ThemeFile::factory()->create(['theme_id' => $secondTheme->getKey(), 'path' => 'second.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:76:    expect(ThemeFile::query()->pluck('path')->all())->toBe(['first.blade.php'])
database/seeders/ThemeFileSeeder.php:20:                'sections/hero.blade.php',
database/seeders/ThemeFileSeeder.php:28:                ThemeFile::withoutGlobalScopes()->updateOrCreate(
database/seeders/ThemeFileSeeder.php:47:            'sections/hero.blade.php' => "
\n
{{ data_get(\$settings, 'home.hero.heading') }}
\n
\n",


Output:
Chunk ID: cc52be
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 360
Output:
tests/Feature/Admin/ContentManagementTest.php:238:    $file = $theme->files()->withoutGlobalScopes()->where('path', 'sections/hero.blade.php')->firstOrFail();
app/Livewire/Admin/Themes/Editor.php:209:        return ThemeFile::withoutGlobalScopes()
app/Livewire/Admin/Themes/Editor.php:226:        return ThemeFile::withoutGlobalScopes()
app/Livewire/Admin/Themes/Index.php:88:                    ThemeFile::withoutGlobalScopes()->create([
app/Models/Theme.php:49:        return $this->hasMany(ThemeFile::class)->orderBy('path');
tests/Feature/Storefront/ThemeDataTest.php:34:        ->and(ThemeFile::withoutGlobalScopes()->count())->toBe(6)
tests/Feature/Storefront/ThemeDataTest.php:67:    ThemeFile::factory()->create(['theme_id' => $firstTheme->getKey(), 'path' => 'first.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:68:    ThemeFile::factory()->create(['theme_id' => $secondTheme->getKey(), 'path' => 'second.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:76:    expect(ThemeFile::query()->pluck('path')->all())->toBe(['first.blade.php'])
database/seeders/ThemeFileSeeder.php:20:                'sections/hero.blade.php',
database/seeders/ThemeFileSeeder.php:28:                ThemeFile::withoutGlobalScopes()->updateOrCreate(
database/seeders/ThemeFileSeeder.php:47:            'sections/hero.blade.php' => "
\n
{{ data_get(\$settings, 'home.hero.heading') }}
\n
\n",


Output:
Chunk ID: b4f3b1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 476
Output:
<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeFile;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;

class ThemeFileSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()->get()->each(function (Theme $theme): void {
            foreach ([
                'layouts/storefront.blade.php',
                'sections/hero.blade.php',
                'sections/featured-products.blade.php',
            ] as $path) {
                $contents = $this->contents($theme, $path);
                $storageKey = "themes/{$theme->getKey()}/{$path}";

                Storage::disk('local')->put($storageKey, $contents);

                ThemeFile::withoutGlobalScopes()->updateOrCreate(
                    [
                        'theme_id' => $theme->getKey(),
                        'path' => $path,
                    ],
                    [
                        'storage_key' => $storageKey,
                        'sha256' => hash('sha256', $contents),
                        'byte_size' => strlen($contents),
                    ],
                );
            }
        });
    }

    private function contents(Theme $theme, string $path): string
    {
        return match ($path) {
            'layouts/storefront.blade.php' => "<x-layouts.storefront :title=\"\$title ?? '{$theme->name}'\">\n    {{ \$slot }}\n</x-layouts.storefront>\n",
            'sections/hero.blade.php' => "
\n
{{ data_get(\$settings, 'home.hero.heading') }}
\n
\n",
            'sections/featured-products.blade.php' => "
\n    @foreach(\$products as \$product)\n
{{ \$product->title }}
\n    @endforeach\n
\n",
            default => '',
        };
    }
}


Output:
Chunk ID: cb1f85
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
database/seeders/ThemeFileSeeder.php
database/seeders/ThemeSeeder.php
database/seeders/ThemeSettingsSeeder.php


Output:
Chunk ID: a4df22
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


/bin/zsh
-lc
php -m | rg '^zip$|^Phar$|^libxml$'


Output:
Chunk ID: cf4a49
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4
Output:
libxml
Phar
zip


rg -n "manifest|theme archive|valid theme|Theme" specs app resources database tests | head -n 120


sed -n '1,220p' database/seeders/ThemeSettingsSeeder.php


rg -n "createApiToken|WebhookService|OauthToken" tests/Feature/Api app/Services app/Models | head -n 100


sed -n '1,220p' app/Services/WebhookService.php


sed -n '1,220p' database/seeders/ThemeSettingsSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Services\ThemeSettingsService;
use Illuminate\Database\Seeder;

class ThemeSettingsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()
            ->with('store')
            ->get()
            ->each(function (Theme $theme): void {
                ThemeSettings::withoutGlobalScopes()->updateOrCreate(
                    ['theme_id' => $theme->getKey()],
                    [
                        'settings_json' => app(ThemeSettingsService::class)->defaultsForStore($theme->store),
                        'updated_at' => now(),
                    ],
                );
            });
    }
}


rg -n "manifest|theme archive|valid theme|Theme" specs app resources database tests | head -n 120

rg -n 'manifest|theme archive|valid theme|Theme' specs app resources database tests

Question: manifest|theme archive|valid theme|Theme

tests/Feature/Foundation/AuthorizationTest.php:12:use App\Models\Theme;
tests/Feature/Foundation/AuthorizationTest.php:23:use App\Policies\ThemePolicy;
tests/Feature/Foundation/AuthorizationTest.php:124:        ThemePolicy::class => [
tests/Feature/Foundation/AuthorizationTest.php:125:            'view' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:126:            'update' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:127:            'publish' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:128:            'delete' => Theme::class,
specs/03-ADMIN-UI.md:67:|  [paint-brush]  Themes        |
specs/03-ADMIN-UI.md:94:  Themes                         -> admin.themes.index
specs/03-ADMIN-UI.md:1655:## 12. Themes Management
specs/03-ADMIN-UI.md:1657:### 12.1 Themes List
specs/03-ADMIN-UI.md:1661:**Livewire component:** `App\Livewire\Admin\Themes\Index`
specs/03-ADMIN-UI.md:1673:| `publishTheme(int $themeId)` | Publishes a theme (sets as active) |
specs/03-ADMIN-UI.md:1674:| `duplicateTheme(int $themeId)` | Creates a copy of the theme |
specs/03-ADMIN-UI.md:1675:| `deleteTheme(int $themeId)` | Deletes a theme |
specs/03-ADMIN-UI.md:1681:| Themes                                                            |
specs/03-ADMIN-UI.md:1687:| | My Theme   v1.0  | | Dark Theme v1.2  | | Minimal    v2.0  |   |
specs/03-ADMIN-UI.md:1696:**Theme grid:** Responsive 3-column grid (1 on mobile, 2 on md, 3 on lg).
specs/03-ADMIN-UI.md:1701:  - Theme name (heading, size md) and version text
specs/03-ADMIN-UI.md:1714:### 12.2 Theme Editor
specs/03-ADMIN-UI.md:1718:**Livewire component:** `App\Livewire\Admin\Themes\Editor`
specs/03-ADMIN-UI.md:1726:| `theme` | `Theme` | The theme being edited |
specs/03-ADMIN-UI.md:1734:| `mount(Theme $theme)` | Loads theme and its sections |
specs/03-ADMIN-UI.md:2390:| Theme published | "Theme published" | success |
specs/03-ADMIN-UI.md:2420:| `App\Livewire\Admin\Themes\Index` | `livewire.admin.themes.index` | `GET /admin/themes` |
specs/03-ADMIN-UI.md:2421:| `App\Livewire\Admin\Themes\Editor` | `livewire.admin.themes.editor` | `GET /admin/themes/{theme}/editor` |
tests/Feature/Admin/ContentManagementTest.php:6:use App\Enums\ThemeStatus;
tests/Feature/Admin/ContentManagementTest.php:10:use App\Livewire\Admin\Themes\Editor as AdminThemeEditor;
tests/Feature/Admin/ContentManagementTest.php:11:use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
tests/Feature/Admin/ContentManagementTest.php:16:use App\Models\Theme;
tests/Feature/Admin/ContentManagementTest.php:17:use App\Models\ThemeSettings;
tests/Feature/Admin/ContentManagementTest.php:62:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:204:    $published = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:207:        ->test(AdminThemesIndex::class)
tests/Feature/Admin/ContentManagementTest.php:208:        ->call('duplicateTheme', $published->getKey())
tests/Feature/Admin/ContentManagementTest.php:211:    $copy = Theme::withoutGlobalScopes()
tests/Feature/Admin/ContentManagementTest.php:222:        ->test(AdminThemeEditor::class, ['theme' => $copy])
tests/Feature/Admin/ContentManagementTest.php:228:    expect(ThemeSettings::withoutGlobalScopes()->where('theme_id', $copy->getKey())->first()?->settings_json['home']['hero']['heading'])->toBe('New Hero')
tests/Feature/Admin/ContentManagementTest.php:229:        ->and($copy->refresh()->status)->toBe(ThemeStatus::Published)
tests/Feature/Admin/ContentManagementTest.php:230:        ->and($published->refresh()->status)->toBe(ThemeStatus::Draft)
tests/Feature/Admin/ContentManagementTest.php:231:        ->and(Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->where('status', ThemeStatus::Published)->count())->toBe(1);
tests/Feature/Admin/ContentManagementTest.php:237:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:243:        ->test(AdminThemeEditor::class, ['theme' => $theme])
tests/Feature/Admin/ContentManagementTest.php:250:        ->assertSee('Theme file saved');
tests/Feature/Admin/ContentManagementTest.php:264:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:271:        ->test(AdminThemesIndex::class)
tests/Feature/Admin/ContentManagementTest.php:283:        ->test(AdminThemeEditor::class, ['theme' => $theme])
specs/progress.md:71:- 2026-05-03: `php artisan test --compact tests/Feature/Storefront/ThemeDataTest.php` passed: 6 tests, 31 assertions.
specs/progress.md:184:- 2026-05-04: `php artisan make:livewire` created `Admin/Settings/Index`, `Admin/Settings/Shipping`, `Admin/Settings/Taxes`, `Admin/Pages/Index`, `Admin/Pages/Form`, `Admin/Navigation/Index`, `Admin/Themes/Index`, and `Admin…2870 chars truncated…ctory.php:21:            'theme_id' => Theme::factory(),
specs/01-DATABASE-SCHEMA.md:367:## Epic 3 - Themes, Storefront, and Localization
specs/01-DATABASE-SCHEMA.md:377:| name | TEXT | No | - | - | Theme display name |
specs/01-DATABASE-SCHEMA.md:1318:### Epic 3 - Themes, Storefront, and Localization
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
app/Services/ThemeSettingsService.php:5:use App\Enums\ThemeStatus;
app/Services/ThemeSettingsService.php:7:use App\Models\Theme;
app/Services/ThemeSettingsService.php:10:class ThemeSettingsService
app/Services/ThemeSettingsService.php:18:            $theme = $this->publishedTheme($store);
app/Services/ThemeSettingsService.php:30:    public function publishedTheme(Store $store): ?Theme
app/Services/ThemeSettingsService.php:32:        return Theme::withoutGlobalScopes()
app/Services/ThemeSettingsService.php:35:            ->where('status', ThemeStatus::Published)
database/seeders/DatabaseSeeder.php:25:            ThemeSeeder::class,
database/seeders/DatabaseSeeder.php:26:            ThemeFileSeeder::class,
database/seeders/DatabaseSeeder.php:27:            ThemeSettingsSeeder::class,
resources/views/layouts/app/sidebar.blade.php:70:                        {{ __('Themes') }}
tests/Feature/Storefront/ThemeDataTest.php:8:use App\Models\Theme;
tests/Feature/Storefront/ThemeDataTest.php:9:use App\Models\ThemeFile;
tests/Feature/Storefront/ThemeDataTest.php:10:use App\Models\ThemeSettings;
tests/Feature/Storefront/ThemeDataTest.php:12:use App\Services\ThemeSettingsService;
tests/Feature/Storefront/ThemeDataTest.php:33:        ->and(Theme::withoutGlobalScopes()->count())->toBe(2)
tests/Feature/Storefront/ThemeDataTest.php:34:        ->and(ThemeFile::withoutGlobalScopes()->count())->toBe(6)
tests/Feature/Storefront/ThemeDataTest.php:35:        ->and(ThemeSettings::withoutGlobalScopes()->count())->toBe(2)
tests/Feature/Storefront/ThemeDataTest.php:45:    Theme::factory()->published()->create(['store_id' => $firstStore->getKey(), 'name' => 'First Theme']);
tests/Feature/Storefront/ThemeDataTest.php:46:    Theme::factory()->published()->create(['store_id' => $secondStore->getKey(), 'name' => 'Second Theme']);
tests/Feature/Storefront/ThemeDataTest.php:54:    expect(Theme::query()->pluck('name')->all())->toBe(['First Theme'])
tests/Feature/Storefront/ThemeDataTest.php:62:    $firstTheme = Theme::factory()->published()->create(['store_id' => $firstStore->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:63:    $secondTheme = Theme::factory()->published()->create(['store_id' => $secondStore->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:67:    ThemeFile::factory()->create(['theme_id' => $firstTheme->getKey(), 'path' => 'first.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:68:    ThemeFile::factory()->create(['theme_id' => $secondTheme->getKey(), 'path' => 'second.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:69:    ThemeSettings::factory()->create(['theme_id' => $firstTheme->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:70:    ThemeSettings::factory()->create(['theme_id' => $secondTheme->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:76:    expect(ThemeFile::query()->pluck('path')->all())->toBe(['first.blade.php'])
tests/Feature/Storefront/ThemeDataTest.php:77:        ->and(ThemeSettings::query()->count())->toBe(1)
tests/Feature/Storefront/ThemeDataTest.php:85:    $settings = app(ThemeSettingsService::class)->forStore($store);
resources/views/livewire/admin/themes/editor.blade.php:3:        <flux:button :href="route('admin.themes.index')" wire:navigate variant="filled" icon="arrow-left">Themes</flux:button>
resources/views/livewire/admin/themes/index.blade.php:3:        <flux:heading size="xl">Themes</flux:heading>
resources/views/livewire/admin/themes/index.blade.php:34:                            <flux:button type="button" wire:click="publishTheme({{ $theme->getKey() }})" variant="filled">Publish</flux:button>
resources/views/livewire/admin/themes/index.blade.php:36:                        <flux:button type="button" wire:click="duplicateTheme({{ $theme->getKey() }})" variant="filled">Duplicate</flux:button>
resources/views/livewire/admin/themes/index.blade.php:38:                            <flux:button type="button" wire:click="deleteTheme({{ $theme->getKey() }})" wire:confirm="Delete this theme?" variant="danger">Delete</flux:button>
specs/06-AUTH-AND-SECURITY.md:352:| Themes | List / View | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:353:| Themes | Create / Upload | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:354:| Themes | Customize | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:355:| Themes | Publish | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:356:| Themes | Delete | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:476:#### ThemePolicy
specs/06-AUTH-AND-SECURITY.md:478:File: `app/Policies/ThemePolicy.php`


/bin/zsh
-lc
sed -n '1,220p' tests/Pest.php

<?php

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/

pest()->extend(Tests\TestCase::class)
    ->in('Feature', 'Browser');

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/

expect()->extend('toBeOne', function () {
    return $this->toBe(1);
});

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function something()
{
    // ..
}


/bin/zsh
-lc
sed -n '1,220p' app/Models/Theme.php

<?php

namespace App\Models;

use App\Enums\ThemeStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Theme extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'name',
        'version',
        'status',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return HasMany<ThemeFile, $this>
     */
    public function files(): HasMany
    {
        return $this->hasMany(ThemeFile::class)->orderBy('path');
    }

    /**
     * @return HasOne<ThemeSettings, $this>
     */
    public function settings(): HasOne
    {
        return $this->hasOne(ThemeSettings::class);
    }

    public function isPublished(): bool
    {
        return $this->status === ThemeStatus::Published && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ThemeStatus::class,
            'published_at' => 'datetime',
        ];
    }
}


sed -n '1,220p' app/Services/WebhookService.php

<?php

namespace App\Services;

use App\Enums\AppInstallationStatus;
use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\App as AppModel;
use App\Models\AppInstallation;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: OauthToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities): array
    {
        $app = AppModel::query()->firstOrCreate(
            ['name' => 'Admin API'],
            [
                'status' => 'active',
                'created_at' => now(),
            ],
        );

        $installation = AppInstallation::withoutGlobalScopes()->firstOrCreate(
            [
                'store_id' => $store->getKey(),
                'app_id' => $app->getKey(),
            ],
            [
                'scopes_json' => $abilities,
                'status' => AppInstallationStatus::Active,
                'installed_at' => now(),
            ],
        );

        $plainText = 'shop_'.Str::random(48);
        $token = OauthToken::query()->create([
            'installation_id' => $installation->getKey(),
            'name' => $name,
            'access_token_hash' => hash('sha256', $plainText),
            'refresh_token_hash' => null,
            'abilities_json' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


Output:
Chunk ID: e999f3
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3218
Output:
tests/Feature/Foundation/AuthorizationTest.php:12:use App\Models\Theme;
tests/Feature/Foundation/AuthorizationTest.php:23:use App\Policies\ThemePolicy;
tests/Feature/Foundation/AuthorizationTest.php:124:        ThemePolicy::class => [
tests/Feature/Foundation/AuthorizationTest.php:125:            'view' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:126:            'update' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:127:            'publish' => Theme::class,
tests/Feature/Foundation/AuthorizationTest.php:128:            'delete' => Theme::class,
specs/03-ADMIN-UI.md:67:|  [paint-brush]  Themes        |
specs/03-ADMIN-UI.md:94:  Themes                         -> admin.themes.index
specs/03-ADMIN-UI.md:1655:## 12. Themes Management
specs/03-ADMIN-UI.md:1657:### 12.1 Themes List
specs/03-ADMIN-UI.md:1661:**Livewire component:** `App\Livewire\Admin\Themes\Index`
specs/03-ADMIN-UI.md:1673:| `publishTheme(int $themeId)` | Publishes a theme (sets as active) |
specs/03-ADMIN-UI.md:1674:| `duplicateTheme(int $themeId)` | Creates a copy of the theme |
specs/03-ADMIN-UI.md:1675:| `deleteTheme(int $themeId)` | Deletes a theme |
specs/03-ADMIN-UI.md:1681:| Themes                                                            |
specs/03-ADMIN-UI.md:1687:| | My Theme   v1.0  | | Dark Theme v1.2  | | Minimal    v2.0  |   |
specs/03-ADMIN-UI.md:1696:**Theme grid:** Responsive 3-column grid (1 on mobile, 2 on md, 3 on lg).
specs/03-ADMIN-UI.md:1701:  - Theme name (heading, size md) and version text
specs/03-ADMIN-UI.md:1714:### 12.2 Theme Editor
specs/03-ADMIN-UI.md:1718:**Livewire component:** `App\Livewire\Admin\Themes\Editor`
specs/03-ADMIN-UI.md:1726:| `theme` | `Theme` | The theme being edited |
specs/03-ADMIN-UI.md:1734:| `mount(Theme $theme)` | Loads theme and its sections |
specs/03-ADMIN-UI.md:2390:| Theme published | "Theme published" | success |
specs/03-ADMIN-UI.md:2420:| `App\Livewire\Admin\Themes\Index` | `livewire.admin.themes.index` | `GET /admin/themes` |
specs/03-ADMIN-UI.md:2421:| `App\Livewire\Admin\Themes\Editor` | `livewire.admin.themes.editor` | `GET /admin/themes/{theme}/editor` |
tests/Feature/Admin/ContentManagementTest.php:6:use App\Enums\ThemeStatus;
tests/Feature/Admin/ContentManagementTest.php:10:use App\Livewire\Admin\Themes\Editor as AdminThemeEditor;
tests/Feature/Admin/ContentManagementTest.php:11:use App\Livewire\Admin\Themes\Index as AdminThemesIndex;
tests/Feature/Admin/ContentManagementTest.php:16:use App\Models\Theme;
tests/Feature/Admin/ContentManagementTest.php:17:use App\Models\ThemeSettings;
tests/Feature/Admin/ContentManagementTest.php:62:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:204:    $published = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:207:        ->test(AdminThemesIndex::class)
tests/Feature/Admin/ContentManagementTest.php:208:        ->call('duplicateTheme', $published->getKey())
tests/Feature/Admin/ContentManagementTest.php:211:    $copy = Theme::withoutGlobalScopes()
tests/Feature/Admin/ContentManagementTest.php:222:        ->test(AdminThemeEditor::class, ['theme' => $copy])
tests/Feature/Admin/ContentManagementTest.php:228:    expect(ThemeSettings::withoutGlobalScopes()->where('theme_id', $copy->getKey())->first()?->settings_json['home']['hero']['heading'])->toBe('New Hero')
tests/Feature/Admin/ContentManagementTest.php:229:        ->and($copy->refresh()->status)->toBe(ThemeStatus::Published)
tests/Feature/Admin/ContentManagementTest.php:230:        ->and($published->refresh()->status)->toBe(ThemeStatus::Draft)
tests/Feature/Admin/ContentManagementTest.php:231:        ->and(Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->where('status', ThemeStatus::Published)->count())->toBe(1);
tests/Feature/Admin/ContentManagementTest.php:237:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:243:        ->test(AdminThemeEditor::class, ['theme' => $theme])
tests/Feature/Admin/ContentManagementTest.php:250:        ->assertSee('Theme file saved');
tests/Feature/Admin/ContentManagementTest.php:264:    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
tests/Feature/Admin/ContentManagementTest.php:271:        ->test(AdminThemesIndex::class)
tests/Feature/Admin/ContentManagementTest.php:283:        ->test(AdminThemeEditor::class, ['theme' => $theme])
specs/progress.md:71:- 2026-05-03: `php artisan test --compact tests/Feature/Storefront/ThemeDataTest.php` passed: 6 tests, 31 assertions.
specs/progress.md:184:- 2026-05-04: `php artisan make:livewire` created `Admin/Settings/Index`, `Admin/Settings/Shipping`, `Admin/Settings/Taxes`, `Admin/Pages/Index`, `Admin/Pages/Form`, `Admin/Navigation/Index`, `Admin/Themes/Index`, and `Admin/Themes/Editor`; `php artisan make:test --pest` created `Admin/ContentManagementTest` and `Admin/SettingsManagementTest`.
specs/progress.md:314:- 2026-05-04: `php artisan test --compact tests/Feature/Storefront/ThemeDataTest.php tests/Feature/Admin/ContentManagementTest.php` passed after the nested navigation changes: 12 tests, 86 assertions.
specs/progress.md:426:- The theme editor uses the `ThemeSettingsService` default settings shape as the editable schema and writes back to `theme_settings.settings_json`.
specs/progress.md:427:- Theme files are seeded and edited through local disk storage at `theme_files.storage_key`; saving a file updates the stored contents plus `sha256` and `byte_size` metadata.
specs/progress.md:428:- Theme duplication copies each file's local disk contents to a new storage key for the copied theme instead of sharing the original file storage path.
specs/progress.md:444:- Resource policies now type concrete model parameters for Product, Collection, Customer, Discount, Order, Fulfillment, Refund, Page, and Theme actions; `ChecksStoreRole::storeIdForModel()` stays generic because it is the shared store-id extraction helper.
database/factories/ThemeFileFactory.php:5:use App\Models\Theme;
database/factories/ThemeFileFactory.php:10: * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeFile>
database/factories/ThemeFileFactory.php:12:class ThemeFileFactory extends Factory
database/factories/ThemeFileFactory.php:24:            'theme_id' => Theme::factory(),
specs/08-PLAYWRIGHT-E2E-PLAN.md:332:    Dashboard --> Themes: click "Themes"
specs/08-PLAYWRIGHT-E2E-PLAN.md:496:5. Click "Themes" in the sidebar
specs/08-PLAYWRIGHT-E2E-PLAN.md:499:- Page displays "Themes"
specs/04-STOREFRONT-UI.md:43:## 1. Theme System Architecture
specs/04-STOREFRONT-UI.md:62:### 1.3 Theme Settings
specs/04-STOREFRONT-UI.md:64:Theme settings are stored in the database as JSON (keyed by theme ID). A singleton service loads and caches the active theme settings for the current store. Settings control:
specs/04-STOREFRONT-UI.md:1765:| Home | Store name | Theme settings |
database/factories/ThemeFactory.php:5:use App\Enums\ThemeStatus;
database/factories/ThemeFactory.php:10: * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
database/factories/ThemeFactory.php:12:class ThemeFactory extends Factory
database/factories/ThemeFactory.php:25:            'status' => ThemeStatus::Draft,
database/factories/ThemeFactory.php:33:            'status' => ThemeStatus::Published,
database/factories/ThemeSettingsFactory.php:5:use App\Models\Theme;
database/factories/ThemeSettingsFactory.php:9: * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeSettings>
database/factories/ThemeSettingsFactory.php:11:class ThemeSettingsFactory extends Factory
database/factories/ThemeSettingsFactory.php:21:            'theme_id' => Theme::factory(),
specs/01-DATABASE-SCHEMA.md:367:## Epic 3 - Themes, Storefront, and Localization
specs/01-DATABASE-SCHEMA.md:377:| name | TEXT | No | - | - | Theme display name |
specs/01-DATABASE-SCHEMA.md:1318:### Epic 3 - Themes, Storefront, and Localization
specs/01-DATABASE-SCHEMA.md:1675:| 3 - Themes, Storefront, Localization | themes, theme_files, theme_settings, pages, navigation_menus, navigation_items | 6 |
app/Services/ThemeSettingsService.php:5:use App\Enums\ThemeStatus;
app/Services/ThemeSettingsService.php:7:use App\Models\Theme;
app/Services/ThemeSettingsService.php:10:class ThemeSettingsService
app/Services/ThemeSettingsService.php:18:            $theme = $this->publishedTheme($store);
app/Services/ThemeSettingsService.php:30:    public function publishedTheme(Store $store): ?Theme
app/Services/ThemeSettingsService.php:32:        return Theme::withoutGlobalScopes()
app/Services/ThemeSettingsService.php:35:            ->where('status', ThemeStatus::Published)
database/seeders/DatabaseSeeder.php:25:            ThemeSeeder::class,
database/seeders/DatabaseSeeder.php:26:            ThemeFileSeeder::class,
database/seeders/DatabaseSeeder.php:27:            ThemeSettingsSeeder::class,
resources/views/layouts/app/sidebar.blade.php:70:                        {{ __('Themes') }}
tests/Feature/Storefront/ThemeDataTest.php:8:use App\Models\Theme;
tests/Feature/Storefront/ThemeDataTest.php:9:use App\Models\ThemeFile;
tests/Feature/Storefront/ThemeDataTest.php:10:use App\Models\ThemeSettings;
tests/Feature/Storefront/ThemeDataTest.php:12:use App\Services\ThemeSettingsService;
tests/Feature/Storefront/ThemeDataTest.php:33:        ->and(Theme::withoutGlobalScopes()->count())->toBe(2)
tests/Feature/Storefront/ThemeDataTest.php:34:        ->and(ThemeFile::withoutGlobalScopes()->count())->toBe(6)
tests/Feature/Storefront/ThemeDataTest.php:35:        ->and(ThemeSettings::withoutGlobalScopes()->count())->toBe(2)
tests/Feature/Storefront/ThemeDataTest.php:45:    Theme::factory()->published()->create(['store_id' => $firstStore->getKey(), 'name' => 'First Theme']);
tests/Feature/Storefront/ThemeDataTest.php:46:    Theme::factory()->published()->create(['store_id' => $secondStore->getKey(), 'name' => 'Second Theme']);
tests/Feature/Storefront/ThemeDataTest.php:54:    expect(Theme::query()->pluck('name')->all())->toBe(['First Theme'])
tests/Feature/Storefront/ThemeDataTest.php:62:    $firstTheme = Theme::factory()->published()->create(['store_id' => $firstStore->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:63:    $secondTheme = Theme::factory()->published()->create(['store_id' => $secondStore->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:67:    ThemeFile::factory()->create(['theme_id' => $firstTheme->getKey(), 'path' => 'first.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:68:    ThemeFile::factory()->create(['theme_id' => $secondTheme->getKey(), 'path' => 'second.blade.php']);
tests/Feature/Storefront/ThemeDataTest.php:69:    ThemeSettings::factory()->create(['theme_id' => $firstTheme->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:70:    ThemeSettings::factory()->create(['theme_id' => $secondTheme->getKey()]);
tests/Feature/Storefront/ThemeDataTest.php:76:    expect(ThemeFile::query()->pluck('path')->all())->toBe(['first.blade.php'])
tests/Feature/Storefront/ThemeDataTest.php:77:        ->and(ThemeSettings::query()->count())->toBe(1)
tests/Feature/Storefront/ThemeDataTest.php:85:    $settings = app(ThemeSettingsService::class)->forStore($store);
resources/views/livewire/admin/themes/editor.blade.php:3:        <flux:button :href="route('admin.themes.index')" wire:navigate variant="filled" icon="arrow-left">Themes</flux:button>
resources/views/livewire/admin/themes/index.blade.php:3:        <flux:heading size="xl">Themes</flux:heading>
resources/views/livewire/admin/themes/index.blade.php:34:                            <flux:button type="button" wire:click="publishTheme({{ $theme->getKey() }})" variant="filled">Publish</flux:button>
resources/views/livewire/admin/themes/index.blade.php:36:                        <flux:button type="button" wire:click="duplicateTheme({{ $theme->getKey() }})" variant="filled">Duplicate</flux:button>
resources/views/livewire/admin/themes/index.blade.php:38:                            <flux:button type="button" wire:click="deleteTheme({{ $theme->getKey() }})" wire:confirm="Delete this theme?" variant="danger">Delete</flux:button>
specs/06-AUTH-AND-SECURITY.md:352:| Themes | List / View | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:353:| Themes | Create / Upload | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:354:| Themes | Customize | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:355:| Themes | Publish | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:356:| Themes | Delete | Y | Y | N | N |
specs/06-AUTH-AND-SECURITY.md:476:#### ThemePolicy
specs/06-AUTH-AND-SECURITY.md:478:File: `app/Policies/ThemePolicy.php`


Output:
Chunk ID: 380a64
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 199
Output:
<?php

namespace Database\Seeders;

use App\Models\Theme;
use App\Models\ThemeSettings;
use App\Services\ThemeSettingsService;
use Illuminate\Database\Seeder;

class ThemeSettingsSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Theme::withoutGlobalScopes()
            ->with('store')
            ->get()
            ->each(function (Theme $theme): void {
                ThemeSettings::withoutGlobalScopes()->updateOrCreate(
                    ['theme_id' => $theme->getKey()],
                    [
                        'settings_json' => app(ThemeSettingsService::class)->defaultsForStore($theme->store),
                        'updated_at' => now(),
                    ],
                );
            });
    }
}


Output:
Chunk ID: e19334
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 405
Output:
<?php

namespace App\Models;

use App\Enums\ThemeStatus;
use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Theme extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeFactory> */
    use BelongsToStore, HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'name',
        'version',
        'status',
        'published_at',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'status' => 'draft',
    ];

    /**
     * @return BelongsTo<Store, $this>
     */
    public function store(): BelongsTo
    {
        return $this->belongsTo(Store::class);
    }

    /**
     * @return HasMany<ThemeFile, $this>
     */
    public function files(): HasMany
    {
        return $this->hasMany(ThemeFile::class)->orderBy('path');
    }

    /**
     * @return HasOne<ThemeSettings, $this>
     */
    public function settings(): HasOne
    {
        return $this->hasOne(ThemeSettings::class);
    }

    public function isPublished(): bool
    {
        return $this->status === ThemeStatus::Published && $this->published_at !== null;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'status' => ThemeStatus::class,
            'published_at' => 'datetime',
        ];
    }
}


Output:
Chunk ID: a3bd3f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 383
Output:
<?php

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/

pest()->extend(Tests\TestCase::class)
    ->in('Feature', 'Browser');

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/

expect()->extend('toBeOne', function () {
    return $this->toBe(1);
});

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/

function something()
{
    // ..
}


rg -n "createApiToken|WebhookService|OauthToken" tests/Feature/Api app/Services app/Models | head -n 100

rg -n 'createApiToken|WebhookService|OauthToken' tests/Feature/Api app/Services app/Models

Question: createApiToken|WebhookService|OauthToken

app/Models/OauthToken.php:9:class OauthToken extends Model
app/Models/OauthToken.php:11:    /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
tests/Feature/Api/AdminSearchIndexApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminSearchIndexApiTest.php:32: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminSearchIndexApiTest.php:36:    return app(WebhookService::class)->createApiToken($store, 'Search integration', $abilities);
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:33: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:37:    return app(WebhookService::class)->createApiToken($store, 'Analytics integration', $abilities);
tests/Feature/Api/AdminCatalogApiTest.php:10:use App\Services\WebhookService;
tests/Feature/Api/AdminCatalogApiTest.php:33: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminCatalogApiTest.php:37:    return app(WebhookService::class)->createApiToken($store, 'Catalog integration', $abilities);
tests/Feature/Api/AdminShippingSettingsApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminShippingSettingsApiTest.php:31: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminShippingSettingsApiTest.php:35:    return app(WebhookService::class)->createApiToken($store, 'Shipping settings integration', $abilities);
tests/Feature/Api/AdminCollectionApiTest.php:7:use App\Services\WebhookService;
tests/Feature/Api/AdminCollectionApiTest.php:30: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminCollectionApiTest.php:34:    return app(WebhookService::class)->createApiToken($store, 'Collection integration', $abilities);
tests/Feature/Api/AdminDiscountApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminDiscountApiTest.php:32: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminDiscountApiTest.php:36:    return app(WebhookService::class)->createApiToken($store, 'Discount integration', $abilities);
app/Services/WebhookService.php:12:use App\Models\OauthToken;
app/Services/WebhookService.php:19:class WebhookService
app/Services/WebhookService.php:65:     * @return array{token: OauthToken, plain_text: string}
app/Services/WebhookService.php:67:    public function createApiToken(Store $store, string $name, array $abilities): array
app/Services/WebhookService.php:90:        $token = OauthToken::query()->create([
tests/Feature/Api/AdminOrderApiTest.php:15:use App\Services\WebhookService;
tests/Feature/Api/AdminOrderApiTest.php:38: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminOrderApiTest.php:42:    return app(WebhookService::class)->createApiToken($store, 'Order integration', $abilities);
tests/Feature/Api/AdminPageApiTest.php:6:use App\Services\WebhookService;
tests/Feature/Api/AdminPageApiTest.php:29: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminPageApiTest.php:33:    return app(WebhookService::class)->createApiToken($store, 'Content integration', $abilities);
tests/Feature/Api/AdminTaxSettingsApiTest.php:7:use App\Services\WebhookService;
tests/Feature/Api/AdminTaxSettingsApiTest.php:30: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminTaxSettingsApiTest.php:34:    return app(WebhookService::class)->createApiToken($store, 'Tax settings integration', $abilities);
app/Models/AppInstallation.php:55:     * @return HasMany<OauthToken, $this>
app/Models/AppInstallation.php:59:        return $this->hasMany(OauthToken::class, 'installation_id');


Output:
Chunk ID: f72804
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 961
Output:
app/Models/OauthToken.php:9:class OauthToken extends Model
app/Models/OauthToken.php:11:    /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
tests/Feature/Api/AdminSearchIndexApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminSearchIndexApiTest.php:32: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminSearchIndexApiTest.php:36:    return app(WebhookService::class)->createApiToken($store, 'Search integration', $abilities);
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:33: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:37:    return app(WebhookService::class)->createApiToken($store, 'Analytics integration', $abilities);
tests/Feature/Api/AdminCatalogApiTest.php:10:use App\Services\WebhookService;
tests/Feature/Api/AdminCatalogApiTest.php:33: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminCatalogApiTest.php:37:    return app(WebhookService::class)->createApiToken($store, 'Catalog integration', $abilities);
tests/Feature/Api/AdminShippingSettingsApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminShippingSettingsApiTest.php:31: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminShippingSettingsApiTest.php:35:    return app(WebhookService::class)->createApiToken($store, 'Shipping settings integration', $abilities);
tests/Feature/Api/AdminCollectionApiTest.php:7:use App\Services\WebhookService;
tests/Feature/Api/AdminCollectionApiTest.php:30: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminCollectionApiTest.php:34:    return app(WebhookService::class)->createApiToken($store, 'Collection integration', $abilities);
tests/Feature/Api/AdminDiscountApiTest.php:8:use App\Services\WebhookService;
tests/Feature/Api/AdminDiscountApiTest.php:32: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminDiscountApiTest.php:36:    return app(WebhookService::class)->createApiToken($store, 'Discount integration', $abilities);
app/Services/WebhookService.php:12:use App\Models\OauthToken;
app/Services/WebhookService.php:19:class WebhookService
app/Services/WebhookService.php:65:     * @return array{token: OauthToken, plain_text: string}
app/Services/WebhookService.php:67:    public function createApiToken(Store $store, string $name, array $abilities): array
app/Services/WebhookService.php:90:        $token = OauthToken::query()->create([
tests/Feature/Api/AdminOrderApiTest.php:15:use App\Services\WebhookService;
tests/Feature/Api/AdminOrderApiTest.php:38: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminOrderApiTest.php:42:    return app(WebhookService::class)->createApiToken($store, 'Order integration', $abilities);
tests/Feature/Api/AdminPageApiTest.php:6:use App\Services\WebhookService;
tests/Feature/Api/AdminPageApiTest.php:29: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminPageApiTest.php:33:    return app(WebhookService::class)->createApiToken($store, 'Content integration', $abilities);
tests/Feature/Api/AdminTaxSettingsApiTest.php:7:use App\Services\WebhookService;
tests/Feature/Api/AdminTaxSettingsApiTest.php:30: * @return array{token: \App\Models\OauthToken, plain_text: string}
tests/Feature/Api/AdminTaxSettingsApiTest.php:34:    return app(WebhookService::class)->createApiToken($store, 'Tax settings integration', $abilities);
app/Models/AppInstallation.php:55:     * @return HasMany<OauthToken, $this>
app/Models/AppInstallation.php:59:        return $this->hasMany(OauthToken::class, 'installation_id');


Output:
Chunk ID: a7cb1e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1429
Output:
<?php

namespace App\Services;

use App\Enums\AppInstallationStatus;
use App\Enums\WebhookDeliveryStatus;
use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Jobs\DeliverWebhook;
use App\Models\App as AppModel;
use App\Models\AppInstallation;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookDelivery;
use App\Models\WebhookSubscription;
use Illuminate\Support\Str;
use InvalidArgumentException;

class WebhookService
{
    /**
     * @param  array<string, mixed>  $payload
     */
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        $event = WebhookEventType::tryFrom($eventType);

        if (! $event instanceof WebhookEventType) {
            throw new InvalidArgumentException("Unsupported webhook event type [{$eventType}].");
        }

        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('event_type', $event->value)
            ->where('status', WebhookSubscriptionStatus::Active->value)
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($store, $event, $payload): void {
                $delivery = WebhookDelivery::query()->create([
                    'subscription_id' => $subscription->getKey(),
                    'event_id' => (string) Str::uuid(),
                    'attempt_count' => 1,
                    'status' => WebhookDeliveryStatus::Pending,
                ]);

                DeliverWebhook::dispatch(
                    $delivery->getKey(),
                    $event->value,
                    $this->envelope($store, $event, $delivery->event_id, $payload),
                )->onConnection('database')->onQueue('webhooks');
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }

    /**
     * @param  list<string>  $abilities
     * @return array{token: OauthToken, plain_text: string}
     */
    public function createApiToken(Store $store, string $name, array $abilities): array
    {
        $app = AppModel::query()->firstOrCreate(
            ['name' => 'Admin API'],
            [
                'status' => 'active',
                'created_at' => now(),
            ],
        );

        $installation = AppInstallation::withoutGlobalScopes()->firstOrCreate(
            [
                'store_id' => $store->getKey(),
                'app_id' => $app->getKey(),
            ],
            [
                'scopes_json' => $abilities,
                'status' => AppInstallationStatus::Active,
                'installed_at' => now(),
            ],
        );

        $plainText = 'shop_'.Str::random(48);
        $token = OauthToken::query()->create([
            'installation_id' => $installation->getKey(),
            'name' => $name,
            'access_token_hash' => hash('sha256', $plainText),
            'refresh_token_hash' => null,
            'abilities_json' => $abilities,
            'expires_at' => now()->addYear(),
            'created_at' => now(),
        ]);

        return [
            'token' => $token,
            'plain_text' => $plainText,
        ];
    }

    public function createSigningSecret(): string
    {
        return 'whsec_'.Str::random(40);
    }

    public function recordSuccess(WebhookDelivery $delivery, int $attemptCount, int $responseCode, string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Success,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit($responseBody, 1000, ''),
        ])->save();
    }

    public function recordFailure(WebhookDelivery $delivery, int $attemptCount, ?int $responseCode, ?string $responseBody): void
    {
        $delivery->forceFill([
            'attempt_count' => $attemptCount,
            'status' => WebhookDeliveryStatus::Failed,
            'last_attempt_at' => now(),
            'response_code' => $responseCode,
            'response_body_snippet' => Str::limit((string) $responseBody, 1000, ''),
        ])->save();

        $subscription = $delivery->subscription()->first();

        if ($subscription instanceof WebhookSubscription && $this->hasFiveConsecutiveFailures($subscription)) {
            $subscription->forceFill([
                'status' => WebhookSubscriptionStatus::Paused,
            ])->save();
        }
    }

    public function hasFiveConsecutiveFailures(WebhookSubscription $subscription): bool
    {
        $recentStatuses = $subscription->deliveries()
            ->latest('id')
            ->limit(5)
            ->pluck('status');

        return $recentStatuses->count() === 5
            && $recentStatuses->every(fn (WebhookDeliveryStatus|string $status): bool => $status === WebhookDeliveryStatus::Failed || $status === WebhookDeliveryStatus::Failed->value);
    }

    /**
     * @param  array<string, mixed>  $payload
     * @return array<string, mixed>
     */
    private function envelope(Store $store, WebhookEventType $event, string $eventId, array $payload): array
    {
        return [
            'id' => $eventId,
            'api_version' => '2026-05',
            'event_type' => $event->value,
            'store_id' => $store->getKey(),
            'occurred_at' => now()->toISOString(),
            'data' => $payload,
        ];
    }
}


sed -n '2588,2638p' specs/02-API-ROUTES.md


sed -n '2588,2638p' specs/02-API-ROUTES.md

**Success response:** `200 OK`

**Error responses:**
- `403` - Insufficient permissions
- `404` - Theme not found
- `422` - Settings do not conform to theme schema

---

### 3.9 Pages

#### Admin Pages Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/pages` | `read-content` | List content pages | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/pages` | `write-content` | Create a content page | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Update a content page | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Delete a content page | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/pages

List content pages.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter: `draft`, `published`. |
| `page` | integer | No | Default: 1. |
| `per_page` | integer | No | Default: 25, max: 100. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 1,
      "store_id": 1,
      "title": "About Us",
      "handle": "about-us",
      "status": "published",
      "published_at": "2026-02-01T10:00:00Z",
      "created_at": "2026-01-15T10:00:00Z",
      "updated_at": "2026-02-01T10:00:00Z"
    }
  ],
  "meta": {
    "current_page": 1,


Output:
Chunk ID: b3eff1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 378
Output:

**Success response:** `200 OK`

**Error responses:**
- `403` - Insufficient permissions
- `404` - Theme not found
- `422` - Settings do not conform to theme schema

---

### 3.9 Pages

#### Admin Pages Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| GET | `/api/admin/v1/stores/{storeId}/pages` | `read-content` | List content pages | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/pages` | `write-content` | Create a content page | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Update a content page | `api.admin` (60/min) |
| DELETE | `/api/admin/v1/stores/{storeId}/pages/{pageId}` | `write-content` | Delete a content page | `api.admin` (60/min) |

#### GET /api/admin/v1/stores/{storeId}/pages

List content pages.

**Query parameters:**

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `status` | string | No | Filter: `draft`, `published`. |
| `page` | integer | No | Default: 1. |
| `per_page` | integer | No | Default: 25, max: 100. |

**Success response:** `200 OK`

```json
{
  "data": [
    {
      "id": 1,
      "store_id": 1,
      "title": "About Us",
      "handle": "about-us",
      "status": "published",
      "published_at": "2026-02-01T10:00:00Z",
      "created_at": "2026-01-15T10:00:00Z",
      "updated_at": "2026-02-01T10:00:00Z"
    }
  ],
  "meta": {
    "current_page": 1,


I’m going to add a small archive installer service instead of putting ZIP parsing in the controller. It will require a manifest plus the same core template paths the seeded default theme uses, write imported files into local storage, and create a draft theme with default or manifest-provided settings.


php artisan make:class Services/ThemeArchiveInstaller --no-interaction


php artisan make:controller Api/Admin/V1/ThemeController --no-interaction


php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction


php artisan make:resource Admin/V1/ThemeResource --no-interaction


php artisan make:test --pest Api/AdminThemeApiTest --no-interaction


Output:
Chunk ID: 894fe1
Wall time: 0.1574 seconds
Process exited with code 0
Original token count: 21
Output:

   INFO  Class [app/Services/ThemeArchiveInstaller.php] created successfully.


Output:
Chunk ID: 524682
Wall time: 0.1516 seconds
Process exited with code 0
Original token count: 26
Output:

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/ThemeController.php] created successfully.


Output:
Chunk ID: 189d71
Wall time: 0.1515 seconds
Process exited with code 0
Original token count: 28
Output:

   INFO  Controller [app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php] created successfully.


Output:
Chunk ID: e6f284
Wall time: 0.1453 seconds
Process exited with code 0
Original token count: 23
Output:

   INFO  Resource [app/Http/Resources/Admin/V1/ThemeResource.php] created successfully.


Output:
Chunk ID: 60c574
Wall time: 0.1427 seconds
Process exited with code 0
Original token count: 21
Output:

   INFO  Test [tests/Feature/Api/AdminThemeApiTest.php] created successfully.


php artisan make:controller Api/Admin/V1/ThemeSettingsController --no-interaction

INFO  Controller [app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php] created successfully.


php artisan make:class Services/ThemeArchiveInstaller --no-interaction

INFO  Class [app/Services/ThemeArchiveInstaller.php] created successfully.


php artisan make:controller Api/Admin/V1/ThemeController --no-interaction

INFO  Controller [app/Http/Controllers/Api/Admin/V1/ThemeController.php] created successfully.


php artisan make:resource Admin/V1/ThemeResource --no-interaction

INFO  Resource [app/Http/Resources/Admin/V1/ThemeResource.php] created successfully.


php artisan make:test --pest Api/AdminThemeApiTest --no-interaction

INFO  Test [tests/Feature/Api/AdminThemeApiTest.php] created successfully.


sed -n '1,220p' app/Services/ThemeArchiveInstaller.php


sed -n '1,200p' app/Http/Controllers/Api/Admin/V1/ThemeController.php


sed -n '1,200p' app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php


sed -n '1,160p' app/Http/Resources/Admin/V1/ThemeResource.php


sed -n '1,160p' tests/Feature/Api/AdminThemeApiTest.php


sed -n '1,220p' app/Services/ThemeArchiveInstaller.php

<?php

namespace App\Services;

class ThemeArchiveInstaller
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


sed -n '1,200p' app/Http/Controllers/Api/Admin/V1/ThemeController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ThemeController extends Controller
{
    //
}


sed -n '1,200p' app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php

<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ThemeSettingsController extends Controller
{
    //
}


sed -n '1,160p' app/Http/Resources/Admin/V1/ThemeResource.php

<?php

namespace App\Http\Resources\Admin\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ThemeResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}


sed -n '1,160p' tests/Feature/Api/AdminThemeApiTest.php

<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


Output:
Chunk ID: 8f7bac
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 44
Output:
<?php

namespace App\Services;

class ThemeArchiveInstaller
{
    /**
     * Create a new class instance.
     */
    public function __construct()
    {
        //
    }
}


Output:
Chunk ID: 5ea041
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 43
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ThemeController extends Controller
{
    //
}


Output:
Chunk ID: 1cb7d0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 45
Output:
<?php

namespace App\Http\Controllers\Api\Admin\V1;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class ThemeSettingsController extends Controller
{
    //
}


Output:
Chunk ID: a60ee2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 96
Output:
<?php

namespace App\Http\Resources\Admin\V1;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ThemeResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return parent::toArray($request);
    }
}


Output:
Chunk ID: 33b5a7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 28
Output:
<?php

test('example', function () {
    $response = $this->get('/');

    $response->assertStatus(200);
});


?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


Output:
Chunk ID: 61e777
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 64
Output:
?? app/Http/Controllers/Api/Admin/V1/ThemeController.php
?? app/Http/Controllers/Api/Admin/V1/ThemeSettingsController.php
?? app/Http/Resources/Admin/V1/ThemeResource.php
?? app/Services/ThemeArchiveInstaller.php
?? tests/Feature/Api/AdminThemeApiTest.php


sed -n '1,180p' app/Livewire/Admin/Developers/Index.php


sed -n '1,180p' app/Livewire/Admin/Developers/Index.php

<?php

namespace App\Livewire\Admin\Developers;

use App\Enums\WebhookEventType;
use App\Enums\WebhookSubscriptionStatus;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\WebhookSubscription;
use App\Services\WebhookService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Locked;
use Livewire\Component;

class Index extends Component
{
    use AuthorizesRequests;

    #[Locked]
    public int $storeId;

    public string $newTokenName = '';

    public ?string $generatedToken = null;

    public string $webhookEventType = 'order.created';

    public string $webhookUrl = '';

    public ?int $editingWebhookId = null;

    /**
     * @var list<string>
     */
    public array $tokenAbilities = [
        'read-products',
        'write-products',
        'read-orders',
        'write-orders',
        'read-customers',
        'write-customers',
        'read-analytics',
    ];

    public function mount(): void
    {
        $store = $this->store();

        $this->authorize('update', $store);
        $this->storeId = $store->getKey();
    }

    public function generateToken(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'newTokenName' => ['required', 'string', 'max:255'],
        ], [], [
            'newTokenName' => 'token name',
        ]);

        $result = $webhooks->createApiToken($this->scopedStore(), $this->newTokenName, $this->tokenAbilities);

        $this->generatedToken = $result['plain_text'];
        $this->newTokenName = '';
        $this->modal('generate-token')->close();
        $this->dispatch('toast', type: 'success', message: __('API token generated'));
    }

    public function revokeToken(int $tokenId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->token($tokenId)->delete();

        session()->flash('status', __('API token revoked'));
        $this->dispatch('toast', type: 'success', message: __('API token revoked'));
    }

    public function openWebhookModal(?int $webhookId = null): void
    {
        $this->editingWebhookId = $webhookId;

        if ($webhookId !== null) {
            $webhook = $this->webhook($webhookId);
            $this->webhookEventType = $webhook->event_type->value;
            $this->webhookUrl = $webhook->target_url;
        } else {
            $this->webhookEventType = WebhookEventType::OrderCreated->value;
            $this->webhookUrl = '';
        }

        $this->modal('webhook-form')->show();
    }

    public function saveWebhook(WebhookService $webhooks): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->validate([
            'webhookEventType' => ['required', Rule::enum(WebhookEventType::class)],
            'webhookUrl' => ['required', 'url', 'max:2048'],
        ], [], [
            'webhookEventType' => 'event type',
            'webhookUrl' => 'endpoint URL',
        ]);

        $webhook = $this->editingWebhookId !== null
            ? $this->webhook($this->editingWebhookId)
            : new WebhookSubscription([
                'store_id' => $this->storeId,
                'signing_secret_encrypted' => $webhooks->createSigningSecret(),
            ]);

        $webhook->forceFill([
            'event_type' => $this->webhookEventType,
            'target_url' => $this->webhookUrl,
            'status' => WebhookSubscriptionStatus::Active,
        ])->save();

        $this->editingWebhookId = null;
        $this->webhookUrl = '';
        $this->modal('webhook-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Webhook saved'));
    }

    public function deleteWebhook(int $webhookId): void
    {
        $this->authorize('update', $this->scopedStore());

        $this->webhook($webhookId)->delete();
        $this->dispatch('toast', type: 'success', message: __('Webhook deleted'));
    }

    /**
     * @return Collection<int, OauthToken>
     */
    public function tokens(): Collection
    {
        return OauthToken::query()
            ->with('installation.app')
            ->whereHas('installation', function ($query): void {
                $query->withoutGlobalScopes()->where('store_id', $this->storeId);
            })
            ->latest('created_at')
            ->get();
    }

    /**
     * @return Collection<int, WebhookSubscription>
     */
    public function webhooks(): Collection
    {
        return WebhookSubscription::withoutGlobalScopes()
            ->withCount([
                'deliveries',
                'deliveries as failed_deliveries_count' => fn ($query) => $query->where('status', 'failed'),
            ])
            ->where('store_id', $this->storeId)
            ->orderBy('event_type')
            ->get();
    }

    public function render()
    {
        return view('livewire.admin.developers.index', [
            'tokens' => $this->tokens(),
            'webhooks' => $this->webhooks(),
            'eventTypes' => WebhookEventType::selectable(),
        ])->layout('layouts.app', [
            'title' => __('Developers'),
        ]);
    }

    private function token(int $tokenId): OauthToken
    {
        return OauthToken::query()