output:
Chunk ID: 134539
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1299
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']);
});


output:
Chunk ID: 65c363
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 1307
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']);
});


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

AuthenticateAdminApi.php

type: read

aggregated_output:
<?php

namespace App\Http\Middleware;

use App\Enums\AppInstallationStatus;
use App\Models\OauthToken;
use App\Models\Store;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AuthenticateAdminApi
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$abilities): Response
    {
        $store = $this->storeFromRoute($request);

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

        if ($request->user() !== null) {
            if ($request->user()->stores()->whereKey($store->getKey())->exists()) {
                return $next($request);
            }

            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $plainTextToken = $request->bearerToken();

        if (! is_string($plainTextToken) || $plainTextToken === '') {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $token = OauthToken::query()
            ->with('installation')
            ->where('access_token_hash', hash('sha256', $plainTextToken))
            ->first();

        if (! $token instanceof OauthToken || $token->isExpired()) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $installation = $token->installation;

        if ($installation === null
            || $installation->status !== AppInstallationStatus::Active
            || (int) $installation->store_id !== $store->getKey()) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        if (! $this->hasAbilities($token, $abilities)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $token->forceFill(['last_used_at' => now()])->save();

        $request->attributes->set('admin_api_oauth_token', $token);
        app()->instance('admin_api_oauth_token', $token);

        return $next($request);
    }

    private function storeFromRoute(Request $request): Store
    {
        $store = $request->route('store');

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

        return Store::query()->findOrFail($store);
    }

    /**
     * @param  list<string>  $abilities
     */
    private function hasAbilities(OauthToken $token, array $abilities): bool
    {
        if ($abilities === []) {
            return true;
        }

        $tokenAbilities = $token->abilities_json ?? [];

        if (in_array('*', $tokenAbilities, true)) {
            return true;
        }

        foreach ($abilities as $ability) {
            if (! in_array($ability, $tokenAbilities, true)) {
                return false;
            }
        }

        return true;
    }
}

status: completed


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

namespace App\Http\Middleware;

use App\Enums\AppInstallationStatus;
use App\Models\OauthToken;
use App\Models\Store;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class AuthenticateAdminApi
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$abilities): Response
    {
        $store = $this->storeFromRoute($request);

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

        if ($request->user() !== null) {
            if ($request->user()->stores()->whereKey($store->getKey())->exists()) {
                return $next($request);
            }

            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $plainTextToken = $request->bearerToken();

        if (! is_string($plainTextToken) || $plainTextToken === '') {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $token = OauthToken::query()
            ->with('installation')
            ->where('access_token_hash', hash('sha256', $plainTextToken))
            ->first();

        if (! $token instanceof OauthToken || $token->isExpired()) {
            return response()->json(['message' => 'Unauthenticated.'], 401);
        }

        $installation = $token->installation;

        if ($installation === null
            || $installation->status !== AppInstallationStatus::Active
            || (int) $installation->store_id !== $store->getKey()) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        if (! $this->hasAbilities($token, $abilities)) {
            return response()->json(['message' => 'Forbidden.'], 403);
        }

        $token->forceFill(['last_used_at' => now()])->save();

        $request->attributes->set('admin_api_oauth_token', $token);
        app()->instance('admin_api_oauth_token', $token);

        return $next($request);
    }

    private function storeFromRoute(Request $request): Store
    {
        $store = $request->route('store');

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

        return Store::query()->findOrFail($store);
    }

    /**
     * @param  list<string>  $abilities
     */
    private function hasAbilities(OauthToken $token, array $abilities): bool
    {
        if ($abilities === []) {
            return true;
        }

        $tokenAbilities = $token->abilities_json ?? [];

        if (in_array('*', $tokenAbilities, true)) {
            return true;
        }

        foreach ($abilities as $ability) {
            if (! in_array($ability, $tokenAbilities, true)) {
                return false;
            }
        }

        return true;
    }
}


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

ShippingRateController.php

aggregated_output:
<?php

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

type: unknown

aggregated_output:
<?php

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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


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


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


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


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

aggregated_output:
<?php

namespace Database\Factories;

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

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

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


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

aggregated_output:
<?php

namespace App\Models;

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

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

    public $timestamps = false;

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

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

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

            $store = app('current_store');

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

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

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

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


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

namespace App\Models;

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

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

    public $timestamps = false;

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

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

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

            $store = app('current_store');

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

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

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

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


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

namespace Database\Factories;

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

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

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


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

aggregated_output:
<?php

namespace Database\Seeders;

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

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

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

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

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


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

path: seeders

type: search

cmd: rg 'Theme|Content|Storefront'

query: Theme|Content|Storefront


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

type: search

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

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

path: database

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


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


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

namespace Database\Seeders;

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

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

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

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

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


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


command:
/bin/zsh
-lc
git status --short

cmd: git status --short


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


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

cmd: php -m | rg '^zip$|^Phar$|^libxml$'

aggregated_output:
libxml
Phar
zip


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


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


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


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


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


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

aggregated_output:
<?php

namespace Database\Seeders;

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

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


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

type: search

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

query: manifest|theme archive|valid theme|Theme

path: specs

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


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

Pest.php

cmd: sed -n '1,220p' tests/Pest.php

aggregated_output:
<?php

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

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

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

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

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

function something()
{
    // ..
}


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

Theme.php

cmd: sed -n '1,220p' app/Models/Theme.php

aggregated_output:
<?php

namespace App\Models;

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

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

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

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

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

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

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

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

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


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

aggregated_output:
<?php

namespace App\Services;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

namespace Database\Seeders;

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

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


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

namespace App\Models;

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

function something()
{
    // ..
}


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

type: search

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

query: createApiToken|WebhookService|OauthToken

path: Api

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


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


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

namespace App\Services;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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