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

<?php

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

use App\Http\Controllers\Controller;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class StoreMembershipController extends Controller
{
    public function show(Request $request, Store $store): JsonResponse
    {
        $token = $request->attributes->get('admin_api_oauth_token');

        if ($token instanceof OauthToken) {
            return response()->json([
                'data' => [
                    'user_id' => null,
                    'store_id' => $store->getKey(),
                    'role' => 'integration',
                    'email' => null,
                    'name' => $token->name,
                    'permissions' => $token->abilities_json ?? [],
                ],
            ]);
        }

        $user = $request->user();

        abort_unless($user instanceof User, 401);

        $role = $user->roleForStore($store);

        abort_unless($role !== null, 403);

        return response()->json([
            'data' => [
                'user_id' => $user->getKey(),
                'store_id' => $store->getKey(),
                'role' => $role->value,
                'email' => $user->email,
                'name' => $user->name,
                'permissions' => $this->permissionsForRole($role->value),
            ],
        ]);
    }

    /**
     * @return list<string>
     */
    private function permissionsForRole(string $role): array
    {
        return match ($role) {
            'owner', 'admin' => [
                'manage-platform',
                'read-products',
                'write-products',
                'read-collections',
                'write-collections',
                'read-orders',
                'write-orders',
                'read-customers',
                'write-customers',
                'read-discounts',
                'write-discounts',
                'read-content',
                'write-content',
                'read-settings',
                'write-settings',
                'read-analytics',
                'write-themes',
                'manage-apps',
            ],
            'staff' => [
                'read-products',
                'write-products',
                'read-collections',
                'write-collections',
                'read-orders',
                'write-orders',
                'read-customers',
                'read-discounts',
                'write-discounts',
                'read-analytics',
            ],
            default => [
                'read-orders',
                'read-customers',
            ],
        };
    }
}


sed -n '1,220p' tests/Feature/Admin/AppsDevelopersTest.php

AppsDevelopersTest.php

<?php

use App\Enums\StoreUserRole;
use App\Enums\WebhookEventType;
use App\Livewire\Admin\Developers\Index as DevelopersIndex;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\User;
use App\Models\WebhookSubscription;
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 appsDevelopersStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function appsDevelopersUser(?StoreUserRole $role = null): User
{
    $store = appsDevelopersStore();
    $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('apps route renders installed apps for owners and admins', function (): void {
    $this->actingAs(appsDevelopersUser(StoreUserRole::Admin))
        ->get('/admin/apps')
        ->assertSuccessful()
        ->assertSee('Apps')
        ->assertSee('Inventory Sync');
});

test('developer route rejects staff users', function (): void {
    $this->actingAs(appsDevelopersUser(StoreUserRole::Staff))
        ->get('/admin/developers')
        ->assertForbidden();
});

test('developers component generates tokens and manages webhooks', function (): void {
    $store = appsDevelopersStore();
    app()->instance('current_store', $store);
    $user = appsDevelopersUser(StoreUserRole::Owner);

    $initialTokenCount = OauthToken::query()
        ->whereHas('installation', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->count();

    Livewire::actingAs($user)
        ->test(DevelopersIndex::class)
        ->assertSee('API tokens')
        ->set('newTokenName', 'CI Pipeline')
        ->call('generateToken')
        ->assertHasNoErrors()
        ->assertSet('generatedToken', fn (?string $token): bool => str_starts_with((string) $token, 'shop_'))
        ->call('openWebhookModal')
        ->set('webhookEventType', WebhookEventType::OrderCreated->value)
        ->set('webhookUrl', 'https://example.com/webhooks/orders')
        ->call('saveWebhook')
        ->assertHasNoErrors()
        ->assertSee('https://example.com/webhooks/orders');

    expect(OauthToken::query()
        ->whereHas('installation', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->count())->toBe($initialTokenCount + 1)
        ->and(WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('target_url', 'https://example.com/webhooks/orders')
            ->exists())->toBeTrue();
});


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

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

use App\Http\Controllers\Controller;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class StoreMembershipController extends Controller
{
    public function show(Request $request, Store $store): JsonResponse
    {
        $token = $request->attributes->get('admin_api_oauth_token');

        if ($token instanceof OauthToken) {
            return response()->json([
                'data' => [
                    'user_id' => null,
                    'store_id' => $store->getKey(),
                    'role' => 'integration',
                    'email' => null,
                    'name' => $token->name,
                    'permissions' => $token->abilities_json ?? [],
                ],
            ]);
        }

        $user = $request->user();

        abort_unless($user instanceof User, 401);

        $role = $user->roleForStore($store);

        abort_unless($role !== null, 403);

        return response()->json([
            'data' => [
                'user_id' => $user->getKey(),
                'store_id' => $store->getKey(),
                'role' => $role->value,
                'email' => $user->email,
                'name' => $user->name,
                'permissions' => $this->permissionsForRole($role->value),
            ],
        ]);
    }

    /**
     * @return list<string>
     */
    private function permissionsForRole(string $role): array
    {
        return match ($role) {
            'owner', 'admin' => [
                'manage-platform',
                'read-products',
                'write-products',
                'read-collections',
                'write-collections',
                'read-orders',
                'write-orders',
                'read-customers',
                'write-customers',
                'read-discounts',
                'write-discounts',
                'read-content',
                'write-content',
                'read-settings',
                'write-settings',
                'read-analytics',
                'write-themes',
                'manage-apps',
            ],
            'staff' => [
                'read-products',
                'write-products',
                'read-collections',
                'write-collections',
                'read-orders',
                'write-orders',
                'read-customers',
                'read-discounts',
                'write-discounts',
                'read-analytics',
            ],
            default => [
                'read-orders',
                'read-customers',
            ],
        };
    }
}


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

use App\Enums\StoreUserRole;
use App\Enums\WebhookEventType;
use App\Livewire\Admin\Developers\Index as DevelopersIndex;
use App\Models\OauthToken;
use App\Models\Store;
use App\Models\User;
use App\Models\WebhookSubscription;
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 appsDevelopersStore(): Store
{
    return Store::query()->where('handle', 'acme-fashion')->firstOrFail();
}

function appsDevelopersUser(?StoreUserRole $role = null): User
{
    $store = appsDevelopersStore();
    $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('apps route renders installed apps for owners and admins', function (): void {
    $this->actingAs(appsDevelopersUser(StoreUserRole::Admin))
        ->get('/admin/apps')
        ->assertSuccessful()
        ->assertSee('Apps')
        ->assertSee('Inventory Sync');
});

test('developer route rejects staff users', function (): void {
    $this->actingAs(appsDevelopersUser(StoreUserRole::Staff))
        ->get('/admin/developers')
        ->assertForbidden();
});

test('developers component generates tokens and manages webhooks', function (): void {
    $store = appsDevelopersStore();
    app()->instance('current_store', $store);
    $user = appsDevelopersUser(StoreUserRole::Owner);

    $initialTokenCount = OauthToken::query()
        ->whereHas('installation', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->count();

    Livewire::actingAs($user)
        ->test(DevelopersIndex::class)
        ->assertSee('API tokens')
        ->set('newTokenName', 'CI Pipeline')
        ->call('generateToken')
        ->assertHasNoErrors()
        ->assertSet('generatedToken', fn (?string $token): bool => str_starts_with((string) $token, 'shop_'))
        ->call('openWebhookModal')
        ->set('webhookEventType', WebhookEventType::OrderCreated->value)
        ->set('webhookUrl', 'https://example.com/webhooks/orders')
        ->call('saveWebhook')
        ->assertHasNoErrors()
        ->assertSee('https://example.com/webhooks/orders');

    expect(OauthToken::query()
        ->whereHas('installation', fn ($query) => $query->withoutGlobalScopes()->where('store_id', $store->getKey()))
        ->count())->toBe($initialTokenCount + 1)
        ->and(WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where('target_url', 'https://example.com/webhooks/orders')
            ->exists())->toBeTrue();
});


sed -n '1,280p' tests/Feature/Admin/OrderManagementTest.php

OrderManagementTest.php

<?php

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminOrderManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

function adminOrderManagementUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderManagementOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin Test Product',
        'sku_snapshot' => 'ADMIN-TEST-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'method' => $order->payment_method,
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order routes require authentication and render scoped orders', function () {
    $store = adminOrderManagementStore();
    [$order] = adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);

    $otherStore = Store::factory()->create();
    adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->get('/admin/orders')->assertRedirect('/admin/login');

    $user = adminOrderManagementUser();

    $this->actingAs($user)
        ->get('/admin/orders')
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertDontSee('#9001');

    $this->actingAs($user)
        ->get('/admin/orders/'.$order->getKey())
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertSee('Admin Test Product');
});

test('admin order index filters by search and status dimensions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);
    adminOrderManagementOrder($store, [
        'order_number' => '#7002',
        'email' => 'bravo@example.test',
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
        'payment_method' => PaymentMethod::BankTransfer,
    ]);

    Livewire::actingAs($user)
        ->test(AdminOrdersIndex::class)
        ->assertSee('#7001')
        ->assertSee('#7002')
        ->set('search', 'alpha')
        ->assertSee('#7001')
        ->assertDontSee('#7002')
        ->set('search', '')
        ->set('financialStatusFilter', 'pending')
        ->assertSee('#7002')
        ->assertDontSee('#7001');
});

test('admin order detail confirms bank transfer payments', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line, $variant] = adminOrderManagementOrder($store, [
        'payment_method' => PaymentMethod::BankTransfer,
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
    ]);

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update(['quantity_reserved' => $line->quantity]);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->call('confirmBankTransferPayment')
        ->assertHasNoErrors();

    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($order->refresh()->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->payments()->first()?->status)->toBe(PaymentStatus::Captured)
        ->and($inventory->quantity_on_hand)->toBe(8)
        ->and($inventory->quantity_reserved)->toBe(0);
});

test('admin order detail processes refunds and fulfillment transitions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line] = adminOrderManagementOrder($store, quantity: 2, unitPrice: 2500);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->set('refundAmount', '10.00')
        ->set('refundReason', 'Customer return')
        ->call('processRefund')
        ->assertHasNoErrors();

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded)
        ->and($order->refunds()->first()?->amount)->toBe(1000);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->set("fulfillmentLineQuantities.{$line->getKey()}", 1)
        ->set('trackingCompany', 'DHL')
        ->set('trackingNumber', 'DHL123')
        ->call('createFulfillment')
        ->assertHasNoErrors();

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->lines()->first()?->quantity)->toBe(1);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->call('markFulfillmentShipped', $fulfillment->getKey())
        ->call('markFulfillmentDelivered', $fulfillment->getKey())
        ->assertHasNoErrors();

    expect($fulfillment->refresh()->status)->toBe(FulfillmentShipmentStatus::Delivered)
        ->and($fulfillment->delivered_at)->not->toBeNull();
});

test('admin order detail rejects orders from another store', function () {
    $store = adminOrderManagementStore();
    $otherStore = Store::factory()->create();
    $user = adminOrderManagementUser();
    [$otherOrder] = adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
    ]);

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

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $otherOrder])
        ->assertStatus(404);
});


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

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentShipmentStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Livewire\Admin\Orders\Index as AdminOrdersIndex;
use App\Livewire\Admin\Orders\Show as AdminOrderShow;
use App\Models\Fulfillment;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\User;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;

uses(RefreshDatabase::class);

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

function adminOrderManagementStore(): Store
{
    $store = Store::query()->where('handle', 'acme-fashion')->firstOrFail();
    app()->instance('current_store', $store);

    return $store;
}

function adminOrderManagementUser(): User
{
    return User::query()->where('email', 'admin@acme.test')->firstOrFail();
}

/**
 * @return array{0: Order, 1: OrderLine, 2: ProductVariant}
 */
function adminOrderManagementOrder(Store $store, array $orderAttributes = [], int $quantity = 2, int $unitPrice = 2500): array
{
    $product = Product::factory()
        ->withDefaultVariant($unitPrice)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    $total = $quantity * $unitPrice;
    $order = Order::factory()->paid()->create(array_merge([
        'store_id' => $store->getKey(),
        'subtotal_amount' => $total,
        'shipping_amount' => 0,
        'tax_amount' => 0,
        'total_amount' => $total,
        'email' => 'buyer@example.test',
    ], $orderAttributes));
    $line = OrderLine::factory()->create([
        'order_id' => $order->getKey(),
        'product_id' => $product->getKey(),
        'variant_id' => $variant->getKey(),
        'title_snapshot' => 'Admin Test Product',
        'sku_snapshot' => 'ADMIN-TEST-001',
        'quantity' => $quantity,
        'unit_price_amount' => $unitPrice,
        'total_amount' => $total,
    ]);

    Payment::factory()->create([
        'order_id' => $order->getKey(),
        'method' => $order->payment_method,
        'status' => $order->financial_status === FinancialStatus::Pending ? PaymentStatus::Pending : PaymentStatus::Captured,
        'amount' => $total,
        'currency' => $order->currency,
    ]);

    return [$order, $line, $variant];
}

test('admin order routes require authentication and render scoped orders', function () {
    $store = adminOrderManagementStore();
    [$order] = adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);

    $otherStore = Store::factory()->create();
    adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
        'email' => 'other@example.test',
    ]);

    $this->get('/admin/orders')->assertRedirect('/admin/login');

    $user = adminOrderManagementUser();

    $this->actingAs($user)
        ->get('/admin/orders')
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertDontSee('#9001');

    $this->actingAs($user)
        ->get('/admin/orders/'.$order->getKey())
        ->assertSuccessful()
        ->assertSee('#7001')
        ->assertSee('Admin Test Product');
});

test('admin order index filters by search and status dimensions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    adminOrderManagementOrder($store, [
        'order_number' => '#7001',
        'email' => 'alpha@example.test',
    ]);
    adminOrderManagementOrder($store, [
        'order_number' => '#7002',
        'email' => 'bravo@example.test',
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
        'payment_method' => PaymentMethod::BankTransfer,
    ]);

    Livewire::actingAs($user)
        ->test(AdminOrdersIndex::class)
        ->assertSee('#7001')
        ->assertSee('#7002')
        ->set('search', 'alpha')
        ->assertSee('#7001')
        ->assertDontSee('#7002')
        ->set('search', '')
        ->set('financialStatusFilter', 'pending')
        ->assertSee('#7002')
        ->assertDontSee('#7001');
});

test('admin order detail confirms bank transfer payments', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line, $variant] = adminOrderManagementOrder($store, [
        'payment_method' => PaymentMethod::BankTransfer,
        'status' => OrderStatus::Pending,
        'financial_status' => FinancialStatus::Pending,
    ]);

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update(['quantity_reserved' => $line->quantity]);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->call('confirmBankTransferPayment')
        ->assertHasNoErrors();

    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($order->refresh()->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->payments()->first()?->status)->toBe(PaymentStatus::Captured)
        ->and($inventory->quantity_on_hand)->toBe(8)
        ->and($inventory->quantity_reserved)->toBe(0);
});

test('admin order detail processes refunds and fulfillment transitions', function () {
    $store = adminOrderManagementStore();
    $user = adminOrderManagementUser();
    [$order, $line] = adminOrderManagementOrder($store, quantity: 2, unitPrice: 2500);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order])
        ->set('refundAmount', '10.00')
        ->set('refundReason', 'Customer return')
        ->call('processRefund')
        ->assertHasNoErrors();

    expect($order->refresh()->financial_status)->toBe(FinancialStatus::PartiallyRefunded)
        ->and($order->refunds()->first()?->amount)->toBe(1000);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->set("fulfillmentLineQuantities.{$line->getKey()}", 1)
        ->set('trackingCompany', 'DHL')
        ->set('trackingNumber', 'DHL123')
        ->call('createFulfillment')
        ->assertHasNoErrors();

    $fulfillment = Fulfillment::query()->where('order_id', $order->getKey())->firstOrFail();

    expect($order->refresh()->fulfillment_status)->toBe(FulfillmentStatus::Partial)
        ->and($fulfillment->lines()->first()?->quantity)->toBe(1);

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $order->refresh()])
        ->call('markFulfillmentShipped', $fulfillment->getKey())
        ->call('markFulfillmentDelivered', $fulfillment->getKey())
        ->assertHasNoErrors();

    expect($fulfillment->refresh()->status)->toBe(FulfillmentShipmentStatus::Delivered)
        ->and($fulfillment->delivered_at)->not->toBeNull();
});

test('admin order detail rejects orders from another store', function () {
    $store = adminOrderManagementStore();
    $otherStore = Store::factory()->create();
    $user = adminOrderManagementUser();
    [$otherOrder] = adminOrderManagementOrder($otherStore, [
        'order_number' => '#9001',
    ]);

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

    Livewire::actingAs($user)
        ->test(AdminOrderShow::class, ['order' => $otherOrder])
        ->assertStatus(404);
});


sed -n '1,240p' tests/Feature/Admin/ProductMediaManagementTest.php

ProductMediaManagementTest.php

<?php

use App\Enums\MediaStatus;
use App\Enums\StoreUserRole;
use App\Jobs\ProcessMediaUpload;
use App\Livewire\Admin\Products\Form as ProductForm;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\Store;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;

uses(RefreshDatabase::class);

function productMediaAdminUser(Store $store): User
{
    $user = User::factory()->create();
    $user->stores()->attach($store->getKey(), [
        'role' => StoreUserRole::Admin->value,
    ]);

    return $user;
}

test('admin product form uploads images and queues media processing', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create([
        'store_id' => $store->getKey(),
        'title' => 'Media Test Product',
    ]);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('newMedia', [
            UploadedFile::fake()->image('front.jpg', 40, 30),
        ])
        ->call('uploadMedia')
        ->assertHasNoErrors()
        ->assertSee('Media uploaded');

    $media = ProductMedia::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    expect($media->status)->toBe(MediaStatus::Processing)
        ->and($media->position)->toBe(0)
        ->and($media->alt_text)->toBe('Media Test Product')
        ->and($media->storage_key)->toStartWith("media/originals/{$product->getKey()}/");

    Storage::disk('public')->assertExists($media->storage_key);

    Queue::assertPushed(ProcessMediaUpload::class, function (ProcessMediaUpload $job) use ($media, $store): bool {
        return $job->productMediaId === $media->getKey()
            && $job->storeId === $store->getKey();
    });
});

test('admin product form attaches selected media when creating a product', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Created Media Product')
        ->set('handle', 'created-media-product')
        ->set('variants.0.price', '19.99')
        ->set('variants.0.quantity', 7)
        ->set('newMedia', [
            UploadedFile::fake()->image('created.png', 32, 24),
        ])
        ->call('save')
        ->assertHasNoErrors()
        ->assertSee('Product saved');

    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'created-media-product')
        ->firstOrFail();
    $media = ProductMedia::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    Storage::disk('public')->assertExists($media->storage_key);
    Queue::assertPushed(ProcessMediaUpload::class, 1);
});

test('admin product form rejects non image media uploads', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create(['store_id' => $store->getKey()]);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('newMedia', [
            UploadedFile::fake()->create('notes.txt', 1, 'text/plain'),
        ])
        ->call('uploadMedia')
        ->assertHasErrors(['newMedia.0']);

    expect(ProductMedia::withoutGlobalScopes()->where('product_id', $product->getKey())->count())->toBe(0);

    Queue::assertNothingPushed();
});

test('admin product form updates alt text reorders and deletes media', function (): void {
    Storage::fake('public');

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create(['store_id' => $store->getKey()]);

    $media = collect([0, 1, 2])->map(function (int $position) use ($product): ProductMedia {
        $item = ProductMedia::factory()->create([
            'product_id' => $product->getKey(),
            'storage_key' => "media/originals/{$product->getKey()}/{$position}.png",
            'position' => $position,
            'status' => MediaStatus::Ready,
        ]);

        Storage::disk('public')->put($item->storage_key, 'image-bytes');

        return $item;
    });

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

    $component = Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('media.1.altText', 'Updated sleeve detail')
        ->call('updateMediaAlt', $media[1]->getKey())
        ->assertHasNoErrors();

    expect($media[1]->refresh()->alt_text)->toBe('Updated sleeve detail');

    $component->call('moveMedia', $media[2]->getKey(), 'up');

    expect($media[0]->refresh()->position)->toBe(0)
        ->and($media[2]->refresh()->position)->toBe(1)
        ->and($media[1]->refresh()->position)->toBe(2);

    $component->call('removeMedia', $media[2]->getKey());

    $this->assertModelMissing($media[2]);
    Storage::disk('public')->assertMissing($media[2]->storage_key);
});


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

use App\Enums\MediaStatus;
use App\Enums\StoreUserRole;
use App\Jobs\ProcessMediaUpload;
use App\Livewire\Admin\Products\Form as ProductForm;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\Store;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Livewire\Livewire;

uses(RefreshDatabase::class);

function productMediaAdminUser(Store $store): User
{
    $user = User::factory()->create();
    $user->stores()->attach($store->getKey(), [
        'role' => StoreUserRole::Admin->value,
    ]);

    return $user;
}

test('admin product form uploads images and queues media processing', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create([
        'store_id' => $store->getKey(),
        'title' => 'Media Test Product',
    ]);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('newMedia', [
            UploadedFile::fake()->image('front.jpg', 40, 30),
        ])
        ->call('uploadMedia')
        ->assertHasNoErrors()
        ->assertSee('Media uploaded');

    $media = ProductMedia::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    expect($media->status)->toBe(MediaStatus::Processing)
        ->and($media->position)->toBe(0)
        ->and($media->alt_text)->toBe('Media Test Product')
        ->and($media->storage_key)->toStartWith("media/originals/{$product->getKey()}/");

    Storage::disk('public')->assertExists($media->storage_key);

    Queue::assertPushed(ProcessMediaUpload::class, function (ProcessMediaUpload $job) use ($media, $store): bool {
        return $job->productMediaId === $media->getKey()
            && $job->storeId === $store->getKey();
    });
});

test('admin product form attaches selected media when creating a product', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class)
        ->set('title', 'Created Media Product')
        ->set('handle', 'created-media-product')
        ->set('variants.0.price', '19.99')
        ->set('variants.0.quantity', 7)
        ->set('newMedia', [
            UploadedFile::fake()->image('created.png', 32, 24),
        ])
        ->call('save')
        ->assertHasNoErrors()
        ->assertSee('Product saved');

    $product = Product::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('handle', 'created-media-product')
        ->firstOrFail();
    $media = ProductMedia::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    Storage::disk('public')->assertExists($media->storage_key);
    Queue::assertPushed(ProcessMediaUpload::class, 1);
});

test('admin product form rejects non image media uploads', function (): void {
    Storage::fake('public');
    Queue::fake([ProcessMediaUpload::class]);

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create(['store_id' => $store->getKey()]);

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

    Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('newMedia', [
            UploadedFile::fake()->create('notes.txt', 1, 'text/plain'),
        ])
        ->call('uploadMedia')
        ->assertHasErrors(['newMedia.0']);

    expect(ProductMedia::withoutGlobalScopes()->where('product_id', $product->getKey())->count())->toBe(0);

    Queue::assertNothingPushed();
});

test('admin product form updates alt text reorders and deletes media', function (): void {
    Storage::fake('public');

    $store = Store::factory()->create();
    $user = productMediaAdminUser($store);
    $product = Product::factory()->create(['store_id' => $store->getKey()]);

    $media = collect([0, 1, 2])->map(function (int $position) use ($product): ProductMedia {
        $item = ProductMedia::factory()->create([
            'product_id' => $product->getKey(),
            'storage_key' => "media/originals/{$product->getKey()}/{$position}.png",
            'position' => $position,
            'status' => MediaStatus::Ready,
        ]);

        Storage::disk('public')->put($item->storage_key, 'image-bytes');

        return $item;
    });

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

    $component = Livewire::actingAs($user)
        ->test(ProductForm::class, ['product' => $product])
        ->set('media.1.altText', 'Updated sleeve detail')
        ->call('updateMediaAlt', $media[1]->getKey())
        ->assertHasNoErrors();

    expect($media[1]->refresh()->alt_text)->toBe('Updated sleeve detail');

    $component->call('moveMedia', $media[2]->getKey(), 'up');

    expect($media[0]->refresh()->position)->toBe(0)
        ->and($media[2]->refresh()->position)->toBe(1)
        ->and($media[1]->refresh()->position)->toBe(2);

    $component->call('removeMedia', $media[2]->getKey());

    $this->assertModelMissing($media[2]);
    Storage::disk('public')->assertMissing($media[2]->storage_key);
});


sed -n '1,240p' tests/Feature/Admin/CollectionManagementTest.php

CollectionManagementTest.php

sed: tests/Feature/Admin/CollectionManagementTest.php: No such file or directory


Output:
Chunk ID: f666b2
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 21
Output:
sed: tests/Feature/Admin/CollectionManagementTest.php: No such file or directory


rg --files tests/Feature/Admin | rg 'Collection|Product|Customer'


rg --files tests/Feature/Admin | rg 'Collection|Product|Customer'

Question: Collection|Product|Customer


Output:
Chunk ID: e29dec
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 25
Output:
tests/Feature/Admin/CustomerManagementTest.php
tests/Feature/Admin/ProductMediaManagementTest.php


/bin/zsh
-lc
sed -n '100,160p' routes/web.php

    ->middleware('guest')
    ->name('admin.password.update');

Route::post('admin/logout', function () {
    Auth::guard('web')->logout();

    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->middleware('auth')->name('admin.logout');

Route::get('oauth/authorize', [OAuthController::class, 'authorize'])
    ->middleware('auth')
    ->name('oauth.authorize');

Route::post('oauth/token', [OAuthController::class, 'token'])
    ->name('oauth.token');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('analytics', AdminAnalyticsIndex::class)->name('analytics.index');
    Route::livewire('apps', AdminAppsIndex::class)->name('apps.index');
    Route::livewire('apps/{installation}', AdminAppShow::class)->name('apps.show');
    Route::livewire('developers', AdminDevelopersIndex::class)->name('developers.index');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
    Route::livewire('customers', AdminCustomersIndex::class)->name('customers.index');
    Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
    Route::livewire('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    Route::livewire('settings/checkout', AdminSettingsCheckout::class)->name('settings.checkout');
    Route::livewire('settings/notifications', AdminSettingsNotifications::class)->name('settings.notifications');
    Route::livewire('search/settings', AdminSearchSettings::class)->name('search.settings');
    Route::livewire('collections', AdminCollectionsIndex::class)->name('collections.index');
    Route::livewire('collections/create', AdminCollectionForm::class)->name('collections.create');
    Route::livewire('collections/{collection}/edit', AdminCollectionForm::class)->name('collections.edit');
});

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('account/login', CustomerLogin::class)
        ->middleware('guest:customer')
        ->name('account.login');

    Route::livewire('account/register', CustomerRegister::class)
        ->middleware('guest:customer')
        ->name('account.register');


Output:
Chunk ID: a6088a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 942
Output:
    ->middleware('guest')
    ->name('admin.password.update');

Route::post('admin/logout', function () {
    Auth::guard('web')->logout();

    request()->session()->invalidate();
    request()->session()->regenerateToken();

    return redirect()->route('admin.login');
})->middleware('auth')->name('admin.logout');

Route::get('oauth/authorize', [OAuthController::class, 'authorize'])
    ->middleware('auth')
    ->name('oauth.authorize');

Route::post('oauth/token', [OAuthController::class, 'token'])
    ->name('oauth.token');

Route::middleware(['auth', EnsureUserEmailIsVerified::class, 'admin'])->prefix('admin')->name('admin.')->group(function (): void {
    Route::livewire('/', AdminDashboard::class)->name('dashboard');
    Route::livewire('analytics', AdminAnalyticsIndex::class)->name('analytics.index');
    Route::livewire('apps', AdminAppsIndex::class)->name('apps.index');
    Route::livewire('apps/{installation}', AdminAppShow::class)->name('apps.show');
    Route::livewire('developers', AdminDevelopersIndex::class)->name('developers.index');
    Route::livewire('products', AdminProductsIndex::class)->name('products.index');
    Route::livewire('products/create', AdminProductForm::class)->name('products.create');
    Route::livewire('products/{product}/edit', AdminProductForm::class)->name('products.edit');
    Route::livewire('inventory', AdminInventoryIndex::class)->name('inventory.index');
    Route::livewire('orders', AdminOrdersIndex::class)->name('orders.index');
    Route::livewire('orders/{order}', AdminOrderShow::class)->name('orders.show');
    Route::livewire('customers', AdminCustomersIndex::class)->name('customers.index');
    Route::livewire('customers/{customer}', AdminCustomerShow::class)->name('customers.show');
    Route::livewire('discounts', AdminDiscountsIndex::class)->name('discounts.index');
    Route::livewire('discounts/create', AdminDiscountForm::class)->name('discounts.create');
    Route::livewire('discounts/{discount}/edit', AdminDiscountForm::class)->name('discounts.edit');
    Route::livewire('pages', AdminPagesIndex::class)->name('pages.index');
    Route::livewire('pages/create', AdminPageForm::class)->name('pages.create');
    Route::livewire('pages/{page}/edit', AdminPageForm::class)->name('pages.edit');
    Route::livewire('navigation', AdminNavigationIndex::class)->name('navigation.index');
    Route::livewire('themes', AdminThemesIndex::class)->name('themes.index');
    Route::livewire('themes/{theme}/editor', AdminThemeEditor::class)->name('themes.editor');
    Route::livewire('settings', AdminSettingsIndex::class)->name('settings.index');
    Route::livewire('settings/shipping', AdminSettingsShipping::class)->name('settings.shipping');
    Route::livewire('settings/taxes', AdminSettingsTaxes::class)->name('settings.taxes');
    Route::livewire('settings/checkout', AdminSettingsCheckout::class)->name('settings.checkout');
    Route::livewire('settings/notifications', AdminSettingsNotifications::class)->name('settings.notifications');
    Route::livewire('search/settings', AdminSearchSettings::class)->name('search.settings');
    Route::livewire('collections', AdminCollectionsIndex::class)->name('collections.index');
    Route::livewire('collections/create', AdminCollectionForm::class)->name('collections.create');
    Route::livewire('collections/{collection}/edit', AdminCollectionForm::class)->name('collections.edit');
});

Route::middleware(['storefront'])->group(function (): void {
    Route::livewire('account/login', CustomerLogin::class)
        ->middleware('guest:customer')
        ->name('account.login');

    Route::livewire('account/register', CustomerRegister::class)
        ->middleware('guest:customer')
        ->name('account.register');


<subagent_notification>
{"agent_path":"019df44f-02d9-7d72-a7c8-19b7a3b04910","status":{"completed":"No files edited.\n\n**Findings**\n\n1. **High: admin Livewire mutation actions still bypass policies.**\n   Policies exist for the targeted resources, but these components do not use `AuthorizesRequests` or call `$this->authorize()` before writes:\n   - [Products/Form.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Products/Form.php:165): `save()` creates/updates products; media writes at lines 240, 258, 305, 330; archive/delete at line 347.\n   - [Products/Index.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Products/Index.php:75): bulk archive/active/delete flow reaches `transitionSelected()` at line 131 without per-product authorization.\n   - [Collections/Form.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Collections/Form.php:71): create/update/sync products without `create`/`update`.\n   - [Collections/Index.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Collections/Index.php:29): deletes collection without `delete`.\n   - [Customers/Show.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Customers/Show.php:75): address save/delete/default mutations at lines 75, 105, 118 without `update` on the customer.\n   - [Orders/Show.php](/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Orders/Show.php:65): payment confirmation, refund, fulfillment, and fulfillment status mutations at lines 65, 79, 108, 149, 162 lack `update` / `createRefund` / `createFulfillment` / fulfillment `update`.\n   \n   Minimal change: add `AuthorizesRequests` to these components and authorize immediately before each mutation, using existing policies such as [ProductPolicy.php](/Users/fabianwesner/Herd/shop/app/Policies/ProductPolicy.php:23), [CollectionPolicy.php](/Users/fabianwesner/Herd/shop/app/Policies/CollectionPolicy.php:23), [CustomerPolicy.php](/Users/fabianwesner/Herd/shop/app/Policies/CustomerPolicy.php:23), and [OrderPolicy.php](/Users/fabianwesner/Herd/shop/app/Policies/OrderPolicy.php:23). Add focused Livewire tests for support/staff/admin role denial where policy behavior differs.\n\n2. **High: checkout web routes still violate the spec route contract.**\n   Specs require `GET /checkout/{checkoutId}` and `GET /checkout/{checkoutId}/confirmation`, but current routes are [routes/web.php](/Users/fabianwesner/Herd/shop/routes/web.php:61) `/checkout` and [routes/web.php](/Users/fabianwesner/Herd/shop/routes/web.php:62) `/checkout/confirmation/{order}`. The Livewire checkout component also mounts without a checkout id and auto-selects/creates the latest session checkout in [Show.php](/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Show.php:80) and [Show.php](/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Show.php:288). Confirmation takes an `Order` instead of checkout id in [Confirmation.php](/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Confirmation.php:20).\n   \n   Minimal change: change web routes to checkout model/id based URLs, have `Show::mount(Checkout $checkout)` validate store/session/customer access, redirect after payment using the checkout id, and have confirmation resolve the order by `checkout_id`.\n\n3. **Medium: checkout idempotency implementation is present, but regression coverage is thin.**\n   The core idempotency guard exists: [OrderService.php](/Users/fabianwesner/Herd/shop/app/Services/OrderService.php:51) locks the checkout and returns an existing order by `checkout_id` before status validation, backed by the unique index in [create_orders_table](/Users/fabianwesner/Herd/shop/database/migrations/2026_05_04_000408_create_orders_table.php:37). However, I did not find a test that pays the same checkout twice through API or Livewire.\n   \n   Minimal change: add one API test that posts `/checkouts/{id}/pay` twice and asserts one order/payment, same order response, no duplicate inventory commit.\n\n4. **High: admin API tests still use session auth against `auth:sanctum` routes.**\n   The new `sanctum-compatible` guard only authenticates bearer tokens in [AppServiceProvider.php](/Users/fabianwesner/Herd/shop/app/Providers/AppServiceProvider.php:61). Routes now require `auth:sanctum` at [routes/api.php](/Users/fabianwesner/Herd/shop/routes/api.php:70) and [routes/api.php](/Users/fabianwesner/Herd/shop/routes/api.php:79). Tests like [AdminPlatformApiTest.php](/Users/fabianwesner/Herd/shop/tests/Feature/Api/AdminPlatformApiTest.php:40) still call `actingAs($user)` and now fail with 401; I confirmed this exact test fails expecting 201 but receiving 401.\n   \n   Minimal change: update admin API tests to use `WebhookService::createApiToken(..., user: $user)` plus `withToken(...)`, or add a shared helper for bearer-token admin API requests. Keep browser/Livewire admin tests on session auth."}}
</subagent_notification>


The explorer independently confirmed the same remaining blockers: API tests still assume session auth, Livewire writes need explicit policies, and checkout routes still don’t match the spec. I’m applying those fixes now, starting with the lowest-risk API/auth cleanup and policy checks.


I’m editing the API auth surface first: token permission reporting, role ability coverage, and policy authorization on API product/order mutation requests/controllers.


Input:
*** Begin Patch
*** Update File: app/Http/Middleware/AuthenticateAdminApi.php
@@
             'read-products',
             'read-orders',
             'read-customers',
             'read-collections',
-            'read-discounts' => true,
+            'read-discounts',
+            'read-content' => true,
*** Update File: app/Http/Middleware/AuthenticatePlatformApi.php
@@
     private function userCanManagePlatform(User $user): bool
     {
         return $user->stores()
             ->wherePivot('role', StoreUserRole::Owner->value)
             ->exists();
     }
-
 }
*** Update File: app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
@@
 use App\Http\Controllers\Controller;
-use App\Models\OauthToken;
+use App\Models\PersonalAccessToken;
 use App\Models\Store;
 use App\Models\User;
 use Illuminate\Http\JsonResponse;
@@
     public function show(Request $request, Store $store): JsonResponse
     {
-        $token = $request->attributes->get('admin_api_oauth_token');
+        $token = $request->attributes->get('sanctum_personal_access_token');
+        $user = $request->user();

-        if ($token instanceof OauthToken) {
-            return response()->json([
-                'data' => [
-                    'user_id' => null,
-                    'store_id' => $store->getKey(),
-                    'role' => 'integration',
-                    'email' => null,
-                    'name' => $token->name,
-                    'permissions' => $token->abilities_json ?? [],
-                ],
-            ]);
-        }
-
-        $user = $request->user();
-
-        abort_unless($user instanceof User, 401);
+        abort_unless($user instanceof User && $token instanceof PersonalAccessToken, 401);

         $role = $user->roleForStore($store);
@@
                 'role' => $role->value,
                 'email' => $user->email,
                 'name' => $user->name,
-                'permissions' => $this->permissionsForRole($role->value),
+                'permissions' => array_values(array_intersect(
+                    $this->permissionsForRole($role->value),
+                    $token->abilities ?? [],
+                )),
             ],
         ]);
*** Update File: app/Services/WebhookService.php
@@
     public function createApiToken(Store $store, string $name, array $abilities, ?User $user = null): array
     {
-        $user ??= auth()->user() instanceof User
-            ? auth()->user()
+        $authenticatedUser = auth()->user();
+        $user ??= $authenticatedUser instanceof User
+            ? $authenticatedUser
             : $store->users()
                 ->wherePivot('role', 'owner')
                 ->firstOrFail();
*** End Patch


Success. Updated the following files:
M app/Http/Middleware/AuthenticateAdminApi.php
M app/Http/Middleware/AuthenticatePlatformApi.php
M app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
M app/Services/WebhookService.php

@@ -66,4 +66,5 @@
     {
-        $user ??= auth()->user() instanceof User
-            ? auth()->user()
+        $authenticatedUser = auth()->user();
+        $user ??= $authenticatedUser instanceof User
+            ? $authenticatedUser
             : $store->users()

@@ -105,3 +105,4 @@
             'read-collections',
-            'read-discounts' => true,
+            'read-discounts',
+            'read-content' => true,
             'write-products',

@@ -5,3 +5,3 @@
 use App\Http\Controllers\Controller;
-use App\Models\OauthToken;
+use App\Models\PersonalAccessToken;
 use App\Models\Store;
@@ -15,20 +15,6 @@
     {
-        $token = $request->attributes->get('admin_api_oauth_token');
-
-        if ($token instanceof OauthToken) {
-            return response()->json([
-                'data' => [
-                    'user_id' => null,
-                    'store_id' => $store->getKey(),
-                    'role' => 'integration',
-                    'email' => null,
-                    'name' => $token->name,
-                    'permissions' => $token->abilities_json ?? [],
-                ],
-            ]);
-        }
-
+        $token = $request->attributes->get('sanctum_personal_access_token');
         $user = $request->user();

-        abort_unless($user instanceof User, 401);
+        abort_unless($user instanceof User && $token instanceof PersonalAccessToken, 401);

@@ -45,3 +31,6 @@
                 'name' => $user->name,
-                'permissions' => $this->permissionsForRole($role->value),
+                'permissions' => array_values(array_intersect(
+                    $this->permissionsForRole($role->value),
+                    $token->abilities ?? [],
+                )),
             ],


Output: {"output":"Success. Updated the following files:\nM app/Http/Middleware/AuthenticateAdminApi.php\nM app/Http/Middleware/AuthenticatePlatformApi.php\nM app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php\nM app/Services/WebhookService.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,180p' app/Models/PersonalAccessToken.php


sed -n '1,180p' app/Models/PersonalAccessToken.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class PersonalAccessToken extends Model
{
    /**
     * @var list<string>
     */
    protected $fillable = [
        'tokenable_type',
        'tokenable_id',
        'name',
        'token',
        'abilities',
        'last_used_at',
        'expires_at',
    ];

    /**
     * @return MorphTo<Model, $this>
     */
    public function tokenable(): MorphTo
    {
        return $this->morphTo();
    }

    public function can(string $ability): bool
    {
        $abilities = $this->abilities ?? [];

        return in_array('*', $abilities, true) || in_array($ability, $abilities, true);
    }

    public function isExpired(): bool
    {
        return $this->expires_at !== null && $this->expires_at->isPast();
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'abilities' => 'array',
            'last_used_at' => 'datetime',
            'expires_at' => 'datetime',
        ];
    }
}


Output:
Chunk ID: 1463a4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 276
Output:
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class PersonalAccessToken extends Model
{
    /**
     * @var list<string>
     */
    protected $fillable = [
        'tokenable_type',
        'tokenable_id',
        'name',
        'token',
        'abilities',
        'last_used_at',
        'expires_at',
    ];

    /**
     * @return MorphTo<Model, $this>
     */
    public function tokenable(): MorphTo
    {
        return $this->morphTo();
    }

    public function can(string $ability): bool
    {
        $abilities = $this->abilities ?? [];

        return in_array('*', $abilities, true) || in_array($ability, $abilities, true);
    }

    public function isExpired(): bool
    {
        return $this->expires_at !== null && $this->expires_at->isPast();
    }

    /**
     * @return array<string, string>
     */
    protected function casts(): array
    {
        return [
            'abilities' => 'array',
            'last_used_at' => 'datetime',
            'expires_at' => 'datetime',
        ];
    }
}


Input:
*** Begin Patch
*** Update File: app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php
@@
-        return response()->json([
+        $permissions = $this->permissionsForRole($role->value);
+
+        if (! $token->can('*')) {
+            $permissions = array_values(array_intersect($permissions, $token->abilities ?? []));
+        }
+
+        return response()->json([
             'data' => [
                 'user_id' => $user->getKey(),
                 'store_id' => $store->getKey(),
                 'role' => $role->value,
                 'email' => $user->email,
                 'name' => $user->name,
-                'permissions' => array_values(array_intersect(
-                    $this->permissionsForRole($role->value),
-                    $token->abilities ?? [],
-                )),
+                'permissions' => $permissions,
             ],
         ]);
*** End Patch


Success. Updated the following files:
M app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php

@@ -24,2 +24,8 @@

+        $permissions = $this->permissionsForRole($role->value);
+
+        if (! $token->can('*')) {
+            $permissions = array_values(array_intersect($permissions, $token->abilities ?? []));
+        }
+
         return response()->json([
@@ -31,6 +37,3 @@
                 'name' => $user->name,
-                'permissions' => array_values(array_intersect(
-                    $this->permissionsForRole($role->value),
-                    $token->abilities ?? [],
-                )),
+                'permissions' => $permissions,
             ],


Output: {"output":"Success. Updated the following files:\nM app/Http/Controllers/Api/Admin/V1/StoreMembershipController.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Http/Requests/Api/Admin/V1/StoreProductRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\Product;
 use App\Models\ProductVariant;
 use App\Models\Store;
@@
     public function authorize(): bool
     {
-        return true;
+        $store = $this->routeStore();
+
+        app()->instance('current_store', $store);
+
+        return $this->user()?->can('create', Product::class) ?? false;
     }
*** Update File: app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php
@@
     public function authorize(): bool
     {
-        return true;
+        $store = $this->routeStore();
+        $product = $this->routeProduct();
+
+        app()->instance('current_store', $store);
+
+        if (! $product instanceof Product || (int) $product->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('update', $product) ?? false;
     }
*** Update File: app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\Product;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@
     public function authorize(): bool
     {
-        return true;
+        $store = $this->route('store');
+        $product = $this->route('product');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $product instanceof Product || (int) $product->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('update', $product) ?? false;
     }
*** Update File: app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\Order;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@
     public function authorize(): bool
     {
-        return true;
+        $store = $this->route('store');
+        $order = $this->route('order');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $order instanceof Order || (int) $order->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('createRefund', $order) ?? false;
     }
*** Update File: app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php
@@
 namespace App\Http\Requests\Api\Admin\V1;

+use App\Models\Order;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@
     public function authorize(): bool
     {
-        return true;
+        $store = $this->route('store');
+        $order = $this->route('order');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $order instanceof Order || (int) $order->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('createFulfillment', $order) ?? false;
     }
*** End Patch


Success. Updated the following files:
M app/Http/Requests/Api/Admin/V1/StoreProductRequest.php
M app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php
M app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php
M app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php
M app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php

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

+use App\Models\Product;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@ -12,3 +14,18 @@
     {
-        return true;
+        $store = $this->route('store');
+        $product = $this->route('product');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $product instanceof Product || (int) $product->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('update', $product) ?? false;
     }

@@ -16,3 +16,12 @@
     {
-        return true;
+        $store = $this->routeStore();
+        $product = $this->routeProduct();
+
+        app()->instance('current_store', $store);
+
+        if (! $product instanceof Product || (int) $product->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('update', $product) ?? false;
     }

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

+use App\Models\Order;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@ -10,3 +12,18 @@
     {
-        return true;
+        $store = $this->route('store');
+        $order = $this->route('order');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $order instanceof Order || (int) $order->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('createFulfillment', $order) ?? false;
     }

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

+use App\Models\Order;
+use App\Models\Store;
 use Illuminate\Foundation\Http\FormRequest;
@@ -10,3 +12,18 @@
     {
-        return true;
+        $store = $this->route('store');
+        $order = $this->route('order');
+
+        $store = $store instanceof Store ? $store : Store::query()->find($store);
+
+        if (! $store instanceof Store) {
+            return false;
+        }
+
+        app()->instance('current_store', $store);
+
+        if (! $order instanceof Order || (int) $order->store_id !== $store->getKey()) {
+            return true;
+        }
+
+        return $this->user()?->can('createRefund', $order) ?? false;
     }

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

+use App\Models\Product;
 use App\Models\ProductVariant;
@@ -15,3 +16,7 @@
     {
-        return true;
+        $store = $this->routeStore();
+
+        app()->instance('current_store', $store);
+
+        return $this->user()?->can('create', Product::class) ?? false;
     }


Output: {"output":"Success. Updated the following files:\nM app/Http/Requests/Api/Admin/V1/StoreProductRequest.php\nM app/Http/Requests/Api/Admin/V1/UpdateProductRequest.php\nM app/Http/Requests/Api/Admin/V1/CreateProductMediaUploadRequest.php\nM app/Http/Requests/Api/Admin/V1/CreateOrderRefundRequest.php\nM app/Http/Requests/Api/Admin/V1/CreateOrderFulfillmentRequest.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Next I’m adding the missing Livewire policy checks and tightening product/collection pivot sync so tampered IDs from another store are ignored instead of persisted.


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Orders/Show.php
@@
 use App\Services\RefundService;
 use App\Support\Money;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Validation\ValidationException;
 use Livewire\Attributes\Locked;
 use Livewire\Component;

 class Show extends Component
 {
+    use AuthorizesRequests;
+
@@
         abort_unless($order instanceof Order, 404);

+        $this->authorize('view', $order);
+
         $this->storeId = $store->getKey();
         $this->orderId = $order->getKey();
         $this->resetFulfillmentLineQuantities($this->order());
@@
     public function confirmBankTransferPayment(OrderService $orders): void
     {
+        $this->authorize('update', $this->order());
+
         try {
             $orders->confirmBankTransferPayment($this->order());
@@
     public function processRefund(RefundService $refunds): void
     {
+        $this->authorize('createRefund', $this->order());
+
         $this->validate([
@@
     public function createFulfillment(FulfillmentService $fulfillments): void
     {
+        $this->authorize('createFulfillment', $this->order());
+
         $this->validate([
@@
     public function markFulfillmentShipped(int $fulfillmentId, FulfillmentService $fulfillments): void
     {
+        $this->authorize('update', $this->fulfillment($fulfillmentId));
+
         try {
             $fulfillments->markShipped($this->fulfillment($fulfillmentId));
@@
     public function markFulfillmentDelivered(int $fulfillmentId, FulfillmentService $fulfillments): void
     {
+        $this->authorize('update', $this->fulfillment($fulfillmentId));
+
         try {
             $fulfillments->markDelivered($this->fulfillment($fulfillmentId));
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Orders/Show.php

@@ -17,2 +17,3 @@
 use App\Support\Money;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Validation\ValidationException;
@@ -23,2 +24,4 @@
 {
+    use AuthorizesRequests;
+
     #[Locked]
@@ -59,2 +62,4 @@

+        $this->authorize('view', $order);
+
         $this->storeId = $store->getKey();
@@ -66,2 +71,4 @@
     {
+        $this->authorize('update', $this->order());
+
         try {
@@ -80,2 +87,4 @@
     {
+        $this->authorize('createRefund', $this->order());
+
         $this->validate([
@@ -109,2 +118,4 @@
     {
+        $this->authorize('createFulfillment', $this->order());
+
         $this->validate([
@@ -150,2 +161,4 @@
     {
+        $this->authorize('update', $this->fulfillment($fulfillmentId));
+
         try {
@@ -163,2 +176,4 @@
     {
+        $this->authorize('update', $this->fulfillment($fulfillmentId));
+
         try {


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Orders/Show.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


sed -n '1,180p' app/Models/Concerns/BelongsToStore.php


sed -n '1,180p' app/Models/Concerns/BelongsToStore.php

<?php

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function (object $model): void {
            if ($model->store_id || ! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if ($store instanceof Store) {
                $model->store_id = $store->getKey();
            }
        });
    }
}


Output:
Chunk ID: 260ca9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 143
Output:
<?php

namespace App\Models\Concerns;

use App\Models\Scopes\StoreScope;
use App\Models\Store;

trait BelongsToStore
{
    public static function bootBelongsToStore(): void
    {
        static::addGlobalScope(new StoreScope);

        static::creating(function (object $model): void {
            if ($model->store_id || ! app()->bound('current_store')) {
                return;
            }

            $store = app('current_store');

            if ($store instanceof Store) {
                $model->store_id = $store->getKey();
            }
        });
    }
}


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Products/Form.php
@@
 use App\Services\ProductService;
 use App\Support\Money;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Facades\DB;
@@
 class Form extends Component
 {
+    use AuthorizesRequests;
     use WithFileUploads;
@@
             abort_unless($store instanceof Store && (int) $product->store_id === $store->getKey(), 404);

+            $this->authorize('update', $product);
+
             $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
             $this->fillFromProduct($this->product);

             return;
         }

+        $this->authorize('create', Product::class);
+
         $this->variants = [[
@@
         $store = app('current_store');
         abort_unless($store instanceof Store, 404);

+        $this->authorizeSave();
         $this->ensureVariantMatrixMatchesOptions();
@@
-            $product->collections()->sync($this->collectionIds);
+            $product->collections()->sync($this->collectionIdsForSync($store));
@@
     public function deleteProduct(): void
     {
         abort_unless($this->product instanceof Product, 404);

+        $this->authorize('archive', $this->product);
+
         app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);
@@
     public function render(): mixed
     {
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
         return view('livewire.admin.products.form', [
-            'availableCollections' => Collection::query()->orderBy('title')->get(),
+            'availableCollections' => Collection::withoutGlobalScopes()
+                ->where('store_id', $store->getKey())
+                ->orderBy('title')
+                ->get(),
             'isEditing' => $this->product !== null,
         ])->layout('layouts.app', [
@@
     private function storeNewMedia(Product $product): void
     {
+        $this->authorize('update', $product);
+
         $maxPosition = ProductMedia::withoutGlobalScopes()
@@
     private function productForAction(): Product
     {
         $store = app('current_store');
@@
-        return Product::withoutGlobalScopes()
+        $product = Product::withoutGlobalScopes()
             ->where('store_id', $store->getKey())
             ->findOrFail($this->product->getKey());
+
+        $this->authorize('update', $product);
+
+        return $product;
     }
@@
     private function refreshProductMedia(): void
     {
         $this->product = $this->productForAction()->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
         $this->fillFromProduct($this->product);
     }
+
+    private function authorizeSave(): void
+    {
+        if ($this->product instanceof Product) {
+            $this->authorize('update', $this->product);
+
+            return;
+        }
+
+        $this->authorize('create', Product::class);
+    }
+
+    /**
+     * @return list<int>
+     */
+    private function collectionIdsForSync(Store $store): array
+    {
+        if ($this->collectionIds === []) {
+            return [];
+        }
+
+        return Collection::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereIn('id', $this->collectionIds)
+            ->pluck('id')
+            ->map(fn (int $id): int => $id)
+            ->all();
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Products/Form.php

@@ -20,2 +20,3 @@
 use App\Support\Money;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Facades\DB;
@@ -30,2 +31,3 @@
 {
+    use AuthorizesRequests;
     use WithFileUploads;
@@ -82,2 +84,4 @@

+            $this->authorize('update', $product);
+
             $this->product = $product->load(['options.values', 'variants.inventoryItem', 'variants.optionValues.option', 'collections', 'media']);
@@ -88,2 +92,4 @@

+        $this->authorize('create', Product::class);
+
         $this->variants = [[
@@ -169,2 +175,3 @@

+        $this->authorizeSave();
         $this->ensureVariantMatrixMatchesOptions();
@@ -223,3 +230,3 @@

-            $product->collections()->sync($this->collectionIds);
+            $product->collections()->sync($this->collectionIdsForSync($store));

@@ -350,2 +357,4 @@

+        $this->authorize('archive', $this->product);
+
         app(ProductService::class)->transitionStatus($this->product, ProductStatus::Archived);
@@ -358,4 +367,11 @@
     {
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
         return view('livewire.admin.products.form', [
-            'availableCollections' => Collection::query()->orderBy('title')->get(),
+            'availableCollections' => Collection::withoutGlobalScopes()
+                ->where('store_id', $store->getKey())
+                ->orderBy('title')
+                ->get(),
             'isEditing' => $this->product !== null,
@@ -750,2 +766,4 @@
     {
+        $this->authorize('update', $product);
+
         $maxPosition = ProductMedia::withoutGlobalScopes()
@@ -784,5 +802,9 @@

-        return Product::withoutGlobalScopes()
+        $product = Product::withoutGlobalScopes()
             ->where('store_id', $store->getKey())
             ->findOrFail($this->product->getKey());
+
+        $this->authorize('update', $product);
+
+        return $product;
     }
@@ -810,2 +832,30 @@
     }
+
+    private function authorizeSave(): void
+    {
+        if ($this->product instanceof Product) {
+            $this->authorize('update', $this->product);
+
+            return;
+        }
+
+        $this->authorize('create', Product::class);
+    }
+
+    /**
+     * @return list<int>
+     */
+    private function collectionIdsForSync(Store $store): array
+    {
+        if ($this->collectionIds === []) {
+            return [];
+        }
+
+        return Collection::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereIn('id', $this->collectionIds)
+            ->pluck('id')
+            ->map(fn (int $id): int => $id)
+            ->all();
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Products/Form.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Products/Index.php
@@
 use App\Models\Product;
 use App\Services\ProductService;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Collection;
 use Livewire\Component;
 use Livewire\WithPagination;

 class Index extends Component
 {
+    use AuthorizesRequests;
     use WithPagination;
@@
     public function render(): mixed
     {
+        $this->authorize('viewAny', Product::class);
+
         return view('livewire.admin.products.index', [
@@
         Product::query()
             ->whereKey($this->selectedIds)
             ->get()
-            ->each(fn (Product $product) => $service->transitionStatus($product, $status));
+            ->each(function (Product $product) use ($service, $status): void {
+                $this->authorize($status === ProductStatus::Archived ? 'archive' : 'update', $product);
+
+                $service->transitionStatus($product, $status);
+            });
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Products/Index.php

@@ -9,2 +9,3 @@
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Collection;
@@ -15,2 +16,3 @@
 {
+    use AuthorizesRequests;
     use WithPagination;
@@ -122,2 +124,4 @@
     {
+        $this->authorize('viewAny', Product::class);
+
         return view('livewire.admin.products.index', [
@@ -137,3 +141,7 @@
             ->get()
-            ->each(fn (Product $product) => $service->transitionStatus($product, $status));
+            ->each(function (Product $product) use ($service, $status): void {
+                $this->authorize($status === ProductStatus::Archived ? 'archive' : 'update', $product);
+
+                $service->transitionStatus($product, $status);
+            });


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Products/Index.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


rg -n "description_html|{!!.*description|body_html" resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php


rg -n "description_html|{!!.*description|body_html" resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php

rg -n 'description_html|{!!.*description|body_html' resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php

Question: description_html|{!!.*description|body_html

rg: regex parse error:
    (?:description_html|{!!.*description|body_html)
                        ^
error: repetition operator missing expression


Output:
Chunk ID: 6c5833
Wall time: 0.0000 seconds
Process exited with code 2
Original token count: 37
Output:
rg: regex parse error:
    (?:description_html|{!!.*description|body_html)
                        ^
error: repetition operator missing expression


rg -n "description_html|body_html|descriptionHtml|\{!!" resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php


rg -n "description_html|body_html|descriptionHtml|\{!!" resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php

rg -n "description_html|body_html|descriptionHtml|\\{"'!!' resources app/Livewire/Storefront app/Http/Controllers/Api/Admin/V1/CollectionController.php app/Livewire/Admin/Collections/Form.php

Question: description_html|body_html|descriptionHtml|\{!!

app/Livewire/Storefront/Pages/Show.php:27:        $this->bodyHtml = (string) $page->body_html;
app/Http/Controllers/Api/Admin/V1/CollectionController.php:135:            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
app/Http/Controllers/Api/Admin/V1/CollectionController.php:159:            'description_html' => $validated['description_html'] ?? null,
app/Http/Controllers/Api/Admin/V1/CollectionController.php:171:        $attributes = Arr::only($validated, ['title', 'description_html', 'type', 'status']);
app/Livewire/Admin/Collections/Form.php:22:    public string $descriptionHtml = '';
app/Livewire/Admin/Collections/Form.php:86:            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
app/Livewire/Admin/Collections/Form.php:94:            'description_html' => $this->descriptionHtml ?: null,
app/Livewire/Admin/Collections/Form.php:161:        $this->descriptionHtml = (string) $collection->description_html;
resources/views/livewire/storefront/pages/show.blade.php:8:            {!! $bodyHtml !!}
resources/views/livewire/storefront/products/show.blade.php:74:            @if ($product->description_html)
resources/views/livewire/storefront/products/show.blade.php:76:                    {!! $product->description_html !!}
resources/views/livewire/storefront/collections/show.blade.php:9:        @if ($collection->description_html)
resources/views/livewire/storefront/collections/show.blade.php:10:
{!! $collection->description_html !!}

resources/views/livewire/settings/two-factor.blade.php:138:                                    {!! $qrCodeSvg !!}
resources/views/livewire/admin/collections/form.blade.php:26:                    <flux:textarea wire:model="descriptionHtml" label="Description" rows="6" placeholder="Describe this collection..." />
resources/views/livewire/admin/products/form.blade.php:29:                    <flux:textarea wire:model="descriptionHtml" label="Description" rows="8" placeholder="Describe your product..." />
resources/views/livewire/admin/products/form.blade.php:30:                    <flux:error name="descriptionHtml" />


Output:
Chunk ID: cd293b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 544
Output:
app/Livewire/Storefront/Pages/Show.php:27:        $this->bodyHtml = (string) $page->body_html;
app/Http/Controllers/Api/Admin/V1/CollectionController.php:135:            'description_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
app/Http/Controllers/Api/Admin/V1/CollectionController.php:159:            'description_html' => $validated['description_html'] ?? null,
app/Http/Controllers/Api/Admin/V1/CollectionController.php:171:        $attributes = Arr::only($validated, ['title', 'description_html', 'type', 'status']);
app/Livewire/Admin/Collections/Form.php:22:    public string $descriptionHtml = '';
app/Livewire/Admin/Collections/Form.php:86:            'descriptionHtml' => ['nullable', 'string', 'max:65535'],
app/Livewire/Admin/Collections/Form.php:94:            'description_html' => $this->descriptionHtml ?: null,
app/Livewire/Admin/Collections/Form.php:161:        $this->descriptionHtml = (string) $collection->description_html;
resources/views/livewire/storefront/pages/show.blade.php:8:            {!! $bodyHtml !!}
resources/views/livewire/storefront/products/show.blade.php:74:            @if ($product->description_html)
resources/views/livewire/storefront/products/show.blade.php:76:                    {!! $product->description_html !!}
resources/views/livewire/storefront/collections/show.blade.php:9:        @if ($collection->description_html)
resources/views/livewire/storefront/collections/show.blade.php:10:
{!! $collection->description_html !!}

resources/views/livewire/settings/two-factor.blade.php:138:                                    {!! $qrCodeSvg !!}
resources/views/livewire/admin/collections/form.blade.php:26:                    <flux:textarea wire:model="descriptionHtml" label="Description" rows="6" placeholder="Describe this collection..." />
resources/views/livewire/admin/products/form.blade.php:29:                    <flux:textarea wire:model="descriptionHtml" label="Description" rows="8" placeholder="Describe your product..." />
resources/views/livewire/admin/products/form.blade.php:30:                    <flux:error name="descriptionHtml" />


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Collections/Form.php
@@
 namespace App\Livewire\Admin\Collections;

+use App\Actions\SanitizeHtml;
 use App\Models\Collection;
 use App\Models\Product;
 use App\Models\Store;
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Collection as SupportCollection;
@@
 class Form extends Component
 {
+    use AuthorizesRequests;
+
@@
             abort_unless($store instanceof Store && (int) $collection->store_id === $store->getKey(), 404);

+            $this->authorize('update', $collection);
+
             $this->collection = $collection->load('products');
             $this->fillFromCollection($this->collection);
+
+            return;
         }
+
+        $this->authorize('create', Collection::class);
     }
@@
     public function addProduct(int $productId): void
     {
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $exists = Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereKey($productId)
+            ->exists();
+
+        if (! $exists) {
+            return;
+        }
+
         if (! in_array($productId, $this->assignedProductIds, true)) {
             $this->assignedProductIds[] = $productId;
@@
         $store = app('current_store');
         abort_unless($store instanceof Store, 404);

+        $this->authorizeSave();
+
         $this->validate([
@@
             'store_id' => $store->getKey(),
             'title' => $this->title,
             'handle' => Str::slug($this->handle),
-            'description_html' => $this->descriptionHtml ?: null,
+            'description_html' => $this->sanitizeHtml($this->descriptionHtml),
             'type' => 'manual',
             'status' => $this->status,
         ];
@@
-        $collection->products()->sync(collect($this->assignedProductIds)
+        $collection->products()->sync(collect($this->assignedProductIdsForSync($store))
             ->values()
             ->mapWithKeys(fn (int $productId, int $position): array => [$productId => ['position' => $position]])
@@
-        return Product::query()
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
             ->whereNotIn('id', $this->assignedProductIds)
             ->where(function (Builder $query): void {
@@
-        return Product::query()
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
             ->whereIn('id', $this->assignedProductIds)
             ->get()
@@
         $this->descriptionHtml = (string) $collection->description_html;
         $this->status = $collection->status->value;
         $this->assignedProductIds = $collection->products->pluck('id')->map(fn (int $id): int => $id)->all();
     }
+
+    private function sanitizeHtml(?string $html): ?string
+    {
+        $sanitized = app(SanitizeHtml::class)($html);
+
+        return $sanitized === '' ? null : $sanitized;
+    }
+
+    private function authorizeSave(): void
+    {
+        if ($this->collection instanceof Collection) {
+            $this->authorize('update', $this->collection);
+
+            return;
+        }
+
+        $this->authorize('create', Collection::class);
+    }
+
+    /**
+     * @return list<int>
+     */
+    private function assignedProductIdsForSync(Store $store): array
+    {
+        if ($this->assignedProductIds === []) {
+            return [];
+        }
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereIn('id', $this->assignedProductIds)
+            ->pluck('id')
+            ->map(fn (int $id): int => $id)
+            ->all();
+    }
 }
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Collections/Form.php

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

+use App\Actions\SanitizeHtml;
 use App\Models\Collection;
@@ -8,2 +9,3 @@
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Collection as SupportCollection;
@@ -15,2 +17,4 @@
 {
+    use AuthorizesRequests;
+
     public ?Collection $collection = null;
@@ -41,5 +45,11 @@

+            $this->authorize('update', $collection);
+
             $this->collection = $collection->load('products');
             $this->fillFromCollection($this->collection);
+
+            return;
         }
+
+        $this->authorize('create', Collection::class);
     }
@@ -55,2 +65,15 @@
     {
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        $exists = Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereKey($productId)
+            ->exists();
+
+        if (! $exists) {
+            return;
+        }
+
         if (! in_array($productId, $this->assignedProductIds, true)) {
@@ -75,2 +98,4 @@

+        $this->authorizeSave();
+
         $this->validate([
@@ -93,3 +118,3 @@
             'handle' => Str::slug($this->handle),
-            'description_html' => $this->descriptionHtml ?: null,
+            'description_html' => $this->sanitizeHtml($this->descriptionHtml),
             'type' => 'manual',
@@ -102,3 +127,3 @@

-        $collection->products()->sync(collect($this->assignedProductIds)
+        $collection->products()->sync(collect($this->assignedProductIdsForSync($store))
             ->values()
@@ -121,3 +146,8 @@

-        return Product::query()
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
             ->whereNotIn('id', $this->assignedProductIds)
@@ -138,3 +168,8 @@

-        return Product::query()
+        $store = app('current_store');
+
+        abort_unless($store instanceof Store, 404);
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
             ->whereIn('id', $this->assignedProductIds)
@@ -164,2 +199,37 @@
     }
+
+    private function sanitizeHtml(?string $html): ?string
+    {
+        $sanitized = app(SanitizeHtml::class)($html);
+
+        return $sanitized === '' ? null : $sanitized;
+    }
+
+    private function authorizeSave(): void
+    {
+        if ($this->collection instanceof Collection) {
+            $this->authorize('update', $this->collection);
+
+            return;
+        }
+
+        $this->authorize('create', Collection::class);
+    }
+
+    /**
+     * @return list<int>
+     */
+    private function assignedProductIdsForSync(Store $store): array
+    {
+        if ($this->assignedProductIds === []) {
+            return [];
+        }
+
+        return Product::withoutGlobalScopes()
+            ->where('store_id', $store->getKey())
+            ->whereIn('id', $this->assignedProductIds)
+            ->pluck('id')
+            ->map(fn (int $id): int => $id)
+            ->all();
+    }
 }


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Collections/Form.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Collections/Index.php
@@
 use App\Models\Collection;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Livewire\Component;
 use Livewire\WithPagination;

 class Index extends Component
 {
+    use AuthorizesRequests;
     use WithPagination;
@@
     public function deleteCollection(int $id): void
     {
-        Collection::query()->findOrFail($id)->delete();
+        $collection = Collection::query()->findOrFail($id);
+
+        $this->authorize('delete', $collection);
+
+        $collection->delete();
@@
     public function render(): mixed
     {
+        $this->authorize('viewAny', Collection::class);
+
         return view('livewire.admin.collections.index', [
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Collections/Index.php

@@ -7,2 +7,3 @@
 use Illuminate\Database\Eloquent\Builder;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Livewire\Component;
@@ -12,2 +13,3 @@
 {
+    use AuthorizesRequests;
     use WithPagination;
@@ -30,3 +32,7 @@
     {
-        Collection::query()->findOrFail($id)->delete();
+        $collection = Collection::query()->findOrFail($id);
+
+        $this->authorize('delete', $collection);
+
+        $collection->delete();

@@ -49,2 +55,4 @@
     {
+        $this->authorize('viewAny', Collection::class);
+
         return view('livewire.admin.collections.index', [


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Collections/Index.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}


Input:
*** Begin Patch
*** Update File: app/Livewire/Admin/Customers/Show.php
@@
 use App\Models\Order;
 use App\Models\Store;
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Facades\DB;
@@
 class Show extends Component
 {
+    use AuthorizesRequests;
     use WithPagination;
@@
         abort_unless($customer instanceof Customer, 404);

+        $this->authorize('view', $customer);
+
         $this->storeId = $store->getKey();
         $this->customerId = $customer->getKey();
@@
     public function saveAddress(): void
     {
+        $this->authorize('update', $this->customer());
+
         $this->validate([
@@
     public function deleteAddress(int $addressId): void
     {
+        $this->authorize('update', $this->customer());
+
         $address = $this->address($addressId);
@@
     public function setDefaultAddress(int $addressId): void
     {
+        $this->authorize('update', $this->customer());
+
         $address = $this->address($addressId);
*** End Patch


Success. Updated the following files:
M app/Livewire/Admin/Customers/Show.php

@@ -9,2 +9,3 @@
 use Illuminate\Contracts\Pagination\LengthAwarePaginator;
+use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
 use Illuminate\Support\Facades\DB;
@@ -16,2 +17,3 @@
 {
+    use AuthorizesRequests;
     use WithPagination;
@@ -55,2 +57,4 @@

+        $this->authorize('view', $customer);
+
         $this->storeId = $store->getKey();
@@ -76,2 +80,4 @@
     {
+        $this->authorize('update', $this->customer());
+
         $this->validate([
@@ -106,2 +112,4 @@
     {
+        $this->authorize('update', $this->customer());
+
         $address = $this->address($addressId);
@@ -119,2 +127,4 @@
     {
+        $this->authorize('update', $this->customer());
+
         $address = $this->address($addressId);


Output: {"output":"Success. Updated the following files:\nM app/Livewire/Admin/Customers/Show.php\n","metadata":{"exit_code":0,"duration_seconds":0.0}}