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

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontCheckoutHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontCheckoutStart(): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout')
        ->assertSee('Checkout');
}

/**
 * @param  array<string, string>  $overrides
 */
function storefrontCheckoutFillAddress(mixed $page, array $overrides = []): mixed
{
    $address = [
        'email' => 'test-buyer@example.com',
        'first_name' => 'Test',
        'last_name' => 'Buyer',
        'address1' => 'Teststrasse 1',
        'city' => 'Berlin',
        'postal_code' => '10115',
        'country' => 'DE',
    ];

    $address = array_replace($address, $overrides);

    return $page
        ->fill('input[wire\\:model="email"]', $address['email'])
        ->fill('input[wire\\:model="shippingAddress.first_name"]', $address['first_name'])
        ->fill('input[wire\\:model="shippingAddress.last_name"]', $address['last_name'])
        ->fill('input[wire\\:model="shippingAddress.address1"]', $address['address1'])
        ->fill('input[wire\\:model="shippingAddress.city"]', $address['city'])
        ->fill('input[wire\\:model="shippingAddress.postal_code"]', $address['postal_code'])
        ->select('select[wire\\:model="shippingAddress.country"]', $address['country']);
}

function storefrontCheckoutSubmitAddress(mixed $page): mixed
{
    return $page
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1);
}

function storefrontCheckoutReachShipping(array $address = []): mixed
{
    return storefrontCheckoutSubmitAddress(
        storefrontCheckoutFillAddress(storefrontCheckoutStart(), $address),
    );
}

function storefrontCheckoutReachPayment(array $address = []): mixed
{
    return storefrontCheckoutReachShipping($address)
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('Payment')
        ->assertSee('Pay now');
}

function storefrontCheckoutFillSuccessfulCard(mixed $page): mixed
{
    return $page
        ->fill('input[wire\\:model="cardNumber"]', '4242 4242 4242 4242')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123');
}

function storefrontCheckoutApplyCartDiscount(string $code): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCheckoutHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->fill('input[wire\\:model="discountCode"]', $code)
        ->click('button:has-text("Apply")')
        ->wait(1);
}

test('completes full checkout with credit card', function (): void {
    storefrontCheckoutFillSuccessfulCard(storefrontCheckoutReachPayment())
        ->assertSee('29.98')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows shipping methods based on german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'Hans',
        'last_name' => 'Mueller',
        'address1' => 'Berliner Str. 10',
        'city' => 'Munich',
        'postal_code' => '80331',
        'country' => 'DE',
    ])
        ->assertSee('Standard Shipping')
        ->assertSee('4.99')
        ->assertNoJavaScriptErrors();
});

test('shows international shipping methods for non german address', function (): void {
    storefrontCheckoutReachShipping([
        'email' => 'test@example.com',
        'first_name' => 'John',
        'last_name' => 'Smith',
        'address1' => '123 Main St',
        'city' => 'New York',
        'postal_code' => '10001',
        'country' => 'US',
    ])
        ->assertSee('International Shipping')
        ->assertSee('14.99')
        ->assertNoJavaScriptErrors();
});

test('applies discount during checkout', function (): void {
    $page = storefrontCheckoutApplyCartDiscount('FLAT5')
        ->assertSee('FLAT5')
        ->click('main button:has-text("Checkout")')
        ->wait(1)
        ->assertPathIs('/checkout');

    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress($page))
        ->click('button:has-text("Standard Shipping")')
        ->wait(1)
        ->click('button[wire\\:click="selectShippingMethod"]')
        ->wait(1)
        ->assertSee('FLAT5')
        ->assertSee('5.00')
        ->assertSee('24.98')
        ->assertNoJavaScriptErrors();
});

test('validates required contact email', function (): void {
    storefrontCheckoutStart()
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('validates required shipping address fields', function (): void {
    storefrontCheckoutStart()
        ->fill('input[wire\\:model="email"]', 'test@example.com')
        ->click('form[wire\\:submit="saveAddress"] button[type="submit"]')
        ->wait(1)
        ->assertSee('first name field is required')
        ->assertSee('last name field is required')
        ->assertSee('address1 field is required')
        ->assertSee('city field is required')
        ->assertSee('postal code field is required')
        ->assertNoJavaScriptErrors();
});

test('validates invalid postal code format', function (): void {
    storefrontCheckoutSubmitAddress(storefrontCheckoutFillAddress(storefrontCheckoutStart(), [
        'postal_code' => 'INVALID',
    ]))
        ->assertSee('postal code format is invalid')
        ->assertNoJavaScriptErrors();
});

test('prevents checkout with empty cart', function (): void {
    visit('/cart', storefrontCheckoutHost())
        ->assertSee('Your cart is empty')
        ->assertButtonDisabled('main button:has-text("Checkout")')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with paypal', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'paypal')
        ->wait(1)
        ->assertSee('Pay with PayPal')
        ->click('button:has-text("Pay with PayPal")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('PayPal')
        ->assertNoJavaScriptErrors();
});

test('completes checkout with bank transfer', function (): void {
    storefrontCheckoutReachPayment()
        ->select('select[wire\\:model\\.live="paymentMethod"]', 'bank_transfer')
        ->wait(1)
        ->assertSee('bank transfer instructions')
        ->click('button:has-text("Place order")')
        ->wait(2)
        ->assertPathBeginsWith('/checkout/confirmation')
        ->assertSee('Thank you')
        ->assertSee('IBAN')
        ->assertSee('BIC')
        ->assertSee('Reference')
        ->assertSee('#1001')
        ->assertNoJavaScriptErrors();
});

test('shows error for declined credit card', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 0002')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')
        ->click('button:has-text("Pay now")')
        ->wait(2)
        ->assertPathIs('/checkout')
        ->assertSee('declined')
        ->assertNoJavaScriptErrors();
});

test('shows error for insufficient funds', function (): void {
    storefrontCheckoutReachPayment()
        ->fill('input[wire\\:model="cardNumber"]', '4000 0000 0000 9995')
        ->fill('input[wire\\:model="cardName"]', 'Test Buyer')
        ->fill('input[wire\\:model="cardExpiry"]', '12/28')
        ->fill('input[wire\\:model="cardCvc"]', '123')


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

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontCartHost(): array
{
    return ['host' => 'shop.test'];
}

function storefrontCartAddClassicTShirt(): mixed
{
    return visit('/products/classic-cotton-t-shirt', storefrontCartHost())
        ->click('M')
        ->wait(1)
        ->click('Black')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1);
}

function storefrontCartPageWithClassicTShirt(): mixed
{
    return storefrontCartAddClassicTShirt()
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Your Cart')
        ->assertSee('Classic Cotton T-Shirt');
}

function storefrontCartApplyDiscount(string $code): mixed
{
    return storefrontCartPageWithClassicTShirt()
        ->fill('input[wire\\:model="discountCode"]', $code)
        ->click('button:has-text("Apply")')
        ->wait(1);
}

test('can add product to cart', function (): void {
    storefrontCartAddClassicTShirt()
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});

test('can view cart with added item', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});

test('can update quantity in cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->click('main button[aria-label="Increase Classic Cotton T-Shirt quantity"]')
        ->wait(1)
        ->assertSee('2 items')
        ->assertSee('49.98')
        ->assertNoJavaScriptErrors();
});

test('can remove item from cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->click('main button[aria-label="Remove Classic Cotton T-Shirt"]')
        ->wait(1)
        ->assertSee('Your cart is empty')
        ->assertNoJavaScriptErrors();
});

test('can add multiple different products', function (): void {
    storefrontCartAddClassicTShirt()
        ->navigate('/products/premium-slim-fit-jeans')
        ->wait(1)
        ->click('Add to cart')
        ->wait(1)
        ->navigate('/cart')
        ->wait(1)
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('Premium Slim Fit Jeans')
        ->assertNoJavaScriptErrors();
});

test('can apply valid discount code welcome ten', function (): void {
    storefrontCartApplyDiscount('WELCOME10')
        ->assertSee('WELCOME10')
        ->assertSee('Discount')
        ->assertSee('2.50')
        ->assertNoJavaScriptErrors();
});

test('shows error for invalid discount code', function (): void {
    storefrontCartApplyDiscount('INVALID')
        ->assertSee('Invalid discount code')
        ->assertNoJavaScriptErrors();
});

test('shows error for expired discount code', function (): void {
    storefrontCartApplyDiscount('EXPIRED20')
        ->assertSee('expired')
        ->assertNoJavaScriptErrors();
});

test('shows error for maxed out discount code', function (): void {
    storefrontCartApplyDiscount('MAXED')
        ->assertSee('usage limit')
        ->assertNoJavaScriptErrors();
});

test('can apply free shipping discount', function (): void {
    storefrontCartApplyDiscount('FREESHIP')
        ->assertSee('FREESHIP')
        ->assertSee('Free shipping')
        ->assertNoJavaScriptErrors();
});

test('can apply flat five discount for fixed amount off', function (): void {
    storefrontCartApplyDiscount('FLAT5')
        ->assertSee('FLAT5')
        ->assertSee('5.00')
        ->assertNoJavaScriptErrors();
});

test('shows subtotal and total in cart', function (): void {
    storefrontCartPageWithClassicTShirt()
        ->assertSee('Subtotal')
        ->assertSee('Estimated total')
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});


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

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Pest\Browser\Playwright\Playwright;

uses(RefreshDatabase::class);

beforeEach(function (): void {
    Playwright::setHost('shop.test');

    $this->seed(DatabaseSeeder::class);
});

afterEach(function (): void {
    Playwright::setHost(null);
});

function storefrontBrowsingHost(): array
{
    return ['host' => 'shop.test'];
}

test('shows featured products on home page', function (): void {
    visit('/', storefrontBrowsingHost())
        ->assertSee('Acme Fashion')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99')
        ->assertNoJavaScriptErrors();
});

test('shows collection with product grid', function (): void {
    visit('/collections/t-shirts', storefrontBrowsingHost())
        ->assertSee('T-Shirts')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors();
});

test('can navigate from collection to product', function (): void {
    visit('/collections/t-shirts', storefrontBrowsingHost())
        ->click('a[href$="/products/classic-cotton-t-shirt"]')
        ->wait(1)
        ->assertPathIs('/products/classic-cotton-t-shirt')
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99')
        ->assertSee('Add to cart')
        ->assertNoJavaScriptErrors();
});

test('shows product detail with variant options', function (): void {
    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
        ->assertSee('Classic Cotton T-Shirt')
        ->assertSee('24.99')
        ->assertSee('Size')
        ->assertSee('Color')
        ->assertNoJavaScriptErrors();
});

test('shows size and color option values', function (): void {
    visit('/products/classic-cotton-t-shirt', storefrontBrowsingHost())
        ->assertSee('S')
        ->assertSee('M')
        ->assertSee('L')
        ->assertSee('XL')
        ->assertSee('Black')
        ->assertSee('White')
        ->assertSee('Navy')
        ->assertNoJavaScriptErrors();
});

test('shows sale and compare at pricing for sale product', function (): void {
    visit('/products/premium-slim-fit-jeans', storefrontBrowsingHost())
        ->assertSee('Premium Slim Fit Jeans')
        ->assertSee('79.99')
        ->assertSee('99.99')
        ->assertPresent('.line-through')
        ->assertNoJavaScriptErrors();
});

test('shows search results for valid query', function (): void {
    visit('/search?q=cotton', storefrontBrowsingHost())
        ->assertSee('Classic Cotton T-Shirt')
        ->assertNoJavaScriptErrors();
});

test('shows no results message for invalid query', function (): void {
    visit('/search?q=zznonexistentproductzz', storefrontBrowsingHost())
        ->assertSee('No products found')
        ->assertNoJavaScriptErrors();
});

test('does not show draft products on storefront collections', function (): void {
    visit('/collections', storefrontBrowsingHost())
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});

test('does not show draft products in search results', function (): void {
    visit('/search?q=draft', storefrontBrowsingHost())
        ->assertDontSee('Unreleased Winter Jacket')
        ->assertNoJavaScriptErrors();
});

test('shows out of stock messaging for deny policy product', function (): void {
    visit('/products/limited-edition-sneakers', storefrontBrowsingHost())
        ->assertSee('Limited Edition Sneakers')
        ->assertSee('Out of stock')
        ->assertSee('Sold out')
        ->assertButtonDisabled('button:has-text("Sold out")')
        ->assertDontSee('Add to cart')
        ->assertNoJavaScriptErrors();
});

test('shows backorder messaging for continue policy product', function (): void {
    visit('/products/backorder-denim-jacket', storefrontBrowsingHost())
        ->assertSee('Backorder Denim Jacket')
        ->assertSee('Available on backorder')
        ->assertSee('Add to cart')
        ->assertButtonEnabled('button:has-text("Add to cart")')
        ->assertNoJavaScriptErrors();
});

test('shows new arrivals collection', function (): void {
    visit('/collections/new-arrivals', storefrontBrowsingHost())
        ->assertSee('New Arrivals')
        ->assertNoJavaScriptErrors();
});

test('shows static about page', function (): void {
    visit('/pages/about', storefrontBrowsingHost())
        ->assertSee('About')
        ->assertNoJavaScriptErrors();
});

test('navigates between pages using the main navigation', function (): void {
    visit('/', storefrontBrowsingHost())
        ->hover('nav[aria-label="Main navigation"] a[href$="/collections"]')
        ->click('nav[aria-label="Main navigation"] a[href$="/collections/t-shirts"]')
        ->wait(1)
        ->assertPathIs('/collections/t-shirts')
        ->assertSee('T-Shirts')
        ->assertNoJavaScriptErrors();
});


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

use Database\Seeders\DatabaseSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

function adminAuthenticationHost(): array
{
    return ['host' => 'shop.test'];
}

function adminAuthenticationLogin(): mixed
{
    return visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'password')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin')
        ->assertSee('Dashboard')
        ->assertNoJavaScriptErrors();
}

function adminAuthenticationSubmitWithoutBrowserRequired(string $email, string $password): mixed
{
    $page = visit('/admin/login', adminAuthenticationHost());

    $page->script('() => document.querySelectorAll("[required]").forEach((element) => element.removeAttribute("required"))');

    if ($email !== '') {
        $page->fill('input[type=email]', $email);
    }

    if ($password !== '') {
        $page->fill('input[type=password]', $password);
    }

    return $page->click('@admin-login-button')->wait(1);
}

test('can log in as admin', function (): void {
    adminAuthenticationLogin();
});

test('shows error for invalid credentials', function (): void {
    visit('/admin/login', adminAuthenticationHost())
        ->fill('input[type=email]', 'admin@acme.test')
        ->fill('input[type=password]', 'wrongpassword')
        ->click('@admin-login-button')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Invalid credentials')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty email', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('', 'password')
        ->assertPathIs('/admin/login')
        ->assertSee('email field is required')
        ->assertNoJavaScriptErrors();
});

test('shows error for empty password', function (): void {
    adminAuthenticationSubmitWithoutBrowserRequired('admin@acme.test', '')
        ->assertPathIs('/admin/login')
        ->assertSee('password field is required')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from dashboard', function (): void {
    visit('/admin', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('redirects unauthenticated users to login from products', function (): void {
    visit('/admin/products', adminAuthenticationHost())
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can log out', function (): void {
    adminAuthenticationLogin()
        ->click('@sidebar-menu-button')
        ->click('button[data-test="logout-button"]:visible')
        ->wait(1)
        ->assertPathIs('/admin/login')
        ->assertSee('Sign in')
        ->assertNoJavaScriptErrors();
});

test('can navigate through admin sidebar sections', function (): void {
    $page = adminAuthenticationLogin();

    foreach ([
        '/admin/products' => 'Products',
        '/admin/orders' => 'Orders',
        '/admin/customers' => 'Customers',
        '/admin/discounts' => 'Discounts',
        '/admin/settings' => 'Store Settings',
    ] as $path => $heading) {
        $page->click("a[href$=\"{$path}\"]")
            ->wait(1)
            ->assertPathIs($path)
            ->assertSee($heading)
            ->assertNoJavaScriptErrors();
    }
});

test('can navigate to analytics from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/analytics"]')
        ->wait(1)
        ->assertPathIs('/admin/analytics')
        ->assertSee('Analytics')
        ->assertNoJavaScriptErrors();
});

test('can navigate to themes from sidebar', function (): void {
    adminAuthenticationLogin()
        ->click('a[href$="/admin/themes"]')
        ->wait(1)
        ->assertPathIs('/admin/themes')
        ->assertSee('Themes')
        ->assertNoJavaScriptErrors();
});


cmd: rg "class Customer|addresses\(" -n app/Models database/migrations | sed -n '1,200p'


cmd: rg "create_customer_addresses|customer_addresses" -n database/migrations specs/01-DATABASE-SCHEMA.md | sed -n '1,220p'


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

Pest.php

type: read

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

aggregated_output:
<?php

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

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

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

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

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

function something()
{
    // ..
}

status: completed


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

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

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

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

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

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

function something()
{
    // ..
}


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

Customer.php

cmd: sed -n '1,260p' app/Models/Customer.php

aggregated_output:
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;

class Customer extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\CustomerFactory> */
    use BelongsToStore, HasFactory, Notifiable;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'email',
        'password',
        'name',
        'marketing_opt_in',
    ];

    /**
     * @var list<string>
     */
    protected $hidden = [
        'password_hash',
        'remember_token',
    ];

    public function getAuthPassword(): ?string
    {
        return $this->password_hash;
    }

    public function setPasswordAttribute(?string $value): void
    {
        $this->attributes['password_hash'] = $value && Hash::needsRehash($value) ? Hash::make($value) : $value;
    }

    public function getPasswordAttribute(): ?string
    {
        return $this->password_hash;
    }

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

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

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

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

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


command: rg "class Customer|addresses\(" -n app/Models database/migrations | sed -n '1,200p'

type: search

cmd: rg "class Customer|addresses\\(" -n app/Models database/migrations

query: class Customer|addresses\(

path: Models

aggregated_output:
app/Models/Customer.php:12:class Customer extends Authenticatable
app/Models/Customer.php:70:    public function addresses(): HasMany
app/Models/CustomerAddress.php:9:class CustomerAddress extends Model


command: rg "create_customer_addresses|customer_addresses" -n database/migrations specs/01-DATABASE-SCHEMA.md | sed -n '1,220p'

type: search

cmd: rg 'create_customer_addresses|customer_addresses' -n database/migrations specs/01-DATABASE-SCHEMA.md

query: create_customer_addresses|customer_addresses

path: migrations

aggregated_output:
specs/01-DATABASE-SCHEMA.md:730:### customer_addresses
specs/01-DATABASE-SCHEMA.md:744:- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
specs/01-DATABASE-SCHEMA.md:745:- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address
specs/01-DATABASE-SCHEMA.md:1372:    customers ||--o{ customer_addresses : "has"
specs/01-DATABASE-SCHEMA.md:1546:        customer_addresses
specs/01-DATABASE-SCHEMA.md:1603:    customers --> customer_addresses
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1678:| 6 - Payments, Orders, Fulfillment, Customers | customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines | 8 |
database/migrations/2026_05_04_000408_create_customer_addresses_table.php:14:        Schema::create('customer_addresses', function (Blueprint $table) {
database/migrations/2026_05_04_000408_create_customer_addresses_table.php:31:        Schema::dropIfExists('customer_addresses');


output:
Chunk ID: 37869d
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 51
Output:
app/Models/Customer.php:12:class Customer extends Authenticatable
app/Models/Customer.php:70:    public function addresses(): HasMany
app/Models/CustomerAddress.php:9:class CustomerAddress extends Model


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

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;

class Customer extends Authenticatable
{
    /** @use HasFactory<\Database\Factories\CustomerFactory> */
    use BelongsToStore, HasFactory, Notifiable;

    /**
     * @var list<string>
     */
    protected $fillable = [
        'store_id',
        'email',
        'password',
        'name',
        'marketing_opt_in',
    ];

    /**
     * @var list<string>
     */
    protected $hidden = [
        'password_hash',
        'remember_token',
    ];

    public function getAuthPassword(): ?string
    {
        return $this->password_hash;
    }

    public function setPasswordAttribute(?string $value): void
    {
        $this->attributes['password_hash'] = $value && Hash::needsRehash($value) ? Hash::make($value) : $value;
    }

    public function getPasswordAttribute(): ?string
    {
        return $this->password_hash;
    }

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

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

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

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

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


output:
Chunk ID: 3563d9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 329
Output:
specs/01-DATABASE-SCHEMA.md:730:### customer_addresses
specs/01-DATABASE-SCHEMA.md:744:- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
specs/01-DATABASE-SCHEMA.md:745:- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address
specs/01-DATABASE-SCHEMA.md:1372:    customers ||--o{ customer_addresses : "has"
specs/01-DATABASE-SCHEMA.md:1546:        customer_addresses
specs/01-DATABASE-SCHEMA.md:1603:    customers --> customer_addresses
specs/01-DATABASE-SCHEMA.md:1646:- **Batch 4** (depends on Batch 3): `product_options`, `product_variants`, `product_media`, `collection_products`, `customer_addresses`, `carts`, `navigation_items`, `theme_files`, `theme_settings`, `search_queries`, `shipping_rates`, `oauth_tokens`, `webhook_subscriptions`
specs/01-DATABASE-SCHEMA.md:1678:| 6 - Payments, Orders, Fulfillment, Customers | customers, customer_addresses, orders, order_lines, payments, refunds, fulfillments, fulfillment_lines | 8 |
database/migrations/2026_05_04_000408_create_customer_addresses_table.php:14:        Schema::create('customer_addresses', function (Blueprint $table) {
database/migrations/2026_05_04_000408_create_customer_addresses_table.php:31:        Schema::dropIfExists('customer_addresses');


cmd: sed -n '1,120p' database/migrations/2026_05_04_000408_create_customer_addresses_table.php


cmd: sed -n '720,756p' specs/01-DATABASE-SCHEMA.md


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


cmd: rg "CustomerAddressSeeder|Address saved|account\.addresses|Logout|My Account|account/orders" -n . | sed -n '1,240p'


command: sed -n '1,120p' database/migrations/2026_05_04_000408_create_customer_addresses_table.php

aggregated_output:
<?php

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
            $table->string('label')->nullable();
            $table->text('address_json')->default('{}');
            $table->boolean('is_default')->default(false);

            $table->index('customer_id');
            $table->index(['customer_id', 'is_default']);
        });
    }

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


command: sed -n '720,756p' specs/01-DATABASE-SCHEMA.md

aggregated_output:
**Indexes:**

- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store

**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.

---

### customer_addresses

Saved addresses for a customer.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| customer_id | INTEGER | No | - | FK -> customers(id) ON DELETE CASCADE | Owning customer |
| label | TEXT | Yes | NULL | - | User label, e.g. "Home", "Work" |
| address_json | TEXT | No | '{}' | - | JSON address object (see notes) |
| is_default | INTEGER | No | 0 | - | Boolean: default address flag |

**Indexes:**

- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address

**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.

---

### orders

Placed orders with financial and fulfillment tracking.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|


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

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

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customer_addresses', function (Blueprint $table) {
            $table->id();
            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
            $table->string('label')->nullable();
            $table->text('address_json')->default('{}');
            $table->boolean('is_default')->default(false);

            $table->index('customer_id');
            $table->index(['customer_id', 'is_default']);
        });
    }

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


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

**Indexes:**

- `idx_customers_store_email` UNIQUE on (`store_id`, `email`) - Unique email per store
- `idx_customers_store_id` on (`store_id`) - FK lookup, listing customers per store

**Notes:** `password_hash` is nullable to allow guest checkout customers who may later claim their account. Email is unique per store, not globally.

---

### customer_addresses

Saved addresses for a customer.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|
| id | INTEGER | No | autoincrement | PRIMARY KEY | |
| customer_id | INTEGER | No | - | FK -> customers(id) ON DELETE CASCADE | Owning customer |
| label | TEXT | Yes | NULL | - | User label, e.g. "Home", "Work" |
| address_json | TEXT | No | '{}' | - | JSON address object (see notes) |
| is_default | INTEGER | No | 0 | - | Boolean: default address flag |

**Indexes:**

- `idx_customer_addresses_customer_id` on (`customer_id`) - FK lookup, listing addresses per customer
- `idx_customer_addresses_default` on (`customer_id`, `is_default`) - Quick lookup of default address

**Notes:** `address_json` structure: `{"first_name": "", "last_name": "", "company": "", "address1": "", "address2": "", "city": "", "province": "", "province_code": "", "country": "", "country_code": "", "zip": "", "phone": ""}`.

---

### orders

Placed orders with financial and fulfillment tracking.

| Column | Type | Nullable | Default | Constraints | Notes |
|---|---|---|---|---|---|


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

aggregated_output:
<?php

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Customer>
 */
class CustomerFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'email' => fake()->unique()->safeEmail(),
            'password' => 'password',
            'name' => fake()->name(),
            'marketing_opt_in' => false,
        ];
    }

    public function guest(): static
    {
        return $this->state(fn (array $attributes): array => [
            'password' => null,
        ]);
    }
}


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

namespace Database\Factories;

use App\Models\Store;
use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Customer>
 */
class CustomerFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'store_id' => Store::factory(),
            'email' => fake()->unique()->safeEmail(),
            'password' => 'password',
            'name' => fake()->name(),
            'marketing_opt_in' => false,
        ];
    }

    public function guest(): static
    {
        return $this->state(fn (array $attributes): array => [
            'password' => null,
        ]);
    }
}


command: rg "CustomerAddressSeeder|Address saved|account\.addresses|Logout|My Account|account/orders" -n . | sed -n '1,240p'

type: search

cmd: rg "CustomerAddressSeeder|Address saved|account\\.addresses|Logout|My Account|account/orders" -n .

query: CustomerAddressSeeder|Address saved|account\.addresses|Logout|My Account|account/orders

aggregated_output:
./routes/web.php:173:    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
./specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
./specs/progress.md:128:- 2026-05-04: Playwright MCP verified product add-to-cart through checkout order completion, `http://shop.test/checkout/confirmation/1`, `http://shop.test/account`, and `http://shop.test/account/orders/1`; order number, line item, totals, payment/fulfillment badges, and account order history rendered with no console warnings/errors.
./specs/06-AUTH-AND-SECURITY.md:105:#### Admin Logout
./specs/06-AUTH-AND-SECURITY.md:1083:| `Illuminate\Auth\Events\Logout` | `auth.logout` | user_id from event |
./specs/05-BUSINESS-LOGIC.md:593:| `CheckoutAddressed` | Address saved on checkout |
./specs/04-STOREFRONT-UI.md:1329:| Log out card | Logout icon, "Log out", submits POST logout form |
./specs/04-STOREFRONT-UI.md:1332:### 10.4 Order History (GET /account/orders)
./specs/04-STOREFRONT-UI.md:1342:### 10.5 Order Detail (GET /account/orders/{orderNumber})
./specs/02-API-ROUTES.md:19:| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
./specs/02-API-ROUTES.md:169:| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
./specs/02-API-ROUTES.md:170:| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
./database/seeders/CustomerAddressSeeder.php:7:class CustomerAddressSeeder extends Seeder
./specs/08-PLAYWRIGHT-E2E-PLAN.md:333:    Dashboard --> LoginPage: click profile dropdown then "Logout"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:437:7. Click "Logout"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2090:    Register -->|valid data + click 'Create account'| Dashboard[My Account Dashboard]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2093:    Dashboard --> OrderHistory[/account/orders]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2094:    OrderHistory --> OrderDetail[/account/orders/number]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2098:    Dashboard -->|click 'Logout'| Guest
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2116:- Page displays "My Account"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2168:- Page displays "My Account"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2273:- Page displays "Address saved"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2293:- Page displays "Address saved"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2308:5. Verify "My Account" is displayed
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2309:6. Click "Logout"
./specs/09-IMPLEMENTATION-ROADMAP.md:185:| Livewire component `Admin\Auth\Logout` | Admin logout action |
./specs/09-IMPLEMENTATION-ROADMAP.md:404:| `account/orders/` | `index.blade.php`, `show.blade.php` |
./specs/09-IMPLEMENTATION-ROADMAP.md:765:| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
./specs/09-IMPLEMENTATION-ROADMAP.md:766:| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
./specs/09-IMPLEMENTATION-ROADMAP.md:769:Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
./specs/09-IMPLEMENTATION-ROADMAP.md:1480:| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
./specs/09-IMPLEMENTATION-ROADMAP.md:1481:| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
./specs/09-IMPLEMENTATION-ROADMAP.md:1482:| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
./app/Livewire/Actions/Logout.php:8:class Logout
./app/Livewire/Admin/Customers/Show.php:102:        $this->dispatch('toast', type: 'success', message: __('Address saved'));
./app/Livewire/Settings/DeleteUserForm.php:6:use App\Livewire\Actions\Logout;
./app/Livewire/Settings/DeleteUserForm.php:19:    public function deleteUser(Logout $logout): void


output:
Chunk ID: c95d9e
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1541
Output:
./routes/web.php:173:    Route::livewire('account/orders/{order}', CustomerOrderShow::class)
./specs/progress.md:30:| Routes/API | `specs/02-API-ROUTES.md` | partial | Phase 1 auth routes plus catalog/cart/order/customer/discount/content/settings/search/analytics/apps/developers UI routes exist: `/`, `/collections`, `/collections/{handle}`, `/products/{handle}`, `/cart`, `/checkout`, `/checkout/confirmation/{order}`, `/search`, `/pages/{handle}`, `/account/login`, `/account/register`, `/forgot-password`, `/reset-password/{token}`, `/account/forgot-password`, `/account/reset-password/{token}`, `/account`, `/account/orders/{order}`, `/admin/login`, `/admin/forgot-password`, `/admin/reset-password/{token}`, `/admin/products`, `/admin/products/create`, `/admin/products/{product}/edit`, `/admin/collections`, `/admin/collections/create`, `/admin/collections/{collection}/edit`, `/admin/inventory`, `/admin/orders`, `/admin/orders/{order}`, `/admin/customers`, `/admin/customers/{customer}`, `/admin/discounts`, `/admin/discounts/create`, `/admin/discounts/{discount}/edit`, `/admin/pages`, `/admin/pages/create`, `/admin/pages/{page}/edit`, `/admin/navigation`, `/admin/themes`, `/admin/themes/{theme}/editor`, `/admin/settings`, `/admin/settings/shipping`, `/admin/settings/taxes`, `/admin/settings/checkout`, `/admin/settings/notifications`, `/admin/search/settings`, `/admin/analytics`, `/admin/apps`, `/admin/apps/{installation}`, and `/admin/developers`. Storefront REST API now includes cart line CRUD, checkout address/shipping/discount/payment-method selection, checkout payment completion, token-gated order lookup, product search, search suggestions, and analytics event ingestion under `/api/storefront/v1`. Admin REST API now includes store-scoped product/customer/order list/detail, collection CRUD, discount CRUD, content page CRUD, store settings read/update, search status/reindex endpoints, analytics summary, order export creation/status, shipping zone/rate settings, tax settings read/update, theme upload/publish/settings update, fulfillment creation, and refund creation under `/api/admin/v1/stores/{store}` with session or scoped bearer-token authentication. `/oauth/authorize`, `/oauth/token`, and `/api/apps/v1/stores/{store}` app routes are registered as explicit `501 Not Implemented` stubs per the deferred OAuth/app ecosystem scope note. |
./specs/progress.md:128:- 2026-05-04: Playwright MCP verified product add-to-cart through checkout order completion, `http://shop.test/checkout/confirmation/1`, `http://shop.test/account`, and `http://shop.test/account/orders/1`; order number, line item, totals, payment/fulfillment badges, and account order history rendered with no console warnings/errors.
./specs/06-AUTH-AND-SECURITY.md:105:#### Admin Logout
./specs/06-AUTH-AND-SECURITY.md:1083:| `Illuminate\Auth\Events\Logout` | `auth.logout` | user_id from event |
./specs/05-BUSINESS-LOGIC.md:593:| `CheckoutAddressed` | Address saved on checkout |
./specs/04-STOREFRONT-UI.md:1329:| Log out card | Logout icon, "Log out", submits POST logout form |
./specs/04-STOREFRONT-UI.md:1332:### 10.4 Order History (GET /account/orders)
./specs/04-STOREFRONT-UI.md:1342:### 10.5 Order Detail (GET /account/orders/{orderNumber})
./specs/02-API-ROUTES.md:19:| POST | `/admin/logout` | Logout and invalidate session | Handled by Livewire action |
./specs/02-API-ROUTES.md:169:| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
./specs/02-API-ROUTES.md:170:| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
./database/seeders/CustomerAddressSeeder.php:7:class CustomerAddressSeeder extends Seeder
./specs/08-PLAYWRIGHT-E2E-PLAN.md:333:    Dashboard --> LoginPage: click profile dropdown then "Logout"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:437:7. Click "Logout"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2090:    Register -->|valid data + click 'Create account'| Dashboard[My Account Dashboard]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2093:    Dashboard --> OrderHistory[/account/orders]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2094:    OrderHistory --> OrderDetail[/account/orders/number]
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2098:    Dashboard -->|click 'Logout'| Guest
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2116:- Page displays "My Account"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2168:- Page displays "My Account"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2273:- Page displays "Address saved"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2293:- Page displays "Address saved"
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2308:5. Verify "My Account" is displayed
./specs/08-PLAYWRIGHT-E2E-PLAN.md:2309:6. Click "Logout"
./specs/09-IMPLEMENTATION-ROADMAP.md:185:| Livewire component `Admin\Auth\Logout` | Admin logout action |
./specs/09-IMPLEMENTATION-ROADMAP.md:404:| `account/orders/` | `index.blade.php`, `show.blade.php` |
./specs/09-IMPLEMENTATION-ROADMAP.md:765:| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
./specs/09-IMPLEMENTATION-ROADMAP.md:766:| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
./specs/09-IMPLEMENTATION-ROADMAP.md:769:Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.
./specs/09-IMPLEMENTATION-ROADMAP.md:1480:| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
./specs/09-IMPLEMENTATION-ROADMAP.md:1481:| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
./specs/09-IMPLEMENTATION-ROADMAP.md:1482:| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
./app/Livewire/Actions/Logout.php:8:class Logout
./app/Livewire/Admin/Customers/Show.php:102:        $this->dispatch('toast', type: 'success', message: __('Address saved'));
./app/Livewire/Settings/DeleteUserForm.php:6:use App\Livewire\Actions\Logout;
./app/Livewire/Settings/DeleteUserForm.php:19:    public function deleteUser(Logout $logout): void


cmd: sed -n '1280,1365p' specs/04-STOREFRONT-UI.md


cmd: sed -n '2080,2320p' specs/08-PLAYWRIGHT-E2E-PLAN.md


cmd: sed -n '740,780p' specs/09-IMPLEMENTATION-ROADMAP.md && sed -n '1468,1490p' specs/09-IMPLEMENTATION-ROADMAP.md


cmd: sed -n '150,180p' specs/02-API-ROUTES.md


command: sed -n '1280,1365p' specs/04-STOREFRONT-UI.md

aggregated_output:
|  Create an account                                            |
|                                                               |
|  [Name input]                                                 |
|  [Email input]                                                |
|  [Password input]                                             |
|  [Confirm password input]                                     |
|  [ ] Subscribe to marketing emails                            |
|  [Create account button]                                      |
|                                                               |
|  Already have an account? [Log in]                            |
+--------------------------------------------------------------+
```

| Element | Description |
|---|---|
| Container | Narrow max-width, centered, generous vertical padding |
| Heading | H1, medium-large, bold, centered |
| Form | Vertical stack with gap |
| Name | Text type, required, autocomplete for name |
| Email | Email type, required, autocomplete for email |
| Password | Password type, required, minimum 8 characters, autocomplete for new password |
| Confirm password | Password type, required, autocomplete for new password |
| Marketing checkbox | Checkbox with label "Subscribe to marketing emails" |
| Submit button | Primary, full-width, "Create account" |
| On success | Redirect to account dashboard |

### 10.3 Account Dashboard (GET /account)

```
+--------------------------------------------------------------+
|  Welcome back, Jane!                                          |
+--------------------------------------------------------------+
|  [Orders card]     [Addresses card]     [Log out card]        |
+--------------------------------------------------------------+
|  Recent Orders                                                |
|  # | Date       | Status  | Total   | Action                 |
|  --|------------|---------|---------|--------                 |
|  1042 | Jan 15  | Paid    | $98.70  | View                   |
|  1041 | Jan 10  | Shipped | $45.00  | View                   |
+--------------------------------------------------------------+
```

| Element | Description |
|---|---|
| Container | Constrained max-width, centered, with padding |
| Welcome heading | H1, medium-large bold |
| Quick links | Responsive grid: 1 column on mobile, 3 on tablet+. Each card has border, rounded corners, padding, hover shadow |
| Orders card | Order icon, "Order history", "View all your orders" |
| Addresses card | Location icon, "Addresses", "Manage your addresses" |
| Log out card | Logout icon, "Log out", submits POST logout form |
| Recent orders | Table showing last 5 orders. Status shown as badge component |

### 10.4 Order History (GET /account/orders)

| Attribute | Description |
|---|---|
| Heading | H1, "Order History" |
| Table columns | Order (link), Date, Status (badge), Total, Action ("View" link) |
| Status badge colors | Pending: yellow. Paid: green. Fulfilled: blue. Cancelled: gray. Refunded: red |
| Mobile | Card-based layout instead of table. Each card shows order number, date, status, and total |
| Pagination | Standard pagination component |

### 10.5 Order Detail (GET /account/orders/{orderNumber})

```
+--------------------------------------------------------------+
|  Order #1042                      Status: [Paid] [Fulfilled] |
|  Placed on January 15, 2026                                  |
+--------------------------------------------------------------+
|  Items                                                        |
|  [Thumb] Product Title x2              $49.98                 |
|          Size: M                                              |
+--------------------------------------------------------------+
|  Shipping Address    |  Billing Address   | Payment           |
|  Jane Doe            |  Same as shipping  | Visa ****4242     |
|  123 Main St         |                    |                   |
+--------------------------------------------------------------+
|  Subtotal        $84.97                                       |
|  Shipping         $7.99                                       |
|  Tax              $5.74                                       |
|  Discount        -$0.00                                       |
|  Total           $98.70                                       |
+--------------------------------------------------------------+
|  Fulfillment                                                  |
|  Shipped via UPS - 1Z999AA10123456784                         |
|  [Track shipment ->]                                          |


command: sed -n '2080,2320p' specs/08-PLAYWRIGHT-E2E-PLAN.md

aggregated_output:
**File:** `tests/Browser/Storefront/CustomerAccountTest.php`

**Purpose:** Verify customer registration, login, account dashboard, order history, order detail, address management, and logout on the storefront.

### Customer Account Flow

```mermaid
flowchart TD
    Guest[Guest visitor] -->|visit /account/register| Register[Registration Form]
    Guest -->|visit /account/login| Login[Login Form]
    Register -->|valid data + click 'Create account'| Dashboard[My Account Dashboard]
    Login -->|valid credentials + click 'Log in'| Dashboard
    Login -->|invalid credentials| Login
    Dashboard --> OrderHistory[/account/orders]
    OrderHistory --> OrderDetail[/account/orders/number]
    Dashboard --> Addresses[/account/addresses]
    Addresses -->|click 'Add address'| AddAddress[Address form]
    Addresses -->|click 'Edit'| EditAddress[Edit address form]
    Dashboard -->|click 'Logout'| Guest
```

---

### Test 10.1: Can register a new customer

**Preconditions:** None

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `New Customer`
3. Enter email: `new-customer-e2e@example.com`
4. Enter password: `password123`
5. Enter password confirmation: `password123`
6. Click "Create account"

**Expected Results:**
- Page displays "My Account"
- No JavaScript errors

---

### Test 10.2: Shows validation errors for duplicate email registration

**Preconditions:** customer@acme.test already exists

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `Duplicate Customer`
3. Enter email: `customer@acme.test`
4. Enter password: `password123`
5. Enter password confirmation: `password123`
6. Click "Create account"

**Expected Results:**
- Page displays "already been taken" error
- No JavaScript errors

---

### Test 10.3: Shows validation errors for mismatched passwords

**Preconditions:** None

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `Test Customer`
3. Enter email: `mismatch@example.com`
4. Enter password: `password123`
5. Enter password confirmation: `different456`
6. Click "Create account"

**Expected Results:**
- Validation error referencing "password" is displayed
- No JavaScript errors

---

### Test 10.4: Can log in as existing customer

**Preconditions:** customer@acme.test seeded

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"

**Expected Results:**
- Page displays "My Account"
- Page displays "John Doe"
- No JavaScript errors

---

### Test 10.5: Shows error for invalid customer credentials

**Preconditions:** customer@acme.test seeded

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `wrongpassword`
4. Click "Log in"

**Expected Results:**
- Page displays "Invalid credentials"
- No JavaScript errors

---

### Test 10.6: Redirects unauthenticated customers to login

**Preconditions:** Not logged in

**Steps:**
1. Navigate to `/account` (without logging in)

**Expected Results:**
- Page displays "Log in" (redirected to login form)
- No JavaScript errors

---

### Test 10.7: Shows order history for logged-in customer

**Preconditions:** Logged in as customer@acme.test, Orders #1001/#1002/#1004 exist

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Click "Orders" or navigate to order history

**Expected Results:**
- Page displays "#1001"
- Page displays "#1002"
- Page displays "#1004"
- No JavaScript errors

---

### Test 10.8: Shows order detail for customer order

**Preconditions:** Logged in as customer, Order #1001 exists

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Orders"
4. Click "#1001"

**Expected Results:**
- Page displays "#1001"
- Page displays "Subtotal"
- Page displays "Total"
- No JavaScript errors

---

### Test 10.9: Can view addresses

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"

**Expected Results:**
- Existing addresses are displayed
- No JavaScript errors

---

### Test 10.10: Can add a new address

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"
4. Click "Add address"
5. Enter first name: `John`
6. Enter last name: `Doe`
7. Enter address: `New Street 42`
8. Enter city: `Hamburg`
9. Enter postal code: `20095`
10. Enter country: `DE`
11. Click "Save"

**Expected Results:**
- Page displays "Address saved"
- Page displays "New Street 42"
- Page displays "Hamburg"
- No JavaScript errors

---

### Test 10.11: Can edit an existing address

**Preconditions:** Logged in as customer, at least one address exists

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"
4. Click "Edit" on the first address
5. Change city to: `Frankfurt`
6. Click "Save"

**Expected Results:**
- Page displays "Address saved"
- Page displays "Frankfurt"
- No JavaScript errors

---

### Test 10.12: Can log out

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Verify "My Account" is displayed
6. Click "Logout"

**Expected Results:**
- Page displays "Log in" (redirected to login or home page)
- No JavaScript errors

**Total: 12 tests**

---

## Suite 11: Inventory Enforcement


command: sed -n '150,180p' specs/02-API-ROUTES.md

aggregated_output:
These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |


command: sed -n '740,780p' specs/09-IMPLEMENTATION-ROADMAP.md && sed -n '1468,1490p' specs/09-IMPLEMENTATION-ROADMAP.md

aggregated_output:
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |
|-----------|-------|-------------|
| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |

Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.

---

### Phase 7: Admin Panel

**Priority:** MEDIUM - merchant management interface.

#### Step 7.1: Admin Layout

**File to create:** `resources/views/livewire/admin/layout/app.blade.php`

| cannot confirm already confirmed payment | order with financial_status=paid | error thrown | validation |
| auto-cancel job cancels unpaid bank transfer orders after config days | order with payment_method=bank_transfer, placed_at > 7 days ago | order cancelled, inventory released | happy path |
| auto-cancel job does not cancel orders within config days | order with payment_method=bank_transfer, placed_at = 2 days ago | order remains pending | edge case |
| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |

---

#### tests/Feature/Customers/CustomerAccountTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the customer dashboard | acting as customer; GET /account | 200 response, sees customer name | happy path |
| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
| redirects unauthenticated requests to login | GET /account without auth | redirected to /account/login | authorization |
| updates customer profile | update name and marketing_opt_in | changes persisted in DB | happy path |

---

#### tests/Feature/Customers/AddressManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |


output:
Chunk ID: 6e90b9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1155
Output:
|  Create an account                                            |
|                                                               |
|  [Name input]                                                 |
|  [Email input]                                                |
|  [Password input]                                             |
|  [Confirm password input]                                     |
|  [ ] Subscribe to marketing emails                            |
|  [Create account button]                                      |
|                                                               |
|  Already have an account? [Log in]                            |
+--------------------------------------------------------------+
```

| Element | Description |
|---|---|
| Container | Narrow max-width, centered, generous vertical padding |
| Heading | H1, medium-large, bold, centered |
| Form | Vertical stack with gap |
| Name | Text type, required, autocomplete for name |
| Email | Email type, required, autocomplete for email |
| Password | Password type, required, minimum 8 characters, autocomplete for new password |
| Confirm password | Password type, required, autocomplete for new password |
| Marketing checkbox | Checkbox with label "Subscribe to marketing emails" |
| Submit button | Primary, full-width, "Create account" |
| On success | Redirect to account dashboard |

### 10.3 Account Dashboard (GET /account)

```
+--------------------------------------------------------------+
|  Welcome back, Jane!                                          |
+--------------------------------------------------------------+
|  [Orders card]     [Addresses card]     [Log out card]        |
+--------------------------------------------------------------+
|  Recent Orders                                                |
|  # | Date       | Status  | Total   | Action                 |
|  --|------------|---------|---------|--------                 |
|  1042 | Jan 15  | Paid    | $98.70  | View                   |
|  1041 | Jan 10  | Shipped | $45.00  | View                   |
+--------------------------------------------------------------+
```

| Element | Description |
|---|---|
| Container | Constrained max-width, centered, with padding |
| Welcome heading | H1, medium-large bold |
| Quick links | Responsive grid: 1 column on mobile, 3 on tablet+. Each card has border, rounded corners, padding, hover shadow |
| Orders card | Order icon, "Order history", "View all your orders" |
| Addresses card | Location icon, "Addresses", "Manage your addresses" |
| Log out card | Logout icon, "Log out", submits POST logout form |
| Recent orders | Table showing last 5 orders. Status shown as badge component |

### 10.4 Order History (GET /account/orders)

| Attribute | Description |
|---|---|
| Heading | H1, "Order History" |
| Table columns | Order (link), Date, Status (badge), Total, Action ("View" link) |
| Status badge colors | Pending: yellow. Paid: green. Fulfilled: blue. Cancelled: gray. Refunded: red |
| Mobile | Card-based layout instead of table. Each card shows order number, date, status, and total |
| Pagination | Standard pagination component |

### 10.5 Order Detail (GET /account/orders/{orderNumber})

```
+--------------------------------------------------------------+
|  Order #1042                      Status: [Paid] [Fulfilled] |
|  Placed on January 15, 2026                                  |
+--------------------------------------------------------------+
|  Items                                                        |
|  [Thumb] Product Title x2              $49.98                 |
|          Size: M                                              |
+--------------------------------------------------------------+
|  Shipping Address    |  Billing Address   | Payment           |
|  Jane Doe            |  Same as shipping  | Visa ****4242     |
|  123 Main St         |                    |                   |
+--------------------------------------------------------------+
|  Subtotal        $84.97                                       |
|  Shipping         $7.99                                       |
|  Tax              $5.74                                       |
|  Discount        -$0.00                                       |
|  Total           $98.70                                       |
+--------------------------------------------------------------+
|  Fulfillment                                                  |
|  Shipped via UPS - 1Z999AA10123456784                         |
|  [Track shipment ->]                                          |


output:
Chunk ID: 611dbd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1414
Output:
**File:** `tests/Browser/Storefront/CustomerAccountTest.php`

**Purpose:** Verify customer registration, login, account dashboard, order history, order detail, address management, and logout on the storefront.

### Customer Account Flow

```mermaid
flowchart TD
    Guest[Guest visitor] -->|visit /account/register| Register[Registration Form]
    Guest -->|visit /account/login| Login[Login Form]
    Register -->|valid data + click 'Create account'| Dashboard[My Account Dashboard]
    Login -->|valid credentials + click 'Log in'| Dashboard
    Login -->|invalid credentials| Login
    Dashboard --> OrderHistory[/account/orders]
    OrderHistory --> OrderDetail[/account/orders/number]
    Dashboard --> Addresses[/account/addresses]
    Addresses -->|click 'Add address'| AddAddress[Address form]
    Addresses -->|click 'Edit'| EditAddress[Edit address form]
    Dashboard -->|click 'Logout'| Guest
```

---

### Test 10.1: Can register a new customer

**Preconditions:** None

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `New Customer`
3. Enter email: `new-customer-e2e@example.com`
4. Enter password: `password123`
5. Enter password confirmation: `password123`
6. Click "Create account"

**Expected Results:**
- Page displays "My Account"
- No JavaScript errors

---

### Test 10.2: Shows validation errors for duplicate email registration

**Preconditions:** customer@acme.test already exists

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `Duplicate Customer`
3. Enter email: `customer@acme.test`
4. Enter password: `password123`
5. Enter password confirmation: `password123`
6. Click "Create account"

**Expected Results:**
- Page displays "already been taken" error
- No JavaScript errors

---

### Test 10.3: Shows validation errors for mismatched passwords

**Preconditions:** None

**Steps:**
1. Navigate to `/account/register`
2. Enter name: `Test Customer`
3. Enter email: `mismatch@example.com`
4. Enter password: `password123`
5. Enter password confirmation: `different456`
6. Click "Create account"

**Expected Results:**
- Validation error referencing "password" is displayed
- No JavaScript errors

---

### Test 10.4: Can log in as existing customer

**Preconditions:** customer@acme.test seeded

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"

**Expected Results:**
- Page displays "My Account"
- Page displays "John Doe"
- No JavaScript errors

---

### Test 10.5: Shows error for invalid customer credentials

**Preconditions:** customer@acme.test seeded

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `wrongpassword`
4. Click "Log in"

**Expected Results:**
- Page displays "Invalid credentials"
- No JavaScript errors

---

### Test 10.6: Redirects unauthenticated customers to login

**Preconditions:** Not logged in

**Steps:**
1. Navigate to `/account` (without logging in)

**Expected Results:**
- Page displays "Log in" (redirected to login form)
- No JavaScript errors

---

### Test 10.7: Shows order history for logged-in customer

**Preconditions:** Logged in as customer@acme.test, Orders #1001/#1002/#1004 exist

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Click "Orders" or navigate to order history

**Expected Results:**
- Page displays "#1001"
- Page displays "#1002"
- Page displays "#1004"
- No JavaScript errors

---

### Test 10.8: Shows order detail for customer order

**Preconditions:** Logged in as customer, Order #1001 exists

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Orders"
4. Click "#1001"

**Expected Results:**
- Page displays "#1001"
- Page displays "Subtotal"
- Page displays "Total"
- No JavaScript errors

---

### Test 10.9: Can view addresses

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"

**Expected Results:**
- Existing addresses are displayed
- No JavaScript errors

---

### Test 10.10: Can add a new address

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"
4. Click "Add address"
5. Enter first name: `John`
6. Enter last name: `Doe`
7. Enter address: `New Street 42`
8. Enter city: `Hamburg`
9. Enter postal code: `20095`
10. Enter country: `DE`
11. Click "Save"

**Expected Results:**
- Page displays "Address saved"
- Page displays "New Street 42"
- Page displays "Hamburg"
- No JavaScript errors

---

### Test 10.11: Can edit an existing address

**Preconditions:** Logged in as customer, at least one address exists

**Steps:**
1. Navigate to `/account/login`
2. Log in as customer (email: `customer@acme.test`, password: `password`)
3. Click "Addresses"
4. Click "Edit" on the first address
5. Change city to: `Frankfurt`
6. Click "Save"

**Expected Results:**
- Page displays "Address saved"
- Page displays "Frankfurt"
- No JavaScript errors

---

### Test 10.12: Can log out

**Preconditions:** Logged in as customer

**Steps:**
1. Navigate to `/account/login`
2. Enter email: `customer@acme.test`
3. Enter password: `password`
4. Click "Log in"
5. Verify "My Account" is displayed
6. Click "Logout"

**Expected Results:**
- Page displays "Log in" (redirected to login or home page)
- No JavaScript errors

**Total: 12 tests**

---

## Suite 11: Inventory Enforcement


output:
Chunk ID: 323e38
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 802
Output:
| Event | `OrderFulfilled` | Dispatched when all order lines are fulfilled |
| Event | `OrderCancelled` | Dispatched when order is cancelled |
| Event | `OrderRefunded` | Dispatched when refund is processed |

---

### Phase 6: Customer Accounts

**Priority:** MEDIUM - enhances the shopping experience.

#### Step 6.1: Customer Auth

- `Customer` model with custom `customer` guard (already configured in Phase 1)
- Custom `CustomerUserProvider` - scopes queries by `store_id`
- Customer email unique per store, not globally

#### Step 6.2: Customer Account Pages (Livewire)

**Files to create:**

| Component | Route | Description |
|-----------|-------|-------------|
| `Storefront\Account\Auth\Login` | `GET /account/login` | Login form |
| `Storefront\Account\Auth\Register` | `GET /account/register` | Registration form |
| `Storefront\Account\Dashboard` | `GET /account` | Overview with recent orders |
| `Storefront\Account\Orders\Index` | `GET /account/orders` | Paginated order history |
| `Storefront\Account\Orders\Show` | `GET /account/orders/{orderNumber}` | Order detail with timeline |
| `Storefront\Account\Addresses\Index` | `GET /account/addresses` | Address book CRUD |

Logout: `POST /account/logout` handled by Livewire action. Invalidates session, regenerates CSRF token, redirects to `/account/login`.

---

### Phase 7: Admin Panel

**Priority:** MEDIUM - merchant management interface.

#### Step 7.1: Admin Layout

**File to create:** `resources/views/livewire/admin/layout/app.blade.php`

| cannot confirm already confirmed payment | order with financial_status=paid | error thrown | validation |
| auto-cancel job cancels unpaid bank transfer orders after config days | order with payment_method=bank_transfer, placed_at > 7 days ago | order cancelled, inventory released | happy path |
| auto-cancel job does not cancel orders within config days | order with payment_method=bank_transfer, placed_at = 2 days ago | order remains pending | edge case |
| auto-fulfills digital products on payment confirmation | bank_transfer order with only digital items, confirm payment | fulfillment auto-created with status=delivered | happy path |

---

#### tests/Feature/Customers/CustomerAccountTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |
|-----------|----------------------|----------------|----------|
| renders the customer dashboard | acting as customer; GET /account | 200 response, sees customer name | happy path |
| lists customer orders | customer with 3 orders; GET /account/orders | sees all 3 order numbers | happy path |
| shows order detail | customer with order #1001; GET /account/orders/1001 | sees order items and totals | happy path |
| prevents accessing another customers orders | customer A logged in; GET /account/orders/{customerB-order-number} | 404 response | authorization |
| redirects unauthenticated requests to login | GET /account without auth | redirected to /account/login | authorization |
| updates customer profile | update name and marketing_opt_in | changes persisted in DB | happy path |

---

#### tests/Feature/Customers/AddressManagementTest.php

| Test Name | Setup / Preconditions | Key Assertions | Category |


output:
Chunk ID: 973445
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 479
Output:
These routes serve the public-facing storefront using Blade templates rendered through the active theme engine.

| Method | Path | Description | Component / View |
|--------|------|-------------|------------------|
| GET | `/` | Home page | `Storefront\Home` |
| GET | `/collections` | All collections listing | `Storefront\Collections\Index` |
| GET | `/collections/{handle}` | Single collection page with product grid and filters | `Storefront\Collections\Show` |
| GET | `/products/{handle}` | Product page - gallery, variant selector, add-to-cart | `Storefront\Products\Show` |
| GET | `/cart` | Full cart page - line items, quantity controls, discount input, totals | `Storefront\Cart\Show` |
| GET | `/search` | Search results page with filter panel, sort dropdown, product grid | `Storefront\Search\Index` |
| GET | `/pages/{handle}` | Static content page (About, FAQ, etc.) | `Storefront\Pages\Show` |

#### Customer Account Routes

**Additional middleware:** `auth:customer`

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account` | Customer account overview | `Storefront\Account\Dashboard` |
| GET | `/account/orders` | Order history list | `Storefront\Account\Orders\Index` |
| GET | `/account/orders/{orderNumber}` | Order detail | `Storefront\Account\Orders\Show` |
| GET | `/account/addresses` | Address book management | `Storefront\Account\Addresses\Index` |

#### Customer Auth Routes (no auth required)

| Method | Path | Description | Component |
|--------|------|-------------|-----------|
| GET | `/account/login` | Customer login page | `Storefront\Account\Auth\Login` |
| GET | `/account/register` | Customer registration page | `Storefront\Account\Auth\Register` |
| GET | `/forgot-password` | Forgot password page | `Storefront\Account\Auth\ForgotPassword` |
| POST | `/forgot-password` | Send reset link | Handled by Livewire action |


step: Add customer account routes, logout, account navigation, and address book component/UI

step: Seed a default customer address and add focused account feature coverage

status: pending

step: Add Spec 08 Suite 10 browser tests

step: Run formatter and focused/full tests

step: Update progress and commit the slice

explanation: Suite 10 needs a small product change before tests can match the spec: `/account/orders`, `/account/addresses`, and customer logout are currently absent.


output: Plan updated


message: I’ve got the shape now. I’m going to add the missing account surface in the smallest coherent pass: route aliases/logout, a reusable account nav partial, a Livewire address book page, and a seeded default address for the seeded customer.

phase: commentary


type: message

role: assistant

text: I’ve got the shape now. I’m going to add the missing account surface in the smallest coherent pass: route aliases/logout, a reusable account nav partial, a Livewire address book page, and a seeded default address for the seeded customer.


cmd: php artisan make:livewire Storefront/Account/Addresses/Index --class --no-interaction


output:
Chunk ID: 8f47b2
Wall time: 0.2052 seconds
Process exited with code 0
Original token count: 27
Output:

   INFO  Livewire component [app/Livewire/Storefront/Account/Addresses/Index.php] created successfully.


command: php artisan make:livewire Storefront/Account/Addresses/Index --class --no-interaction

type: unknown

aggregated_output: INFO  Livewire component [app/Livewire/Storefront/Account/Addresses/Index.php] created successfully.


cmd: sed -n '1,200p' app/Livewire/Storefront/Account/Addresses/Index.php


cmd: sed -n '1,220p' resources/views/livewire/storefront/account/addresses/index.blade.php


cmd: find resources/views/livewire/storefront/account -maxdepth 3 -type d -print | sort


command: sed -n '1,200p' app/Livewire/Storefront/Account/Addresses/Index.php

aggregated_output:
<?php

namespace App\Livewire\Storefront\Account\Addresses;

use Livewire\Component;

class Index extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.addresses.index');
    }
}


command: sed -n '1,220p' resources/views/livewire/storefront/account/addresses/index.blade.php

aggregated_output: {{-- Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less. - Maria Skłodowska-Curie --}}


command: find resources/views/livewire/storefront/account -maxdepth 3 -type d -print | sort

path: account


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

namespace App\Livewire\Storefront\Account\Addresses;

use Livewire\Component;

class Index extends Component
{
    public function render()
    {
        return view('livewire.storefront.account.addresses.index');
    }
}


output:
Chunk ID: 9bf621
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 45
Output:
<div>
    {{-- Nothing in life is to be feared, it is only to be understood. Now is the time to understand more, so that we may fear less. - Maria Skłodowska-Curie --}}
</div>


output:
Chunk ID: f2ad62
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 50
Output:
resources/views/livewire/storefront/account
resources/views/livewire/storefront/account/addresses
resources/views/livewire/storefront/account/auth
resources/views/livewire/storefront/account/orders


message: This change also touches Blade utility classes, so I’m applying the Tailwind skill for the account layout styling before editing those views.


text: This change also touches Blade utility classes, so I’m applying the Tailwind skill for the account layout styling before editing those views.


cmd: sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/tailwindcss-development/SKILL.md


command: sed -n '1,220p' /Users/fabianwesner/Herd/shop/.agents/skills/tailwindcss-development/SKILL.md

aggregated_output:
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
  author: laravel
---

# Tailwind CSS Development

## Documentation

Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.

## Basic Usage

- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.

## Tailwind CSS v4 Specifics

- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.

### CSS-First Configuration

In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:

<!-- CSS-First Config -->
```css
@theme {
  --color-brand: oklch(0.72 0.11 178);
}
```

### Import Syntax

In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:

<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```

### Replaced Utilities

Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.

| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |

## Spacing

Use `gap` utilities instead of margins for spacing between siblings:

<!-- Gap Utilities -->
```html
<div class="flex gap-8">
    <div>Item 1</div>
    <div>Item 2</div>
</div>
```

## Dark Mode

If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:

<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
    Content adapts to color scheme
</div>
```

## Common Patterns

### Flexbox Layout

<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
    <div>Left content</div>
    <div>Right content</div>
</div>
```

### Grid Layout

<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <div>Card 1</div>
    <div>Card 2</div>
    <div>Card 3</div>
</div>
```

## Common Pitfalls

- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode


output:
Chunk ID: b28acd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 962
Output:
---
name: tailwindcss-development
description: "Always invoke when the user's message includes 'tailwind' in any form. Also invoke for: building responsive grid layouts (multi-column card grids, product grids), flex/grid page structures (dashboards with sidebars, fixed topbars, mobile-toggle navs), styling UI components (cards, tables, navbars, pricing sections, forms, inputs, badges), adding dark mode variants, fixing spacing or typography, and Tailwind v3/v4 work. The core use case: writing or fixing Tailwind utility classes in HTML templates (Blade, JSX, Vue). Skip for backend PHP logic, database queries, API routes, JavaScript with no HTML/CSS component, CSS file audits, build tool configuration, and vanilla CSS."
license: MIT
metadata:
  author: laravel
---

# Tailwind CSS Development

## Documentation

Use `search-docs` for detailed Tailwind CSS v4 patterns and documentation.

## Basic Usage

- Use Tailwind CSS classes to style HTML. Check and follow existing Tailwind conventions in the project before introducing new patterns.
- Offer to extract repeated patterns into components that match the project's conventions (e.g., Blade, JSX, Vue).
- Consider class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child elements carefully to reduce repetition, and group elements logically.

## Tailwind CSS v4 Specifics

- Always use Tailwind CSS v4 and avoid deprecated utilities.
- `corePlugins` is not supported in Tailwind v4.

### CSS-First Configuration

In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed:

<!-- CSS-First Config -->
```css
@theme {
  --color-brand: oklch(0.72 0.11 178);
}
```

### Import Syntax

In Tailwind v4, import Tailwind with a regular CSS `@import` statement instead of the `@tailwind` directives used in v3:

<!-- v4 Import Syntax -->
```diff
- @tailwind base;
- @tailwind components;
- @tailwind utilities;
+ @import "tailwindcss";
```

### Replaced Utilities

Tailwind v4 removed deprecated utilities. Use the replacements shown below. Opacity values remain numeric.

| Deprecated | Replacement |
|------------|-------------|
| bg-opacity-* | bg-black/* |
| text-opacity-* | text-black/* |
| border-opacity-* | border-black/* |
| divide-opacity-* | divide-black/* |
| ring-opacity-* | ring-black/* |
| placeholder-opacity-* | placeholder-black/* |
| flex-shrink-* | shrink-* |
| flex-grow-* | grow-* |
| overflow-ellipsis | text-ellipsis |
| decoration-slice | box-decoration-slice |
| decoration-clone | box-decoration-clone |

## Spacing

Use `gap` utilities instead of margins for spacing between siblings:

<!-- Gap Utilities -->
```html
<div class="flex gap-8">
    <div>Item 1</div>
    <div>Item 2</div>
</div>
```

## Dark Mode

If existing pages and components support dark mode, new pages and components must support it the same way, typically using the `dark:` variant:

<!-- Dark Mode -->
```html
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
    Content adapts to color scheme
</div>
```

## Common Patterns

### Flexbox Layout

<!-- Flexbox Layout -->
```html
<div class="flex items-center justify-between gap-4">
    <div>Left content</div>
    <div>Right content</div>
</div>
```

### Grid Layout

<!-- Grid Layout -->
```html
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    <div>Card 1</div>
    <div>Card 2</div>
    <div>Card 3</div>
</div>
```

## Common Pitfalls

- Using deprecated v3 utilities (bg-opacity-*, flex-shrink-*, etc.)
- Using `@tailwind` directives instead of `@import "tailwindcss"`
- Trying to use `tailwind.config.js` instead of CSS `@theme` directive
- Using margins for spacing between siblings instead of gap utilities
- Forgetting to add dark mode variants when the project uses dark mode


packages: tailwindcss livewire/flux

queries:
responsive grid dark mode forms buttons
badge button input select form field