sed -n '1,260p' tests/Feature/Admin/AnalyticsDashboardTest.php

<?php

use App\Enums\StoreUserRole;
use App\Livewire\Admin\Analytics\Index as AnalyticsIndex;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminAnalyticsStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminAnalyticsUser(?StoreUserRole $role = null): User
{
    $store = adminAnalyticsStore();
    $user = User::factory()->create([
        'email_verified_at' => now(),
    ]);

    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $user->getKey(),
        'role' => ($role ?? StoreUserRole::Owner)->value,
        'created_at' => now(),
    ]);

    return $user;
}

test('admin analytics route renders metrics for store staff and above', function (): void {
    $this->actingAs(adminAnalyticsUser(StoreUserRole::Staff))
        ->get('/admin/analytics')
        ->assertSuccessful()
        ->assertSee('Analytics')
        ->assertSee('Total sales')
        ->assertSee('Top referrers');
});

test('admin analytics rejects support users', function (): void {
    $this->actingAs(adminAnalyticsUser(StoreUserRole::Support))
        ->get('/admin/analytics')
        ->assertForbidden();
});

test('admin analytics component filters and exports csv data', function (): void {
    $store = adminAnalyticsStore();
    app()->instance('current_store', $store);

    Livewire::actingAs(adminAnalyticsUser(StoreUserRole::Admin))
        ->test(AnalyticsIndex::class)
        ->assertSee('Sales over time')
        ->set('dateRange', 'last_7_days')
        ->set('channelFilter', 'storefront')
        ->set('deviceFilter', 'mobile')
        ->call('exportCsv')
        ->assertSet('isExporting', false)
        ->assertSet('exportUrl', fn (?string $url): bool => str_starts_with((string) $url, 'data:text/csv'));
});


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

use App\Enums\StoreUserRole;
use App\Livewire\Admin\Analytics\Index as AnalyticsIndex;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    $this->withoutVite();
    $this->seed(DatabaseSeeder::class);
});

function adminAnalyticsStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function adminAnalyticsUser(?StoreUserRole $role = null): User
{
    $store = adminAnalyticsStore();
    $user = User::factory()->create([
        'email_verified_at' => now(),
    ]);

    DB::table('store_users')->insert([
        'store_id' => $store->getKey(),
        'user_id' => $user->getKey(),
        'role' => ($role ?? StoreUserRole::Owner)->value,
        'created_at' => now(),
    ]);

    return $user;
}

test('admin analytics route renders metrics for store staff and above', function (): void {
    $this->actingAs(adminAnalyticsUser(StoreUserRole::Staff))
        ->get('/admin/analytics')
        ->assertSuccessful()
        ->assertSee('Analytics')
        ->assertSee('Total sales')
        ->assertSee('Top referrers');
});

test('admin analytics rejects support users', function (): void {
    $this->actingAs(adminAnalyticsUser(StoreUserRole::Support))
        ->get('/admin/analytics')
        ->assertForbidden();
});

test('admin analytics component filters and exports csv data', function (): void {
    $store = adminAnalyticsStore();
    app()->instance('current_store', $store);

    Livewire::actingAs(adminAnalyticsUser(StoreUserRole::Admin))
        ->test(AnalyticsIndex::class)
        ->assertSee('Sales over time')
        ->set('dateRange', 'last_7_days')
        ->set('channelFilter', 'storefront')
        ->set('deviceFilter', 'mobile')
        ->call('exportCsv')
        ->assertSet('isExporting', false)
        ->assertSet('exportUrl', fn (?string $url): bool => str_starts_with((string) $url, 'data:text/csv'));
});


Output:
Chunk ID: 4b7765
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 254
Output:
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            OrganizationSeeder::class,
            StoreSeeder::class,
            StoreDomainSeeder::class,
            UserSeeder::class,
            StoreUserSeeder::class,
            StoreSettingsSeeder::class,
            SearchSettingsSeeder::class,
            CollectionSeeder::class,
            ProductSeeder::class,
            ThemeSeeder::class,
            ThemeFileSeeder::class,
            ThemeSettingsSeeder::class,
            PageSeeder::class,
            NavigationMenuSeeder::class,
            NavigationItemSeeder::class,
            TaxSettingsSeeder::class,
            ShippingZoneSeeder::class,
            ShippingRateSeeder::class,
            DiscountSeeder::class,
            CustomerSeeder::class,
            AnalyticsSeeder::class,
        ]);
    }
}


sed -n '1,260p' resources/views/layouts/app/sidebar.blade.php

sidebar.blade.php

<!DOCTYPE html>
getLocale()) }}" class="dark">

        @include('partials.head')

        <flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:sidebar.header>
                <x-app-logo :sidebar="true" href="{{ route('admin.dashboard') }}" wire:navigate />
                <flux:sidebar.collapse class="lg:hidden" />
            </flux:sidebar.header>

            <flux:sidebar.nav>
                <flux:sidebar.group :heading="__('Platform')" class="grid">
                    <flux:sidebar.item icon="home" :href="route('admin.dashboard')" :current="request()->routeIs('admin.dashboard')" wire:navigate>
                        {{ __('Dashboard') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="chart-bar" :href="route('admin.analytics.index')" :current="request()->routeIs('admin.analytics.*')" wire:navigate>
                        {{ __('Analytics') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Products')" class="grid">
                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
                        {{ __('Products') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="rectangle-stack" :href="route('admin.collections.index')" :current="request()->routeIs('admin.collections.*')" wire:navigate>
                        {{ __('Collections') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="archive-box" :href="route('admin.inventory.index')" :current="request()->routeIs('admin.inventory.*')" wire:navigate>
                        {{ __('Inventory') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Operations')" class="grid">
                    <flux:sidebar.item icon="shopping-bag" :href="route('admin.orders.index')" :current="request()->routeIs('admin.orders.*')" wire:navigate>
                        {{ __('Orders') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="users" :href="route('admin.customers.index')" :current="request()->routeIs('admin.customers.*')" wire:navigate>
                        {{ __('Customers') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="tag" :href="route('admin.discounts.index')" :current="request()->routeIs('admin.discounts.*')" wire:navigate>
                        {{ __('Discounts') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Storefront')" class="grid">
                    <flux:sidebar.item icon="document-text" :href="route('admin.pages.index')" :current="request()->routeIs('admin.pages.*')" wire:navigate>
                        {{ __('Pages') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="bars-3" :href="route('admin.navigation.index')" :current="request()->routeIs('admin.navigation.*')" wire:navigate>
                        {{ __('Navigation') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="paint-brush" :href="route('admin.themes.index')" :current="request()->routeIs('admin.themes.*')" wire:navigate>
                        {{ __('Themes') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Configuration')" class="grid">
                    <flux:sidebar.item icon="cog-6-tooth" :href="route('admin.settings.index')" :current="request()->routeIs('admin.settings.*')" wire:navigate>
                        {{ __('Settings') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="magnifying-glass" :href="route('admin.search.settings')" :current="request()->routeIs('admin.search.*')" wire:navigate>
                        {{ __('Search') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>
            </flux:sidebar.nav>

            <flux:spacer />

            <flux:sidebar.nav>
                <flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
                    {{ __('Repository') }}
                </flux:sidebar.item>

                <flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
                    {{ __('Documentation') }}
                </flux:sidebar.item>
            </flux:sidebar.nav>

            <x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
        </flux:sidebar>

        <flux:header class="lg:hidden">
            <flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />

            <flux:spacer />

            <flux:dropdown position="top" align="end">
                <flux:profile
                    :initials="auth()->user()->initials()"
                    icon-trailing="chevron-down"
                />

                <flux:menu>
                    <flux:menu.radio.group>

                                <flux:avatar
                                    :name="auth()->user()->name"
                                    :initials="auth()->user()->initials()"
                                />

                                    <flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
                                    <flux:text class="truncate">{{ auth()->user()->email }}</flux:text>

                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <flux:menu.radio.group>
                        <flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
                            {{ __('Settings') }}
                        </flux:menu.item>
                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <form method="POST" action="{{ route('logout') }}" class="w-full">
                        @csrf
                        <flux:menu.item
                            as="button"
                            type="submit"
                            icon="arrow-right-start-on-rectangle"
                            class="w-full cursor-pointer"
                            data-test="logout-button"
                        >
                            {{ __('Log Out') }}
                        </flux:menu.item>
                    </form>
                </flux:menu>
            </flux:dropdown>
        </flux:header>

        {{ $slot }}

        @fluxScripts


Output:
Chunk ID: 02e426
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1853
Output:
<!DOCTYPE html>
getLocale()) }}" class="dark">

        @include('partials.head')

        <flux:sidebar sticky collapsible="mobile" class="border-e border-zinc-200 bg-zinc-50 dark:border-zinc-700 dark:bg-zinc-900">
            <flux:sidebar.header>
                <x-app-logo :sidebar="true" href="{{ route('admin.dashboard') }}" wire:navigate />
                <flux:sidebar.collapse class="lg:hidden" />
            </flux:sidebar.header>

            <flux:sidebar.nav>
                <flux:sidebar.group :heading="__('Platform')" class="grid">
                    <flux:sidebar.item icon="home" :href="route('admin.dashboard')" :current="request()->routeIs('admin.dashboard')" wire:navigate>
                        {{ __('Dashboard') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="chart-bar" :href="route('admin.analytics.index')" :current="request()->routeIs('admin.analytics.*')" wire:navigate>
                        {{ __('Analytics') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Products')" class="grid">
                    <flux:sidebar.item icon="cube" :href="route('admin.products.index')" :current="request()->routeIs('admin.products.*')" wire:navigate>
                        {{ __('Products') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="rectangle-stack" :href="route('admin.collections.index')" :current="request()->routeIs('admin.collections.*')" wire:navigate>
                        {{ __('Collections') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="archive-box" :href="route('admin.inventory.index')" :current="request()->routeIs('admin.inventory.*')" wire:navigate>
                        {{ __('Inventory') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Operations')" class="grid">
                    <flux:sidebar.item icon="shopping-bag" :href="route('admin.orders.index')" :current="request()->routeIs('admin.orders.*')" wire:navigate>
                        {{ __('Orders') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="users" :href="route('admin.customers.index')" :current="request()->routeIs('admin.customers.*')" wire:navigate>
                        {{ __('Customers') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="tag" :href="route('admin.discounts.index')" :current="request()->routeIs('admin.discounts.*')" wire:navigate>
                        {{ __('Discounts') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Storefront')" class="grid">
                    <flux:sidebar.item icon="document-text" :href="route('admin.pages.index')" :current="request()->routeIs('admin.pages.*')" wire:navigate>
                        {{ __('Pages') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="bars-3" :href="route('admin.navigation.index')" :current="request()->routeIs('admin.navigation.*')" wire:navigate>
                        {{ __('Navigation') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="paint-brush" :href="route('admin.themes.index')" :current="request()->routeIs('admin.themes.*')" wire:navigate>
                        {{ __('Themes') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>

                <flux:sidebar.group :heading="__('Configuration')" class="grid">
                    <flux:sidebar.item icon="cog-6-tooth" :href="route('admin.settings.index')" :current="request()->routeIs('admin.settings.*')" wire:navigate>
                        {{ __('Settings') }}
                    </flux:sidebar.item>

                    <flux:sidebar.item icon="magnifying-glass" :href="route('admin.search.settings')" :current="request()->routeIs('admin.search.*')" wire:navigate>
                        {{ __('Search') }}
                    </flux:sidebar.item>
                </flux:sidebar.group>
            </flux:sidebar.nav>

            <flux:spacer />

            <flux:sidebar.nav>
                <flux:sidebar.item icon="folder-git-2" href="https://github.com/laravel/livewire-starter-kit" target="_blank">
                    {{ __('Repository') }}
                </flux:sidebar.item>

                <flux:sidebar.item icon="book-open-text" href="https://laravel.com/docs/starter-kits#livewire" target="_blank">
                    {{ __('Documentation') }}
                </flux:sidebar.item>
            </flux:sidebar.nav>

            <x-desktop-user-menu class="hidden lg:block" :name="auth()->user()->name" />
        </flux:sidebar>

        <flux:header class="lg:hidden">
            <flux:sidebar.toggle class="lg:hidden" icon="bars-2" inset="left" />

            <flux:spacer />

            <flux:dropdown position="top" align="end">
                <flux:profile
                    :initials="auth()->user()->initials()"
                    icon-trailing="chevron-down"
                />

                <flux:menu>
                    <flux:menu.radio.group>

                                <flux:avatar
                                    :name="auth()->user()->name"
                                    :initials="auth()->user()->initials()"
                                />

                                    <flux:heading class="truncate">{{ auth()->user()->name }}</flux:heading>
                                    <flux:text class="truncate">{{ auth()->user()->email }}</flux:text>

                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <flux:menu.radio.group>
                        <flux:menu.item :href="route('profile.edit')" icon="cog" wire:navigate>
                            {{ __('Settings') }}
                        </flux:menu.item>
                    </flux:menu.radio.group>

                    <flux:menu.separator />

                    <form method="POST" action="{{ route('logout') }}" class="w-full">
                        @csrf
                        <flux:menu.item
                            as="button"
                            type="submit"
                            icon="arrow-right-start-on-rectangle"
                            class="w-full cursor-pointer"
                            data-test="logout-button"
                        >
                            {{ __('Log Out') }}
                        </flux:menu.item>
                    </form>
                </flux:menu>
            </flux:dropdown>
        </flux:header>

        {{ $slot }}

        @fluxScripts


sed -n '1,120p' app/Jobs/DeliverWebhook.php


sed -n '1,120p' app/Jobs/DeliverWebhook.php

<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class DeliverWebhook implements ShouldQueue
{
    use Queueable;

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

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        //
    }
}


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

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class DeliverWebhook implements ShouldQueue
{
    use Queueable;

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

    /**
     * Execute the job.
     */
    public function handle(): void
    {
        //
    }
}


rg -n "flux:modal|modal.trigger|wire:model=\".*Modal|open.*Modal" resources/views/livewire app/Livewire/Admin | sed -n '1,200p'


rg -n "flux:modal|modal.trigger|wire:model=\".*Modal|open.*Modal" resources/views/livewire app/Livewire/Admin | sed -n '1,200p'

rg -n "flux:modal|modal.trigger|wire:model=\\\".*Modal|open.*Modal" resources/views/livewire app/Livewire/Admin

Question: flux:modal|modal.trigger|wire:model=\".*Modal|open.*Modal

resources/views/livewire/storefront/cart-drawer.blade.php:1:<flux:modal name="cart-drawer" class="md:w-[32rem]">
resources/views/livewire/storefront/cart-drawer.blade.php:70:</flux:modal>
resources/views/livewire/settings/two-factor.blade.php:57:    <flux:modal
resources/views/livewire/settings/two-factor.blade.php:61:        wire:model="showModal"
resources/views/livewire/settings/two-factor.blade.php:209:    </flux:modal>
resources/views/livewire/settings/delete-user-form.blade.php:7:    <flux:modal.trigger name="confirm-user-deletion">
resources/views/livewire/settings/delete-user-form.blade.php:11:    </flux:modal.trigger>
resources/views/livewire/settings/delete-user-form.blade.php:13:    <flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
resources/views/livewire/settings/delete-user-form.blade.php:26:                <flux:modal.close>
resources/views/livewire/settings/delete-user-form.blade.php:28:                </flux:modal.close>
resources/views/livewire/settings/delete-user-form.blade.php:33:    </flux:modal>
resources/views/livewire/admin/customers/show.blade.php:143:    <flux:modal name="address-form" class="md:w-[32rem]">
resources/views/livewire/admin/customers/show.blade.php:177:                <flux:modal.close>
resources/views/livewire/admin/customers/show.blade.php:179:                </flux:modal.close>
resources/views/livewire/admin/customers/show.blade.php:183:    </flux:modal>
resources/views/livewire/admin/orders/show.blade.php:22:                <flux:modal.trigger name="refund-order">
resources/views/livewire/admin/orders/show.blade.php:26:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:30:                <flux:modal.trigger name="fulfillment-order">
resources/views/livewire/admin/orders/show.blade.php:34:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:219:    <flux:modal name="refund-order" class="md:w-[32rem]">
resources/views/livewire/admin/orders/show.blade.php:231:                <flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:233:                </flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:237:    </flux:modal>
resources/views/livewire/admin/orders/show.blade.php:239:    <flux:modal name="fulfillment-order" class="md:w-[38rem]">
resources/views/livewire/admin/orders/show.blade.php:271:                <flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:273:                </flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:277:    </flux:modal>


Output:
Chunk ID: 7250e7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 661
Output:
resources/views/livewire/storefront/cart-drawer.blade.php:1:<flux:modal name="cart-drawer" class="md:w-[32rem]">
resources/views/livewire/storefront/cart-drawer.blade.php:70:</flux:modal>
resources/views/livewire/settings/two-factor.blade.php:57:    <flux:modal
resources/views/livewire/settings/two-factor.blade.php:61:        wire:model="showModal"
resources/views/livewire/settings/two-factor.blade.php:209:    </flux:modal>
resources/views/livewire/settings/delete-user-form.blade.php:7:    <flux:modal.trigger name="confirm-user-deletion">
resources/views/livewire/settings/delete-user-form.blade.php:11:    </flux:modal.trigger>
resources/views/livewire/settings/delete-user-form.blade.php:13:    <flux:modal name="confirm-user-deletion" :show="$errors->isNotEmpty()" focusable class="max-w-lg">
resources/views/livewire/settings/delete-user-form.blade.php:26:                <flux:modal.close>
resources/views/livewire/settings/delete-user-form.blade.php:28:                </flux:modal.close>
resources/views/livewire/settings/delete-user-form.blade.php:33:    </flux:modal>
resources/views/livewire/admin/customers/show.blade.php:143:    <flux:modal name="address-form" class="md:w-[32rem]">
resources/views/livewire/admin/customers/show.blade.php:177:                <flux:modal.close>
resources/views/livewire/admin/customers/show.blade.php:179:                </flux:modal.close>
resources/views/livewire/admin/customers/show.blade.php:183:    </flux:modal>
resources/views/livewire/admin/orders/show.blade.php:22:                <flux:modal.trigger name="refund-order">
resources/views/livewire/admin/orders/show.blade.php:26:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:30:                <flux:modal.trigger name="fulfillment-order">
resources/views/livewire/admin/orders/show.blade.php:34:                </flux:modal.trigger>
resources/views/livewire/admin/orders/show.blade.php:219:    <flux:modal name="refund-order" class="md:w-[32rem]">
resources/views/livewire/admin/orders/show.blade.php:231:                <flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:233:                </flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:237:    </flux:modal>
resources/views/livewire/admin/orders/show.blade.php:239:    <flux:modal name="fulfillment-order" class="md:w-[38rem]">
resources/views/livewire/admin/orders/show.blade.php:271:                <flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:273:                </flux:modal.close>
resources/views/livewire/admin/orders/show.blade.php:277:    </flux:modal>


sed -n '120,195p' resources/views/livewire/admin/customers/show.blade.php


sed -n '120,195p' resources/views/livewire/admin/customers/show.blade.php

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


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

{{ data_get($addressData, 'country') }}

                            <flux:button wire:click="openAddressForm({{ $address->getKey() }})" size="sm" variant="filled">Edit</flux:button>
                            @unless ($address->is_default)
                                <flux:button wire:click="setDefaultAddress({{ $address->getKey() }})" size="sm" variant="ghost">Set default</flux:button>
                            @endunless
                            <flux:button wire:click="deleteAddress({{ $address->getKey() }})" wire:confirm="Delete this address?" size="sm" variant="danger">Delete</flux:button>

                @empty

                        <flux:icon name="map-pin" class="mx-auto size-8 text-zinc-400" />
                        <flux:text class="mt-3">No addresses saved.</flux:text>

                @endforelse

    <flux:modal name="address-form" class="md:w-[32rem]">
        <form wire:submit="saveAddress" class="space-y-6">

                <flux:heading size="lg">{{ $editingAddressId ? 'Edit address' : 'Add address' }}</flux:heading>

            <flux:input wire:model="addressLabel" label="Label" placeholder="Home, Office..." />

                <flux:input wire:model="addressJson.first_name" label="First name" />
                <flux:input wire:model="addressJson.last_name" label="Last name" />

                    <flux:input wire:model="addressJson.address1" label="Address line 1" />
                    <flux:error name="addressJson.address1" />

                    <flux:input wire:model="addressJson.address2" label="Address line 2" />

                <flux:input wire:model="addressJson.city" label="City" />
                <flux:input wire:model="addressJson.province_code" label="State / Province" />
                <flux:input wire:model="addressJson.postal_code" label="Postal code" />
                <flux:select wire:model="addressJson.country" label="Country">
                    <flux:select.option value="DE">Germany</flux:select.option>
                    <flux:select.option value="US">United States</flux:select.option>
                    <flux:select.option value="GB">United Kingdom</flux:select.option>
                    <flux:select.option value="FR">France</flux:select.option>
                    <flux:select.option value="NL">Netherlands</flux:select.option>
                </flux:select>

            <flux:error name="addressJson.city" />
            <flux:error name="addressJson.postal_code" />
            <flux:error name="addressJson.country" />

                <flux:modal.close>
                    <flux:button variant="filled">Cancel</flux:button>
                </flux:modal.close>
                <flux:button type="submit" variant="primary">Save</flux:button>

        </form>
    </flux:modal>


sed -n '1,150p' app/Livewire/Admin/Customers/Show.php


sed -n '1,150p' app/Livewire/Admin/Customers/Show.php

<?php

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

    /**
     * @var array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    public array $addressJson = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public function mount(Customer $customer): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($customer->getKey())
            ->first();

        abort_unless($customer instanceof Customer, 404);

        $this->storeId = $store->getKey();
        $this->customerId = $customer->getKey();
    }

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

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

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [


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

namespace App\Livewire\Admin\Customers;

use App\Models\Customer;
use App\Models\CustomerAddress;
use App\Models\Order;
use App\Models\Store;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Locked;
use Livewire\Component;
use Livewire\WithPagination;

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

    /**
     * @var array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    public array $addressJson = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'province_code' => '',
        'country' => 'DE',
        'postal_code' => '',
    ];

    public function mount(Customer $customer): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $customer = Customer::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->whereKey($customer->getKey())
            ->first();

        abort_unless($customer instanceof Customer, 404);

        $this->storeId = $store->getKey();
        $this->customerId = $customer->getKey();
    }

    public function openAddressForm(?int $addressId = null): void
    {
        $this->editingAddressId = $addressId;

        if ($addressId) {
            $address = $this->address($addressId);
            $this->addressLabel = (string) $address->label;
            $this->addressJson = array_merge($this->emptyAddressJson(), $address->address_json ?? []);
        } else {
            $this->resetAddressForm();
        }

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

    public function saveAddress(): void
    {
        $this->validate([
            'addressLabel' => ['nullable', 'string', 'max:255'],
            'addressJson.first_name' => ['nullable', 'string', 'max:255'],
            'addressJson.last_name' => ['nullable', 'string', 'max:255'],
            'addressJson.address1' => ['required', 'string', 'max:255'],
            'addressJson.address2' => ['nullable', 'string', 'max:255'],
            'addressJson.city' => ['required', 'string', 'max:255'],
            'addressJson.province_code' => ['nullable', 'string', 'max:255'],
            'addressJson.country' => ['required', 'string', 'size:2'],
            'addressJson.postal_code' => ['required', 'string', 'max:32'],
        ]);

        $address = $this->editingAddressId
            ? $this->address($this->editingAddressId)
            : new CustomerAddress(['customer_id' => $this->customerId]);

        $address->fill([
            'label' => $this->addressLabel !== '' ? $this->addressLabel : null,
            'address_json' => $this->addressJson,
            'is_default' => $address->exists ? $address->is_default : ! $this->customer()->addresses()->exists(),
        ]);
        $address->save();

        $this->resetAddressForm();
        $this->modal('address-form')->close();
        $this->dispatch('toast', type: 'success', message: __('Address saved'));
    }

    public function deleteAddress(int $addressId): void
    {
        $address = $this->address($addressId);
        $wasDefault = $address->is_default;
        $address->delete();

        if ($wasDefault) {
            $this->setFirstAddressAsDefault();
        }

        $this->dispatch('toast', type: 'success', message: __('Address deleted'));
    }

    public function setDefaultAddress(int $addressId): void
    {
        $address = $this->address($addressId);

        DB::transaction(function () use ($address): void {
            CustomerAddress::query()
                ->where('customer_id', $this->customerId)
                ->update(['is_default' => false]);

            $address->forceFill(['is_default' => true])->save();
        });

        $this->dispatch('toast', type: 'success', message: __('Default address updated'));
    }

    public function orders(): LengthAwarePaginator
    {
        return Order::withoutGlobalScopes()
            ->where('store_id', $this->storeId)
            ->where('customer_id', $this->customerId)
            ->latest('placed_at')
            ->latest('id')
            ->paginate(10);
    }

    public function render(): mixed
    {
        $customer = $this->customer();

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [


I’m editing the schema/model layer first: enum-backed statuses, encrypted secrets, JSON casts, store scoping, and deterministic seed data for the app directory and webhook management screens.


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/AppStatus.php
@@
 <?php

 namespace App\Enums;

-enum AppStatus
+enum AppStatus: string
 {
+    case Active = 'active';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Disabled => 'Disabled',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/AppInstallationStatus.php
@@
 <?php

 namespace App\Enums;

-enum AppInstallationStatus
+enum AppInstallationStatus: string
 {
+    case Active = 'active';
+    case Suspended = 'suspended';
+    case Uninstalled = 'uninstalled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Suspended => 'Suspended',
+            self::Uninstalled => 'Uninstalled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Suspended => 'amber',
+            self::Uninstalled => 'zinc',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookSubscriptionStatus.php
@@
 <?php

 namespace App\Enums;

-enum WebhookSubscriptionStatus
+enum WebhookSubscriptionStatus: string
 {
+    case Active = 'active';
+    case Paused = 'paused';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Paused => 'Paused',
+            self::Disabled => 'Disabled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Paused => 'amber',
+            self::Disabled => 'zinc',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookDeliveryStatus.php
@@
 <?php

 namespace App\Enums;

-enum WebhookDeliveryStatus
+enum WebhookDeliveryStatus: string
 {
+    case Pending = 'pending';
+    case Success = 'success';
+    case Failed = 'failed';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Pending => 'Pending',
+            self::Success => 'Success',
+            self::Failed => 'Failed',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Pending => 'blue',
+            self::Success => 'green',
+            self::Failed => 'red',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookEventType.php
@@
 <?php

 namespace App\Enums;

-enum WebhookEventType
+enum WebhookEventType: string
 {
+    case OrderCreated = 'order.created';
+    case OrderPaid = 'order.paid';
+    case OrderUpdated = 'order.updated';
+    case OrderCancelled = 'order.cancelled';
+    case OrderFulfilled = 'order.fulfilled';
+    case OrderRefunded = 'order.refunded';
+    case ProductCreated = 'product.created';
+    case ProductUpdated = 'product.updated';
+    case ProductDeleted = 'product.deleted';
+    case CustomerCreated = 'customer.created';
+    case CheckoutCompleted = 'checkout.completed';
+    case FulfillmentCreated = 'fulfillment.created';
+    case RefundCreated = 'refund.created';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::OrderCreated => 'Order created',
+            self::OrderPaid => 'Order paid',
+            self::OrderUpdated => 'Order updated',
+            self::OrderCancelled => 'Order cancelled',
+            self::OrderFulfilled => 'Order fulfilled',
+            self::OrderRefunded => 'Order refunded',
+            self::ProductCreated => 'Product created',
+            self::ProductUpdated => 'Product updated',
+            self::ProductDeleted => 'Product deleted',
+            self::CustomerCreated => 'Customer created',
+            self::CheckoutCompleted => 'Checkout completed',
+            self::FulfillmentCreated => 'Fulfillment created',
+            self::RefundCreated => 'Refund created',
+        };
+    }
+
+    /**
+     * @return list<self>
+     */
+    public static function selectable(): array
+    {
+        return [
+            self::OrderCreated,
+            self::OrderUpdated,
+            self::OrderCancelled,
+            self::ProductCreated,
+            self::ProductUpdated,
+            self::ProductDeleted,
+            self::CustomerCreated,
+            self::CheckoutCompleted,
+            self::FulfillmentCreated,
+            self::RefundCreated,
+        ];
+    }
 }
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/app/Enums/AppStatus.php:
<?php

namespace App\Enums;

enum AppStatus
{
}


sed -n '1,80p' app/Enums/AppInstallationStatus.php


sed -n '1,80p' app/Enums/WebhookEventType.php


/bin/zsh
-lc
sed -n '1,80p' app/Enums/AppStatus.php

<?php

namespace App\Enums;

enum AppStatus
{
    //
}


sed -n '1,80p' app/Enums/AppInstallationStatus.php

<?php

namespace App\Enums;

enum AppInstallationStatus
{
    //
}


sed -n '1,80p' app/Enums/WebhookEventType.php

<?php

namespace App\Enums;

enum WebhookEventType
{
    //
}


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

namespace App\Enums;

enum AppStatus
{
    //
}


Output:
Chunk ID: 274f56
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 17
Output:
<?php

namespace App\Enums;

enum AppInstallationStatus
{
    //
}


Output:
Chunk ID: 318fc0
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 16
Output:
<?php

namespace App\Enums;

enum WebhookEventType
{
    //
}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/AppStatus.php
@@
-enum AppStatus
+enum AppStatus: string
 {
-    //
+    case Active = 'active';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Disabled => 'Disabled',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/AppInstallationStatus.php
@@
-enum AppInstallationStatus
+enum AppInstallationStatus: string
 {
-    //
+    case Active = 'active';
+    case Suspended = 'suspended';
+    case Uninstalled = 'uninstalled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Suspended => 'Suspended',
+            self::Uninstalled => 'Uninstalled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Suspended => 'amber',
+            self::Uninstalled => 'zinc',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookSubscriptionStatus.php
@@
-enum WebhookSubscriptionStatus
+enum WebhookSubscriptionStatus: string
 {
-    //
+    case Active = 'active';
+    case Paused = 'paused';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Paused => 'Paused',
+            self::Disabled => 'Disabled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Paused => 'amber',
+            self::Disabled => 'zinc',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookDeliveryStatus.php
@@
-enum WebhookDeliveryStatus
+enum WebhookDeliveryStatus: string
 {
-    //
+    case Pending = 'pending';
+    case Success = 'success';
+    case Failed = 'failed';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Pending => 'Pending',
+            self::Success => 'Success',
+            self::Failed => 'Failed',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Pending => 'blue',
+            self::Success => 'green',
+            self::Failed => 'red',
+        };
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Enums/WebhookEventType.php
@@
-enum WebhookEventType
+enum WebhookEventType: string
 {
-    //
+    case OrderCreated = 'order.created';
+    case OrderPaid = 'order.paid';
+    case OrderUpdated = 'order.updated';
+    case OrderCancelled = 'order.cancelled';
+    case OrderFulfilled = 'order.fulfilled';
+    case OrderRefunded = 'order.refunded';
+    case ProductCreated = 'product.created';
+    case ProductUpdated = 'product.updated';
+    case ProductDeleted = 'product.deleted';
+    case CustomerCreated = 'customer.created';
+    case CheckoutCompleted = 'checkout.completed';
+    case FulfillmentCreated = 'fulfillment.created';
+    case RefundCreated = 'refund.created';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::OrderCreated => 'Order created',
+            self::OrderPaid => 'Order paid',
+            self::OrderUpdated => 'Order updated',
+            self::OrderCancelled => 'Order cancelled',
+            self::OrderFulfilled => 'Order fulfilled',
+            self::OrderRefunded => 'Order refunded',
+            self::ProductCreated => 'Product created',
+            self::ProductUpdated => 'Product updated',
+            self::ProductDeleted => 'Product deleted',
+            self::CustomerCreated => 'Customer created',
+            self::CheckoutCompleted => 'Checkout completed',
+            self::FulfillmentCreated => 'Fulfillment created',
+            self::RefundCreated => 'Refund created',
+        };
+    }
+
+    /**
+     * @return list<self>
+     */
+    public static function selectable(): array
+    {
+        return [
+            self::OrderCreated,
+            self::OrderUpdated,
+            self::OrderCancelled,
+            self::ProductCreated,
+            self::ProductUpdated,
+            self::ProductDeleted,
+            self::CustomerCreated,
+            self::CheckoutCompleted,
+            self::FulfillmentCreated,
+            self::RefundCreated,
+        ];
+    }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Enums/AppStatus.php
M /Users/fabianwesner/Herd/shop/app/Enums/AppInstallationStatus.php
M /Users/fabianwesner/Herd/shop/app/Enums/WebhookSubscriptionStatus.php
M /Users/fabianwesner/Herd/shop/app/Enums/WebhookDeliveryStatus.php
M /Users/fabianwesner/Herd/shop/app/Enums/WebhookEventType.php

@@ -4,5 +4,25 @@

-enum WebhookDeliveryStatus
+enum WebhookDeliveryStatus: string
 {
-    //
+    case Pending = 'pending';
+    case Success = 'success';
+    case Failed = 'failed';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Pending => 'Pending',
+            self::Success => 'Success',
+            self::Failed => 'Failed',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Pending => 'blue',
+            self::Success => 'green',
+            self::Failed => 'red',
+        };
+    }
 }

@@ -4,5 +4,25 @@

-enum AppInstallationStatus
+enum AppInstallationStatus: string
 {
-    //
+    case Active = 'active';
+    case Suspended = 'suspended';
+    case Uninstalled = 'uninstalled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Suspended => 'Suspended',
+            self::Uninstalled => 'Uninstalled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Suspended => 'amber',
+            self::Uninstalled => 'zinc',
+        };
+    }
 }

@@ -4,5 +4,25 @@

-enum WebhookSubscriptionStatus
+enum WebhookSubscriptionStatus: string
 {
-    //
+    case Active = 'active';
+    case Paused = 'paused';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Paused => 'Paused',
+            self::Disabled => 'Disabled',
+        };
+    }
+
+    public function badgeColor(): string
+    {
+        return match ($this) {
+            self::Active => 'green',
+            self::Paused => 'amber',
+            self::Disabled => 'zinc',
+        };
+    }
 }

@@ -4,5 +4,14 @@

-enum AppStatus
+enum AppStatus: string
 {
-    //
+    case Active = 'active';
+    case Disabled = 'disabled';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::Active => 'Active',
+            self::Disabled => 'Disabled',
+        };
+    }
 }

@@ -4,5 +4,55 @@

-enum WebhookEventType
+enum WebhookEventType: string
 {
-    //
+    case OrderCreated = 'order.created';
+    case OrderPaid = 'order.paid';
+    case OrderUpdated = 'order.updated';
+    case OrderCancelled = 'order.cancelled';
+    case OrderFulfilled = 'order.fulfilled';
+    case OrderRefunded = 'order.refunded';
+    case ProductCreated = 'product.created';
+    case ProductUpdated = 'product.updated';
+    case ProductDeleted = 'product.deleted';
+    case CustomerCreated = 'customer.created';
+    case CheckoutCompleted = 'checkout.completed';
+    case FulfillmentCreated = 'fulfillment.created';
+    case RefundCreated = 'refund.created';
+
+    public function label(): string
+    {
+        return match ($this) {
+            self::OrderCreated => 'Order created',
+            self::OrderPaid => 'Order paid',
+            self::OrderUpdated => 'Order updated',
+            self::OrderCancelled => 'Order cancelled',
+            self::OrderFulfilled => 'Order fulfilled',
+            self::OrderRefunded => 'Order refunded',
+            self::ProductCreated => 'Product created',
+            self::ProductUpdated => 'Product updated',
+            self::ProductDeleted => 'Product deleted',
+            self::CustomerCreated => 'Customer created',
+            self::CheckoutCompleted => 'Checkout completed',
+            self::FulfillmentCreated => 'Fulfillment created',
+            self::RefundCreated => 'Refund created',
+        };
+    }
+
+    /**
+     * @return list<self>
+     */
+    public static function selectable(): array
+    {
+        return [
+            self::OrderCreated,
+            self::OrderUpdated,
+            self::OrderCancelled,
+            self::ProductCreated,
+            self::ProductUpdated,
+            self::ProductDeleted,
+            self::CustomerCreated,
+            self::CheckoutCompleted,
+            self::FulfillmentCreated,
+            self::RefundCreated,
+        ];
+    }
 }


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/app/Enums/AppStatus.php\nM /Users/fabianwesner/Herd/shop/app/Enums/AppInstallationStatus.php\nM /Users/fabianwesner/Herd/shop/app/Enums/WebhookSubscriptionStatus.php\nM /Users/fabianwesner/Herd/shop/app/Enums/WebhookDeliveryStatus.php\nM /Users/fabianwesner/Herd/shop/app/Enums/WebhookEventType.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034556_create_apps_table.php
@@
         Schema::create('apps', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->string('name');
+            $table->string('status')->default('active');
+            $table->timestamp('created_at')->nullable();
+
+            $table->index('status', 'idx_apps_status');
         });
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034601_create_app_installations_table.php
@@
         Schema::create('app_installations', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('app_id')->constrained('apps')->cascadeOnDelete();
+            $table->json('scopes_json')->default('[]');
+            $table->string('status')->default('active');
+            $table->timestamp('installed_at')->nullable();
+
+            $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app');
+            $table->index('store_id', 'idx_app_installations_store_id');
+            $table->index('app_id', 'idx_app_installations_app_id');
         });
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034606_create_oauth_clients_table.php
@@
         Schema::create('oauth_clients', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('app_id')->constrained('apps')->cascadeOnDelete();
+            $table->string('client_id')->unique('idx_oauth_clients_client_id');
+            $table->text('client_secret_encrypted');
+            $table->json('redirect_uris_json')->default('[]');
+
+            $table->index('app_id', 'idx_oauth_clients_app_id');
         });
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034611_create_oauth_tokens_table.php
@@
         Schema::create('oauth_tokens', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('installation_id')->constrained('app_installations')->cascadeOnDelete();
+            $table->string('name')->nullable();
+            $table->string('access_token_hash')->unique('idx_oauth_tokens_access_hash');
+            $table->string('refresh_token_hash')->nullable();
+            $table->json('abilities_json')->default('[]');
+            $table->timestamp('expires_at');
+            $table->timestamp('last_used_at')->nullable();
+            $table->timestamp('created_at')->nullable();
+
+            $table->index('installation_id', 'idx_oauth_tokens_installation_id');
+            $table->index('expires_at', 'idx_oauth_tokens_expires_at');
         });
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034616_create_webhook_subscriptions_table.php
@@
         Schema::create('webhook_subscriptions', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('app_installation_id')->nullable()->constrained('app_installations')->cascadeOnDelete();
+            $table->string('event_type');
+            $table->string('target_url', 2048);
+            $table->text('signing_secret_encrypted');
+            $table->string('status')->default('active');
+
+            $table->index('store_id', 'idx_webhook_subscriptions_store_id');
+            $table->index(['store_id', 'event_type'], 'idx_webhook_subscriptions_store_event');
+            $table->index('app_installation_id', 'idx_webhook_subscriptions_installation');
         });
*** Update File: /Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_034621_create_webhook_deliveries_table.php
@@
         Schema::create('webhook_deliveries', function (Blueprint $table) {
             $table->id();
-            $table->timestamps();
+            $table->foreignId('subscription_id')->constrained('webhook_subscriptions')->cascadeOnDelete();
+            $table->uuid('event_id');
+            $table->unsignedSmallInteger('attempt_count')->default(1);
+            $table->string('status')->default('pending');
+            $table->timestamp('last_attempt_at')->nullable();
+            $table->unsignedSmallInteger('response_code')->nullable();
+            $table->text('response_body_snippet')->nullable();
+
+            $table->index('subscription_id', 'idx_webhook_deliveries_subscription_id');
+            $table->index('event_id', 'idx_webhook_deliveries_event_id');
+            $table->index('status', 'idx_webhook_deliveries_status');
+            $table->index('last_attempt_at', 'idx_webhook_deliveries_last_attempt');
         });
*** End Patch


@@ -15,3 +15,14 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('subscription_id')->constrained('webhook_subscriptions')->cascadeOnDelete();
+            $table->uuid('event_id');
+            $table->unsignedSmallInteger('attempt_count')->default(1);
+            $table->string('status')->default('pending');
+            $table->timestamp('last_attempt_at')->nullable();
+            $table->unsignedSmallInteger('response_code')->nullable();
+            $table->text('response_body_snippet')->nullable();
+
+            $table->index('subscription_id', 'idx_webhook_deliveries_subscription_id');
+            $table->index('event_id', 'idx_webhook_deliveries_event_id');
+            $table->index('status', 'idx_webhook_deliveries_status');
+            $table->index('last_attempt_at', 'idx_webhook_deliveries_last_attempt');
         });

@@ -15,3 +15,7 @@
             $table->id();
-            $table->timestamps();
+            $table->string('name');
+            $table->string('status')->default('active');
+            $table->timestamp('created_at')->nullable();
+
+            $table->index('status', 'idx_apps_status');
         });

@@ -15,3 +15,12 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('app_installation_id')->nullable()->constrained('app_installations')->cascadeOnDelete();
+            $table->string('event_type');
+            $table->string('target_url', 2048);
+            $table->text('signing_secret_encrypted');
+            $table->string('status')->default('active');
+
+            $table->index('store_id', 'idx_webhook_subscriptions_store_id');
+            $table->index(['store_id', 'event_type'], 'idx_webhook_subscriptions_store_event');
+            $table->index('app_installation_id', 'idx_webhook_subscriptions_installation');
         });

@@ -15,3 +15,8 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('app_id')->constrained('apps')->cascadeOnDelete();
+            $table->string('client_id')->unique('idx_oauth_clients_client_id');
+            $table->text('client_secret_encrypted');
+            $table->json('redirect_uris_json')->default('[]');
+
+            $table->index('app_id', 'idx_oauth_clients_app_id');
         });

@@ -15,3 +15,13 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('installation_id')->constrained('app_installations')->cascadeOnDelete();
+            $table->string('name')->nullable();
+            $table->string('access_token_hash')->unique('idx_oauth_tokens_access_hash');
+            $table->string('refresh_token_hash')->nullable();
+            $table->json('abilities_json')->default('[]');
+            $table->timestamp('expires_at');
+            $table->timestamp('last_used_at')->nullable();
+            $table->timestamp('created_at')->nullable();
+
+            $table->index('installation_id', 'idx_oauth_tokens_installation_id');
+            $table->index('expires_at', 'idx_oauth_tokens_expires_at');
         });

@@ -15,3 +15,11 @@
             $table->id();
-            $table->timestamps();
+            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
+            $table->foreignId('app_id')->constrained('apps')->cascadeOnDelete();
+            $table->json('scopes_json')->default('[]');
+            $table->string('status')->default('active');
+            $table->timestamp('installed_at')->nullable();
+
+            $table->unique(['store_id', 'app_id'], 'idx_app_installations_store_app');
+            $table->index('store_id', 'idx_app_installations_store_id');
+            $table->index('app_id', 'idx_app_installations_app_id');
         });


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/App.php
@@
 namespace App\Models;

+use App\Enums\AppStatus;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;

 class App extends Model
 {
     /** @use HasFactory<\Database\Factories\AppFactory> */
     use HasFactory;
+
+    public const UPDATED_AT = null;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'name',
+        'status',
+        'created_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'active',
+    ];
+
+    /**
+     * @return HasMany<AppInstallation, $this>
+     */
+    public function installations(): HasMany
+    {
+        return $this->hasMany(AppInstallation::class);
+    }
+
+    /**
+     * @return HasMany<OauthClient, $this>
+     */
+    public function oauthClients(): HasMany
+    {
+        return $this->hasMany(OauthClient::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => AppStatus::class,
+            'created_at' => 'datetime',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/AppInstallation.php
@@
 namespace App\Models;

+use App\Enums\AppInstallationStatus;
+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 AppInstallation extends Model
 {
     /** @use HasFactory<\Database\Factories\AppInstallationFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'app_id',
+        'scopes_json',
+        'status',
+        'installed_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'scopes_json' => '[]',
+        'status' => 'active',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return BelongsTo<App, $this>
+     */
+    public function app(): BelongsTo
+    {
+        return $this->belongsTo(App::class);
+    }
+
+    /**
+     * @return HasMany<OauthToken, $this>
+     */
+    public function oauthTokens(): HasMany
+    {
+        return $this->hasMany(OauthToken::class, 'installation_id');
+    }
+
+    /**
+     * @return HasMany<WebhookSubscription, $this>
+     */
+    public function webhookSubscriptions(): HasMany
+    {
+        return $this->hasMany(WebhookSubscription::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'scopes_json' => 'array',
+            'status' => AppInstallationStatus::class,
+            'installed_at' => 'datetime',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/OauthClient.php
@@
 namespace App\Models;

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

 class OauthClient extends Model
 {
     /** @use HasFactory<\Database\Factories\OauthClientFactory> */
     use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'app_id',
+        'client_id',
+        'client_secret_encrypted',
+        'redirect_uris_json',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'redirect_uris_json' => '[]',
+    ];
+
+    /**
+     * @return BelongsTo<App, $this>
+     */
+    public function app(): BelongsTo
+    {
+        return $this->belongsTo(App::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'client_secret_encrypted' => 'encrypted',
+            'redirect_uris_json' => 'array',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/OauthToken.php
@@
 namespace App\Models;

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

 class OauthToken extends Model
 {
     /** @use HasFactory<\Database\Factories\OauthTokenFactory> */
     use HasFactory;
+
+    public const UPDATED_AT = null;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'installation_id',
+        'name',
+        'access_token_hash',
+        'refresh_token_hash',
+        'abilities_json',
+        'expires_at',
+        'last_used_at',
+        'created_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'abilities_json' => '[]',
+    ];
+
+    /**
+     * @return BelongsTo<AppInstallation, $this>
+     */
+    public function installation(): BelongsTo
+    {
+        return $this->belongsTo(AppInstallation::class, 'installation_id');
+    }
+
+    public function isExpired(): bool
+    {
+        return $this->expires_at->isPast();
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'abilities_json' => 'array',
+            'expires_at' => 'datetime',
+            'last_used_at' => 'datetime',
+            'created_at' => 'datetime',
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/WebhookSubscription.php
@@
 namespace App\Models;

+use App\Enums\WebhookEventType;
+use App\Enums\WebhookSubscriptionStatus;
+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 WebhookSubscription extends Model
 {
     /** @use HasFactory<\Database\Factories\WebhookSubscriptionFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'app_installation_id',
+        'event_type',
+        'target_url',
+        'signing_secret_encrypted',
+        'status',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'active',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return BelongsTo<AppInstallation, $this>
+     */
+    public function appInstallation(): BelongsTo
+    {
+        return $this->belongsTo(AppInstallation::class);
+    }
+
+    /**
+     * @return HasMany<WebhookDelivery, $this>
+     */
+    public function deliveries(): HasMany
+    {
+        return $this->hasMany(WebhookDelivery::class, 'subscription_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'event_type' => WebhookEventType::class,
+            'signing_secret_encrypted' => 'encrypted',
+            'status' => WebhookSubscriptionStatus::class,
+        ];
+    }
 }
*** Update File: /Users/fabianwesner/Herd/shop/app/Models/WebhookDelivery.php
@@
 namespace App\Models;

+use App\Enums\WebhookDeliveryStatus;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

 class WebhookDelivery extends Model
 {
     /** @use HasFactory<\Database\Factories\WebhookDeliveryFactory> */
     use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'subscription_id',
+        'event_id',
+        'attempt_count',
+        'status',
+        'last_attempt_at',
+        'response_code',
+        'response_body_snippet',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'attempt_count' => 1,
+        'status' => 'pending',
+    ];
+
+    /**
+     * @return BelongsTo<WebhookSubscription, $this>
+     */
+    public function subscription(): BelongsTo
+    {
+        return $this->belongsTo(WebhookSubscription::class, 'subscription_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => WebhookDeliveryStatus::class,
+            'last_attempt_at' => 'datetime',
+        ];
+    }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/app/Models/App.php
M /Users/fabianwesner/Herd/shop/app/Models/AppInstallation.php
M /Users/fabianwesner/Herd/shop/app/Models/OauthClient.php
M /Users/fabianwesner/Herd/shop/app/Models/OauthToken.php
M /Users/fabianwesner/Herd/shop/app/Models/WebhookSubscription.php
M /Users/fabianwesner/Herd/shop/app/Models/WebhookDelivery.php

@@ -4,4 +4,6 @@

+use App\Enums\WebhookDeliveryStatus;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

@@ -11,2 +13,44 @@
     use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'subscription_id',
+        'event_id',
+        'attempt_count',
+        'status',
+        'last_attempt_at',
+        'response_code',
+        'response_body_snippet',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'attempt_count' => 1,
+        'status' => 'pending',
+    ];
+
+    /**
+     * @return BelongsTo<WebhookSubscription, $this>
+     */
+    public function subscription(): BelongsTo
+    {
+        return $this->belongsTo(WebhookSubscription::class, 'subscription_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => WebhookDeliveryStatus::class,
+            'last_attempt_at' => 'datetime',
+        ];
+    }
 }

@@ -4,4 +4,6 @@

+use App\Enums\AppStatus;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\HasMany;

@@ -11,2 +13,47 @@
     use HasFactory;
+
+    public const UPDATED_AT = null;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'name',
+        'status',
+        'created_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'active',
+    ];
+
+    /**
+     * @return HasMany<AppInstallation, $this>
+     */
+    public function installations(): HasMany
+    {
+        return $this->hasMany(AppInstallation::class);
+    }
+
+    /**
+     * @return HasMany<OauthClient, $this>
+     */
+    public function oauthClients(): HasMany
+    {
+        return $this->hasMany(OauthClient::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'status' => AppStatus::class,
+            'created_at' => 'datetime',
+        ];
+    }
 }

@@ -6,2 +6,3 @@
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

@@ -11,2 +12,40 @@
     use HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'app_id',
+        'client_id',
+        'client_secret_encrypted',
+        'redirect_uris_json',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'redirect_uris_json' => '[]',
+    ];
+
+    /**
+     * @return BelongsTo<App, $this>
+     */
+    public function app(): BelongsTo
+    {
+        return $this->belongsTo(App::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'client_secret_encrypted' => 'encrypted',
+            'redirect_uris_json' => 'array',
+        ];
+    }
 }

@@ -4,4 +4,9 @@

+use App\Enums\WebhookEventType;
+use App\Enums\WebhookSubscriptionStatus;
+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;

@@ -10,3 +15,60 @@
     /** @use HasFactory<\Database\Factories\WebhookSubscriptionFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'app_installation_id',
+        'event_type',
+        'target_url',
+        'signing_secret_encrypted',
+        'status',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'status' => 'active',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return BelongsTo<AppInstallation, $this>
+     */
+    public function appInstallation(): BelongsTo
+    {
+        return $this->belongsTo(AppInstallation::class);
+    }
+
+    /**
+     * @return HasMany<WebhookDelivery, $this>
+     */
+    public function deliveries(): HasMany
+    {
+        return $this->hasMany(WebhookDelivery::class, 'subscription_id');
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'event_type' => WebhookEventType::class,
+            'signing_secret_encrypted' => 'encrypted',
+            'status' => WebhookSubscriptionStatus::class,
+        ];
+    }
 }

@@ -4,4 +4,8 @@

+use App\Enums\AppInstallationStatus;
+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;

@@ -10,3 +14,68 @@
     /** @use HasFactory<\Database\Factories\AppInstallationFactory> */
-    use HasFactory;
+    use BelongsToStore, HasFactory;
+
+    public $timestamps = false;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'store_id',
+        'app_id',
+        'scopes_json',
+        'status',
+        'installed_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'scopes_json' => '[]',
+        'status' => 'active',
+    ];
+
+    /**
+     * @return BelongsTo<Store, $this>
+     */
+    public function store(): BelongsTo
+    {
+        return $this->belongsTo(Store::class);
+    }
+
+    /**
+     * @return BelongsTo<App, $this>
+     */
+    public function app(): BelongsTo
+    {
+        return $this->belongsTo(App::class);
+    }
+
+    /**
+     * @return HasMany<OauthToken, $this>
+     */
+    public function oauthTokens(): HasMany
+    {
+        return $this->hasMany(OauthToken::class, 'installation_id');
+    }
+
+    /**
+     * @return HasMany<WebhookSubscription, $this>
+     */
+    public function webhookSubscriptions(): HasMany
+    {
+        return $this->hasMany(WebhookSubscription::class);
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'scopes_json' => 'array',
+            'status' => AppInstallationStatus::class,
+            'installed_at' => 'datetime',
+        ];
+    }
 }

@@ -6,2 +6,3 @@
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;

@@ -11,2 +12,51 @@
     use HasFactory;
+
+    public const UPDATED_AT = null;
+
+    /**
+     * @var list<string>
+     */
+    protected $fillable = [
+        'installation_id',
+        'name',
+        'access_token_hash',
+        'refresh_token_hash',
+        'abilities_json',
+        'expires_at',
+        'last_used_at',
+        'created_at',
+    ];
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected $attributes = [
+        'abilities_json' => '[]',
+    ];
+
+    /**
+     * @return BelongsTo<AppInstallation, $this>
+     */
+    public function installation(): BelongsTo
+    {
+        return $this->belongsTo(AppInstallation::class, 'installation_id');
+    }
+
+    public function isExpired(): bool
+    {
+        return $this->expires_at->isPast();
+    }
+
+    /**
+     * @return array<string, string>
+     */
+    protected function casts(): array
+    {
+        return [
+            'abilities_json' => 'array',
+            'expires_at' => 'datetime',
+            'last_used_at' => 'datetime',
+            'created_at' => 'datetime',
+        ];
+    }
 }


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


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/AppFactory.php
@@
 namespace Database\Factories;

+use App\Enums\AppStatus;
 use Illuminate\Database\Eloquent\Factories\Factory;

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\App>
@@
     public function definition(): array
     {
         return [
-            //
+            'name' => fake()->company().' App',
+            'status' => AppStatus::Active,
+            'created_at' => now(),
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/AppInstallationFactory.php
@@
 namespace Database\Factories;

+use App\Enums\AppInstallationStatus;
+use App\Models\App;
+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AppInstallation>
@@
     public function definition(): array
     {
         return [
-            //
+            'store_id' => Store::factory(),
+            'app_id' => App::factory(),
+            'scopes_json' => ['read-products', 'read-orders'],
+            'status' => AppInstallationStatus::Active,
+            'installed_at' => now(),
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/OauthClientFactory.php
@@
 namespace Database\Factories;

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

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OauthClient>
@@
     public function definition(): array
     {
         return [
-            //
+            'app_id' => App::factory(),
+            'client_id' => 'app_client_'.Str::random(24),
+            'client_secret_encrypted' => 'secret_'.Str::random(40),
+            'redirect_uris_json' => [fake()->url().'/oauth/callback'],
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/OauthTokenFactory.php
@@
 namespace Database\Factories;

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

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\OauthToken>
@@
     public function definition(): array
     {
+        $token = 'shop_'.Str::random(48);
+
         return [
-            //
+            'installation_id' => AppInstallation::factory(),
+            'name' => fake()->words(2, true),
+            'access_token_hash' => hash('sha256', $token),
+            'refresh_token_hash' => hash('sha256', 'refresh_'.Str::random(48)),
+            'abilities_json' => ['read-products', 'read-orders'],
+            'expires_at' => now()->addYear(),
+            'last_used_at' => null,
+            'created_at' => now(),
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/WebhookSubscriptionFactory.php
@@
 namespace Database\Factories;

+use App\Enums\WebhookEventType;
+use App\Enums\WebhookSubscriptionStatus;
+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebhookSubscription>
@@
     public function definition(): array
     {
         return [
-            //
+            'store_id' => Store::factory(),
+            'app_installation_id' => null,
+            'event_type' => WebhookEventType::OrderCreated,
+            'target_url' => fake()->url().'/webhooks/shop',
+            'signing_secret_encrypted' => 'whsec_'.Str::random(40),
+            'status' => WebhookSubscriptionStatus::Active,
         ];
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/factories/WebhookDeliveryFactory.php
@@
 namespace Database\Factories;

+use App\Enums\WebhookDeliveryStatus;
+use App\Models\WebhookSubscription;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;

 /**
  * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WebhookDelivery>
@@
     public function definition(): array
     {
         return [
-            //
+            'subscription_id' => WebhookSubscription::factory(),
+            'event_id' => (string) Str::uuid(),
+            'attempt_count' => 1,
+            'status' => WebhookDeliveryStatus::Pending,
+            'last_attempt_at' => null,
+            'response_code' => null,
+            'response_body_snippet' => null,
         ];
     }
 }
*** End Patch


Success. Updated the following files:
M /Users/fabianwesner/Herd/shop/database/factories/AppFactory.php
M /Users/fabianwesner/Herd/shop/database/factories/AppInstallationFactory.php
M /Users/fabianwesner/Herd/shop/database/factories/OauthClientFactory.php
M /Users/fabianwesner/Herd/shop/database/factories/OauthTokenFactory.php
M /Users/fabianwesner/Herd/shop/database/factories/WebhookSubscriptionFactory.php
M /Users/fabianwesner/Herd/shop/database/factories/WebhookDeliveryFactory.php

@@ -4,3 +4,6 @@

+use App\Enums\WebhookDeliveryStatus;
+use App\Models\WebhookSubscription;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;

@@ -19,3 +22,9 @@
         return [
-            //
+            'subscription_id' => WebhookSubscription::factory(),
+            'event_id' => (string) Str::uuid(),
+            'attempt_count' => 1,
+            'status' => WebhookDeliveryStatus::Pending,
+            'last_attempt_at' => null,
+            'response_code' => null,
+            'response_body_snippet' => null,
         ];

@@ -4,3 +4,5 @@

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

@@ -18,4 +20,13 @@
     {
+        $token = 'shop_'.Str::random(48);
+
         return [
-            //
+            'installation_id' => AppInstallation::factory(),
+            'name' => fake()->words(2, true),
+            'access_token_hash' => hash('sha256', $token),
+            'refresh_token_hash' => hash('sha256', 'refresh_'.Str::random(48)),
+            'abilities_json' => ['read-products', 'read-orders'],
+            'expires_at' => now()->addYear(),
+            'last_used_at' => null,
+            'created_at' => now(),
         ];

@@ -4,3 +4,5 @@

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

@@ -19,3 +21,6 @@
         return [
-            //
+            'app_id' => App::factory(),
+            'client_id' => 'app_client_'.Str::random(24),
+            'client_secret_encrypted' => 'secret_'.Str::random(40),
+            'redirect_uris_json' => [fake()->url().'/oauth/callback'],
         ];

@@ -4,3 +4,7 @@

+use App\Enums\WebhookEventType;
+use App\Enums\WebhookSubscriptionStatus;
+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;
+use Illuminate\Support\Str;

@@ -19,3 +23,8 @@
         return [
-            //
+            'store_id' => Store::factory(),
+            'app_installation_id' => null,
+            'event_type' => WebhookEventType::OrderCreated,
+            'target_url' => fake()->url().'/webhooks/shop',
+            'signing_secret_encrypted' => 'whsec_'.Str::random(40),
+            'status' => WebhookSubscriptionStatus::Active,
         ];

@@ -4,2 +4,3 @@

+use App\Enums\AppStatus;
 use Illuminate\Database\Eloquent\Factories\Factory;
@@ -19,3 +20,5 @@
         return [
-            //
+            'name' => fake()->company().' App',
+            'status' => AppStatus::Active,
+            'created_at' => now(),
         ];

@@ -4,2 +4,5 @@

+use App\Enums\AppInstallationStatus;
+use App\Models\App;
+use App\Models\Store;
 use Illuminate\Database\Eloquent\Factories\Factory;
@@ -19,3 +22,7 @@
         return [
-            //
+            'store_id' => Store::factory(),
+            'app_id' => App::factory(),
+            'scopes_json' => ['read-products', 'read-orders'],
+            'status' => AppInstallationStatus::Active,
+            'installed_at' => now(),
         ];


Output: {"output":"Success. Updated the following files:\nM /Users/fabianwesner/Herd/shop/database/factories/AppFactory.php\nM /Users/fabianwesner/Herd/shop/database/factories/AppInstallationFactory.php\nM /Users/fabianwesner/Herd/shop/database/factories/OauthClientFactory.php\nM /Users/fabianwesner/Herd/shop/database/factories/OauthTokenFactory.php\nM /Users/fabianwesner/Herd/shop/database/factories/WebhookSubscriptionFactory.php\nM /Users/fabianwesner/Herd/shop/database/factories/WebhookDeliveryFactory.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,160p' app/Models/Scopes/StoreScope.php


sed -n '1,160p' app/Models/Scopes/StoreScope.php

<?php

namespace App\Models\Scopes;

use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (! app()->bound('current_store')) {
            return;
        }

        $store = app('current_store');

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

        $builder->where($model->qualifyColumn('store_id'), $store->getKey());
    }
}


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

namespace App\Models\Scopes;

use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class StoreScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if (! app()->bound('current_store')) {
            return;
        }

        $store = app('current_store');

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

        $builder->where($model->qualifyColumn('store_id'), $store->getKey());
    }
}


Input:
*** Begin Patch
*** Update File: /Users/fabianwesner/Herd/shop/database/seeders/AppSeeder.php
@@
 namespace Database\Seeders;

+use App\Enums\AppInstallationStatus;
+use App\Enums\AppStatus;
+use App\Enums\WebhookEventType;
+use App\Enums\WebhookSubscriptionStatus;
+use App\Models\App as AppModel;
+use App\Models\AppInstallation;
+use App\Models\OauthClient;
+use App\Models\OauthToken;
+use App\Models\Store;
+use App\Models\WebhookSubscription;
 use Illuminate\Database\Seeder;
+use Illuminate\Support\Str;

 class AppSeeder extends Seeder
 {
     /**
      * Run the database seeds.
      */
     public function run(): void
     {
-        //
+        Store::query()->get()->each(function (Store $store): void {
+            $this->seedInstalledApps($store);
+        });
+    }
+
+    private function seedInstalledApps(Store $store): void
+    {
+        $apps = [
+            [
+                'name' => 'Inventory Sync',
+                'scopes' => ['read-products', 'write-products', 'read-orders'],
+                'events' => [WebhookEventType::OrderCreated, WebhookEventType::ProductUpdated],
+            ],
+            [
+                'name' => 'Fulfillment Bridge',
+                'scopes' => ['read-orders', 'write-orders'],
+                'events' => [WebhookEventType::OrderCreated, WebhookEventType::FulfillmentCreated],
+            ],
+        ];
+
+        foreach ($apps as $index => $appData) {
+            $app = AppModel::query()->updateOrCreate(
+                ['name' => $appData['name']],
+                [
+                    'status' => AppStatus::Active,
+                    'created_at' => now()->subMonths(3 - $index),
+                ],
+            );
+
+            $installation = AppInstallation::withoutGlobalScopes()->updateOrCreate(
+                [
+                    'store_id' => $store->getKey(),
+                    'app_id' => $app->getKey(),
+                ],
+                [
+                    'scopes_json' => $appData['scopes'],
+                    'status' => AppInstallationStatus::Active,
+                    'installed_at' => now()->subWeeks(8 - ($index * 3)),
+                ],
+            );
+
+            OauthClient::query()->updateOrCreate(
+                ['client_id' => 'app_client_'.$store->handle.'_'.Str::slug($app->name)],
+                [
+                    'app_id' => $app->getKey(),
+                    'client_secret_encrypted' => 'secret_'.Str::random(40),
+                    'redirect_uris_json' => ["https://{$store->handle}.integrations.example/oauth/callback"],
+                ],
+            );
+
+            OauthToken::query()->updateOrCreate(
+                [
+                    'installation_id' => $installation->getKey(),
+                    'name' => $app->name.' token',
+                ],
+                [
+                    'access_token_hash' => hash('sha256', 'shop_seed_'.$store->handle.'_'.Str::slug($app->name)),
+                    'refresh_token_hash' => hash('sha256', 'refresh_seed_'.$store->handle.'_'.Str::slug($app->name)),
+                    'abilities_json' => $appData['scopes'],
+                    'expires_at' => now()->addYear(),
+                    'last_used_at' => $index === 0 ? now()->subHours(2) : null,
+                    'created_at' => $installation->installed_at,
+                ],
+            );
+
+            foreach ($appData['events'] as $eventType) {
+                WebhookSubscription::withoutGlobalScopes()->updateOrCreate(
+                    [
+                        'store_id' => $store->getKey(),
+                        'app_installation_id' => $installation->getKey(),
+                        'event_type' => $eventType->value,
+                    ],
+                    [
+                        'target_url' => "https://{$store->handle}.integrations.example/webhooks/{$eventType->value}",
+                        'signing_secret_encrypted' => 'whsec_'.Str::random(40),
+                        'status' => WebhookSubscriptionStatus::Active,
+                    ],
+                );
+            }
+        }
     }
 }
*** Update File: /Users/fabianwesner/Herd/shop/database/seeders/DatabaseSeeder.php
@@
             StoreSeeder::class,
             StoreDomainSeeder::class,
+            AppSeeder::class,
             UserSeeder::class,
             StoreUserSeeder::class,
*** End Patch


Output:
apply_patch verification failed: Failed to find expected lines in /Users/fabianwesner/Herd/shop/database/seeders/AppSeeder.php:
namespace Database\Seeders;

use Illuminate\Database\Seeder;

class AppSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        //
    }
}


sed -n '1,80p' database/seeders/AppSeeder.php


sed -n '1,80p' database/seeders/AppSeeder.php

<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class AppSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        //
    }
}


Output:
Chunk ID: 78ee22
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 66
Output:
<?php

namespace Database\Seeders;

use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class AppSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        //
    }
}