sed -n '1,180p' database/seeders/ProductSeeder.php

<?php

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        DB::transaction(function (): void {
            $this->seedProducts('acme-fashion', $this->fashionProducts());
            $this->seedProducts('acme-electronics', $this->electronicsProducts());
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $products
     */
    private function seedProducts(string $storeHandle, array $products): void
    {
        $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
        $collections = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->keyBy('handle');

        foreach ($products as $index => $data) {
            $product = Product::withoutGlobalScopes()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'handle' => $data['handle'],
                ],
                [
                    ...Arr::only($data, ['title', 'status', 'description_html', 'vendor', 'product_type', 'tags', 'published_at']),
                    'store_id' => $store->getKey(),
                ],
            );

            $this->replaceCatalogChildren($product);

            $optionValueIds = $this->createOptions($product, $data['options'] ?? []);
            $this->createVariants($store, $product, $data, $optionValueIds);

            $collectionIds = collect($data['collections'] ?? [])
                ->map(fn (string $handle): ?int => $collections->get($handle)?->getKey())
                ->filter()
                ->mapWithKeys(fn (int $collectionId): array => [$collectionId => ['position' => $index]])
                ->all();

            $product->collections()->sync($collectionIds);
        }
    }

    private function replaceCatalogChildren(Product $product): void
    {
        ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->delete();
    }

    /**
     * @param  array<string, array<int, string>>  $options
     * @return array<string, array<string, int>>
     */
    private function createOptions(Product $product, array $options): array
    {
        $optionValueIds = [];

        foreach ($options as $optionPosition => $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $option = ProductOption::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'name' => $optionName,
                'position' => $optionPosition,
            ]);

            foreach (array_values($valuesByName[$optionName]) as $valuePosition => $value) {
                $optionValue = ProductOptionValue::withoutGlobalScopes()->create([
                    'product_option_id' => $option->getKey(),
                    'value' => $value,
                    'position' => $valuePosition,
                ]);

                $optionValueIds[$optionName][$value] = $optionValue->getKey();
            }
        }

        return $optionValueIds;
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, array<string, int>>  $optionValueIds
     */
    private function createVariants(Store $store, Product $product, array $data, array $optionValueIds): void
    {
        $variants = $data['variants'] ?? $this->variantDefinitions($data);

        foreach ($variants as $position => $variantData) {
            $variant = ProductVariant::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'sku' => $variantData['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), $variantData['options'] ?? []),
                'barcode' => $variantData['barcode'] ?? null,
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'] ?? null,
                'currency' => $store->default_currency,
                'weight_g' => $variantData['weight_g'] ?? 250,
                'requires_shipping' => $variantData['requires_shipping'] ?? true,
                'is_default' => $position === 0,
                'position' => $position,
                'status' => $variantData['status'] ?? 'active',
            ]);

            InventoryItem::withoutGlobalScopes()->updateOrCreate(
                ['variant_id' => $variant->getKey()],
                [
                    'store_id' => $store->getKey(),
                    'quantity_on_hand' => $variantData['quantity_on_hand'] ?? 0,
                    'quantity_reserved' => 0,
                    'policy' => $variantData['policy'] ?? 'deny',
                ],
            );

            $selectedOptions = $variantData['options'] ?? [];

            if ($selectedOptions !== []) {
                $variant->optionValues()->sync($this->selectedOptionValueIds($selectedOptions, $optionValueIds));
            }
        }
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, array<string, mixed>>
     */
    private function variantDefinitions(array $data): array
    {
        $options = $data['options'] ?? [];
        $defaults = $data['variant_defaults'];

        if ($options === []) {
            return [[
                ...$defaults,
                'options' => [],
                'sku' => $data['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), []),
            ]];
        }

        return collect($this->optionCombinations($options))
            ->map(fn (array $selection): array => [
                ...$defaults,
                'options' => $selection,
            ])
            ->all();
    }


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

namespace Database\Seeders;

use App\Models\Collection as ProductCollection;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use Illuminate\Database\Seeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        DB::transaction(function (): void {
            $this->seedProducts('acme-fashion', $this->fashionProducts());
            $this->seedProducts('acme-electronics', $this->electronicsProducts());
        });
    }

    /**
     * @param  array<int, array<string, mixed>>  $products
     */
    private function seedProducts(string $storeHandle, array $products): void
    {
        $store = Store::query()->where('handle', $storeHandle)->firstOrFail();
        $collections = ProductCollection::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->keyBy('handle');

        foreach ($products as $index => $data) {
            $product = Product::withoutGlobalScopes()->updateOrCreate(
                [
                    'store_id' => $store->getKey(),
                    'handle' => $data['handle'],
                ],
                [
                    ...Arr::only($data, ['title', 'status', 'description_html', 'vendor', 'product_type', 'tags', 'published_at']),
                    'store_id' => $store->getKey(),
                ],
            );

            $this->replaceCatalogChildren($product);

            $optionValueIds = $this->createOptions($product, $data['options'] ?? []);
            $this->createVariants($store, $product, $data, $optionValueIds);

            $collectionIds = collect($data['collections'] ?? [])
                ->map(fn (string $handle): ?int => $collections->get($handle)?->getKey())
                ->filter()
                ->mapWithKeys(fn (int $collectionId): array => [$collectionId => ['position' => $index]])
                ->all();

            $product->collections()->sync($collectionIds);
        }
    }

    private function replaceCatalogChildren(Product $product): void
    {
        ProductMedia::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductVariant::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->get()
            ->each
            ->delete();

        ProductOption::withoutGlobalScopes()
            ->where('product_id', $product->getKey())
            ->delete();
    }

    /**
     * @param  array<string, array<int, string>>  $options
     * @return array<string, array<string, int>>
     */
    private function createOptions(Product $product, array $options): array
    {
        $optionValueIds = [];

        foreach ($options as $optionPosition => $valuesByName) {
            $optionName = (string) array_key_first($valuesByName);
            $option = ProductOption::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'name' => $optionName,
                'position' => $optionPosition,
            ]);

            foreach (array_values($valuesByName[$optionName]) as $valuePosition => $value) {
                $optionValue = ProductOptionValue::withoutGlobalScopes()->create([
                    'product_option_id' => $option->getKey(),
                    'value' => $value,
                    'position' => $valuePosition,
                ]);

                $optionValueIds[$optionName][$value] = $optionValue->getKey();
            }
        }

        return $optionValueIds;
    }

    /**
     * @param  array<string, mixed>  $data
     * @param  array<string, array<string, int>>  $optionValueIds
     */
    private function createVariants(Store $store, Product $product, array $data, array $optionValueIds): void
    {
        $variants = $data['variants'] ?? $this->variantDefinitions($data);

        foreach ($variants as $position => $variantData) {
            $variant = ProductVariant::withoutGlobalScopes()->create([
                'product_id' => $product->getKey(),
                'sku' => $variantData['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), $variantData['options'] ?? []),
                'barcode' => $variantData['barcode'] ?? null,
                'price_amount' => $variantData['price_amount'],
                'compare_at_amount' => $variantData['compare_at_amount'] ?? null,
                'currency' => $store->default_currency,
                'weight_g' => $variantData['weight_g'] ?? 250,
                'requires_shipping' => $variantData['requires_shipping'] ?? true,
                'is_default' => $position === 0,
                'position' => $position,
                'status' => $variantData['status'] ?? 'active',
            ]);

            InventoryItem::withoutGlobalScopes()->updateOrCreate(
                ['variant_id' => $variant->getKey()],
                [
                    'store_id' => $store->getKey(),
                    'quantity_on_hand' => $variantData['quantity_on_hand'] ?? 0,
                    'quantity_reserved' => 0,
                    'policy' => $variantData['policy'] ?? 'deny',
                ],
            );

            $selectedOptions = $variantData['options'] ?? [];

            if ($selectedOptions !== []) {
                $variant->optionValues()->sync($this->selectedOptionValueIds($selectedOptions, $optionValueIds));
            }
        }
    }

    /**
     * @param  array<string, mixed>  $data
     * @return array<int, array<string, mixed>>
     */
    private function variantDefinitions(array $data): array
    {
        $options = $data['options'] ?? [];
        $defaults = $data['variant_defaults'];

        if ($options === []) {
            return [[
                ...$defaults,
                'options' => [],
                'sku' => $data['sku'] ?? $this->sku($data['sku_prefix'] ?? Str::upper(Str::slug($data['handle'], '-')), []),
            ]];
        }

        return collect($this->optionCombinations($options))
            ->map(fn (array $selection): array => [
                ...$defaults,
                'options' => $selection,
            ])
            ->all();
    }


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


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


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


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

<?php

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use RuntimeException;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::saving(function (ProductVariant $variant): void {
            $variant->assertSkuIsUniqueForStore();
        });

        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = Product::withoutGlobalScopes()
                ->whereKey($variant->product_id)
                ->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ]);
        });
    }

    private function assertSkuIsUniqueForStore(): void
    {
        $sku = trim((string) $this->sku);

        if ($sku === '') {
            return;
        }

        $storeId = Product::withoutGlobalScopes()
            ->whereKey($this->product_id)
            ->value('store_id');

        if ($storeId === null) {
            return;
        }

        $query = self::withoutGlobalScopes()
            ->where('sku', $sku)
            ->whereHas('product', function (Builder $query) use ($storeId): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $storeId);
            });

        if ($this->exists) {
            $query->whereKeyNot($this->getKey());
        }

        if ($query->exists()) {
            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
        }
    }

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

    /**
     * @return HasOne<InventoryItem, $this>
     */
    public function inventoryItem(): HasOne
    {
        return $this->hasOne(InventoryItem::class, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}


sed -n '1,220p' app/Models/ProductOption.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;
use Illuminate\Database\Eloquent\Relations\HasMany;

class ProductOption extends Model
{
    /** @use HasFactory<\Database\Factories\ProductOptionFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'name',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'position' => 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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

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

    /**
     * @return HasMany<ProductOptionValue, $this>
     */
    public function values(): HasMany
    {
        return $this->hasMany(ProductOptionValue::class)->orderBy('position');
    }

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


Output:
Chunk ID: db7522
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 453
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;
use Illuminate\Database\Eloquent\Relations\HasMany;

class ProductOption extends Model
{
    /** @use HasFactory<\Database\Factories\ProductOptionFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'name',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'position' => 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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

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

    /**
     * @return HasMany<ProductOptionValue, $this>
     */
    public function values(): HasMany
    {
        return $this->hasMany(ProductOptionValue::class)->orderBy('position');
    }

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


Output:
Chunk ID: 5846a0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1140
Output:
<?php

namespace App\Models;

use App\Enums\InventoryPolicy;
use App\Enums\VariantStatus;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use RuntimeException;

class ProductVariant extends Model
{
    /** @use HasFactory<\Database\Factories\ProductVariantFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'sku',
        'barcode',
        'price_amount',
        'compare_at_amount',
        'currency',
        'weight_g',
        'requires_shipping',
        'is_default',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'price_amount' => 0,
        'currency' => 'USD',
        'requires_shipping' => true,
        'is_default' => false,
        'position' => 0,
        'status' => 'active',
    ];

    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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::saving(function (ProductVariant $variant): void {
            $variant->assertSkuIsUniqueForStore();
        });

        static::created(function (ProductVariant $variant): void {
            if ($variant->inventoryItem()->exists()) {
                return;
            }

            $storeId = Product::withoutGlobalScopes()
                ->whereKey($variant->product_id)
                ->value('store_id');

            if (! $storeId) {
                return;
            }

            InventoryItem::withoutGlobalScopes()->create([
                'store_id' => $storeId,
                'variant_id' => $variant->getKey(),
                'quantity_on_hand' => 0,
                'quantity_reserved' => 0,
                'policy' => InventoryPolicy::Deny,
            ]);
        });
    }

    private function assertSkuIsUniqueForStore(): void
    {
        $sku = trim((string) $this->sku);

        if ($sku === '') {
            return;
        }

        $storeId = Product::withoutGlobalScopes()
            ->whereKey($this->product_id)
            ->value('store_id');

        if ($storeId === null) {
            return;
        }

        $query = self::withoutGlobalScopes()
            ->where('sku', $sku)
            ->whereHas('product', function (Builder $query) use ($storeId): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $storeId);
            });

        if ($this->exists) {
            $query->whereKeyNot($this->getKey());
        }

        if ($query->exists()) {
            throw new RuntimeException("The SKU [{$sku}] is already used in this store.");
        }
    }

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

    /**
     * @return HasOne<InventoryItem, $this>
     */
    public function inventoryItem(): HasOne
    {
        return $this->hasOne(InventoryItem::class, 'variant_id');
    }

    /**
     * @return BelongsToMany<ProductOptionValue, $this>
     */
    public function optionValues(): BelongsToMany
    {
        return $this->belongsToMany(ProductOptionValue::class, 'variant_option_values', 'variant_id', 'product_option_value_id');
    }

    public function isPurchasable(): bool
    {
        return $this->status === VariantStatus::Active;
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'price_amount' => 'integer',
            'compare_at_amount' => 'integer',
            'weight_g' => 'integer',
            'requires_shipping' => 'bool',
            'is_default' => 'bool',
            'position' => 'integer',
            'status' => VariantStatus::class,
        ];
    }
}


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

<?php

namespace App\Models;

use App\Enums\MediaStatus;
use App\Enums\MediaType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;

class ProductMedia extends Model
{
    /** @use HasFactory<\Database\Factories\ProductMediaFactory> */
    use HasFactory;

    public const UPDATED_AT = null;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'type',
        'storage_key',
        'alt_text',
        'width',
        'height',
        'mime_type',
        'byte_size',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'image',
        'position' => 0,
        'status' => 'processing',
    ];

    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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::deleted(function (ProductMedia $media): void {
            $disk = Storage::disk('public');

            $disk->delete($media->storage_key);
            $disk->deleteDirectory("media/{$media->product_id}/{$media->getKey()}");
        });
    }

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => MediaType::class,
            'width' => 'integer',
            'height' => 'integer',
            'byte_size' => 'integer',
            'position' => 'integer',
            'status' => MediaStatus::class,
        ];
    }
}


Output:
Chunk ID: 6be99f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 575
Output:
<?php

namespace App\Models;

use App\Enums\MediaStatus;
use App\Enums\MediaType;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\Storage;

class ProductMedia extends Model
{
    /** @use HasFactory<\Database\Factories\ProductMediaFactory> */
    use HasFactory;

    public const UPDATED_AT = null;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'product_id',
        'type',
        'storage_key',
        'alt_text',
        'width',
        'height',
        'mime_type',
        'byte_size',
        'position',
        'status',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'image',
        'position' => 0,
        'status' => 'processing',
    ];

    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('product', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });

        static::deleted(function (ProductMedia $media): void {
            $disk = Storage::disk('public');

            $disk->delete($media->storage_key);
            $disk->deleteDirectory("media/{$media->product_id}/{$media->getKey()}");
        });
    }

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

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => MediaType::class,
            'width' => 'integer',
            'height' => 'integer',
            'byte_size' => 'integer',
            'position' => 'integer',
            'status' => MediaStatus::class,
        ];
    }
}


php artisan make:test Storefront/ThemeDataTest --pest --no-interaction


Output:
Chunk ID: e33dc2
Wall time: 0.2091 seconds
Process exited with code 0
Original token count: 22
Output:

   INFO  Test [tests/Feature/Storefront/ThemeDataTest.php] created successfully.


php artisan make:test Storefront/ThemeDataTest --pest --no-interaction

INFO  Test [tests/Feature/Storefront/ThemeDataTest.php] created successfully.


The Phase 3 stubs are in place. I’m replacing them with the schema, model relationships, seed fixtures, navigation/theme services, and a DB-backed storefront page component in one scoped patch.


Input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/ThemeStatus.php
+<?php
+
+namespace App\Enums;
+
+enum ThemeStatus: string
+{
+    case Draft = 'draft';
+    case Published = 'published';
+}
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/PageStatus.php
+<?php
+
+namespace App\Enums;
+
+enum PageStatus: string
+{
+    case Draft = 'draft';
+    case Published = 'published';
+    case Archived = 'archived';
+}
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/NavigationItemType.php
+<?php
+
+namespace App\Enums;
+
+enum NavigationItemType: string
+{
+    case Link = 'link';
+    case Page = 'page';
+    case Collection = 'collection';
+    case Product = 'product';
+}
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223824_create_themes_table.php
@@
     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']);
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223830_create_theme_files_table.php
@@
     public function up(): void
     {
         Schema::create('theme_files', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $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');
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php
@@
     public function up(): void
     {
         Schema::create('theme_settings', function (Blueprint $table) {
-            $table->id();
-            $table->timestamps();
+            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
+            $table->json('settings_json')->default('{}');
+            $table->timestamp('updated_at')->nullable();
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223841_create_pages_table.php
@@
     public function up(): void
     {
         Schema::create('pages', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('title');
+            $table->string('handle');
+            $table->text('body_html')->nullable();
+            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
+            $table->timestamp('published_at')->nullable();
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
+            $table->index(['store_id', 'status']);
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223844_create_navigation_menus_table.php
@@
     public function up(): void
     {
         Schema::create('navigation_menus', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('handle');
+            $table->string('title');
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223848_create_navigation_items_table.php
@@
     public function up(): void
     {
         Schema::create('navigation_items', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
+            $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link');
+            $table->string('label');
+            $table->string('url')->nullable();
+            $table->unsignedBigInteger('resource_id')->nullable();
+            $table->unsignedInteger('position')->default(0);
+
+            $table->index('menu_id');
+            $table->index(['menu_id', 'position']);
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Theme.php
@@
 namespace App\Models;

-use Illuminate\Database\Eloquent\Model;
+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 HasFactory;
+    /** @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',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
@@
 namespace App\Models;

-use Illuminate\Database\Eloquent\Model;
+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',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
@@
 namespace App\Models;

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

 class ThemeSettings extends Model
 {
     /** @use HasFactory<\Database\Factories\ThemeSettingsFactory> */
     use HasFactory;
+
+    public $incrementing = false;
+
+    public const CREATED_AT = null;
+
+    protected $primaryKey = 'theme_id';
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'theme_id',
+        'settings_json',
+    ];
+
+    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 [
+            'settings_json' => 'array',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Page.php
@@
 namespace App\Models;

-use Illuminate\Database\Eloquent\Model;
+use App\Enums\PageStatus;
+use App\Models\Concerns\BelongsToStore;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

 class Page extends Model
 {
     /** @use HasFactory<\Database\Factories\PageFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'title',
+        'handle',
+        'body_html',
+        'status',
+        'published_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'draft',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    public function isPublished(): bool
+    {
+        return $this->status === PageStatus::Published && $this->published_at !== null;
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => PageStatus::class,
+            'published_at' => 'datetime',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
@@
 namespace App\Models;

-use Illuminate\Database\Eloquent\Model;
+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;

 class NavigationMenu extends Model
 {
     /** @use HasFactory<\Database\Factories\NavigationMenuFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'handle',
+        'title',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return HasMany<NavigationItem, $this>
+     */
+    public function items(): HasMany
+    {
+        return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position');
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
@@
 namespace App\Models;

-use Illuminate\Database\Eloquent\Model;
+use App\Enums\NavigationItemType;
+use Illuminate\Database\Eloquent\Builder;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

 class NavigationItem extends Model
 {
     /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
     use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'menu_id',
+        'type',
+        'label',
+        'url',
+        'resource_id',
+        'position',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'type' => 'link',
+        'position' => 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('menu', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
+    /**
+     * @return BelongsTo<NavigationMenu, $this>
+     */
+    public function menu(): BelongsTo
+    {
+        return $this->belongsTo(NavigationMenu::class, 'menu_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'type' => NavigationItemType::class,
+            'resource_id' => 'integer',
+            'position' => 'integer',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Store.php
@@
     public function settings(): HasOne
     {
         return $this->hasOne(StoreSettings::class);
     }
+
+    /**
+     * @return HasMany<Theme, $this>
+     */
+    public function themes(): HasMany
+    {
+        return $this->hasMany(Theme::class);
+    }
+
+    /**
+     * @return HasMany<Page, $this>
+     */
+    public function pages(): HasMany
+    {
+        return $this->hasMany(Page::class);
+    }
+
+    /**
+     * @return HasMany<NavigationMenu, $this>
+     */
+    public function navigationMenus(): HasMany
+    {
+        return $this->hasMany(NavigationMenu::class);
+    }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
@@
 namespace Database\Factories;

-use Illuminate\Database\Eloquent\Factories\Factory;
+use App\Enums\ThemeStatus;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
@@
     public function definition(): array
     {
         return [
+            'store_id' => Store::factory(),
+            'name' => fake()->words(2, true).' theme',
+            'version' => '1.0.0',
+            'status' => ThemeStatus::Draft,
+            'published_at' => null,
         ];
     }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => ThemeStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
@@
 namespace Database\Factories;

+use App\Models\Theme;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
@@
     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),
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
@@
 namespace Database\Factories;

+use App\Models\Theme;
 use Illuminate\Database\Eloquent\Factories\Factory;
@@
     public function definition(): array
     {
         return [
+            'theme_id' => Theme::factory(),
+            'settings_json' => [
+                'announcement' => [
+                    'enabled' => true,
+                    'text' => 'Free shipping on orders over 75.00 EUR',
+                    'url' => null,
+                ],
+                'header' => [
+                    'sticky' => true,
+                    'main_menu' => 'main-menu',
+                ],
+                'footer' => [
+                    'menu' => 'footer-menu',
+                    'tagline' => 'A self-contained demo storefront.',
+                ],
+            ],
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
@@
 namespace Database\Factories;

+use App\Enums\PageStatus;
+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
@@
     public function definition(): array
     {
+        $title = Str::title(fake()->words(2, true));
+
         return [
+            'store_id' => Store::factory(),
+            'title' => $title,
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'body_html' => '<p>'.fake()->sentence().'</p>',
+            'status' => PageStatus::Draft,
+            'published_at' => null,
         ];
     }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
+
+    public function archived(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Archived,
+            'published_at' => now()->subDay(),
+        ]);
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
@@
 namespace Database\Factories;

+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
@@
     public function definition(): array
     {
+        $title = Str::title(fake()->words(2, true));
+
         return [
+            'store_id' => Store::factory(),
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'title' => $title,
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
@@
 namespace Database\Factories;

+use App\Enums\NavigationItemType;
+use App\Models\NavigationMenu;
 use Illuminate\Database\Eloquent\Factories\Factory;
@@
     public function definition(): array
     {
         return [
+            'menu_id' => NavigationMenu::factory(),
+            'type' => NavigationItemType::Link,
+            'label' => fake()->words(2, true),
+            'url' => '/'.fake()->slug(),
+            'resource_id' => null,
+            'position' => 0,
         ];
     }
+
+    public function page(int $pageId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Page,
+            'url' => null,
+            'resource_id' => $pageId,
+        ]);
+    }
+
+    public function collection(int $collectionId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Collection,
+            'url' => null,
+            'resource_id' => $collectionId,
+        ]);
+    }
+
+    public function product(int $productId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Product,
+            'url' => null,
+            'resource_id' => $productId,
+        ]);
+    }
 }
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/app/Models/Theme.php:
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;

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


sed -n '1,80p' database/factories/ThemeFactory.php


/bin/zsh
-lc
sed -n '1,80p' app/Models/ThemeFile.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

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


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

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

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


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

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

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


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

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

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


sed -n '1,80p' database/factories/ThemeFactory.php

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
 */
class ThemeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            //
        ];
    }
}


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

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
 */
class ThemeFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            //
        ];
    }
}


/bin/zsh
-lc
ls app/Enums | sort | tail -10

CollectionStatus.php
CollectionType.php
InventoryPolicy.php
MediaStatus.php
MediaType.php
ProductStatus.php
StoreDomainType.php
StoreStatus.php
StoreUserRole.php
VariantStatus.php


Output:
Chunk ID: 904787
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 45
Output:
CollectionStatus.php
CollectionType.php
InventoryPolicy.php
MediaStatus.php
MediaType.php
ProductStatus.php
StoreDomainType.php
StoreStatus.php
StoreUserRole.php
VariantStatus.php


Input:
*** Begin Patch
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/ThemeStatus.php
+<?php
+
+namespace App\Enums;
+
+enum ThemeStatus: string
+{
+    case Draft = 'draft';
+    case Published = 'published';
+}
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/PageStatus.php
+<?php
+
+namespace App\Enums;
+
+enum PageStatus: string
+{
+    case Draft = 'draft';
+    case Published = 'published';
+    case Archived = 'archived';
+}
*** Add File: /Users/fabianwesner/Herd/shop/app/Enums/NavigationItemType.php
+<?php
+
+namespace App\Enums;
+
+enum NavigationItemType: string
+{
+    case Link = 'link';
+    case Page = 'page';
+    case Collection = 'collection';
+    case Product = 'product';
+}
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223824_create_themes_table.php
@@
     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']);
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223830_create_theme_files_table.php
@@
     public function up(): void
     {
         Schema::create('theme_files', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $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');
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php
@@
     public function up(): void
     {
         Schema::create('theme_settings', function (Blueprint $table) {
-            $table->id();
-            $table->timestamps();
+            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
+            $table->json('settings_json')->default('{}');
+            $table->timestamp('updated_at')->nullable();
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223841_create_pages_table.php
@@
     public function up(): void
     {
         Schema::create('pages', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('title');
+            $table->string('handle');
+            $table->text('body_html')->nullable();
+            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
+            $table->timestamp('published_at')->nullable();
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
+            $table->index(['store_id', 'status']);
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223844_create_navigation_menus_table.php
@@
     public function up(): void
     {
         Schema::create('navigation_menus', function (Blueprint $table) {
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('handle');
+            $table->string('title');
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
         });
     }
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223848_create_navigation_items_table.php
@@
     public function up(): void
     {
         Schema::create('navigation_items', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
+            $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link');
+            $table->string('label');
+            $table->string('url')->nullable();
+            $table->unsignedBigInteger('resource_id')->nullable();
+            $table->unsignedInteger('position')->default(0);
+
+            $table->index('menu_id');
+            $table->index(['menu_id', 'position']);
         });
     }
*** End Patch


Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Enums/ThemeStatus.php
A /Users/fabianwesner/Herd/shop/app/Enums/PageStatus.php
A /Users/fabianwesner/Herd/shop/app/Enums/NavigationItemType.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223824_create_themes_table.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223830_create_theme_files_table.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223837_create_theme_settings_table.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223841_create_pages_table.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223844_create_navigation_menus_table.php
M /Users/fabianwesner/Herd/shop/database/migrations/2026_05_03_223848_create_navigation_items_table.php

@@ -14,4 +14,5 @@
         Schema::create('theme_settings', function (Blueprint $table) {
-            $table->id();
-            $table->timestamps();
+            $table->foreignId('theme_id')->primary()->constrained()->cascadeOnDelete();
+            $table->json('settings_json')->default('{}');
+            $table->timestamp('updated_at')->nullable();
         });

<?php

namespace App\Enums;

enum NavigationItemType: string
{
    case Link = 'link';
    case Page = 'page';
    case Collection = 'collection';
    case Product = 'product';
}

@@ -15,3 +15,10 @@
             $table->id();
-            $table->timestamps();
+            $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');
         });

@@ -15,3 +15,13 @@
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('title');
+            $table->string('handle');
+            $table->text('body_html')->nullable();
+            $table->enum('status', ['draft', 'published', 'archived'])->default('draft');
+            $table->timestamp('published_at')->nullable();
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
+            $table->index(['store_id', 'status']);
         });

<?php

namespace App\Enums;

enum PageStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
    case Archived = 'archived';
}

<?php

namespace App\Enums;

enum ThemeStatus: string
{
    case Draft = 'draft';
    case Published = 'published';
}

@@ -15,3 +15,11 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('menu_id')->constrained('navigation_menus')->cascadeOnDelete();
+            $table->enum('type', ['link', 'page', 'collection', 'product'])->default('link');
+            $table->string('label');
+            $table->string('url')->nullable();
+            $table->unsignedBigInteger('resource_id')->nullable();
+            $table->unsignedInteger('position')->default(0);
+
+            $table->index('menu_id');
+            $table->index(['menu_id', 'position']);
         });

@@ -15,3 +15,9 @@
             $table->id();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->string('handle');
+            $table->string('title');
             $table->timestamps();
+
+            $table->unique(['store_id', 'handle']);
+            $table->index('store_id');
         });

@@ -15,3 +15,11 @@
             $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']);
         });


Input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/Theme.php
*** Add File: /Users/fabianwesner/Herd/shop/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',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
*** Add File: /Users/fabianwesner/Herd/shop/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',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.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 ThemeSettings extends Model
+{
+    /** @use HasFactory<\Database\Factories\ThemeSettingsFactory> */
+    use HasFactory;
+
+    public $incrementing = false;
+
+    public const CREATED_AT = null;
+
+    protected $primaryKey = 'theme_id';
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'theme_id',
+        'settings_json',
+    ];
+
+    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 [
+            'settings_json' => 'array',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/Page.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/Page.php
+<?php
+
+namespace App\Models;
+
+use App\Enums\PageStatus;
+use App\Models\Concerns\BelongsToStore;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class Page extends Model
+{
+    /** @use HasFactory<\Database\Factories\PageFactory> */
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'title',
+        'handle',
+        'body_html',
+        'status',
+        'published_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'draft',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    public function isPublished(): bool
+    {
+        return $this->status === PageStatus::Published && $this->published_at !== null;
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => PageStatus::class,
+            'published_at' => 'datetime',
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
+<?php
+
+namespace App\Models;
+
+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;
+
+class NavigationMenu extends Model
+{
+    /** @use HasFactory<\Database\Factories\NavigationMenuFactory> */
+    use BelongsToStore, HasFactory;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'handle',
+        'title',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return HasMany<NavigationItem, $this>
+     */
+    public function items(): HasMany
+    {
+        return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position');
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
*** Add File: /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
+<?php
+
+namespace App\Models;
+
+use App\Enums\NavigationItemType;
+use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+class NavigationItem extends Model
+{
+    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
+    use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'menu_id',
+        'type',
+        'label',
+        'url',
+        'resource_id',
+        'position',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'type' => 'link',
+        'position' => 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('menu', function (Builder $query) use ($store): void {
+                $query
+                    ->withoutGlobalScopes()
+                    ->where('store_id', $store->getKey());
+            });
+        });
+    }
+
+    /**
+     * @return BelongsTo<NavigationMenu, $this>
+     */
+    public function menu(): BelongsTo
+    {
+        return $this->belongsTo(NavigationMenu::class, 'menu_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'type' => NavigationItemType::class,
+            'resource_id' => 'integer',
+            'position' => 'integer',
+        ];
+    }
+}
*** End Patch


Success. Updated the following files:
A /Users/fabianwesner/Herd/shop/app/Models/Theme.php
A /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
A /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
A /Users/fabianwesner/Herd/shop/app/Models/Page.php
A /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
A /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php
D /Users/fabianwesner/Herd/shop/app/Models/Theme.php
D /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php
D /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php
D /Users/fabianwesner/Herd/shop/app/Models/Page.php
D /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php
D /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.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',
        ];
    }
}

<?php

namespace App\Models;

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

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

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

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

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

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

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

<?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 ThemeSettings extends Model
{
    /** @use HasFactory<\Database\Factories\ThemeSettingsFactory> */
    use HasFactory;

    public $incrementing = false;

    public const CREATED_AT = null;

    protected $primaryKey = 'theme_id';

    /**
     * @var list<string>
     */
    protected $fillable = [
        'theme_id',
        'settings_json',
    ];

    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 [
            'settings_json' => 'array',
        ];
    }
}

<?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',
        ];
    }
}

<?php

namespace App\Models;

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;

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

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'handle',
        'title',
    ];

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

    /**
     * @return HasMany<NavigationItem, $this>
     */
    public function items(): HasMany
    {
        return $this->hasMany(NavigationItem::class, 'menu_id')->orderBy('position');
    }
}

<?php

namespace App\Models;

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

class NavigationItem extends Model
{
    /** @use HasFactory<\Database\Factories\NavigationItemFactory> */
    use HasFactory;

    public $timestamps = false;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'menu_id',
        'type',
        'label',
        'url',
        'resource_id',
        'position',
    ];

    /**
     * @var array<string, mixed>
     */
    protected $attributes = [
        'type' => 'link',
        'position' => 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('menu', function (Builder $query) use ($store): void {
                $query
                    ->withoutGlobalScopes()
                    ->where('store_id', $store->getKey());
            });
        });
    }

    /**
     * @return BelongsTo<NavigationMenu, $this>
     */
    public function menu(): BelongsTo
    {
        return $this->belongsTo(NavigationMenu::class, 'menu_id');
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'type' => NavigationItemType::class,
            'resource_id' => 'integer',
            'position' => 'integer',
        ];
    }
}


Output: {"output":"Success. Updated the following files:\nA /Users/fabianwesner/Herd/shop/app/Models/Theme.php\nA /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php\nA /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php\nA /Users/fabianwesner/Herd/shop/app/Models/Page.php\nA /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php\nA /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php\nD /Users/fabianwesner/Herd/shop/app/Models/Theme.php\nD /Users/fabianwesner/Herd/shop/app/Models/ThemeFile.php\nD /Users/fabianwesner/Herd/shop/app/Models/ThemeSettings.php\nD /Users/fabianwesner/Herd/shop/app/Models/Page.php\nD /Users/fabianwesner/Herd/shop/app/Models/NavigationMenu.php\nD /Users/fabianwesner/Herd/shop/app/Models/NavigationItem.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/Store.php
@@
     public function settings(): HasOne
     {
         return $this->hasOne(StoreSettings::class);
     }
+
+    /**
+     * @return HasMany<Theme, $this>
+     */
+    public function themes(): HasMany
+    {
+        return $this->hasMany(Theme::class);
+    }
+
+    /**
+     * @return HasMany<Page, $this>
+     */
+    public function pages(): HasMany
+    {
+        return $this->hasMany(Page::class);
+    }
+
+    /**
+     * @return HasMany<NavigationMenu, $this>
+     */
+    public function navigationMenus(): HasMany
+    {
+        return $this->hasMany(NavigationMenu::class);
+    }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Models/Store.php

@@ -64,2 +64,26 @@

+    /**
+     * @return HasMany<Theme, $this>
+     */
+    public function themes(): HasMany
+    {
+        return $this->hasMany(Theme::class);
+    }
+
+    /**
+     * @return HasMany<Page, $this>
+     */
+    public function pages(): HasMany
+    {
+        return $this->hasMany(Page::class);
+    }
+
+    /**
+     * @return HasMany<NavigationMenu, $this>
+     */
+    public function navigationMenus(): HasMany
+    {
+        return $this->hasMany(NavigationMenu::class);
+    }
+
     public function isSuspended(): bool


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Models/Store.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\ThemeStatus;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Theme>
+ */
+class ThemeFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'store_id' => Store::factory(),
+            'name' => fake()->words(2, true).' theme',
+            'version' => '1.0.0',
+            'status' => ThemeStatus::Draft,
+            'published_at' => null,
+        ];
+    }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => ThemeStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeFileFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/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),
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/ThemeSettingsFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Theme;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\ThemeSettings>
+ */
+class ThemeSettingsFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'theme_id' => Theme::factory(),
+            'settings_json' => [
+                'announcement' => [
+                    'enabled' => true,
+                    'text' => 'Free shipping on orders over 75.00 EUR',
+                    'url' => null,
+                ],
+                'header' => [
+                    'sticky' => true,
+                    'main_menu' => 'main-menu',
+                ],
+                'footer' => [
+                    'menu' => 'footer-menu',
+                    'tagline' => 'A self-contained demo storefront.',
+                ],
+            ],
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/PageFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\PageStatus;
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Page>
+ */
+class PageFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        $title = Str::title(fake()->words(2, true));
+
+        return [
+            'store_id' => Store::factory(),
+            'title' => $title,
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'body_html' => '<p>'.fake()->sentence().'</p>',
+            'status' => PageStatus::Draft,
+            'published_at' => null,
+        ];
+    }
+
+    public function published(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Published,
+            'published_at' => now(),
+        ]);
+    }
+
+    public function archived(): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'status' => PageStatus::Archived,
+            'published_at' => now()->subDay(),
+        ]);
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/NavigationMenuFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Models\Store;
+use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationMenu>
+ */
+class NavigationMenuFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        $title = Str::title(fake()->words(2, true));
+
+        return [
+            'store_id' => Store::factory(),
+            'handle' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
+            'title' => $title,
+        ];
+    }
+}
*** Delete File: /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
*** Add File: /Users/fabianwesner/Herd/shop/database/factories/NavigationItemFactory.php
+<?php
+
+namespace Database\Factories;
+
+use App\Enums\NavigationItemType;
+use App\Models\NavigationMenu;
+use Illuminate\Database\Eloquent\Factories\Factory;
+
+/**
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\NavigationItem>
+ */
+class NavigationItemFactory extends Factory
+{
+    /**
+     * Define the model's default state.
+     *
+     * @return array<string, mixed>
+     */
+    public function definition(): array
+    {
+        return [
+            'menu_id' => NavigationMenu::factory(),
+            'type' => NavigationItemType::Link,
+            'label' => fake()->words(2, true),
+            'url' => '/'.fake()->slug(),
+            'resource_id' => null,
+            'position' => 0,
+        ];
+    }
+
+    public function page(int $pageId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Page,
+            'url' => null,
+            'resource_id' => $pageId,
+        ]);
+    }
+
+    public function collection(int $collectionId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Collection,
+            'url' => null,
+            'resource_id' => $collectionId,
+        ]);
+    }
+
+    public function product(int $productId): static
+    {
+        return $this->state(fn (array $attributes): array => [
+            'type' => NavigationItemType::Product,
+            'url' => null,
+            'resource_id' => $productId,
+        ]);
+    }
+}
*** End Patch