output:
Chunk ID: 77e3fc
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1076
Output:

    $shopIndex = collect($component->get('menuItems'))
        ->search(fn (array $item): bool => $item['label'] === 'Shop');

    expect($shopIndex)->not->toBeFalse();

    $component
        ->call('removeItem', (int) $shopIndex)
        ->call('saveMenu')
        ->assertHasNoErrors();

    expect(NavigationItem::withoutGlobalScopes()
        ->where('menu_id', $menu->getKey())
        ->where('label', 'Shop')
        ->exists())->toBeFalse()
        ->and(NavigationItem::withoutGlobalScopes()
            ->where('menu_id', $menu->getKey())
            ->where('label', 'Lookbook')
            ->exists())->toBeFalse();
});

test('themes can be duplicated edited and published', function (): void {
    $store = adminContentStore();
    $user = adminContentUser();
    $published = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    Livewire::actingAs($user)
        ->test(AdminThemesIndex::class)
        ->call('duplicateTheme', $published->getKey())
        ->assertHasNoErrors();

    $copy = Theme::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('name', $published->name.' Copy')
        ->firstOrFail();
    $publishedFile = $published->files()->withoutGlobalScopes()->firstOrFail();
    $copyFile = $copy->files()->withoutGlobalScopes()->where('path', $publishedFile->path)->firstOrFail();

    expect($copyFile->storage_key)->not->toBe($publishedFile->storage_key)
        ->and(Storage::disk('local')->get($copyFile->storage_key))->toBe(Storage::disk('local')->get($publishedFile->storage_key));

    Livewire::actingAs($user)
        ->test(AdminThemeEditor::class, ['theme' => $copy])
        ->set('settings.home.hero.heading', 'New Hero')
        ->call('save')
        ->call('publish')
        ->assertHasNoErrors();

    expect(ThemeSettings::withoutGlobalScopes()->where('theme_id', $copy->getKey())->first()?->settings_json['home']['hero']['heading'])->toBe('New Hero')
        ->and($copy->refresh()->status)->toBe(ThemeStatus::Published)
        ->and($published->refresh()->status)->toBe(ThemeStatus::Draft)
        ->and(Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->where('status', ThemeStatus::Published)->count())->toBe(1);
});

test('theme editor saves file contents and metadata', function (): void {
    $store = adminContentStore();
    $user = adminContentUser();
    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
    $file = $theme->files()->withoutGlobalScopes()->where('path', 'sections/hero.blade.php')->firstOrFail();

    Storage::disk('local')->put($file->storage_key, 'Original theme file');

    Livewire::actingAs($user)
        ->test(AdminThemeEditor::class, ['theme' => $theme])
        ->call('selectFile', $file->getKey())
        ->assertSet('selectedFileId', $file->getKey())
        ->assertSet('fileContents', 'Original theme file')
        ->set('fileContents', '
Edited hero file
')
        ->call('saveFile')
        ->assertHasNoErrors()
        ->assertSee('Theme file saved');

    expect(Storage::disk('local')->get($file->storage_key))->toBe('
Edited hero file
')
        ->and($file->refresh()->sha256)->toBe(hash('sha256', '
Edited hero file
'))
        ->and($file->byte_size)->toBe(strlen('
Edited hero file
'));
});

test('content management honors store roles and store scoping', function (): void {
    $store = adminContentStore();
    $support = adminContentUserWithRole($store, StoreUserRole::Support);
    $staff = adminContentUserWithRole($store, StoreUserRole::Staff);
    $otherPage = Page::factory()->create([
        'store_id' => Store::factory()->create()->getKey(),
    ]);
    $theme = Theme::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    Livewire::actingAs($support)
        ->test(AdminPageForm::class)
        ->assertStatus(403);

    Livewire::actingAs($staff)
        ->test(AdminThemesIndex::class)
        ->assertStatus(403);

    Livewire::actingAs($staff)
        ->test(AdminNavigationIndex::class)
        ->assertStatus(403);

    Livewire::actingAs(adminContentUser())
        ->test(AdminPageForm::class, ['page' => $otherPage])
        ->assertStatus(404);


command: sed -n '1,180p' app/Policies/ThemePolicy.php

ThemePolicy.php

type: read

aggregated_output:
<?php

namespace App\Policies;

use App\Models\Theme;
use App\Models\User;
use App\Traits\ChecksStoreRole;

class ThemePolicy
{
    use ChecksStoreRole;

    public function viewAny(User $user): bool
    {
        return $this->isOwnerOrAdmin($user);
    }

    public function view(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }

    public function create(User $user): bool
    {
        return $this->isOwnerOrAdmin($user);
    }

    public function update(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }

    public function publish(User $user, Theme $theme): bool
    {
        return $this->update($user, $theme);
    }

    public function delete(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }
}

status: completed


output:
Chunk ID: f122f2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 232
Output:
<?php

namespace App\Policies;

use App\Models\Theme;
use App\Models\User;
use App\Traits\ChecksStoreRole;

class ThemePolicy
{
    use ChecksStoreRole;

    public function viewAny(User $user): bool
    {
        return $this->isOwnerOrAdmin($user);
    }

    public function view(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }

    public function create(User $user): bool
    {
        return $this->isOwnerOrAdmin($user);
    }

    public function update(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }

    public function publish(User $user, Theme $theme): bool
    {
        return $this->update($user, $theme);
    }

    public function delete(User $user, Theme $theme): bool
    {
        return $this->isOwnerOrAdmin($user, $this->storeIdForModel($theme));
    }
}


command:
/bin/zsh
-lc
sed -n '1,130p' app/Models/Store.php

Store.php

cmd: sed -n '1,130p' app/Models/Store.php

aggregated_output:
<?php

namespace App\Models;

use App\Enums\StoreStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Store extends Model
{
    /** @use HasFactory<\Database\Factories\StoreFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'organization_id',
        'name',
        'handle',
        'status',
        'default_currency',
        'default_locale',
        'timezone',
    ];

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

    /**
     * @return HasMany<StoreDomain, $this>
     */
    public function domains(): HasMany
    {
        return $this->hasMany(StoreDomain::class);
    }

    /**
     * @return BelongsToMany<User, $this>
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role');
    }

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

    /**
     * @return HasMany<Theme, $this>
     */
    public function themes(): HasMany
    {
        return $this->hasMany(Theme::class);
    }

    /**
     * @return HasMany<Page, $this>
     */
    public function pages(): HasMany
    {
        return $this->hasMany(Page::class);
    }

    /**
     * @return HasMany<NavigationMenu, $this>
     */
    public function navigationMenus(): HasMany
    {
        return $this->hasMany(NavigationMenu::class);
    }

    /**
     * @return HasMany<Cart, $this>
     */
    public function carts(): HasMany
    {
        return $this->hasMany(Cart::class);
    }

    /**
     * @return HasMany<Checkout, $this>
     */
    public function checkouts(): HasMany
    {
        return $this->hasMany(Checkout::class);
    }

    /**
     * @return HasMany<ShippingZone, $this>
     */
    public function shippingZones(): HasMany
    {
        return $this->hasMany(ShippingZone::class);
    }

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

    /**
     * @return HasMany<Discount, $this>
     */
    public function discounts(): HasMany
    {
        return $this->hasMany(Discount::class);
    }

    /**
     * @return HasMany<Order, $this>


output:
Chunk ID: 20e27a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 693
Output:
<?php

namespace App\Models;

use App\Enums\StoreStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;

class Store extends Model
{
    /** @use HasFactory<\Database\Factories\StoreFactory> */
    use HasFactory;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'organization_id',
        'name',
        'handle',
        'status',
        'default_currency',
        'default_locale',
        'timezone',
    ];

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

    /**
     * @return HasMany<StoreDomain, $this>
     */
    public function domains(): HasMany
    {
        return $this->hasMany(StoreDomain::class);
    }

    /**
     * @return BelongsToMany<User, $this>
     */
    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, 'store_users')
            ->using(StoreUser::class)
            ->withPivot('role');
    }

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

    /**
     * @return HasMany<Theme, $this>
     */
    public function themes(): HasMany
    {
        return $this->hasMany(Theme::class);
    }

    /**
     * @return HasMany<Page, $this>
     */
    public function pages(): HasMany
    {
        return $this->hasMany(Page::class);
    }

    /**
     * @return HasMany<NavigationMenu, $this>
     */
    public function navigationMenus(): HasMany
    {
        return $this->hasMany(NavigationMenu::class);
    }

    /**
     * @return HasMany<Cart, $this>
     */
    public function carts(): HasMany
    {
        return $this->hasMany(Cart::class);
    }

    /**
     * @return HasMany<Checkout, $this>
     */
    public function checkouts(): HasMany
    {
        return $this->hasMany(Checkout::class);
    }

    /**
     * @return HasMany<ShippingZone, $this>
     */
    public function shippingZones(): HasMany
    {
        return $this->hasMany(ShippingZone::class);
    }

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

    /**
     * @return HasMany<Discount, $this>
     */
    public function discounts(): HasMany
    {
        return $this->hasMany(Discount::class);
    }

    /**
     * @return HasMany<Order, $this>


command: sed -n '2490,2588p' specs/02-API-ROUTES.md

aggregated_output:
---

### 3.8 Themes

#### Admin Themes Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| POST | `/api/admin/v1/stores/{storeId}/themes` | `write-themes` | Upload or install a new theme | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/themes/{themeId}/publish` | `write-themes` | Publish a theme (make active) | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/themes/{themeId}/settings` | `write-themes` | Update theme customization settings | `api.admin` (60/min) |

#### POST /api/admin/v1/stores/{storeId}/themes

Upload or install a new theme.

**Request body (multipart/form-data):**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `file` | file | Yes | ZIP archive. Max: 50MB. Must contain valid theme structure. |
| `name` | string | No | Max: 255. Defaults to theme manifest name. |

**Success response:** `201 Created`

```json
{
  "data": {
    "id": 1,
    "store_id": 1,
    "name": "Dawn",
    "version": "1.0.0",
    "status": "draft",
    "published_at": null,
    "created_at": "2026-02-11T10:00:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `422` - Invalid theme archive (missing required templates, invalid manifest)

---

#### POST /api/admin/v1/stores/{storeId}/themes/{themeId}/publish

Publish a theme, making it the active storefront theme. The previously published theme transitions to `draft`.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 1,
    "status": "published",
    "published_at": "2026-02-11T10:05:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Theme not found
- `422` - Theme has validation errors preventing publish

---

#### PUT /api/admin/v1/stores/{storeId}/themes/{themeId}/settings

Update theme settings (customization values).

**Request body:**

```json
{
  "settings_json": {
    "colors": {
      "primary": "#1a73e8",
      "secondary": "#fbbc04",
      "background": "#ffffff",
      "text": "#202124"
    },
    "typography": {
      "heading_font": "Inter",
      "body_font": "Inter"
    },
    "header": {
      "show_announcement_bar": true,
      "announcement_text": "Free shipping on orders over 50 EUR!"
    }
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `settings_json` | object | Yes | Must conform to the theme's settings schema. |


output:
Chunk ID: 604809
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 621
Output:
---

### 3.8 Themes

#### Admin Themes Endpoint Summary

| Method | Path | Auth Scope | Description | Rate Limit |
|--------|------|------------|-------------|------------|
| POST | `/api/admin/v1/stores/{storeId}/themes` | `write-themes` | Upload or install a new theme | `api.admin` (60/min) |
| POST | `/api/admin/v1/stores/{storeId}/themes/{themeId}/publish` | `write-themes` | Publish a theme (make active) | `api.admin` (60/min) |
| PUT | `/api/admin/v1/stores/{storeId}/themes/{themeId}/settings` | `write-themes` | Update theme customization settings | `api.admin` (60/min) |

#### POST /api/admin/v1/stores/{storeId}/themes

Upload or install a new theme.

**Request body (multipart/form-data):**

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `file` | file | Yes | ZIP archive. Max: 50MB. Must contain valid theme structure. |
| `name` | string | No | Max: 255. Defaults to theme manifest name. |

**Success response:** `201 Created`

```json
{
  "data": {
    "id": 1,
    "store_id": 1,
    "name": "Dawn",
    "version": "1.0.0",
    "status": "draft",
    "published_at": null,
    "created_at": "2026-02-11T10:00:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `422` - Invalid theme archive (missing required templates, invalid manifest)

---

#### POST /api/admin/v1/stores/{storeId}/themes/{themeId}/publish

Publish a theme, making it the active storefront theme. The previously published theme transitions to `draft`.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 1,
    "status": "published",
    "published_at": "2026-02-11T10:05:00Z"
  }
}
```

**Error responses:**
- `403` - Insufficient permissions
- `404` - Theme not found
- `422` - Theme has validation errors preventing publish

---

#### PUT /api/admin/v1/stores/{storeId}/themes/{themeId}/settings

Update theme settings (customization values).

**Request body:**

```json
{
  "settings_json": {
    "colors": {
      "primary": "#1a73e8",
      "secondary": "#fbbc04",
      "background": "#ffffff",
      "text": "#202124"
    },
    "typography": {
      "heading_font": "Inter",
      "body_font": "Inter"
    },
    "header": {
      "show_announcement_bar": true,
      "announcement_text": "Free shipping on orders over 50 EUR!"
    }
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `settings_json` | object | Yes | Must conform to the theme's settings schema. |


command: sed -n '1,240p' tests/Feature/Api/Admin/AdminShippingSettingsApiTest.php

AdminShippingSettingsApiTest.php

aggregated_output: sed: tests/Feature/Api/Admin/AdminShippingSettingsApiTest.php: No such file or directory

status: failed


output:
Chunk ID: 3ff6b9
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 23
Output:
sed: tests/Feature/Api/Admin/AdminShippingSettingsApiTest.php: No such file or directory


command: sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php

ShippingZoneController.php

aggregated_output:
<?php

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

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\ShippingZoneResource;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\ValidationException;

class ShippingZoneController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $zones = ShippingZone::withoutGlobalScopes()
            ->with(['rates' => fn ($query) => $query->withoutGlobalScopes()->orderBy('id'), 'store'])
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->get();

        return ShippingZoneResource::collection($zones);
    }

    public function store(Request $request, Store $store): JsonResponse
    {
        $this->authorizeStore($request, $store);

        $validated = $this->validatePayload($request, $store);

        $zone = ShippingZone::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            ...$this->attributes($validated),
        ]);

        return ShippingZoneResource::make($this->loadZone($zone))
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Store $store, ShippingZone $shippingZone): ShippingZoneResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessZoneBelongsToStore($shippingZone, $store);

        $validated = $this->validatePayload($request, $store, $shippingZone);

        $shippingZone->update($this->attributes($validated));

        return ShippingZoneResource::make($this->loadZone($shippingZone->refresh()));
    }

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

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

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

    /**
     * @return array<string, mixed>
     */
    private function validatePayload(Request $request, Store $store, ?ShippingZone $zone = null): array
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'countries_json' => ['required', 'array', 'min:1'],
            'countries_json.*' => ['required', 'string', 'size:2'],
            'regions_json' => ['sometimes', 'array'],
            'regions_json.*' => ['string', 'max:20'],
        ]);

        $countries = $this->countryCodes($validated['countries_json']);

        if ($countries === []) {
            throw ValidationException::withMessages([
                'countries_json' => __('Enter at least one ISO country code.'),
            ]);
        }

        $overlap = ShippingZone::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->when($zone instanceof ShippingZone, fn (Builder $query) => $query->whereKeyNot($zone->getKey()))
            ->get()
            ->first(fn (ShippingZone $existing): bool => array_intersect($countries, $existing->countries_json ?? []) !== []);

        if ($overlap instanceof ShippingZone) {
            throw ValidationException::withMessages([
                'countries_json' => __('One or more countries already belong to another shipping zone.'),
            ]);
        }

        $validated['countries_json'] = $countries;
        $validated['regions_json'] = $this->regionCodes($validated['regions_json'] ?? []);

        return $validated;
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributes(array $validated): array
    {
        return [
            'name' => $validated['name'],
            'countries_json' => $validated['countries_json'],
            'regions_json' => $validated['regions_json'],
        ];
    }

    /**
     * @param  list<string>  $countries
     * @return list<string>
     */
    private function countryCodes(array $countries): array
    {
        return collect($countries)
            ->map(fn (string $country): string => strtoupper(trim($country)))
            ->filter(fn (string $country): bool => preg_match('/^[A-Z]{2}$/', $country) === 1)
            ->unique()
            ->values()
            ->all();
    }

    /**
     * @param  list<string>  $regions
     * @return list<string>
     */
    private function regionCodes(array $regions): array
    {
        return collect($regions)
            ->map(fn (string $region): string => strtoupper(trim($region)))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function loadZone(ShippingZone $zone): ShippingZone
    {
        return $zone->load([
            'rates' => fn ($query) => $query->withoutGlobalScopes()->orderBy('id'),
            'store',
        ]);
    }
}


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

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

use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\ShippingZoneResource;
use App\Models\ShippingZone;
use App\Models\Store;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Validation\ValidationException;

class ShippingZoneController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $zones = ShippingZone::withoutGlobalScopes()
            ->with(['rates' => fn ($query) => $query->withoutGlobalScopes()->orderBy('id'), 'store'])
            ->where('store_id', $store->getKey())
            ->orderBy('id')
            ->get();

        return ShippingZoneResource::collection($zones);
    }

    public function store(Request $request, Store $store): JsonResponse
    {
        $this->authorizeStore($request, $store);

        $validated = $this->validatePayload($request, $store);

        $zone = ShippingZone::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            ...$this->attributes($validated),
        ]);

        return ShippingZoneResource::make($this->loadZone($zone))
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Store $store, ShippingZone $shippingZone): ShippingZoneResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessZoneBelongsToStore($shippingZone, $store);

        $validated = $this->validatePayload($request, $store, $shippingZone);

        $shippingZone->update($this->attributes($validated));

        return ShippingZoneResource::make($this->loadZone($shippingZone->refresh()));
    }

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

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

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

    /**
     * @return array<string, mixed>
     */
    private function validatePayload(Request $request, Store $store, ?ShippingZone $zone = null): array
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'countries_json' => ['required', 'array', 'min:1'],
            'countries_json.*' => ['required', 'string', 'size:2'],
            'regions_json' => ['sometimes', 'array'],
            'regions_json.*' => ['string', 'max:20'],
        ]);

        $countries = $this->countryCodes($validated['countries_json']);

        if ($countries === []) {
            throw ValidationException::withMessages([
                'countries_json' => __('Enter at least one ISO country code.'),
            ]);
        }

        $overlap = ShippingZone::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->when($zone instanceof ShippingZone, fn (Builder $query) => $query->whereKeyNot($zone->getKey()))
            ->get()
            ->first(fn (ShippingZone $existing): bool => array_intersect($countries, $existing->countries_json ?? []) !== []);

        if ($overlap instanceof ShippingZone) {
            throw ValidationException::withMessages([
                'countries_json' => __('One or more countries already belong to another shipping zone.'),
            ]);
        }

        $validated['countries_json'] = $countries;
        $validated['regions_json'] = $this->regionCodes($validated['regions_json'] ?? []);

        return $validated;
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributes(array $validated): array
    {
        return [
            'name' => $validated['name'],
            'countries_json' => $validated['countries_json'],
            'regions_json' => $validated['regions_json'],
        ];
    }

    /**
     * @param  list<string>  $countries
     * @return list<string>
     */
    private function countryCodes(array $countries): array
    {
        return collect($countries)
            ->map(fn (string $country): string => strtoupper(trim($country)))
            ->filter(fn (string $country): bool => preg_match('/^[A-Z]{2}$/', $country) === 1)
            ->unique()
            ->values()
            ->all();
    }

    /**
     * @param  list<string>  $regions
     * @return list<string>
     */
    private function regionCodes(array $regions): array
    {
        return collect($regions)
            ->map(fn (string $region): string => strtoupper(trim($region)))
            ->filter()
            ->unique()
            ->values()
            ->all();
    }

    private function loadZone(ShippingZone $zone): ShippingZone
    {
        return $zone->load([
            'rates' => fn ($query) => $query->withoutGlobalScopes()->orderBy('id'),
            'store',
        ]);
    }
}


message: The Livewire theme flow already has the core semantics: duplicating draft themes, saving JSON settings, publishing one active theme per store, and flushing the theme settings cache. I’m going to mirror that behavior in API controllers and keep archive upload validation deliberately narrow to what the current app can actually process.

phase: commentary


type: message

role: assistant

text: The Livewire theme flow already has the core semantics: duplicating draft themes, saving JSON settings, publishing one active theme per store, and flushing the theme settings cache. I’m going to mirror that behavior in API controllers and keep archive upload validation deliberately narrow to what the current app can actually process.


cmd: find tests/Feature/Api/Admin -maxdepth 1 -type f | sort


cmd: sed -n '1,260p' tests/Feature/Api/Admin/AdminTaxSettingsApiTest.php


cmd: sed -n '1,260p' tests/Feature/Api/Admin/AdminPageApiTest.php


cmd: sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php


cmd: sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/PageController.php


cmd: sed -n '1,220p' app/Http/Middleware/EnsureAdminApiScope.php


command: sed -n '1,260p' tests/Feature/Api/Admin/AdminTaxSettingsApiTest.php

aggregated_output: sed: tests/Feature/Api/Admin/AdminTaxSettingsApiTest.php: No such file or directory


command: find tests/Feature/Api/Admin -maxdepth 1 -type f | sort

path: Admin

aggregated_output: find: tests/Feature/Api/Admin: No such file or directory


output:
Chunk ID: f79b99
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 15
Output:
find: tests/Feature/Api/Admin: No such file or directory


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


command: sed -n '1,260p' tests/Feature/Api/Admin/AdminPageApiTest.php

aggregated_output: sed: tests/Feature/Api/Admin/AdminPageApiTest.php: No such file or directory


output:
Chunk ID: 3f5dd4
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 20
Output:
sed: tests/Feature/Api/Admin/AdminPageApiTest.php: No such file or directory


command: sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php

aggregated_output:
<?php

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

use App\Enums\TaxMode;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\TaxSettingsResource;
use App\Models\Store;
use App\Models\TaxSettings;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class TaxSettingsController extends Controller
{
    public function show(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        return TaxSettingsResource::make($this->settings($store));
    }

    public function update(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'mode' => ['required', Rule::in(array_map(fn (TaxMode $mode): string => $mode->value, TaxMode::cases()))],
            'provider' => ['required_if:mode,provider', Rule::in(['none', 'stripe_tax'])],
            'prices_include_tax' => ['required', 'boolean'],
            'config_json' => ['required', 'array'],
            'config_json.default_tax_rate' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.default_rate_bps' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates' => ['nullable', 'array'],
            'config_json.tax_rates.*.country_code' => ['required_with:config_json.tax_rates', 'string', 'size:2'],
            'config_json.tax_rates.*.rate' => ['required_with:config_json.tax_rates', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates.*.name' => ['required_with:config_json.tax_rates', 'string', 'max:50'],
            'config_json.tax_rates.*.shipping_taxed' => ['nullable', 'boolean'],
            'config_json.rates' => ['nullable', 'array'],
            'config_json.rates.*.country' => ['required_with:config_json.rates', 'string', 'size:2'],
            'config_json.rates.*.rate_bps' => ['required_with:config_json.rates', 'integer', 'min:0', 'max:10000'],
            'config_json.rates.*.name' => ['required_with:config_json.rates', 'string', 'max:50'],
        ]);

        $settings = TaxSettings::withoutGlobalScopes()->updateOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::from($validated['mode']),
                'provider' => $validated['mode'] === TaxMode::Provider->value ? $validated['provider'] : 'none',
                'prices_include_tax' => (bool) $validated['prices_include_tax'],
                'config_json' => $this->normalizeConfig($request->input('config_json', [])),
            ],
        );

        return TaxSettingsResource::make($settings->refresh());
    }

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

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

    private function settings(Store $store): TaxSettings
    {
        return TaxSettings::withoutGlobalScopes()->firstOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::Manual,
                'provider' => 'none',
                'prices_include_tax' => false,
                'config_json' => [
                    'name' => 'Tax',
                    'default_rate_bps' => 0,
                    'shipping_taxable' => true,
                    'rates' => [],
                ],
            ],
        );
    }

    /**
     * @param  array<string, mixed>  $config
     * @return array<string, mixed>
     */
    private function normalizeConfig(array $config): array
    {
        $rates = collect($config['tax_rates'] ?? $config['rates'] ?? [])
            ->map(fn (array $rate): array => [
                'country' => strtoupper((string) ($rate['country_code'] ?? $rate['country'])),
                'rate_bps' => (int) ($rate['rate'] ?? $rate['rate_bps']),
                'name' => (string) $rate['name'],
                'shipping_taxed' => (bool) ($rate['shipping_taxed'] ?? true),
            ])
            ->values()
            ->all();

        return [
            ...$config,
            'default_rate_bps' => (int) ($config['default_tax_rate'] ?? $config['default_rate_bps'] ?? 0),
            'shipping_taxable' => (bool) ($config['shipping_taxable'] ?? true),
            'rates' => $rates,
        ];
    }
}


output:
Chunk ID: ff46ce
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1119
Output:
<?php

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

use App\Enums\TaxMode;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\TaxSettingsResource;
use App\Models\Store;
use App\Models\TaxSettings;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;

class TaxSettingsController extends Controller
{
    public function show(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        return TaxSettingsResource::make($this->settings($store));
    }

    public function update(Request $request, Store $store): TaxSettingsResource
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'mode' => ['required', Rule::in(array_map(fn (TaxMode $mode): string => $mode->value, TaxMode::cases()))],
            'provider' => ['required_if:mode,provider', Rule::in(['none', 'stripe_tax'])],
            'prices_include_tax' => ['required', 'boolean'],
            'config_json' => ['required', 'array'],
            'config_json.default_tax_rate' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.default_rate_bps' => ['nullable', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates' => ['nullable', 'array'],
            'config_json.tax_rates.*.country_code' => ['required_with:config_json.tax_rates', 'string', 'size:2'],
            'config_json.tax_rates.*.rate' => ['required_with:config_json.tax_rates', 'integer', 'min:0', 'max:10000'],
            'config_json.tax_rates.*.name' => ['required_with:config_json.tax_rates', 'string', 'max:50'],
            'config_json.tax_rates.*.shipping_taxed' => ['nullable', 'boolean'],
            'config_json.rates' => ['nullable', 'array'],
            'config_json.rates.*.country' => ['required_with:config_json.rates', 'string', 'size:2'],
            'config_json.rates.*.rate_bps' => ['required_with:config_json.rates', 'integer', 'min:0', 'max:10000'],
            'config_json.rates.*.name' => ['required_with:config_json.rates', 'string', 'max:50'],
        ]);

        $settings = TaxSettings::withoutGlobalScopes()->updateOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::from($validated['mode']),
                'provider' => $validated['mode'] === TaxMode::Provider->value ? $validated['provider'] : 'none',
                'prices_include_tax' => (bool) $validated['prices_include_tax'],
                'config_json' => $this->normalizeConfig($request->input('config_json', [])),
            ],
        );

        return TaxSettingsResource::make($settings->refresh());
    }

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

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

    private function settings(Store $store): TaxSettings
    {
        return TaxSettings::withoutGlobalScopes()->firstOrCreate(
            ['store_id' => $store->getKey()],
            [
                'mode' => TaxMode::Manual,
                'provider' => 'none',
                'prices_include_tax' => false,
                'config_json' => [
                    'name' => 'Tax',
                    'default_rate_bps' => 0,
                    'shipping_taxable' => true,
                    'rates' => [],
                ],
            ],
        );
    }

    /**
     * @param  array<string, mixed>  $config
     * @return array<string, mixed>
     */
    private function normalizeConfig(array $config): array
    {
        $rates = collect($config['tax_rates'] ?? $config['rates'] ?? [])
            ->map(fn (array $rate): array => [
                'country' => strtoupper((string) ($rate['country_code'] ?? $rate['country'])),
                'rate_bps' => (int) ($rate['rate'] ?? $rate['rate_bps']),
                'name' => (string) $rate['name'],
                'shipping_taxed' => (bool) ($rate['shipping_taxed'] ?? true),
            ])
            ->values()
            ->all();

        return [
            ...$config,
            'default_rate_bps' => (int) ($config['default_tax_rate'] ?? $config['default_rate_bps'] ?? 0),
            'shipping_taxable' => (bool) ($config['shipping_taxable'] ?? true),
            'rates' => $rates,
        ];
    }
}


command: sed -n '1,220p' app/Http/Middleware/EnsureAdminApiScope.php

aggregated_output: sed: app/Http/Middleware/EnsureAdminApiScope.php: No such file or directory


command: sed -n '1,220p' app/Http/Controllers/Api/Admin/V1/PageController.php

aggregated_output:
<?php

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

use App\Enums\PageStatus;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\PageResource;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Store;
use App\Services\NavigationService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator as ValidationValidator;

class PageController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in($this->pageStatusValues())],
            'query' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $pages = Page::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('title', 'like', '%'.$search.'%')
                        ->orWhere('handle', 'like', '%'.$search.'%');
                });
            })
            ->latest('updated_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return PageResource::collection($pages);
    }

    public function store(Request $request, Store $store, NavigationService $navigation): JsonResponse
    {
        $this->authorizeStore($request, $store);

        $validated = $this->validatePayload($request, $store);

        $page = Page::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            ...$this->attributesForCreate($validated),
        ]);

        $this->forgetNavigation($store, $navigation);

        return PageResource::make($page)
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Store $store, Page $page, NavigationService $navigation): PageResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessPageBelongsToStore($page, $store);

        $validated = $this->validatePayload($request, $store, $page);
        $attributes = $this->attributesForUpdate($validated, $page);

        if ($attributes !== []) {
            $page->update($attributes);
            $this->forgetNavigation($store, $navigation);
        }

        return PageResource::make($page->refresh());
    }

    public function destroy(Request $request, Store $store, Page $page, NavigationService $navigation): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessPageBelongsToStore($page, $store);

        $page->delete();
        $this->forgetNavigation($store, $navigation);

        return response()->json(['message' => 'Page deleted']);
    }

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

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

    private function abortUnlessPageBelongsToStore(Page $page, Store $store): void
    {
        abort_unless((int) $page->store_id === $store->getKey(), 404);
    }

    /**
     * @return array<string, mixed>
     */
    private function validatePayload(Request $request, Store $store, ?Page $page = null): array
    {
        $creating = $page === null;

        $validator = Validator::make($request->all(), [
            'title' => [$creating ? 'required' : 'sometimes', 'string', 'max:255'],
            'handle' => ['sometimes', 'nullable', 'string', 'max:255'],
            'body_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
            'status' => ['sometimes', Rule::in($this->pageStatusValues())],
            'published_at' => ['sometimes', 'nullable', 'date'],
        ]);

        $validator->after(function (ValidationValidator $validator) use ($request, $store, $page, $creating): void {
            if ($validator->errors()->isNotEmpty()) {
                return;
            }

            $handle = $this->handleForRequest($request, $page, $creating);

            if ($handle === '') {
                $validator->errors()->add('handle', __('The handle must contain at least one letter or number.'));

                return;
            }

            $exists = Page::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('handle', $handle)
                ->when($page instanceof Page, fn (Builder $query) => $query->whereKeyNot($page->getKey()))
                ->exists();

            if ($exists) {
                $validator->errors()->add('handle', __('The handle has already been taken.'));
            }
        });

        return $validator->validate();
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForCreate(array $validated): array
    {
        $status = PageStatus::from(data_get($validated, 'status', PageStatus::Draft->value));

        return [
            'title' => $validated['title'],
            'handle' => $this->normalizeHandle($validated['handle'] ?? $validated['title']),
            'body_html' => $validated['body_html'] ?? null,
            'status' => $status,
            'published_at' => $this->publishedAtForCreate($validated, $status),
        ];
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForUpdate(array $validated, Page $page): array
    {
        $attributes = Arr::only($validated, ['title', 'body_html']);

        if (array_key_exists('handle', $validated) && filled($validated['handle'])) {
            $attributes['handle'] = $this->normalizeHandle($validated['handle']);
        }

        if (array_key_exists('status', $validated)) {
            $attributes['status'] = PageStatus::from($validated['status']);
        }

        if (array_key_exists('published_at', $validated)) {
            $attributes['published_at'] = $validated['published_at'];
        } elseif (($attributes['status'] ?? $page->status) === PageStatus::Published && $page->published_at === null) {
            $attributes['published_at'] = now();
        }

        return $attributes;
    }

    /**
     * @param  array<string, mixed>  $validated
     */
    private function publishedAtForCreate(array $validated, PageStatus $status): mixed
    {
        if (array_key_exists('published_at', $validated)) {
            return $validated['published_at'];
        }

        return $status === PageStatus::Published ? now() : null;
    }

    private function handleForRequest(Request $request, ?Page $page, bool $creating): string
    {
        if ($request->exists('handle') && filled($request->input('handle'))) {
            return $this->normalizeHandle($request->input('handle'));
        }

        if ($creating) {
            return $this->normalizeHandle($request->input('title'));
        }

        return (string) $page?->handle;
    }

    private function normalizeHandle(mixed $value): string
    {


output:
Chunk ID: 6858b1
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1948
Output:
<?php

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

use App\Enums\PageStatus;
use App\Http\Controllers\Controller;
use App\Http\Resources\Admin\V1\PageResource;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Store;
use App\Services\NavigationService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator as ValidationValidator;

class PageController extends Controller
{
    public function index(Request $request, Store $store): AnonymousResourceCollection
    {
        $this->authorizeStore($request, $store);

        $validated = $request->validate([
            'status' => ['nullable', Rule::in($this->pageStatusValues())],
            'query' => ['nullable', 'string', 'max:255'],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $pages = Page::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $query->where(function (Builder $query) use ($search): void {
                    $query
                        ->where('title', 'like', '%'.$search.'%')
                        ->orWhere('handle', 'like', '%'.$search.'%');
                });
            })
            ->latest('updated_at')
            ->latest('id')
            ->paginate((int) data_get($validated, 'per_page', 25));

        return PageResource::collection($pages);
    }

    public function store(Request $request, Store $store, NavigationService $navigation): JsonResponse
    {
        $this->authorizeStore($request, $store);

        $validated = $this->validatePayload($request, $store);

        $page = Page::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            ...$this->attributesForCreate($validated),
        ]);

        $this->forgetNavigation($store, $navigation);

        return PageResource::make($page)
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Store $store, Page $page, NavigationService $navigation): PageResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessPageBelongsToStore($page, $store);

        $validated = $this->validatePayload($request, $store, $page);
        $attributes = $this->attributesForUpdate($validated, $page);

        if ($attributes !== []) {
            $page->update($attributes);
            $this->forgetNavigation($store, $navigation);
        }

        return PageResource::make($page->refresh());
    }

    public function destroy(Request $request, Store $store, Page $page, NavigationService $navigation): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessPageBelongsToStore($page, $store);

        $page->delete();
        $this->forgetNavigation($store, $navigation);

        return response()->json(['message' => 'Page deleted']);
    }

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

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

    private function abortUnlessPageBelongsToStore(Page $page, Store $store): void
    {
        abort_unless((int) $page->store_id === $store->getKey(), 404);
    }

    /**
     * @return array<string, mixed>
     */
    private function validatePayload(Request $request, Store $store, ?Page $page = null): array
    {
        $creating = $page === null;

        $validator = Validator::make($request->all(), [
            'title' => [$creating ? 'required' : 'sometimes', 'string', 'max:255'],
            'handle' => ['sometimes', 'nullable', 'string', 'max:255'],
            'body_html' => ['sometimes', 'nullable', 'string', 'max:65535'],
            'status' => ['sometimes', Rule::in($this->pageStatusValues())],
            'published_at' => ['sometimes', 'nullable', 'date'],
        ]);

        $validator->after(function (ValidationValidator $validator) use ($request, $store, $page, $creating): void {
            if ($validator->errors()->isNotEmpty()) {
                return;
            }

            $handle = $this->handleForRequest($request, $page, $creating);

            if ($handle === '') {
                $validator->errors()->add('handle', __('The handle must contain at least one letter or number.'));

                return;
            }

            $exists = Page::withoutGlobalScopes()
                ->where('store_id', $store->getKey())
                ->where('handle', $handle)
                ->when($page instanceof Page, fn (Builder $query) => $query->whereKeyNot($page->getKey()))
                ->exists();

            if ($exists) {
                $validator->errors()->add('handle', __('The handle has already been taken.'));
            }
        });

        return $validator->validate();
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForCreate(array $validated): array
    {
        $status = PageStatus::from(data_get($validated, 'status', PageStatus::Draft->value));

        return [
            'title' => $validated['title'],
            'handle' => $this->normalizeHandle($validated['handle'] ?? $validated['title']),
            'body_html' => $validated['body_html'] ?? null,
            'status' => $status,
            'published_at' => $this->publishedAtForCreate($validated, $status),
        ];
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForUpdate(array $validated, Page $page): array
    {
        $attributes = Arr::only($validated, ['title', 'body_html']);

        if (array_key_exists('handle', $validated) && filled($validated['handle'])) {
            $attributes['handle'] = $this->normalizeHandle($validated['handle']);
        }

        if (array_key_exists('status', $validated)) {
            $attributes['status'] = PageStatus::from($validated['status']);
        }

        if (array_key_exists('published_at', $validated)) {
            $attributes['published_at'] = $validated['published_at'];
        } elseif (($attributes['status'] ?? $page->status) === PageStatus::Published && $page->published_at === null) {
            $attributes['published_at'] = now();
        }

        return $attributes;
    }

    /**
     * @param  array<string, mixed>  $validated
     */
    private function publishedAtForCreate(array $validated, PageStatus $status): mixed
    {
        if (array_key_exists('published_at', $validated)) {
            return $validated['published_at'];
        }

        return $status === PageStatus::Published ? now() : null;
    }

    private function handleForRequest(Request $request, ?Page $page, bool $creating): string
    {
        if ($request->exists('handle') && filled($request->input('handle'))) {
            return $this->normalizeHandle($request->input('handle'));
        }

        if ($creating) {
            return $this->normalizeHandle($request->input('title'));
        }

        return (string) $page?->handle;
    }

    private function normalizeHandle(mixed $value): string
    {


output:
Chunk ID: f831b7
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 19
Output:
sed: app/Http/Middleware/EnsureAdminApiScope.php: No such file or directory


cmd: rg --files tests | rg 'Api|Tax|Page|Shipping|Admin'


cmd: rg -n "class .*AdminApi|admin.api|read-products|write-themes|read-settings" app bootstrap routes tests


cmd: find app/Http/Middleware -maxdepth 1 -type f -print | sort


cmd: sed -n '220,420p' app/Http/Controllers/Api/Admin/V1/PageController.php


cmd: sed -n '1,220p' app/Http/Resources/Admin/V1/TaxSettingsResource.php


cmd: sed -n '1,220p' app/Http/Resources/Admin/V1/PageResource.php


command: find app/Http/Middleware -maxdepth 1 -type f -print | sort

path: Middleware


command: rg --files tests | rg 'Api|Tax|Page|Shipping|Admin'

cmd: rg --files tests

path: tests

type: search

cmd: rg 'Api|Tax|Page|Shipping|Admin'

query: Api|Tax|Page|Shipping|Admin


output:
Chunk ID: 80d82b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 272
Output:
tests/Feature/Api/StorefrontCheckoutApiTest.php
tests/Feature/Api/StorefrontSearchApiTest.php
tests/Feature/Api/AdminSearchIndexApiTest.php
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php
tests/Feature/Api/AdminCatalogApiTest.php
tests/Feature/Api/StorefrontAnalyticsApiTest.php
tests/Feature/Api/AdminShippingSettingsApiTest.php
tests/Feature/Api/AdminCollectionApiTest.php
tests/Feature/Api/AdminDiscountApiTest.php
tests/Feature/Api/StorefrontOrderApiTest.php
tests/Feature/Api/AdminOrderApiTest.php
tests/Feature/Api/AdminPageApiTest.php
tests/Feature/Api/AdminTaxSettingsApiTest.php
tests/Feature/Api/StorefrontCartApiTest.php
tests/Feature/Admin/CustomerManagementTest.php
tests/Feature/Admin/OrderManagementTest.php
tests/Feature/Admin/ContentManagementTest.php
tests/Feature/Admin/DiscountManagementTest.php
tests/Feature/Admin/ProductMediaManagementTest.php
tests/Feature/Admin/DashboardTest.php
tests/Feature/Admin/SettingsManagementTest.php
tests/Feature/Admin/SearchSettingsTest.php
tests/Feature/Admin/AppsDevelopersTest.php
tests/Feature/Admin/AnalyticsDashboardTest.php


command: rg -n "class .*AdminApi|admin.api|read-products|write-themes|read-settings" app bootstrap routes tests

type: search

cmd: rg -n 'class .*AdminApi|admin.api|read-products|write-themes|read-settings' app bootstrap routes tests

query: class .*AdminApi|admin.api|read-products|write-themes|read-settings

path: app

aggregated_output:
routes/api.php:63:        Route::middleware('admin.api:read-products')->group(function (): void {
routes/api.php:68:        Route::middleware('admin.api:read-customers')->group(function (): void {
routes/api.php:73:        Route::middleware('admin.api:read-collections')->group(function (): void {
routes/api.php:77:        Route::middleware('admin.api:write-collections')->group(function (): void {
routes/api.php:83:        Route::middleware('admin.api:read-discounts')->group(function (): void {
routes/api.php:87:        Route::middleware('admin.api:write-discounts')->group(function (): void {
routes/api.php:93:        Route::middleware('admin.api:read-content')->group(function (): void {
routes/api.php:97:        Route::middleware('admin.api:write-content')->group(function (): void {
routes/api.php:103:        Route::middleware('admin.api:read-settings')->group(function (): void {
routes/api.php:109:        Route::middleware('admin.api:write-settings')->group(function (): void {
routes/api.php:117:        Route::middleware('admin.api:read-analytics')->group(function (): void {
routes/api.php:121:        Route::middleware('admin.api:read-orders')->group(function (): void {
routes/api.php:126:        Route::middleware('admin.api:write-orders')->group(function (): void {
bootstrap/app.php:19:            'admin.api' => AuthenticateAdminApi::class,
tests/Feature/Api/AdminSearchIndexApiTest.php:78:    $readToken = adminSearchIndexApiToken($store, ['read-settings']);
tests/Feature/Api/AdminSearchIndexApiTest.php:80:    $otherStoreToken = adminSearchIndexApiToken($otherStore, ['read-settings']);
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:119:    $wrongAbilityToken = adminAnalyticsSummaryApiToken($store, ['read-settings']);
app/Http/Controllers/Api/Admin/V1/PageController.php:98:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php:60:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php:59:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/CustomerController.php:56:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php:45:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ProductController.php:65:        if (! $request->attributes->has('admin_api_oauth_token')) {
tests/Feature/Api/AdminCatalogApiTest.php:47:            'handle' => 'admin-api-jacket',
tests/Feature/Api/AdminCatalogApiTest.php:125:    $productToken = adminCatalogApiToken($store, ['read-products']);
tests/Feature/Api/AdminCatalogApiTest.php:165:        ->getJson('/oauth/authorize?client_id=test&redirect_uri=https://example.test/callback&response_type=code&scope=read-products&state=state')
app/Http/Controllers/Api/Admin/V1/DiscountController.php:77:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php:39:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ShippingRateController.php:54:        if (! $request->attributes->has('admin_api_oauth_token')) {
tests/Feature/Api/AdminShippingSettingsApiTest.php:97:    $readToken = adminShippingSettingsApiToken($store, ['read-settings']);
tests/Feature/Api/AdminShippingSettingsApiTest.php:99:    $otherStoreToken = adminShippingSettingsApiToken($otherStore, ['read-settings']);
tests/Feature/Api/AdminTaxSettingsApiTest.php:82:    $readToken = adminTaxSettingsApiToken($store, ['read-settings']);
tests/Feature/Api/AdminTaxSettingsApiTest.php:84:    $otherStoreToken = adminTaxSettingsApiToken($otherStore, ['read-settings']);
app/Http/Middleware/AuthenticateAdminApi.php:12:class AuthenticateAdminApi
app/Http/Middleware/AuthenticateAdminApi.php:23:        app()->forgetInstance('admin_api_oauth_token');
app/Http/Middleware/AuthenticateAdminApi.php:63:        $request->attributes->set('admin_api_oauth_token', $token);
app/Http/Middleware/AuthenticateAdminApi.php:64:        app()->instance('admin_api_oauth_token', $token);
app/Livewire/Admin/Developers/Index.php:38:        'read-products',
app/Http/Controllers/Api/Admin/V1/CollectionController.php:105:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/SearchIndexController.php:68:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:66:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderController.php:62:        if (! $request->attributes->has('admin_api_oauth_token')) {


output:
Chunk ID: 022d48
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1207
Output:
routes/api.php:63:        Route::middleware('admin.api:read-products')->group(function (): void {
routes/api.php:68:        Route::middleware('admin.api:read-customers')->group(function (): void {
routes/api.php:73:        Route::middleware('admin.api:read-collections')->group(function (): void {
routes/api.php:77:        Route::middleware('admin.api:write-collections')->group(function (): void {
routes/api.php:83:        Route::middleware('admin.api:read-discounts')->group(function (): void {
routes/api.php:87:        Route::middleware('admin.api:write-discounts')->group(function (): void {
routes/api.php:93:        Route::middleware('admin.api:read-content')->group(function (): void {
routes/api.php:97:        Route::middleware('admin.api:write-content')->group(function (): void {
routes/api.php:103:        Route::middleware('admin.api:read-settings')->group(function (): void {
routes/api.php:109:        Route::middleware('admin.api:write-settings')->group(function (): void {
routes/api.php:117:        Route::middleware('admin.api:read-analytics')->group(function (): void {
routes/api.php:121:        Route::middleware('admin.api:read-orders')->group(function (): void {
routes/api.php:126:        Route::middleware('admin.api:write-orders')->group(function (): void {
bootstrap/app.php:19:            'admin.api' => AuthenticateAdminApi::class,
tests/Feature/Api/AdminSearchIndexApiTest.php:78:    $readToken = adminSearchIndexApiToken($store, ['read-settings']);
tests/Feature/Api/AdminSearchIndexApiTest.php:80:    $otherStoreToken = adminSearchIndexApiToken($otherStore, ['read-settings']);
tests/Feature/Api/AdminAnalyticsSummaryApiTest.php:119:    $wrongAbilityToken = adminAnalyticsSummaryApiToken($store, ['read-settings']);
app/Http/Controllers/Api/Admin/V1/PageController.php:98:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ShippingZoneController.php:60:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/TaxSettingsController.php:59:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/CustomerController.php:56:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderRefundController.php:45:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ProductController.php:65:        if (! $request->attributes->has('admin_api_oauth_token')) {
tests/Feature/Api/AdminCatalogApiTest.php:47:            'handle' => 'admin-api-jacket',
tests/Feature/Api/AdminCatalogApiTest.php:125:    $productToken = adminCatalogApiToken($store, ['read-products']);
tests/Feature/Api/AdminCatalogApiTest.php:165:        ->getJson('/oauth/authorize?client_id=test&redirect_uri=https://example.test/callback&response_type=code&scope=read-products&state=state')
app/Http/Controllers/Api/Admin/V1/DiscountController.php:77:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderFulfillmentController.php:39:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/ShippingRateController.php:54:        if (! $request->attributes->has('admin_api_oauth_token')) {
tests/Feature/Api/AdminShippingSettingsApiTest.php:97:    $readToken = adminShippingSettingsApiToken($store, ['read-settings']);
tests/Feature/Api/AdminShippingSettingsApiTest.php:99:    $otherStoreToken = adminShippingSettingsApiToken($otherStore, ['read-settings']);
tests/Feature/Api/AdminTaxSettingsApiTest.php:82:    $readToken = adminTaxSettingsApiToken($store, ['read-settings']);
tests/Feature/Api/AdminTaxSettingsApiTest.php:84:    $otherStoreToken = adminTaxSettingsApiToken($otherStore, ['read-settings']);
app/Http/Middleware/AuthenticateAdminApi.php:12:class AuthenticateAdminApi
app/Http/Middleware/AuthenticateAdminApi.php:23:        app()->forgetInstance('admin_api_oauth_token');
app/Http/Middleware/AuthenticateAdminApi.php:63:        $request->attributes->set('admin_api_oauth_token', $token);
app/Http/Middleware/AuthenticateAdminApi.php:64:        app()->instance('admin_api_oauth_token', $token);
app/Livewire/Admin/Developers/Index.php:38:        'read-products',
app/Http/Controllers/Api/Admin/V1/CollectionController.php:105:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/SearchIndexController.php:68:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/AnalyticsSummaryController.php:66:        if (! $request->attributes->has('admin_api_oauth_token')) {
app/Http/Controllers/Api/Admin/V1/OrderController.php:62:        if (! $request->attributes->has('admin_api_oauth_token')) {


output:
Chunk ID: 87b3aa
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 43
Output:
app/Http/Middleware/AuthenticateAdminApi.php
app/Http/Middleware/CheckStoreRole.php
app/Http/Middleware/EnsureUserEmailIsVerified.php
app/Http/Middleware/ResolveStore.php


command: sed -n '220,420p' app/Http/Controllers/Api/Admin/V1/PageController.php

aggregated_output:
    {
        return Str::slug((string) $value);
    }

    /**
     * @return list<string>
     */
    private function pageStatusValues(): array
    {
        return array_map(fn (PageStatus $status): string => $status->value, PageStatus::cases());
    }

    private function forgetNavigation(Store $store, NavigationService $navigation): void
    {
        NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->each(fn (NavigationMenu $menu): mixed => $navigation->forget($menu));
    }
}


output:
Chunk ID: ff1628
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 141
Output:
    {
        return Str::slug((string) $value);
    }

    /**
     * @return list<string>
     */
    private function pageStatusValues(): array
    {
        return array_map(fn (PageStatus $status): string => $status->value, PageStatus::cases());
    }

    private function forgetNavigation(Store $store, NavigationService $navigation): void
    {
        NavigationMenu::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->get()
            ->each(fn (NavigationMenu $menu): mixed => $navigation->forget($menu));
    }
}


command: sed -n '1,220p' app/Http/Resources/Admin/V1/TaxSettingsResource.php

aggregated_output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class TaxSettingsResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'store_id' => $this->store_id,
            'mode' => $this->mode?->value,
            'provider' => $this->provider,
            'prices_include_tax' => $this->prices_include_tax,
            'config_json' => $this->publicConfig(),
            'updated_at' => null,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function publicConfig(): array
    {
        $config = $this->config_json ?? [];

        if ($this->mode?->value === 'provider') {
            return Arr::except($config, ['provider_api_key']);
        }

        return [
            ...Arr::except($config, ['default_rate_bps', 'rates', 'provider_api_key']),
            'default_tax_rate' => (int) data_get($config, 'default_tax_rate', data_get($config, 'default_rate_bps', 0)),
            'tax_rates' => collect(data_get($config, 'tax_rates', data_get($config, 'rates', [])))
                ->map(fn (array $rate): array => [
                    'country_code' => strtoupper((string) data_get($rate, 'country_code', data_get($rate, 'country'))),
                    'rate' => (int) data_get($rate, 'rate', data_get($rate, 'rate_bps', 0)),
                    'name' => (string) data_get($rate, 'name', 'Tax'),
                    'shipping_taxed' => (bool) data_get($rate, 'shipping_taxed', data_get($config, 'shipping_taxable', true)),
                ])
                ->values()
                ->all(),
        ];
    }
}


output:
Chunk ID: 0da6f2
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 457
Output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class TaxSettingsResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'store_id' => $this->store_id,
            'mode' => $this->mode?->value,
            'provider' => $this->provider,
            'prices_include_tax' => $this->prices_include_tax,
            'config_json' => $this->publicConfig(),
            'updated_at' => null,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function publicConfig(): array
    {
        $config = $this->config_json ?? [];

        if ($this->mode?->value === 'provider') {
            return Arr::except($config, ['provider_api_key']);
        }

        return [
            ...Arr::except($config, ['default_rate_bps', 'rates', 'provider_api_key']),
            'default_tax_rate' => (int) data_get($config, 'default_tax_rate', data_get($config, 'default_rate_bps', 0)),
            'tax_rates' => collect(data_get($config, 'tax_rates', data_get($config, 'rates', [])))
                ->map(fn (array $rate): array => [
                    'country_code' => strtoupper((string) data_get($rate, 'country_code', data_get($rate, 'country'))),
                    'rate' => (int) data_get($rate, 'rate', data_get($rate, 'rate_bps', 0)),
                    'name' => (string) data_get($rate, 'name', 'Tax'),
                    'shipping_taxed' => (bool) data_get($rate, 'shipping_taxed', data_get($config, 'shipping_taxable', true)),
                ])
                ->values()
                ->all(),
        ];
    }
}


command: sed -n '1,220p' app/Http/Resources/Admin/V1/PageResource.php

aggregated_output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class PageResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'title' => $this->title,
            'handle' => $this->handle,
            'body_html' => $this->body_html,
            'status' => $this->status?->value,
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


output:
Chunk ID: 3cafc4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 204
Output:
<?php

namespace App\Http\Resources\Admin\V1;

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

class PageResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'store_id' => $this->store_id,
            'title' => $this->title,
            'handle' => $this->handle,
            'body_html' => $this->body_html,
            'status' => $this->status?->value,
            'published_at' => $this->published_at?->toIso8601String(),
            'created_at' => $this->created_at?->toIso8601String(),
            'updated_at' => $this->updated_at?->toIso8601String(),
        ];
    }
}


cmd: sed -n '1,260p' tests/Feature/Api/AdminShippingSettingsApiTest.php


cmd: sed -n '1,240p' tests/Feature/Api/AdminTaxSettingsApiTest.php


cmd: sed -n '1,260p' tests/Feature/Api/AdminPageApiTest.php


cmd: sed -n '1,260p' app/Http/Middleware/AuthenticateAdminApi.php


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


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


command: sed -n '1,260p' tests/Feature/Api/AdminShippingSettingsApiTest.php

aggregated_output:
<?php

use App\Enums\ShippingRateType;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminShippingSettingsApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Shipping settings integration', $abilities);
}

test('admin shipping settings api lists creates updates zones and adds rates', function (): void {
    $store = adminShippingSettingsApiStore();
    $user = adminShippingSettingsApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertOk()
        ->assertJsonPath('data.0.name', 'DACH')
        ->assertJsonPath('data.0.rates.0.config_json.currency', $store->default_currency);

    $createResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Nordics API',
            'countries_json' => ['se', 'no'],
            'regions_json' => ['stockholm'],
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Nordics API')
        ->assertJsonPath('data.countries_json.0', 'SE')
        ->assertJsonPath('data.regions_json.0', 'STOCKHOLM');

    $zone = ShippingZone::withoutGlobalScopes()->findOrFail($createResponse->json('data.id'));

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}", [
            'name' => 'Nordics and Baltics API',
            'countries_json' => ['SE', 'DK'],
            'regions_json' => [],
        ])
        ->assertOk()
        ->assertJsonPath('data.name', 'Nordics and Baltics API')
        ->assertJsonPath('data.countries_json.1', 'DK');

    $rateResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'API Express',
            'type' => 'flat',
            'config_json' => [
                'price_amount' => 1200,
                'currency' => $store->default_currency,
            ],
            'is_active' => true,
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'API Express')
        ->assertJsonPath('data.type', 'flat')
        ->assertJsonPath('data.config_json.price_amount', 1200);

    $rate = ShippingRate::withoutGlobalScopes()->findOrFail($rateResponse->json('data.id'));

    expect($zone->refresh()->countries_json)->toBe(['SE', 'DK'])
        ->and($rate->type)->toBe(ShippingRateType::Flat)
        ->and($rate->config_json['amount'])->toBe(1200);
});

test('admin shipping settings api enforces token abilities and store scope', function (): void {
    $store = adminShippingSettingsApiStore();
    $otherStore = Store::factory()->create();
    $zone = ShippingZone::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
    $readToken = adminShippingSettingsApiToken($store, ['read-settings']);
    $writeToken = adminShippingSettingsApiToken($store, ['write-settings']);
    $otherStoreToken = adminShippingSettingsApiToken($otherStore, ['read-settings']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertOk();

    $this->withToken($readToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Read Only Zone',
            'countries_json' => ['FI'],
        ])
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'Token Rate',
            'type' => 'carrier',
            'config_json' => [
                'price_amount' => 999,
            ],
        ])
        ->assertCreated();
});

test('admin shipping settings api validates countries overlaps and rate types', function (): void {
    $store = adminShippingSettingsApiStore();
    $zone = ShippingZone::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    $this->actingAs(adminShippingSettingsApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Overlap API',
            'countries_json' => ['DE'],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['countries_json']);

    $this->actingAs(adminShippingSettingsApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'Invalid Rate',
            'type' => 'rocket',
            'config_json' => [],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['type']);
});


command: sed -n '1,240p' tests/Feature/Api/AdminTaxSettingsApiTest.php

aggregated_output:
<?php

use App\Enums\TaxMode;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminTaxSettingsApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Tax settings integration', $abilities);
}

test('admin tax settings api shows and updates manual settings', function (): void {
    $store = adminTaxSettingsApiStore();
    $user = adminTaxSettingsApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings")
        ->assertOk()
        ->assertJsonPath('data.store_id', $store->getKey())
        ->assertJsonPath('data.mode', 'manual')
        ->assertJsonPath('data.config_json.default_tax_rate', 1900);

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings", [
            'mode' => 'manual',
            'provider' => 'none',
            'prices_include_tax' => true,
            'config_json' => [
                'default_tax_rate' => 2100,
                'tax_rates' => [
                    [
                        'country_code' => 'de',
                        'rate' => 2100,
                        'name' => 'VAT',
                        'shipping_taxed' => true,
                    ],
                ],
            ],
        ])
        ->assertOk()
        ->assertJsonPath('data.prices_include_tax', true)
        ->assertJsonPath('data.config_json.default_tax_rate', 2100)
        ->assertJsonPath('data.config_json.tax_rates.0.country_code', 'DE');

    $settings = TaxSettings::withoutGlobalScopes()->whereKey($store->getKey())->firstOrFail();

    expect($settings->mode)->toBe(TaxMode::Manual)
        ->and($settings->provider)->toBe('none')
        ->and($settings->prices_include_tax)->toBeTrue()
        ->and($settings->config_json['default_rate_bps'])->toBe(2100)
        ->and($settings->config_json['rates'][0]['country'])->toBe('DE');
});

test('admin tax settings api enforces token abilities and store scope', function (): void {
    $store = adminTaxSettingsApiStore();
    $otherStore = Store::factory()->create();
    $readToken = adminTaxSettingsApiToken($store, ['read-settings']);
    $writeToken = adminTaxSettingsApiToken($store, ['write-settings']);
    $otherStoreToken = adminTaxSettingsApiToken($otherStore, ['read-settings']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings")
        ->assertOk();

    $this->withToken($readToken['plain_text'])
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings", [
            'mode' => 'manual',
            'provider' => 'none',
            'prices_include_tax' => false,
            'config_json' => [],
        ])
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings", [
            'mode' => 'provider',
            'provider' => 'stripe_tax',
            'prices_include_tax' => false,
            'config_json' => [
                'stripe_tax_settings_id' => 'txr_api',
            ],
        ])
        ->assertOk()
        ->assertJsonPath('data.mode', 'provider')
        ->assertJsonPath('data.provider', 'stripe_tax');
});

test('admin tax settings api validates provider and rate payloads', function (): void {
    $store = adminTaxSettingsApiStore();

    $this->actingAs(adminTaxSettingsApiUser())
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings", [
            'mode' => 'provider',
            'prices_include_tax' => false,
            'config_json' => [],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['provider']);

    $this->actingAs(adminTaxSettingsApiUser())
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/tax/settings", [
            'mode' => 'manual',
            'provider' => 'none',
            'prices_include_tax' => false,
            'config_json' => [
                'tax_rates' => [
                    [
                        'country_code' => 'DEU',
                        'rate' => 12000,
                        'name' => 'VAT',
                    ],
                ],
            ],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['config_json.tax_rates.0.country_code', 'config_json.tax_rates.0.rate']);
});


command: sed -n '1,260p' tests/Feature/Api/AdminPageApiTest.php

aggregated_output:
<?php

use App\Models\Page;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminPageApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Content integration', $abilities);
}

test('admin page api lists creates updates and deletes pages', function (): void {
    $store = adminPageApiStore();
    $otherStore = Store::factory()->create();
    Page::factory()->published()->create([
        'store_id' => $store->getKey(),
        'title' => 'API Visible Page',
        'handle' => 'api-visible-page',
    ]);
    Page::factory()->published()->create([
        'store_id' => $otherStore->getKey(),
        'title' => 'Other Store Page',
        'handle' => 'other-store-page',
    ]);
    $user = adminPageApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/pages?status=published&query=API")
        ->assertOk()
        ->assertJsonFragment(['title' => 'API Visible Page'])
        ->assertJsonMissing(['title' => 'Other Store Page']);

    $createResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/pages", [
            'title' => 'Shipping Policy API',
            'body_html' => '
Shipping Policy

We ship worldwide.
',
            'status' => 'published',
        ])
        ->assertCreated()
        ->assertJsonPath('data.title', 'Shipping Policy API')
        ->assertJsonPath('data.handle', 'shipping-policy-api')
        ->assertJsonPath('data.status', 'published');

    $page = Page::withoutGlobalScopes()->findOrFail($createResponse->json('data.id'));

    expect($page->published_at)->not->toBeNull();

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/pages/{$page->getKey()}", [
            'handle' => 'API Shipping Policy Updated',
            'body_html' => '
Updated policy.
',
            'status' => 'draft',
        ])
        ->assertOk()
        ->assertJsonPath('data.handle', 'api-shipping-policy-updated')
        ->assertJsonPath('data.body_html', '
Updated policy.
')
        ->assertJsonPath('data.status', 'draft');

    $this->actingAs($user)
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/pages/{$page->getKey()}")
        ->assertOk()
        ->assertJsonPath('message', 'Page deleted');

    expect(Page::withoutGlobalScopes()->whereKey($page->getKey())->exists())->toBeFalse();
});

test('admin page api enforces token abilities and store scope', function (): void {
    $store = adminPageApiStore();
    $otherStore = Store::factory()->create();
    $page = Page::factory()->published()->create([
        'store_id' => $store->getKey(),
        'title' => 'Token Content Page',
        'handle' => 'token-content-page',
    ]);
    $readToken = adminPageApiToken($store, ['read-content']);
    $writeToken = adminPageApiToken($store, ['write-content']);
    $otherStoreToken = adminPageApiToken($otherStore, ['read-content']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/pages?query=Token")
        ->assertOk()
        ->assertJsonPath('data.0.title', 'Token Content Page');

    $this->withToken($readToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/pages/{$page->getKey()}")
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/pages")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->deleteJson("/api/admin/v1/stores/{$store->getKey()}/pages/{$page->getKey()}")
        ->assertOk();
});

test('admin page api validates normalized handles and published dates', function (): void {
    $store = adminPageApiStore();
    $existing = Page::factory()->create([
        'store_id' => $store->getKey(),
        'handle' => 'existing-api-page',
    ]);

    $this->actingAs(adminPageApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/pages", [
            'title' => 'Invalid API Page',
            'handle' => str_replace('-', ' ', $existing->handle),
            'status' => 'published',
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['handle']);

    $this->actingAs(adminPageApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/pages", [
            'title' => 'Invalid API Date',
            'status' => 'published',
            'published_at' => 'not-a-date',
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['published_at']);
});


output:
Chunk ID: 193e67
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1395
Output:
<?php

use App\Enums\ShippingRateType;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\User;
use App\Services\WebhookService;
use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

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

/**
 * @param  list<string>  $abilities
 * @return array{token: \App\Models\OauthToken, plain_text: string}
 */
function adminShippingSettingsApiToken(Store $store, array $abilities): array
{
    return app(WebhookService::class)->createApiToken($store, 'Shipping settings integration', $abilities);
}

test('admin shipping settings api lists creates updates zones and adds rates', function (): void {
    $store = adminShippingSettingsApiStore();
    $user = adminShippingSettingsApiUser();

    $this->actingAs($user)
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertOk()
        ->assertJsonPath('data.0.name', 'DACH')
        ->assertJsonPath('data.0.rates.0.config_json.currency', $store->default_currency);

    $createResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Nordics API',
            'countries_json' => ['se', 'no'],
            'regions_json' => ['stockholm'],
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'Nordics API')
        ->assertJsonPath('data.countries_json.0', 'SE')
        ->assertJsonPath('data.regions_json.0', 'STOCKHOLM');

    $zone = ShippingZone::withoutGlobalScopes()->findOrFail($createResponse->json('data.id'));

    $this->actingAs($user)
        ->putJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}", [
            'name' => 'Nordics and Baltics API',
            'countries_json' => ['SE', 'DK'],
            'regions_json' => [],
        ])
        ->assertOk()
        ->assertJsonPath('data.name', 'Nordics and Baltics API')
        ->assertJsonPath('data.countries_json.1', 'DK');

    $rateResponse = $this->actingAs($user)
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'API Express',
            'type' => 'flat',
            'config_json' => [
                'price_amount' => 1200,
                'currency' => $store->default_currency,
            ],
            'is_active' => true,
        ])
        ->assertCreated()
        ->assertJsonPath('data.name', 'API Express')
        ->assertJsonPath('data.type', 'flat')
        ->assertJsonPath('data.config_json.price_amount', 1200);

    $rate = ShippingRate::withoutGlobalScopes()->findOrFail($rateResponse->json('data.id'));

    expect($zone->refresh()->countries_json)->toBe(['SE', 'DK'])
        ->and($rate->type)->toBe(ShippingRateType::Flat)
        ->and($rate->config_json['amount'])->toBe(1200);
});

test('admin shipping settings api enforces token abilities and store scope', function (): void {
    $store = adminShippingSettingsApiStore();
    $otherStore = Store::factory()->create();
    $zone = ShippingZone::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();
    $readToken = adminShippingSettingsApiToken($store, ['read-settings']);
    $writeToken = adminShippingSettingsApiToken($store, ['write-settings']);
    $otherStoreToken = adminShippingSettingsApiToken($otherStore, ['read-settings']);

    $this->withToken($readToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertOk();

    $this->withToken($readToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Read Only Zone',
            'countries_json' => ['FI'],
        ])
        ->assertForbidden();

    $this->withToken($otherStoreToken['plain_text'])
        ->getJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones")
        ->assertForbidden();

    $this->withToken($writeToken['plain_text'])
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'Token Rate',
            'type' => 'carrier',
            'config_json' => [
                'price_amount' => 999,
            ],
        ])
        ->assertCreated();
});

test('admin shipping settings api validates countries overlaps and rate types', function (): void {
    $store = adminShippingSettingsApiStore();
    $zone = ShippingZone::withoutGlobalScopes()->where('store_id', $store->getKey())->firstOrFail();

    $this->actingAs(adminShippingSettingsApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones", [
            'name' => 'Overlap API',
            'countries_json' => ['DE'],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['countries_json']);

    $this->actingAs(adminShippingSettingsApiUser())
        ->postJson("/api/admin/v1/stores/{$store->getKey()}/shipping/zones/{$zone->getKey()}/rates", [
            'name' => 'Invalid Rate',
            'type' => 'rocket',
            'config_json' => [],
        ])
        ->assertUnprocessable()
        ->assertJsonValidationErrors(['type']);
});