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

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

use App\Actions\SanitizeHtml;
use App\Enums\MediaStatus;
use App\Enums\MediaType;
use App\Enums\ProductStatus;
use App\Enums\VariantStatus;
use App\Exceptions\InvalidProductTransitionException;
use App\Http\Controllers\Controller;
use App\Http\Requests\Api\Admin\V1\CreateProductMediaUploadRequest;
use App\Http\Requests\Api\Admin\V1\StoreProductRequest;
use App\Http\Requests\Api\Admin\V1\UpdateProductRequest;
use App\Http\Resources\Admin\V1\ProductResource;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductMedia;
use App\Models\ProductOption;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Services\ProductService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use RuntimeException;
use Throwable;

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

        $validated = $request->validate([
            'status' => ['nullable', Rule::in(['draft', 'active', 'archived'])],
            'query' => ['nullable', 'string', 'max:255'],
            'collection_id' => ['nullable', 'integer'],
            'sort' => ['nullable', Rule::in(['title_asc', 'title_desc', 'created_at_asc', 'created_at_desc', 'updated_at_desc'])],
            'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
        ]);

        $query = Product::withoutGlobalScopes()
            ->with([
                'media',
                'variants.inventoryItem',
            ])
            ->withCount('variants')
            ->where('store_id', $store->getKey())
            ->when(data_get($validated, 'status'), fn (Builder $query, string $status) => $query->where('status', $status))
            ->when(data_get($validated, 'collection_id'), function (Builder $query, int $collectionId): void {
                $query->whereHas('collections', fn (Builder $query) => $query->withoutGlobalScopes()->whereKey($collectionId));
            })
            ->when(data_get($validated, 'query'), function (Builder $query, string $search): void {
                $like = '%'.$search.'%';

                $query->where(function (Builder $query) use ($like): void {
                    $query
                        ->where('title', 'like', $like)
                        ->orWhere('vendor', 'like', $like)
                        ->orWhereHas('variants', fn (Builder $query) => $query->withoutGlobalScopes()->where('sku', 'like', $like));
                });
            });

        $this->applySort($query, (string) data_get($validated, 'sort', 'updated_at_desc'));

        return ProductResource::collection($query->paginate((int) data_get($validated, 'per_page', 25)));
    }

    public function show(Request $request, Store $store, Product $product): ProductResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessProductBelongsToStore($product, $store);

        return ProductResource::make($this->loadProduct($product));
    }

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

        $validated = $request->validated();

        try {
            $product = DB::transaction(function () use ($products, $store, $validated): Product {
                $product = $products->create($store, $this->attributesForCreate($validated, $store));

                if (array_key_exists('collections', $validated)) {
                    $product->collections()->sync($this->integerList($validated['collections']));
                }

                return $product->refresh();
            });
        } catch (InvalidProductTransitionException|InvalidArgumentException|RuntimeException $exception) {
            $this->throwProductValidationException($exception);
        }

        return ProductResource::make($this->loadProduct($product))
            ->response()
            ->setStatusCode(201);
    }

    public function update(UpdateProductRequest $request, Store $store, Product $product, ProductService $products): ProductResource
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessProductBelongsToStore($product, $store);

        $validated = $request->validated();

        try {
            $product = DB::transaction(function () use ($products, $store, $product, $validated): Product {
                $product = Product::withoutGlobalScopes()
                    ->whereKey($product->getKey())
                    ->lockForUpdate()
                    ->firstOrFail();

                $attributes = $this->attributesForUpdate($validated);
                $requestedStatus = null;

                if (array_key_exists('status', $attributes)) {
                    $requestedStatus = ProductStatus::from((string) $attributes['status']);
                    unset($attributes['status']);
                }

                if ($attributes !== []) {
                    $product = $products->update($product, $attributes);
                }

                if (array_key_exists('options', $validated)) {
                    $this->replaceProductOptions($product, $this->optionPayload($validated));
                }

                if (array_key_exists('variants', $validated)) {
                    $this->syncProductVariants($product, $store, $validated['variants']);
                }

                if (array_key_exists('collections', $validated)) {
                    $product->collections()->sync($this->integerList($validated['collections']));
                }

                if ($requestedStatus instanceof ProductStatus && $requestedStatus !== $product->refresh()->status) {
                    $products->transitionStatus($product, $requestedStatus);
                }

                return $product->refresh();
            });
        } catch (InvalidProductTransitionException|InvalidArgumentException|RuntimeException $exception) {
            $this->throwProductValidationException($exception);
        }

        return ProductResource::make($this->loadProduct($product));
    }

    public function destroy(Request $request, Store $store, Product $product, ProductService $products): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessProductBelongsToStore($product, $store);

        $products->transitionStatus($product, ProductStatus::Archived);

        return response()->json([
            'data' => [
                'id' => $product->getKey(),
                'status' => ProductStatus::Archived->value,
                'updated_at' => $product->refresh()->updated_at?->toIso8601String(),
            ],
        ]);
    }

    public function presignUpload(CreateProductMediaUploadRequest $request, Store $store, Product $product): JsonResponse
    {
        $this->authorizeStore($request, $store);
        $this->abortUnlessProductBelongsToStore($product, $store);

        $validated = $request->validated();
        $expiresAt = now()->addMinutes(10);
        $storageKey = sprintf(
            'media/originals/%d/%s.%s',
            $product->getKey(),
            Str::uuid(),
            $this->extensionForContentType((string) $validated['content_type']),
        );
        $media = ProductMedia::withoutGlobalScopes()->create([
            'product_id' => $product->getKey(),
            'type' => $this->mediaTypeForContentType((string) $validated['content_type']),
            'storage_key' => $storageKey,
            'alt_text' => $product->title,
            'mime_type' => $validated['content_type'],
            'byte_size' => (int) $validated['byte_size'],
            'position' => $this->nextMediaPosition($product),
            'status' => MediaStatus::Processing,
        ]);
        $upload = $this->temporaryUploadPayload(Storage::disk('public'), $storageKey, $expiresAt);

        return response()->json([
            'upload_url' => $upload['url'],
            'method' => 'PUT',
            'headers' => [
                ...$upload['headers'],
                'Content-Type' => $validated['content_type'],
            ],
            'storage_key' => $storageKey,
            'media_id' => $media->getKey(),
            'expires_at' => $expiresAt->toIso8601String(),
        ], 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 abortUnlessProductBelongsToStore(Product $product, Store $store): void
    {
        abort_unless((int) $product->store_id === $store->getKey(), 404);
    }

    private function loadProduct(Product $product): Product
    {
        return $product->load([
            'collections',
            'media',
            'options.values',
            'variants.inventoryItem',
            'variants.optionValues.option',
        ])->loadCount('variants');
    }

    private function applySort(Builder $query, string $sort): void
    {
        match ($sort) {
            'title_asc' => $query->orderBy('title')->orderBy('id'),
            'title_desc' => $query->orderByDesc('title')->orderByDesc('id'),
            'created_at_asc' => $query->orderBy('created_at')->orderBy('id'),
            'created_at_desc' => $query->orderByDesc('created_at')->orderByDesc('id'),
            default => $query->orderByDesc('updated_at')->orderByDesc('id'),
        };
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<string, mixed>
     */
    private function attributesForCreate(array $validated, Store $store): array
    {
        $attributes = [
            'title' => $validated['title'],
            'description_html' => $this->sanitizeHtml($validated['description_html'] ?? null),
            'vendor' => $validated['vendor'] ?? null,
            'product_type' => $validated['product_type'] ?? null,
            'status' => $validated['status'] ?? ProductStatus::Draft->value,
            'tags' => $this->tagList($validated['tags'] ?? []),
            'options' => $this->optionPayload($validated),
            'variants' => $this->variantPayloads($validated['variants'], $store, true),
        ];

        if (filled($validated['handle'] ?? null)) {
            $attributes['handle'] = Str::slug((string) $validated['handle']);
        }

        return $attributes;
    }

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

        if (array_key_exists('tags', $validated)) {
            $attributes['tags'] = $this->tagList($validated['tags']);
        }

        if (array_key_exists('description_html', $attributes)) {
            $attributes['description_html'] = $this->sanitizeHtml($attributes['description_html']);
        }

        if (filled($validated['handle'] ?? null)) {
            $attributes['handle'] = Str::slug((string) $validated['handle']);
        }

        return $attributes;
    }

    private function sanitizeHtml(?string $html): ?string
    {
        $sanitized = app(SanitizeHtml::class)($html);

        return $sanitized === '' ? null : $sanitized;
    }

    /**
     * @param  array<string, mixed>  $validated
     * @return array<int, array{name: string, position: int, values: array<int, array{value: string, position: int}>}>
     */
    private function optionPayload(array $validated): array
    {
<?php

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

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

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

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

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

        return PageResource::collection($pages);
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                return;
            }

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

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

        return $validator->validate();
    }

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

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

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

        if (array_key_exists('body_html', $attributes)) {
            $attributes['body_html'] = $this->sanitizeHtml($attributes['body_html']);
        }
<?php

namespace App\Livewire\Admin\Pages;

use App\Actions\SanitizeHtml;
use App\Enums\PageStatus;
use App\Models\NavigationMenu;
use App\Models\Page;
use App\Models\Store;
use App\Services\NavigationService;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;

class Form extends Component
{
    use AuthorizesRequests;

    public ?Page $page = null;

    public string $title = '';

    public string $handle = '';

    public string $bodyHtml = '';

    public string $status = 'draft';

    public string $publishedAt = '';

    public string $actionMessage = '';

    public function mount(?Page $page = null): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        if ($page?->exists) {
            abort_unless((int) $page->store_id === $store->getKey(), 404);

            $this->authorize('update', $page);

            $this->page = $page;
            $this->fillFromPage($page);

            return;
        }

        $this->authorize('create', Page::class);
    }

    public function updatedTitle(string $title): void
    {
        if ($this->page instanceof Page || $this->handle !== '') {
            return;
        }

        $this->handle = Str::slug($title);
    }

    public function save(NavigationService $navigation): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->authorizeSave();

        $this->handle = Str::slug($this->handle);

        $this->validate([
            'title' => ['required', 'string', 'max:255'],
            'handle' => [
                'required',
                'string',
                'max:255',
                'alpha_dash',
                Rule::unique('pages', 'handle')
                    ->where('store_id', $store->getKey())
                    ->ignore($this->page?->getKey()),
            ],
            'bodyHtml' => ['nullable', 'string'],
            'status' => ['required', Rule::in(array_column(PageStatus::cases(), 'value'))],
            'publishedAt' => ['nullable', 'date'],
        ], [], [
            'bodyHtml' => 'body',
            'publishedAt' => 'published at',
        ]);

        $publishedAt = $this->publishedAt !== '' ? $this->publishedAt : null;

        if ($this->status === PageStatus::Published->value && $publishedAt === null) {
            $publishedAt = now();
        }

        $page = $this->page instanceof Page
            ? tap($this->page)->update($this->payload($store, $publishedAt))
            : Page::withoutGlobalScopes()->create($this->payload($store, $publishedAt));

        $this->page = $page->refresh();
        $this->fillFromPage($this->page);
        $this->forgetNavigation($store, $navigation);

        $this->actionMessage = 'Page saved';

        session()->flash('status', 'Page saved');
        $this->dispatch('toast', type: 'success', message: __('Page saved'));
    }

    public function deletePage(NavigationService $navigation): void
    {
        abort_unless($this->page instanceof Page, 404);

        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->authorize('delete', $this->page);

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

        $this->redirectRoute('admin.pages.index', navigate: true);
    }

    public function render(): mixed
    {
        return view('livewire.admin.pages.form', [
            'isEditing' => $this->page instanceof Page,
        ])->layout('layouts.app', [
            'title' => $this->page ? __('Edit page') : __('Create page'),
        ]);
    }

    private function fillFromPage(Page $page): void
    {
        $this->title = $page->title;
        $this->handle = $page->handle;
        $this->bodyHtml = (string) $page->body_html;
        $this->status = $page->status->value;
        $this->publishedAt = $page->published_at?->format('Y-m-d\TH:i') ?? '';
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(Store $store, mixed $publishedAt): array
    {
        return [
            'store_id' => $store->getKey(),
            'title' => $this->title,
            'handle' => $this->handle,
            'body_html' => $this->sanitizeHtml($this->bodyHtml),
            'status' => PageStatus::from($this->status),
            'published_at' => $publishedAt,
        ];
    }


cmd: sed -n '160,260p' app/Http/Controllers/Api/Admin/V1/PageController.php && sed -n '160,240p' app/Livewire/Admin/Pages/Form.php


command: sed -n '160,260p' app/Http/Controllers/Api/Admin/V1/PageController.php && sed -n '160,240p' app/Livewire/Admin/Pages/Form.php

PageController.php

type: read

Form.php

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

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

        if (array_key_exists('body_html', $attributes)) {
            $attributes['body_html'] = $this->sanitizeHtml($attributes['body_html']);
        }

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

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

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

        return $attributes;
    }

    private function sanitizeHtml(?string $html): ?string
    {
        $sanitized = app(SanitizeHtml::class)($html);

        return $sanitized === '' ? null : $sanitized;
    }

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

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

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

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

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

    private function normalizeHandle(mixed $value): string
    {
        return Str::slug((string) $value);
    }

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

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

    private function sanitizeHtml(?string $html): ?string
    {
        $sanitized = app(SanitizeHtml::class)($html);

        return $sanitized === '' ? null : $sanitized;
    }

    private function authorizeSave(): void
    {
        if ($this->page instanceof Page) {
            $this->authorize('update', $this->page);

            return;
        }

        $this->authorize('create', Page::class);
    }

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

status: completed


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

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

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

        if (array_key_exists('body_html', $attributes)) {
            $attributes['body_html'] = $this->sanitizeHtml($attributes['body_html']);
        }

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

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

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

        return $attributes;
    }

    private function sanitizeHtml(?string $html): ?string
    {
        $sanitized = app(SanitizeHtml::class)($html);

        return $sanitized === '' ? null : $sanitized;
    }

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

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

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

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

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

    private function normalizeHandle(mixed $value): string
    {
        return Str::slug((string) $value);
    }

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

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

    private function sanitizeHtml(?string $html): ?string
    {
        $sanitized = app(SanitizeHtml::class)($html);

        return $sanitized === '' ? null : $sanitized;
    }

    private function authorizeSave(): void
    {
        if ($this->page instanceof Page) {
            $this->authorize('update', $this->page);

            return;
        }

        $this->authorize('create', Page::class);
    }

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


message: The API token guard is in place structurally. I’m now replacing the old session-based API test assumptions and adding regression tests for support-role denial, store-scoped product/collection pivots, checkout idempotency, and HTML sanitization.

phase: commentary


type: message

role: assistant

text: The API token guard is in place structurally. I’m now replacing the old session-based API test assumptions and adding regression tests for support-role denial, store-scoped product/collection pivots, checkout idempotency, and HTML sanitization.


cmd: sed -n '220,360p' tests/Feature/Admin/ContentManagementTest.php && sed -n '1,260p' tests/Feature/Admin/CatalogManagementTest.php 2>/dev/null || true && find tests/Feature/Admin -maxdepth 1 -type f -print


cmd: sed -n '1,260p' app/Livewire/Admin/Customers/Show.php && sed -n '1,320p' app/Livewire/Admin/Discounts/Form.php && sed -n '1,140p' app/Livewire/Admin/Collections/Index.php


cmd: find tests/Feature/Checkout -type f -maxdepth 1 -print -exec sed -n '1,220p' {} \;


command: sed -n '220,360p' tests/Feature/Admin/ContentManagementTest.php && sed -n '1,260p' tests/Feature/Admin/CatalogManagementTest.php 2>/dev/null || true && find tests/Feature/Admin -maxdepth 1 -type f -print

type: unknown

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

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

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

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

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

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

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

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

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

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

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

    Livewire::actingAs(adminContentUser())
        ->test(AdminThemeEditor::class, ['theme' => $theme])
        ->assertSuccessful();
});
tests/Feature/Admin/AnalyticsDashboardTest.php
tests/Feature/Admin/AppsDevelopersTest.php
tests/Feature/Admin/SearchSettingsTest.php
tests/Feature/Admin/SettingsManagementTest.php
tests/Feature/Admin/DashboardTest.php
tests/Feature/Admin/ProductMediaManagementTest.php
tests/Feature/Admin/DiscountManagementTest.php
tests/Feature/Admin/ContentManagementTest.php
tests/Feature/Admin/OrderManagementTest.php
tests/Feature/Admin/CustomerManagementTest.php


command: sed -n '1,260p' app/Livewire/Admin/Customers/Show.php && sed -n '1,320p' app/Livewire/Admin/Discounts/Form.php && sed -n '1,140p' app/Livewire/Admin/Collections/Index.php

Index.php

aggregated_output:
<?php

namespace App\Livewire\Admin\Customers;

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

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

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

        abort_unless($store instanceof Store, 404);

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

        abort_unless($customer instanceof Customer, 404);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->or…8690 chars truncated…    'isEditing' => $this->discount instanceof Discount,
            'productResults' => $this->productResults(),
            'collectionResults' => $this->collectionResults(),
            'selectedProducts' => $this->selectedProducts(),
            'selectedCollections' => $this->selectedCollections(),
        ])->layout('layouts.app', [
            'title' => $this->discount ? __('Edit discount') : __('Create discount'),
        ]);
    }

    private function fillFromDiscount(Discount $discount): void
    {
        $this->type = $discount->type->value;
        $this->code = (string) $discount->code;
        $this->valueType = $discount->value_type->value;
        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
            ? number_format($discount->value_amount / 100, 2, '.', '')
            : (string) $discount->value_amount;
        $this->minimumPurchaseAmount = data_get($discount->rules_json, 'min_purchase_amount')
            ? number_format(((int) data_get($discount->rules_json, 'min_purchase_amount')) / 100, 2, '.', '')
            : '';
        $this->specificProductIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->specificCollectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
        $this->onePerCustomer = (bool) data_get($discount->rules_json, 'one_per_customer', false);
        $this->startsAt = $discount->starts_at->format('Y-m-d\TH:i');
        $this->endsAt = $discount->ends_at?->format('Y-m-d\TH:i') ?? '';
        $this->isActive = $discount->status === DiscountStatus::Active;
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(Store $store): array
    {
        return [
            'store_id' => $store->getKey(),
            'type' => DiscountType::from($this->type),
            'code' => $this->type === 'code' ? Str::upper(trim($this->code)) : null,
            'value_type' => DiscountValueType::from($this->valueType),
            'value_amount' => $this->normalizedValueAmount(),
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt !== '' ? $this->endsAt : null,
            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
            'rules_json' => $this->rulesPayload(),
            'status' => $this->statusForPayload(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function rulesPayload(): array
    {
        return [
            'min_purchase_amount' => $this->minimumPurchaseAmount !== '' ? Money::fromDecimalString($this->minimumPurchaseAmount) : 0,
            'applicable_product_ids' => $this->validProductIds(),
            'applicable_collection_ids' => $this->validCollectionIds(),
            'one_per_customer' => $this->onePerCustomer,
            'customer_eligibility' => 'all',
        ];
    }

    private function normalizedValueAmount(): int
    {
        return match ($this->valueType) {
            'fixed' => Money::fromDecimalString($this->valueAmount),
            'free_shipping' => 0,
            default => (int) round((float) $this->valueAmount),
        };
    }

    /**
     * @return array<int, int>
     */
    private function validProductIds(): array
<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public string $search = '';

    public string $statusFilter = 'all';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatusFilter(): void
    {
        $this->resetPage();
    }

    public function deleteCollection(int $id): void
    {
        Collection::query()->findOrFail($id)->delete();

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

    public function collections(): LengthAwarePaginator
    {
        return Collection::query()
            ->withCount('products')
            ->when($this->search !== '', function (Builder $query): void {
                $query->where('title', 'like', '%'.$this->search.'%');
            })
            ->when($this->statusFilter !== 'all', fn (Builder $query) => $query->where('status', $this->statusFilter))
            ->latest('updated_at')
            ->paginate(15);
    }

    public function render(): mixed
    {
        return view('livewire.admin.collections.index', [
            'collections' => $this->collections(),
        ])->layout('layouts.app', [
            'title' => __('Collections'),
        ]);
    }
}


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

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

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

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

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

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

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

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

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

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

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

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

    Livewire::actingAs(adminContentUser())
        ->test(AdminThemeEditor::class, ['theme' => $theme])
        ->assertSuccessful();
});
tests/Feature/Admin/AnalyticsDashboardTest.php
tests/Feature/Admin/AppsDevelopersTest.php
tests/Feature/Admin/SearchSettingsTest.php
tests/Feature/Admin/SettingsManagementTest.php
tests/Feature/Admin/DashboardTest.php
tests/Feature/Admin/ProductMediaManagementTest.php
tests/Feature/Admin/DiscountManagementTest.php
tests/Feature/Admin/ContentManagementTest.php
tests/Feature/Admin/OrderManagementTest.php
tests/Feature/Admin/CustomerManagementTest.php


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

namespace App\Livewire\Admin\Customers;

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

class Show extends Component
{
    use WithPagination;

    #[Locked]
    public int $storeId;

    #[Locked]
    public int $customerId;

    public ?int $editingAddressId = null;

    public string $addressLabel = '';

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

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

        abort_unless($store instanceof Store, 404);

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

        abort_unless($customer instanceof Customer, 404);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return view('livewire.admin.customers.show', [
            'customer' => $customer,
            'orders' => $this->orders(),
        ])->layout('layouts.app', [
            'title' => $customer->name ?: $customer->email,
        ]);
    }

    private function customer(): Customer
    {
        return Customer::withoutGlobalScopes()
            ->with(['addresses' => fn ($query) => $query->orderByDesc('is_default')->orderBy('id')])
            ->where('store_id', $this->storeId)
            ->whereKey($this->customerId)
            ->firstOrFail();
    }

    private function address(int $addressId): CustomerAddress
    {
        return CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->whereKey($addressId)
            ->firstOrFail();
    }

    private function setFirstAddressAsDefault(): void
    {
        $nextAddress = CustomerAddress::query()
            ->where('customer_id', $this->customerId)
            ->oldest('id')
            ->first();

        if ($nextAddress instanceof CustomerAddress) {
            $nextAddress->forceFill(['is_default' => true])->save();
        }
    }

    private function resetAddressForm(): void
    {
        $this->editingAddressId = null;
        $this->addressLabel = '';
        $this->addressJson = $this->emptyAddressJson();
    }

    /**
     * @return array{first_name: string, last_name: string, address1: string, address2: string, city: string, province_code: string, country: string, postal_code: string}
     */
    private function emptyAddressJson(): array
    {
        return [
            'first_name' => '',
            'last_name' => '',
            'address1' => '',
            'address2' => '',
            'city' => '',
            'province_code' => '',
            'country' => 'DE',
            'postal_code' => '',
        ];
    }
}
<?php

namespace App\Livewire\Admin\Discounts;

use App\Enums\DiscountStatus;
use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Models\Collection as ProductCollection;
use App\Models\Discount;
use App\Models\Product;
use App\Models\Store;
use App\Support\Money;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Component;

class Form extends Component
{
    use AuthorizesRequests;

    public ?Discount $discount = null;

    public string $type = 'code';

    public string $code = '';

    public string $valueType = 'percent';

    public string $valueAmount = '10';

    public string $minimumPurchaseAmount = '';

    /**
     * @var array<int, int>
     */
    public array $specificProductIds = [];

    /**
     * @var array<int, int>
     */
    public array $specificCollectionIds = [];

    public string $usageLimit = '';

    public bool $onePerCustomer = false;

    public string $startsAt = '';

    public string $endsAt = '';

    public bool $isActive = false;

    public string $productSearch = '';

    public string $collectionSearch = '';

    public string $storeCurrency = 'EUR';

    public function mount(?Discount $discount = null): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->storeCurrency = $store->default_currency;

        if ($discount?->exists) {
            abort_unless((int) $discount->store_id === $store->getKey(), 404);

            $this->authorize('update', $discount);

            $this->discount = $discount;
            $this->fillFromDiscount($discount);

            return;
        }

        $this->authorize('create', Discount::class);

        $this->startsAt = now()->format('Y-m-d\TH:i');
    }

    public function save(): void
    {
        $store = app('current_store');

        abort_unless($store instanceof Store, 404);

        $this->authorizeSave();
        $this->normalizeCode();

        $this->validate([
            'type' => ['required', Rule::in(['code', 'automatic'])],
            'code' => [
                Rule::requiredIf($this->type === 'code'),
                'nullable',
                'string',
                'max:255',
                function (string $attribute, mixed $value, \Closure $fail) use ($store): void {
                    if ($value === null || trim((string) $value) === '') {
                        return;
                    }

                    $exists = Discount::withoutGlobalScopes()
                        ->where('store_id', $store->getKey())
                        ->whereRaw('lower(code) = ?', [Str::lower(trim((string) $value))])
                        ->when($this->discount instanceof Discount, fn ($query) => $query->where('id', '!=', $this->discount?->getKey()))
                        ->exists();

                    if ($exists) {
                        $fail(__('The discount code has already been taken.'));
                    }
                },
            ],
            'valueType' => ['required', Rule::in(['percent', 'fixed', 'free_shipping'])],
            'valueAmount' => $this->valueAmountRules(),
            'minimumPurchaseAmount' => ['nullable', 'numeric', 'min:0'],
            'usageLimit' => ['nullable', 'integer', 'min:1'],
            'startsAt' => ['required', 'date'],
            'endsAt' => ['nullable', 'date', 'after:startsAt'],
            'onePerCustomer' => ['boolean'],
            'isActive' => ['boolean'],
        ], [], [
            'valueAmount' => 'value amount',
        ]);

        $discount = $this->discount instanceof Discount
            ? tap($this->discount)->update($this->payload($store))
            : Discount::withoutGlobalScopes()->create($this->payload($store));

        $this->discount = $discount->refresh();
        $this->fillFromDiscount($this->discount);

        session()->flash('status', 'Discount saved');
        $this->dispatch('toast', type: 'success', message: __('Discount saved'));
    }

    public function generateCode(): void
    {
        do {
            $code = Str::upper(Str::random(8));
        } while (Discount::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->where('code', $code)
            ->exists());

        $this->code = $code;
    }

    public function addProduct(int $productId): void
    {
        abort_unless($this->productBelongsToStore($productId), 404);

        $this->specificProductIds = collect([...$this->specificProductIds, $productId])
            ->unique()
            ->values()
            ->all();
        $this->productSearch = '';
    }

    public function removeProduct(int $productId): void
    {
        $this->specificProductIds = array_values(array_filter(
            $this->specificProductIds,
            fn (int $selectedProductId): bool => $selectedProductId !== $productId,
        ));
    }

    public function addCollection(int $collectionId): void
    {
        abort_unless($this->collectionBelongsToStore($collectionId), 404);

        $this->specificCollectionIds = collect([...$this->specificCollectionIds, $collectionId])
            ->unique()
            ->values()
            ->all();
        $this->collectionSearch = '';
    }

    public function removeCollection(int $collectionId): void
    {
        $this->specificCollectionIds = array_values(array_filter(
            $this->specificCollectionIds,
            fn (int $selectedCollectionId): bool => $selectedCollectionId !== $collectionId,
        ));
    }

    public function productResults(): SupportCollection
    {
        if (trim($this->productSearch) === '') {
            return collect();
        }

        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereNotIn('id', $this->specificProductIds)
            ->where('title', 'like', '%'.$this->productSearch.'%')
            ->orderBy('title')
            ->limit(5)
            ->get();
    }

    public function collectionResults(): SupportCollection
    {
        if (trim($this->collectionSearch) === '') {
            return collect();
        }

        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereNotIn('id', $this->specificCollectionIds)
            ->where('title', 'like', '%'.$this->collectionSearch.'%')
            ->orderBy('title')
            ->limit(5)
            ->get();
    }

    public function selectedProducts(): SupportCollection
    {
        return Product::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificProductIds)
            ->orderBy('title')
            ->get();
    }

    public function selectedCollections(): SupportCollection
    {
        return ProductCollection::withoutGlobalScopes()
            ->where('store_id', $this->storeId())
            ->whereKey($this->specificCollectionIds)
            ->orderBy('title')
            ->get();
    }

    public function render(): mixed
    {
        return view('livewire.admin.discounts.form', [
            'isEditing' => $this->discount instanceof Discount,
            'productResults' => $this->productResults(),
            'collectionResults' => $this->collectionResults(),
            'selectedProducts' => $this->selectedProducts(),
            'selectedCollections' => $this->selectedCollections(),
        ])->layout('layouts.app', [
            'title' => $this->discount ? __('Edit discount') : __('Create discount'),
        ]);
    }

    private function fillFromDiscount(Discount $discount): void
    {
        $this->type = $discount->type->value;
        $this->code = (string) $discount->code;
        $this->valueType = $discount->value_type->value;
        $this->valueAmount = $discount->value_type === DiscountValueType::Fixed
            ? number_format($discount->value_amount / 100, 2, '.', '')
            : (string) $discount->value_amount;
        $this->minimumPurchaseAmount = data_get($discount->rules_json, 'min_purchase_amount')
            ? number_format(((int) data_get($discount->rules_json, 'min_purchase_amount')) / 100, 2, '.', '')
            : '';
        $this->specificProductIds = collect(data_get($discount->rules_json, 'applicable_product_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->specificCollectionIds = collect(data_get($discount->rules_json, 'applicable_collection_ids', []))
            ->map(fn (mixed $id): int => (int) $id)
            ->all();
        $this->usageLimit = $discount->usage_limit !== null ? (string) $discount->usage_limit : '';
        $this->onePerCustomer = (bool) data_get($discount->rules_json, 'one_per_customer', false);
        $this->startsAt = $discount->starts_at->format('Y-m-d\TH:i');
        $this->endsAt = $discount->ends_at?->format('Y-m-d\TH:i') ?? '';
        $this->isActive = $discount->status === DiscountStatus::Active;
    }

    /**
     * @return array<string, mixed>
     */
    private function payload(Store $store): array
    {
        return [
            'store_id' => $store->getKey(),
            'type' => DiscountType::from($this->type),
            'code' => $this->type === 'code' ? Str::upper(trim($this->code)) : null,
            'value_type' => DiscountValueType::from($this->valueType),
            'value_amount' => $this->normalizedValueAmount(),
            'starts_at' => $this->startsAt,
            'ends_at' => $this->endsAt !== '' ? $this->endsAt : null,
            'usage_limit' => $this->usageLimit !== '' ? (int) $this->usageLimit : null,
            'rules_json' => $this->rulesPayload(),
            'status' => $this->statusForPayload(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function rulesPayload(): array
    {
        return [
            'min_purchase_amount' => $this->minimumPurchaseAmount !== '' ? Money::fromDecimalString($this->minimumPurchaseAmount) : 0,
            'applicable_product_ids' => $this->validProductIds(),
            'applicable_collection_ids' => $this->validCollectionIds(),
            'one_per_customer' => $this->onePerCustomer,
            'customer_eligibility' => 'all',
        ];
    }

    private function normalizedValueAmount(): int
    {
        return match ($this->valueType) {
            'fixed' => Money::fromDecimalString($this->valueAmount),
            'free_shipping' => 0,
            default => (int) round((float) $this->valueAmount),
        };
    }

    /**
     * @return array<int, int>
     */
    private function validProductIds(): array
<?php

namespace App\Livewire\Admin\Collections;

use App\Models\Collection;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
use Livewire\WithPagination;

class Index extends Component
{
    use WithPagination;

    public string $search = '';

    public string $statusFilter = 'all';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatusFilter(): void
    {
        $this->resetPage();
    }

    public function deleteCollection(int $id): void
    {
        Collection::query()->findOrFail($id)->delete();

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

    public function collections(): LengthAwarePaginator
    {
        return Collection::query()
            ->withCount('products')
            ->when($this->search !== '', function (Builder $query): void {
                $query->where('title', 'like', '%'.$this->search.'%');
            })
            ->when($this->statusFilter !== 'all', fn (Builder $query) => $query->where('status', $this->statusFilter))
            ->latest('updated_at')
            ->paginate(15);
    }

    public function render(): mixed
    {
        return view('livewire.admin.collections.index', [
            'collections' => $this->collections(),
        ])->layout('layouts.app', [
            'title' => __('Collections'),
        ]);
    }
}


command: find tests/Feature/Checkout -type f -maxdepth 1 -print -exec sed -n '1,220p' {} \;

cmd: find tests/Feature/Checkout -type f -maxdepth 1 -print -exec sed -n '1,220p' '{}' "\\;"

path: Checkout

aggregated_output:
tests/Feature/Checkout/CheckoutServiceTest.php
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

function checkoutStore(): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    TaxSettings::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => ['default_rate_bps' => 0, 'shipping_taxable' => false],
    ]);

    return $store;
}

function checkoutVariant(Store $store, bool $requiresShipping = true): ProductVariant
{
    $product = Product::factory()
        ->withDefaultVariant(2500)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $variant->forceFill([
        'requires_shipping' => $requiresShipping,
        'weight_g' => $requiresShipping ? 500 : 0,
    ])->save();
    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function checkoutAddress(string $country = 'DE'): array
{
    return [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => $country,
            'postal_code' => '10115',
        ],
    ];
}

test('checkout service transitions through address shipping payment and expiry', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);

    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    expect($checkout->status)->toBe(CheckoutStatus::Addressed)
        ->and($checkout->email)->toBe('buyer@example.test');

    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBe($rate->getKey());

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->expires_at)->not->toBeNull()
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    $expired = app(CheckoutService::class)->expireCheckout($checkout);
    expect($expired->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('checkout service rejects unserviceable shipping addresses for physical carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress('FR'));

    expect(fn () => app(CheckoutService::class)->setShippingMethod($checkout, null))
        ->toThrow(UnserviceableShippingAddressException::class);
});

test('checkout service skips shipping fo…6178 chars truncated… 'city' => 'Berlin',
            'country' => 'DE',
            'postal_code' => '10115',
        ],
    ]);
}

test('pricing engine calculates deterministic exclusive totals with shipping tax', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $checkout = pricingCheckout($store, $variant);
    $checkout->forceFill(['shipping_method_id' => $rate->getKey()])->save();

    $result = app(PricingEngine::class)->calculate($checkout);

    expect($result->subtotal)->toBe(5000)
        ->and($result->shipping)->toBe(499)
        ->and($result->taxTotal)->toBe(1045)
        ->and($result->total)->toBe(6544)
        ->and($checkout->refresh()->totals_json['total'])->toBe(6544);
});

test('pricing engine applies percent and free shipping discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    Discount::factory()->create(['store_id' => $store->getKey(), 'code' => 'SAVE10']);
    Discount::factory()->freeShipping()->create(['store_id' => $store->getKey(), 'code' => 'FREESHIP']);
    $checkout = pricingCheckout($store, $variant);
    $checkout->forceFill([
        'shipping_method_id' => $rate->getKey(),
        'discount_code' => 'save10',
    ])->save();

    $discounted = app(PricingEngine::class)->calculate($checkout);

    expect($discounted->discount)->toBe(500)
        ->and($discounted->shipping)->toBe(499)
        ->and($discounted->total)->toBe(5949);

    $checkout->forceFill(['discount_code' => 'FREESHIP'])->save();
    $freeShipping = app(PricingEngine::class)->calculate($checkout);

    expect($freeShipping->discount)->toBe(0)
        ->and($freeShipping->shipping)->toBe(0)
        ->and($freeShipping->total)->toBe(5950);
});

test('pricing engine applies active automatic discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $checkout = pricingCheckout($store, $variant);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'type' => DiscountType::Automatic,
        'code' => null,
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);

    $result = app(PricingEngine::class)->calculate($checkout);
    $line = $checkout->cart->lines()->firstOrFail();

    expect($result->discount)->toBe(500)
        ->and($result->total)->toBe(5355)
        ->and($line->refresh()->line_discount_amount)->toBe(500)
        ->and($line->line_total_amount)->toBe(4500);
});

test('pricing engine stacks automatic discounts after explicit code discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $checkout = pricingCheckout($store, $variant);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SAVE10',
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'type' => DiscountType::Automatic,
        'code' => null,
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);
    $checkout->forceFill(['discount_code' => 'SAVE10'])->save();

    $result = app(PricingEngine::class)->calculate($checkout);
    $line = $checkout->cart->lines()->firstOrFail();

    expect($result->discount)->toBe(950)
        ->and($result->total)->toBe(4820)
        ->and($line->refresh()->line_discount_amount)->toBe(950)
        ->and($line->line_total_amount)->toBe(4050);
});

test('discount validation enforces one use per customer from order history', function () {
    $store = pricingStore();
    $customer = Customer::factory()->create(['store_id' => $store->getKey()]);
    $otherCustomer = Customer::factory()->create(['store_id' => $store->getKey()]);
    $variant = pricingVariant($store);
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'ONCE',
        'rules_json' => [
            'customer_eligibility' => 'all',
            'one_per_customer' => true,
        ],
    ]);
    $order = Order::factory()
        ->forCustomer($customer)


output:
Chunk ID: 6316f7
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4045
Output:
tests/Feature/Checkout/CheckoutServiceTest.php
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Exceptions\UnserviceableShippingAddressException;
use App\Jobs\CleanupAbandonedCarts;
use App\Jobs\ExpireAbandonedCheckouts;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

function checkoutStore(): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    TaxSettings::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => ['default_rate_bps' => 0, 'shipping_taxable' => false],
    ]);

    return $store;
}

function checkoutVariant(Store $store, bool $requiresShipping = true): ProductVariant
{
    $product = Product::factory()
        ->withDefaultVariant(2500)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $variant->forceFill([
        'requires_shipping' => $requiresShipping,
        'weight_g' => $requiresShipping ? 500 : 0,
    ])->save();
    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => 10,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function checkoutAddress(string $country = 'DE'): array
{
    return [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => $country,
            'postal_code' => '10115',
        ],
    ];
}

test('checkout service transitions through address shipping payment and expiry', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);

    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    expect($checkout->status)->toBe(CheckoutStatus::Addressed)
        ->and($checkout->email)->toBe('buyer@example.test');

    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBe($rate->getKey());

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect($checkout->status)->toBe(CheckoutStatus::PaymentSelected)
        ->and($checkout->expires_at)->not->toBeNull()
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    expect(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(2);

    $expired = app(CheckoutService::class)->expireCheckout($checkout);
    expect($expired->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('checkout service rejects unserviceable shipping addresses for physical carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress('FR'));

    expect(fn () => app(CheckoutService::class)->setShippingMethod($checkout, null))
        ->toThrow(UnserviceableShippingAddressException::class);
});

test('checkout service skips shipping for digital-only carts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store, requiresShipping: false);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, null);

    expect($checkout->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->shipping_method_id)->toBeNull()
        ->and($checkout->totals_json['shipping'])->toBe(0);
});

test('expire abandoned checkouts job expires stale checkouts and releases reservations', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 2);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'credit_card');
    $checkout->forceFill(['expires_at' => now()->subMinute()])->save();

    (new ExpireAbandonedCheckouts)->handle(app(CheckoutService::class));

    expect($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('cleanup abandoned carts job abandons old carts and expires related checkouts', function () {
    $store = checkoutStore();
    $variant = checkoutVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), 1);
    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, checkoutAddress());
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());
    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, 'paypal');
    $cart->forceFill(['updated_at' => now()->subDays(15)])->save();

    (new CleanupAbandonedCarts)->handle(app(CheckoutService::class));

    expect($cart->refresh()->status)->toBe(CartStatus::Abandoned)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::Expired)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});
tests/Feature/Checkout/PricingServicesTest.php
<?php

use App\Enums\DiscountType;
use App\Enums\DiscountValueType;
use App\Exceptions\InvalidDiscountException;
use App\Models\Checkout;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\DiscountService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use App\Services\TaxCalculator;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

function pricingStore(): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    TaxSettings::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'mode' => 'manual',
        'provider' => 'none',
        'prices_include_tax' => false,
        'config_json' => [
            'name' => 'VAT',
            'default_rate_bps' => 1900,
            'shipping_taxable' => true,
            'rates' => [
                ['country' => 'DE', 'rate_bps' => 1900, 'name' => 'VAT'],
            ],
        ],
    ]);

    return $store;
}

function pricingVariant(Store $store, int $price = 2500, bool $requiresShipping = true): ProductVariant
{
    $product = Product::factory()
        ->withDefaultVariant($price)
        ->create(['store_id' => $store->getKey()]);
    $variant = ProductVariant::withoutGlobalScopes()
        ->where('product_id', $product->getKey())
        ->firstOrFail();

    $variant->forceFill([
        'requires_shipping' => $requiresShipping,
        'weight_g' => $requiresShipping ? 500 : 0,
    ])->save();
    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update(['quantity_on_hand' => 20]);

    return $variant->refresh();
}

function pricingCheckout(Store $store, ProductVariant $variant, int $quantity = 2): Checkout
{
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $variant->getKey(), $quantity);

    return Checkout::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'cart_id' => $cart->getKey(),
        'status' => 'shipping_selected',
        'email' => 'buyer@example.test',
        'shipping_address_json' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => 'DE',
            'postal_code' => '10115',
        ],
    ]);
}

test('pricing engine calculates deterministic exclusive totals with shipping tax', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    $checkout = pricingCheckout($store, $variant);
    $checkout->forceFill(['shipping_method_id' => $rate->getKey()])->save();

    $result = app(PricingEngine::class)->calculate($checkout);

    expect($result->subtotal)->toBe(5000)
        ->and($result->shipping)->toBe(499)
        ->and($result->taxTotal)->toBe(1045)
        ->and($result->total)->toBe(6544)
        ->and($checkout->refresh()->totals_json['total'])->toBe(6544);
});

test('pricing engine applies percent and free shipping discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Germany',
        'countries_json' => ['DE'],
        'regions_json' => [],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
    Discount::factory()->create(['store_id' => $store->getKey(), 'code' => 'SAVE10']);
    Discount::factory()->freeShipping()->create(['store_id' => $store->getKey(), 'code' => 'FREESHIP']);
    $checkout = pricingCheckout($store, $variant);
    $checkout->forceFill([
        'shipping_method_id' => $rate->getKey(),
        'discount_code' => 'save10',
    ])->save();

    $discounted = app(PricingEngine::class)->calculate($checkout);

    expect($discounted->discount)->toBe(500)
        ->and($discounted->shipping)->toBe(499)
        ->and($discounted->total)->toBe(5949);

    $checkout->forceFill(['discount_code' => 'FREESHIP'])->save();
    $freeShipping = app(PricingEngine::class)->calculate($checkout);

    expect($freeShipping->discount)->toBe(0)
        ->and($freeShipping->shipping)->toBe(0)
        ->and($freeShipping->total)->toBe(5950);
});

test('pricing engine applies active automatic discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $checkout = pricingCheckout($store, $variant);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'type' => DiscountType::Automatic,
        'code' => null,
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);

    $result = app(PricingEngine::class)->calculate($checkout);
    $line = $checkout->cart->lines()->firstOrFail();

    expect($result->discount)->toBe(500)
        ->and($result->total)->toBe(5355)
        ->and($line->refresh()->line_discount_amount)->toBe(500)
        ->and($line->line_total_amount)->toBe(4500);
});

test('pricing engine stacks automatic discounts after explicit code discounts', function () {
    $store = pricingStore();
    $variant = pricingVariant($store);
    $checkout = pricingCheckout($store, $variant);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'SAVE10',
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);
    Discount::factory()->create([
        'store_id' => $store->getKey(),
        'type' => DiscountType::Automatic,
        'code' => null,
        'value_type' => DiscountValueType::Percent,
        'value_amount' => 10,
    ]);
    $checkout->forceFill(['discount_code' => 'SAVE10'])->save();

    $result = app(PricingEngine::class)->calculate($checkout);
    $line = $checkout->cart->lines()->firstOrFail();

    expect($result->discount)->toBe(950)
        ->and($result->total)->toBe(4820)
        ->and($line->refresh()->line_discount_amount)->toBe(950)
        ->and($line->line_total_amount)->toBe(4050);
});

test('discount validation enforces one use per customer from order history', function () {
    $store = pricingStore();
    $customer = Customer::factory()->create(['store_id' => $store->getKey()]);
    $otherCustomer = Customer::factory()->create(['store_id' => $store->getKey()]);
    $variant = pricingVariant($store);
    $discount = Discount::factory()->create([
        'store_id' => $store->getKey(),
        'code' => 'ONCE',
        'rules_json' => [
            'customer_eligibility' => 'all',
            'one_per_customer' => true,
        ],
    ]);
    $order = Order::factory()
        ->forCustomer($customer)