text:
Script completed
Wall time 0.1 seconds
Output:

text:
<?php

namespace App\Services;

use App\Contracts\TaxProvider;
use App\Models\TaxSettings;
use App\ValueObjects\TaxCalculationRequest;
use App\ValueObjects\TaxLine;
use App\ValueObjects\TaxResult;

class TaxCalculator
{
    public function __construct(private readonly ?TaxProvider $provider = null) {}

    public function calculate(int $amount, TaxSettings $settings, array $address): TaxResult
    {
        return ($this->provider ?? new \App\Services\Tax\ManualTaxProvider())->calculate(new TaxCalculationRequest([['amount' => $amount]], 0, $address, $settings));
    }

    public function extractInclusive(int $grossAmount, int $rateBasisPoints): int
    {
        return $rateBasisPoints > 0 ? intdiv($grossAmount * $rateBasisPoints, 10000 + $rateBasisPoints) : 0;
    }

    public function addExclusive(int $netAmount, int $rateBasisPoints): int
    {
        return intdiv($netAmount * $rateBasisPoints, 10000);
    }
}
<?php

namespace App\Providers;

use App\Auth\CustomerUserProvider;
use App\Auth\StoreScopedPasswordBrokerManager;
use App\Contracts\PaymentProvider as PaymentProviderContract;
use App\Models\Product;
use App\Observers\ProductObserver;
use App\Services\MockPaymentProvider;
use Carbon\CarbonImmutable;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Livewire\Livewire;
use Livewire\Mechanisms\HandleRequests\EndpointResolver;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        $this->app->bind(PaymentProviderContract::class, MockPaymentProvider::class);
        Auth::provider('customer', fn ($app, array $config): CustomerUserProvider => new CustomerUserProvider($app['hash'], $config['model']));
        $this->app->extend('auth.password', fn ($manager, $app): StoreScopedPasswordBrokerManager => new StoreScopedPasswordBrokerManager($app));
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        $this->configureDefaults();
        Model::preventLazyLoading(! app()->isProduction());
        Product::observe(ProductObserver::class);
        Livewire::setUpdateRoute(function ($handle): mixed {
            return Route::post(EndpointResolver::updatePath(), $handle)
                ->middleware(['web', 'store.resolve'])
                ->name('shop.livewire.update');
        });
        RateLimiter::for('login', fn (Request $request): Limit => Limit::perMinute(5)->by($request->ip()));
        RateLimiter::for('api.storefront', fn (Request $request): Limit => Limit::perMinute(120)->by($request->ip()));
        RateLimiter::for('api.admin', fn (Request $request): Limit => Limit::perMinute(60)->by($request->user()?->getAuthIdentifier() ?? $request->ip()));
        RateLimiter::for('checkout', fn (Request $request): Limit => Limit::perMinute(10)->by($request->hasSession() ? $request->session()->getId() : $request->ip()));
        RateLimiter::for('search', fn (Request $request): Limit => Limit::perMinute(60)->by($request->ip()));
        RateLimiter::for('analytics', fn (Request $request): Limit => Limit::perMinute(60)->by($request->ip()));
        RateLimiter::for('webhooks', fn (Request $request): Limit => Limit::perMinute(30)->by($request->ip()));
    }

    /**
     * Configure default behaviors for production-ready applications.
     */
    protected function configureDefaults(): void
    {
        Date::use(CarbonImmutable::class);

        DB::prohibitDestructiveCommands(
            app()->isProduction(),
        );

        Password::defaults(fn (): ?Password => app()->isProduction()
            ? Password::min(12)
                ->mixedCase()
                ->letters()
                ->numbers()
                ->symbols()
                ->uncompromised()
            : null
        );
    }
}


exec

status: completed

input:
const patch = "*** Begin Patch\n*** Update File: /Users/wesner/Herd/shop/app/Providers/AppServiceProvider.php\n@@\n use App\\Auth\\StoreScopedPasswordBrokerManager;\n use App\\Contracts\\PaymentProvider as PaymentProviderContract;\n+use App\\Contracts\\TaxProvider;\n@@\n use App\\Services\\MockPaymentProvider;\n+use App\\Services\\Tax\\ManualTaxProvider;\n@@\n         $this->app->bind(PaymentProviderContract::class, MockPaymentProvider::class);\n+        $this->app->bind(TaxProvider::class, ManualTaxProvider::class);\n*** End Patch";
text(await tools.apply_patch(patch));


type: FileChange

type: update

unified_diff:
@@ -7,2 +7,3 @@
 use App\Contracts\PaymentProvider as PaymentProviderContract;
+use App\Contracts\TaxProvider;
 use App\Models\Product;
@@ -10,2 +11,3 @@
 use App\Services\MockPaymentProvider;
+use App\Services\Tax\ManualTaxProvider;
 use Carbon\CarbonImmutable;
@@ -32,2 +34,3 @@
         $this->app->bind(PaymentProviderContract::class, MockPaymentProvider::class);
+        $this->app->bind(TaxProvider::class, ManualTaxProvider::class);
         Auth::provider('customer', fn ($app, array $config): CustomerUserProvider => new CustomerUserProvider($app['hash'], $config['model']));

status: completed

stdout:
Success. Updated the following files:
M /Users/wesner/Herd/shop/app/Providers/AppServiceProvider.php


text:
Script completed
Wall time 0.0 seconds
Output:


status: completed

input:
const r = await tools.exec_command({cmd:"php artisan test --compact tests/Unit/PricingEngineTest.php tests/Feature/CommerceFlowTest.php","workdir":"/Users/wesner/Herd/shop","yield_time_ms":1000,"max_output_tokens":12000});
text(r.output);
if (r.session_id) text((await tools.write_stdin({session_id:r.session_id, chars:"", yield_time_ms:1000, max_output_tokens:12000})).output);


type: CommandExecution

command: php artisan test --compact tests/Unit/PricingEngineTest.php tests/Feature/CommerceFlowTest.php

type: unknown

status: failed

stdout:
  ....⨯⨯⨯⨯⨯⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > guest cart and card check…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > declined payments release…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > stale cart versions retur…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > guest cart API resources…   Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > cart and checkout APIs re…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > bank transfer keeps inven…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  Tests:    6 failed, 4 passed (10 assertions)
  Duration: 0.14s


text:
Script completed
Wall time 0.3 seconds
Output:

text:
  ....⨯⨯⨯⨯⨯⨯
  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > guest cart and card check…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > declined payments release…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > stale cart versions retur…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > guest cart API resources…   Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > cart and checkout APIs re…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  ────────────────────────────────────────────────────────────────────────────
   FAILED  Tests\Feature\CommerceFlowTest > bank transfer keeps inven…  Error
  Class "Database\Seeders\Organization" not found

  at database/seeders/OrganizationSeeder.php:14
     10▕      * Run the database seeds.
     11▕      */
     12▕     public function run(): void
     13▕     {
  ➜  14▕         Organization::query()->updateOrCreate(
     15▕             ['slug' => 'acme-corp'],
     16▕             ['name' => 'Acme Corp', 'billing_email' => 'billing@acme.test', 'status' => 'active'],
     17▕         );
     18▕     }

  1   database/seeders/OrganizationSeeder.php:14
      +8 vendor frames
  10  database/seeders/ShopSeeder.php:11

  Tests:    6 failed, 4 passed (10 assertions)
  Duration: 0.14s


type: AgentMessage

type: Text

text: The tax unit tests themselves are fine, but the delegated seed-data tranche exposed an integration error: `OrganizationSeeder` is missing its model import, so commerce tests fail before reaching application logic. I’m notifying that worker while I continue the non-overlapping security/business checks.

phase: commentary


type: message

role: assistant


status: completed

input:
const r = await tools.multi_agent_v1__send_input({target:"01a02164-f25e-7991-9215-dbc3ed76f0d7",message:"Your seeders currently cause `Class \"Database\\\\Seeders\\\\Organization\" not found` in OrganizationSeeder.php. Please fix imports/autoload and run the affected commerce/seed tests before reporting back. Keep your scope unchanged."});
text(r);


type: CollabAgentToolCall

status: completed

agent_nickname: Banach

prompt: Your seeders currently cause `Class "Database\\Seeders\\Organization" not found` in OrganizationSeeder.php. Please fix imports/autoload and run the affected commerce/seed tests before reporting back. Keep your scope unchanged.

01a02164-f25e-7991-9215-dbc3ed76f0d7: running


text:
Script completed
Wall time 0.0 seconds
Output:

text: {"submission_id":"01a02175-0cf8-7083-bbc4-e509f198bf86"}


role: user

text:
<subagent_notification>
{"agent_path":"01a02164-f1f8-74a1-99d3-aa0ad4617564","status":{"completed":"Implemented within the requested scope only:\n\n- Enhanced cart quantity controls, discount breakdown/removal, totals, responsive table/cards, and accessibility.\n- Added staged checkout flow with labeled contact/address/payment fields, saved addresses, billing toggle, shipping/payment states, validation, processing state, and discount handling.\n- Added detailed order confirmation with items, address, payment status, totals, bank-transfer instructions, and order link.\n\nChanged files:\n\n- `app/Livewire/Storefront/Cart/Show.php`\n- `app/Livewire/Storefront/Checkout/Show.php`\n- `app/Livewire/Storefront/Checkout/Confirmation.php`\n- `resources/views/livewire/storefront/cart/show.blade.php`\n- `resources/views/livewire/storefront/checkout/show.blade.php`\n- `resources/views/livewire/storefront/checkout/confirmation.blade.php`\n\nVerification passed: PHP syntax, Pint, Blade cache, Vite build, and diff checks. Focused Pest tests were blocked by the existing out-of-scope `OrganizationSeeder` missing model import."}}
</subagent_notification>


status: completed

input:
const r = await tools.exec_command({cmd:"php artisan test --compact tests/Unit/PricingEngineTest.php","workdir":"/Users/wesner/Herd/shop","yield_time_ms":1000,"max_output_tokens":8000});
text(r.output);


command: php artisan test --compact tests/Unit/PricingEngineTest.php

stdout:
...

  Tests:    4 passed (10 assertions)
  Duration: 0.03s


text:
Script completed
Wall time 0.2 seconds
Output:

text:
  ....

  Tests:    4 passed (10 assertions)
  Duration: 0.03s


status: completed

input:
const r = await tools.exec_command({cmd:"git status --short | sed -n '1,180p'; sed -n '1,240p' app/Livewire/Storefront/Checkout/Show.php; sed -n '1,260p' resources/views/livewire/storefront/checkout/show.blade.php","workdir":"/Users/wesner/Herd/shop","yield_time_ms":1000,"max_output_tokens":24000});
text(r.output);


command: git status --short | sed -n '1,180p'; sed -n '1,240p' app/Livewire/Storefront/Checkout/Show.php; sed -n '1,260p' resources/views/livewire/storefront/checkout/show.blade.php

stdout:
M app/Http/Controllers/Api/AdminController.php
 M app/Http/Controllers/Api/StorefrontAnalyticsController.php
 M app/Http/Controllers/Api/StorefrontCheckoutController.php
 M app/Jobs/CleanupAbandonedCarts.php
 M app/Jobs/DeliverWebhook.php
 M app/Livewire/Admin/Analytics/Index.php
 M app/Livewire/Admin/Apps/Index.php
 M app/Livewire/Admin/Auth/Login.php
 M app/Livewire/Admin/Collections/Create.php
 M app/Livewire/Admin/Collections/Edit.php
 M app/Livewire/Admin/Collections/Index.php
 M app/Livewire/Admin/Customers/Index.php
 M app/Livewire/Admin/Customers/Show.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Discounts/Form.php
 M app/Livewire/Admin/Discounts/Index.php
 M app/Livewire/Admin/Inventory/Index.php
 M app/Livewire/Admin/Navigation/Index.php
 M app/Livewire/Admin/Orders/Index.php
 M app/Livewire/Admin/Orders/Show.php
 M app/Livewire/Admin/Pages/Create.php
 M app/Livewire/Admin/Pages/Edit.php
 M app/Livewire/Admin/Pages/Index.php
 M app/Livewire/Admin/Search/Settings.php
 M app/Livewire/Admin/Settings/General.php
 M app/Livewire/Admin/Settings/Shipping.php
 M app/Livewire/Admin/Settings/Taxes.php
 M app/Livewire/Admin/Themes/Editor.php
 M app/Livewire/Admin/Themes/Index.php
 M app/Livewire/Storefront/Account/Auth/Login.php
 M app/Livewire/Storefront/Account/Auth/Register.php
 M app/Livewire/Storefront/Cart/Show.php
 M app/Livewire/Storefront/Checkout/Confirmation.php
 M app/Livewire/Storefront/Checkout/Show.php
 M app/Livewire/Storefront/Products/Show.php
 M app/Models/Collection.php
 M app/Models/Customer.php
 M app/Models/NavigationMenu.php
 M app/Models/Refund.php
 M app/Models/SearchQuery.php
 M app/Models/Theme.php
 M app/Models/User.php
 M app/Providers/AppServiceProvider.php
 M app/Services/AnalyticsService.php
 M app/Services/CartService.php
 M app/Services/FulfillmentService.php
 M app/Services/OrderService.php
 M app/Services/PaymentService.php
 M app/Services/ProductService.php
 M app/Services/RefundService.php
 M app/Services/ShippingCalculator.php
 M app/Services/TaxCalculator.php
 M app/Services/VariantMatrixService.php
 M boost.json
 M bootstrap/app.php
 M config/auth.php
 M config/logging.php
 M config/session.php
 M config/shop.php
 M database/factories/CartFactory.php
 M database/factories/CollectionFactory.php
 M database/factories/CustomerFactory.php
 M database/factories/DiscountFactory.php
 M database/factories/InventoryItemFactory.php
 M database/factories/OrderFactory.php
 M database/factories/ProductFactory.php
 M database/factories/ProductVariantFactory.php
 M database/factories/StoreDomainFactory.php
 M database/factories/StoreFactory.php
 M database/factories/UserFactory.php
 M database/seeders/DatabaseSeeder.php
 M database/seeders/OrganizationSeeder.php
 M database/seeders/ShopSeeder.php
 M database/seeders/StoreDomainSeeder.php
 M database/seeders/StoreSeeder.php
 M database/seeders/StoreSettingsSeeder.php
 M resources/views/layouts/admin.blade.php
 M resources/views/layouts/storefront.blade.php
 M resources/views/livewire/admin/analytics/index.blade.php
 M resources/views/livewire/admin/apps/index.blade.php
 M resources/views/livewire/admin/auth/login.blade.php
 M resources/views/livewire/admin/collections/create.blade.php
 M resources/views/livewire/admin/collections/edit.blade.php
 M resources/views/livewire/admin/collections/index.blade.php
 M resources/views/livewire/admin/customers/index.blade.php
 M resources/views/livewire/admin/customers/show.blade.php
 M resources/views/livewire/admin/developers/index.blade.php
 M resources/views/livewire/admin/discounts/form.blade.php
 M resources/views/livewire/admin/discounts/index.blade.php
 M resources/views/livewire/admin/inventory/index.blade.php
 M resources/views/livewire/admin/navigation/index.blade.php
 M resources/views/livewire/admin/orders/index.blade.php
 M resources/views/livewire/admin/orders/show.blade.php
 M resources/views/livewire/admin/pages/create.blade.php
 M resources/views/livewire/admin/pages/edit.blade.php
 M resources/views/livewire/admin/pages/index.blade.php
 M resources/views/livewire/admin/search/settings.blade.php
 M resources/views/livewire/admin/settings/general.blade.php
 M resources/views/livewire/admin/settings/shipping.blade.php
 M resources/views/livewire/admin/settings/taxes.blade.php
 M resources/views/livewire/admin/themes/editor.blade.php
 M resources/views/livewire/admin/themes/index.blade.php
 M resources/views/livewire/storefront/account/auth/login.blade.php
 M resources/views/livewire/storefront/account/auth/register.blade.php
 M resources/views/livewire/storefront/cart/show.blade.php
 M resources/views/livewire/storefront/checkout/confirmation.blade.php
 M resources/views/livewire/storefront/checkout/show.blade.php
 M resources/views/livewire/storefront/products/show.blade.php
 M routes/api.php
 M routes/web.php
?? .playwright-mcp/
?? app/Auth/StoreScopedPasswordBrokerManager.php
?? app/Auth/StoreScopedTokenRepository.php
?? app/Contracts/TaxProvider.php
?? app/Http/Controllers/Api/StorefrontOrderController.php
?? app/Http/Controllers/Api/StorefrontSearchController.php
?? app/Http/Middleware/EnsureApiAbility.php
?? app/Livewire/Admin/Collections/Form.php
?? app/Livewire/Admin/Pages/Form.php
?? app/Livewire/Admin/Settings/Domains.php
?? app/Livewire/Storefront/CartDrawer.php
?? app/Services/AuditLogger.php
?? app/Services/Tax/
?? app/ValueObjects/TaxCalculationRequest.php
?? config/cors.php
?? config/sanctum.php
?? database/factories/AnalyticsDailyFactory.php
?? database/factories/AnalyticsEventFactory.php
?? database/factories/CartLineFactory.php
?? database/factories/CheckoutFactory.php
?? database/factories/CustomerAddressFactory.php
?? database/factories/FulfillmentFactory.php
?? database/factories/FulfillmentLineFactory.php
?? database/factories/NavigationItemFactory.php
?? database/factories/NavigationMenuFactory.php
?? database/factories/OrderLineFactory.php
?? database/factories/PageFactory.php
?? database/factories/PaymentFactory.php
?? database/factories/ProductMediaFactory.php
?? database/factories/ProductOptionFactory.php
?? database/factories/ProductOptionValueFactory.php
?? database/factories/RefundFactory.php
?? database/factories/SearchSettingFactory.php
?? database/factories/ShippingRateFactory.php
?? database/factories/ShippingZoneFactory.php
?? database/factories/TaxSettingsFactory.php
?? database/factories/ThemeFactory.php
?? database/factories/ThemeSettingFactory.php
?? database/migrations/2026_08_20_224526_add_line_allocations_to_refunds_table.php
?? database/migrations/2026_08_20_230144_add_remaining_spec_contract_fields.php
?? database/seeders/AnalyticsSeeder.php
?? database/seeders/CollectionSeeder.php
?? database/seeders/CustomerSeeder.php
?? database/seeders/DiscountSeeder.php
?? database/seeders/NavigationSeeder.php
?? database/seeders/OrderSeeder.php
?? database/seeders/PageSeeder.php
?? database/seeders/ProductSeeder.php
?? database/seeders/SearchSettingsSeeder.php
?? database/seeders/ShippingSeeder.php
?? database/seeders/StoreUserSeeder.php
?? database/seeders/TaxSettingsSeeder.php
?? database/seeders/ThemeSeeder.php
?? database/seeders/UserSeeder.php
?? resources/views/livewire/admin/collections/form.blade.php
?? resources/views/livewire/admin/pages/form.blade.php
?? resources/views/livewire/storefront/cart-drawer.blade.php
?? tests/Feature/ApiTokenTest.php
?? tests/Feature/SeedDataTest.php
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Enums\PaymentMethod;
use App\Exceptions\InvalidDiscountException;
use App\Models\Checkout as CheckoutModel;
use App\Models\CustomerAddress;
use App\Services\CheckoutService;
use App\Services\DiscountService;
use App\Services\PaymentService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Carbon\Carbon;
use Livewire\Component;
use Throwable;

class Show extends Component
{
    public CheckoutModel $checkout;

    public string $email = '';

    /** @var array<string, string> */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    /** @var array<string, string> */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    public bool $billingSameAsShipping = true;

    public ?int $savedAddressId = null;

    public ?int $shippingRateId = null;

    public string $paymentMethod = PaymentMethod::CreditCard->value;

    public string $cardNumber = '4242424242424242';

    public string $cardholderName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public string $discountCode = '';

    public string $message = '';

    public bool $processing = false;

    public int $activeStep = 1;

    public bool $showOrderSummary = false;

    public function mount(int $checkoutId): void
    {
        $this->checkout = CheckoutModel::query()
            ->with(['cart.lines.variant.product.media', 'shippingRate'])
            ->findOrFail($checkoutId);

        $customerId = auth('customer')->id();
        $sessionCartIds = array_filter([
            session('cart_id'),
            session('cart_id_'.app('current_store')->getKey()),
        ]);

        abort_unless(
            ($customerId !== null && (int) $this->checkout->customer_id === (int) $customerId)
                || ($customerId === null && in_array($this->checkout->cart_id, $sessionCartIds, true)),
            404,
        );

        $this->email = (string) $this->checkout->email;
        $this->discountCode = (string) ($this->checkout->discount_code ?? '');
        $this->shippingRateId = $this->checkout->shipping_rate_id;
        $this->paymentMethod = (string) ($this->checkout->payment_method ?? PaymentMethod::CreditCard->value);

        if ($this->checkout->shipping_address_json !== null) {
            $this->shippingAddress = array_merge($this->shippingAddress, $this->checkout->shipping_address_json);
        }

        if ($this->checkout->billing_address_json !== null) {
            $this->billingAddress = array_merge($this->billingAddress, $this->checkout->billing_address_json);
            $this->billingSameAsShipping = $this->billingAddress === $this->shippingAddress;
        }

        $this->activeStep = match ($this->checkout->status) {
            CheckoutStatus::Started => 1,
            CheckoutStatus::Addressed => 3,
            CheckoutStatus::ShippingSelected, CheckoutStatus::PaymentSelected, CheckoutStatus::PaymentPending, CheckoutStatus::Completed => 4,
            default => 1,
        };
    }

    public function saveContact(): void
    {
        $this->validate([
            'email' => ['required', 'email', 'max:255'],
        ], [
            'email.required' => 'Enter an email address to receive your order confirmation.',
        ]);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout->refresh();
        $this->message = 'Contact information saved.';
        $this->activeStep = 2;
    }

    public function saveAddress(CheckoutService $checkouts, PricingEngine $pricing): void
    {
        $this->normalizeAddresses();

        $rules = [
            'email' => ['required', 'email', 'max:255'],
            ...$this->addressRules('shippingAddress'),
        ];

        if (! $this->billingSameAsShipping) {
            $rules = [...$rules, ...$this->addressRules('billingAddress')];
        }

        $this->validate($rules);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout = $checkouts->setAddress(
            $this->checkout->refresh(),
            $this->shippingAddress,
            $this->billingAddress,
            $this->billingSameAsShipping,
        );

        if ($this->requiresShipping()) {
            $this->checkout->update([
                'shipping_rate_id' => null,
                'shipping_method_id' => null,
                'status' => CheckoutStatus::Addressed,
            ]);
            $this->checkout = $this->checkout->refresh();
            $pricing->calculate($this->checkout);
            $this->checkout = $this->checkout->refresh();
            $this->shippingRateId = null;
            $this->activeStep = 3;
        } else {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, 0);
            $this->shippingRateId = null;
            $this->activeStep = 4;
        }

        $this->message = 'Address saved.';
    }

    public function selectSavedAddress(int|string|null $addressId): void
    {
        $customerId = auth('customer')->id();

        if ($customerId === null || $addressId === null || $addressId === '') {
            $this->savedAddressId = null;

            return;
        }

        $address = CustomerAddress::query()
            ->whereKey((int) $addressId)
            ->where('customer_id', $customerId)
            ->firstOrFail();

        $this->savedAddressId = $address->getKey();
        $this->shippingAddress = array_merge($this->shippingAddress, $address->address_json ?? []);

        if ($this->billingSameAsShipping) {
            $this->billingAddress = $this->shippingAddress;
        }
    }

    public function updatedBillingSameAsShipping(bool $same): void
    {
        if ($same) {
            $this->billingAddress = $this->shippingAddress;
        }
    }

    public function chooseShipping(CheckoutService $checkouts): void
    {
        if (! $this->requiresShipping()) {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, 0);
            $this->activeStep = 4;

            return;
        }

        $this->validate(['shippingRateId' => ['required', 'integer']]);

        try {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, $this->shippingRateId);
            $this->message = 'Shipping method saved.';
            $this->activeStep = 4;
        } catch (Throwable $exception) {
            $this->addError('shippingRateId', $exception->getMessage());
        }
    }

    public function applyDiscount(DiscountService $discounts, PricingEngine $pricing): void
    {
        $this->validate(['discountCode' => ['required', 'string', 'max:64']]);

        try {
            $code = trim($this->discountCode);
            $discount = $discounts->validate($code, app('current_store'), $this->checkout->cart);
            $this->checkout->cart->update(['discount_code' => $discount->code]);
            $this->checkout->update(['discount_code' => $discount->code]);
            $this->checkout = $this->checkout->refresh();
            $pricing->calculate($this->checkout);
            $this->checkout = $this->checkout->refresh();
            $this->checkout->load(['cart.lines.variant.product.media', 'shippingRate']);
            $this->discountCode = $discount->code;
            $this->message = 'Discount applied.';
        } catch (InvalidDiscountException $exception) {
            $this->addError('discountCode', $exception->getMessage());

Secure checkout

Complete your order

Your information is used only to process and deliver this order.

        <button type="button" wire:click="toggleOrderSummary" class="flex min-h-14 w-full items-center justify-between rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left dark:border-zinc-800 dark:bg-zinc-900" aria-controls="mobile-order-summary" aria-expanded="{{ $showOrderSummary ? 'true' : 'false' }}">

                {{ $showOrderSummary ? 'Hide order summary' : 'Show order summary' }}
                {{ $this->formatMoney($totals['total'] ?? 0, $totals['currency'] ?? null) }}

            {{ $showOrderSummary ? '⌃' : '⌄' }}
        </button>

                    <button type="button" wire:click="setActiveStep(1)" class="flex min-w-0 items-start gap-3 text-left focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:focus:ring-offset-zinc-900" aria-controls="checkout-contact-panel" aria-expanded="{{ $activeStep === 1 ? 'true' : 'false' }}">
                         1 ? 'bg-green-600 text-white' : 'bg-blue-600 text-white' }} text-sm font-bold">{{ $activeStep > 1 ? '✓' : '1' }}

                            Contact information
                            @if ($activeStep > 1)
                                {{ $checkout->email }}
                            @endif

                    </button>
                    @if ($activeStep > 1)
                        <button type="button" wire:click="setActiveStep(1)" class="shrink-0 text-sm font-semibold text-blue-600 underline underline-offset-4 focus:outline-none focus:ring-2 focus:ring-blue-600">Edit</button>
                    @endif

                @if ($activeStep === 1)
                    <form id="checkout-contact-panel" wire:submit="saveContact" class="space-y-4 p-5 sm:p-6">

                            Email address *
                            <input id="checkout-email" type="email" wire:model.blur="email" autocomplete="email" required aria-describedby="checkout-email-help checkout-email-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 text-zinc-950 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

We’ll send your order confirmation here.

                            @error('email')

{{ $message }}

                            @enderror

Already have an account? Log in

                        <button type="submit" wire:loading.attr="disabled" class="inline-flex min-h-11 items-center justify-center rounded-full bg-blue-600 px-5 py-3 font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:opacity-60 dark:focus:ring-offset-zinc-950">
                            Continue to shipping address
                            Saving…
                        </button>
                    </form>
                @endif

                    <button type="button" wire:click="setActiveStep(2)" @disabled($activeStep < 2) class="flex min-w-0 items-start gap-3 text-left focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-900" aria-controls="checkout-address-panel" aria-expanded="{{ $activeStep === 2 ? 'true' : 'false' }}">
                        shipping_address_json !== null ? 'bg-green-600 text-white' : 'bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }} text-sm font-bold">{{ $checkout->shipping_address_json !== null && $activeStep !== 2 ? '✓' : '2' }}

                            Shipping address
                            @if ($checkout->shipping_address_json !== null && $activeStep !== 2)
                                {{ $shippingAddress['city'] }}, {{ $shippingAddress['country_code'] }}
                            @endif

                    </button>
                    @if ($checkout->shipping_address_json !== null && $activeStep !== 2)
                        <button type="button" wire:click="setActiveStep(2)" class="shrink-0 text-sm font-semibold text-blue-600 underline underline-offset-4 focus:outline-none focus:ring-2 focus:ring-blue-600">Edit</button>
                    @endif

                @if ($activeStep === 2)
                    <form id="checkout-address-panel" wire:submit="saveAddress" class="space-y-6 p-5 sm:p-6">
                        @if ($savedAddresses->isNotEmpty())

                                Use a saved address
                                <select id="saved-address" wire:model.live="savedAddressId" wire:change="selectSavedAddress($event.target.value)" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 text-zinc-950 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    <option value="">Use a new address</option>
                                    @foreach ($savedAddresses as $address)
                                        <option wire:key="saved-address-{{ $address->id }}" value="{{ $address->id }}">{{ $address->label ?: 'Saved address' }}{{ $address->is_default ? ' (default)' : '' }}</option>
                                    @endforeach
                                </select>

                        @endif

                        <fieldset>
                            <legend class="text-base font-semibold text-zinc-950 dark:text-white">Shipping address</legend>

                                    First name *
                                    <input id="shipping-first-name" type="text" wire:model.blur="shippingAddress.first_name" autocomplete="given-name" required aria-describedby="shipping-first-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.first_name')
{{ $message }}
@enderror

                                    Last name *
                                    <input id="shipping-last-name" type="text" wire:model.blur="shippingAddress.last_name" autocomplete="family-name" required aria-describedby="shipping-last-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.last_name')
{{ $message }}
@enderror

                                    Address line 1 *
                                    <input id="shipping-address1" type="text" wire:model.blur="shippingAddress.address1" autocomplete="address-line1" required aria-describedby="shipping-address1-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.address1')
{{ $message }}
@enderror

                                    Address line 2 (optional)
                                    <input id="shipping-address2" type="text" wire:model.blur="shippingAddress.address2" autocomplete="address-line2" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                                    City *
                                    <input id="shipping-city" type="text" wire:model.blur="shippingAddress.city" autocomplete="address-level2" required aria-describedby="shipping-city-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.city')
{{ $message }}
@enderror

                                    State / province *
                                    <input id="shipping-state" type="text" wire:model.blur="shippingAddress.state" autocomplete="address-level1" required aria-describedby="shipping-state-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.state')
{{ $message }}
@enderror

                                    Postal code *
                                    <input id="shipping-postal-code" type="text" wire:model.blur="shippingAddress.postal_code" autocomplete="postal-code" required aria-describedby="shipping-postal-code-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.postal_code')
{{ $message }}
@enderror

                                    Country *
                                    <select id="shipping-country" wire:model.blur="shippingAddress.country_code" autocomplete="country" required aria-describedby="shipping-country-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        <option value="DE">Germany</option>
                                        <option value="AT">Austria</option>
                                        <option value="CH">Switzerland</option>
                                        <option value="GB">United Kingdom</option>
                                        <option value="US">United States</option>
                                    </select>
                                    @error('shippingAddress.country_code')
{{ $message }}
@enderror

                                    Phone (optional)
                                    <input id="shipping-phone" type="tel" wire:model.blur="shippingAddress.phone" autocomplete="tel" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                        </fieldset>

                            <input type="checkbox" wire:model.live="billingSameAsShipping" class="mt-1 h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-2 focus:ring-blue-600 dark:border-zinc-700" />
                            Billing address is the same as shipping

                        @if (! $billingSameAsShipping)
                            <fieldset>
                                <legend class="text-base font-semibold text-zinc-950 dark:text-white">Billing address</legend>

                                        First name *
                                        <input id="billing-first-name" type="text" wire:model.blur="billingAddress.first_name" autocomplete="billing given-name" required aria-describedby="billing-first-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.first_name')
{{ $message }}
@enderror

                                        Last name *
                                        <input id="billing-last-name" type="text" wire:model.blur="billingAddress.last_name" autocomplete="billing family-name" required aria-describedby="billing-last-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.last_name')
{{ $message }}
@enderror

                                        Address line 1 *
                                        <input id="billing-address1" type="text" wire:model.blur="billingAddress.address1" autocomplete="billing address-line1" required aria-describedby="billing-address1-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.address1')
{{ $message }}
@enderror

                                        Address line 2 (optional)
                                        <input id="billing-address2" type="text" wire:model.blur="billingAddress.address2" autocomplete="billing address-line2" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                                        City *
                                        <input id="billing-city" type="text" wire:model.blur="billingAddress.city" autocomplete="billing address-level2" required aria-describedby="billing-city-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.city')
{{ $message }}
@enderror

                                        State / province *
                                        <input id="billing-state" type="text" wire:model.blur="billingAddress.state" autocomplete="billing address-level1" required aria-describedby="billing-state-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.state')
{{ $message }}
@enderror

                                        Postal code *
                                        <input id="billing-postal-code" type="text" wire:model.blur="billingAddress.postal_code" autocomplete="billing postal-code" required aria-describedby="billing-postal-code-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.postal_code')
{{ $message }}
@enderror

                                        Country *
                                        <select id="billing-country" wire:model.blur="billingAddress.country_code" autocomplete="billing country" required aria-describedby="billing-country-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                            <option value="DE">Germany</option>
                                            <option value="AT">Austria</option>
                                            <option value="CH">Switzerland</option>
                                            <option value="GB">United Kingdom</option>
                                            <option value="US">United States</option>
                                        </select>
                                        @error('billingAddress.country_code')
{{ $message }}
@enderror

                            </fieldset>
                        @endif

                        <button type="submit" wire:loading.attr="disabled" class="inline-flex min-h-11 items-center justify-center rounded-full bg-blue-600 px-5 py-3 font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:opacity-60 dark:focus:ring-offset-zinc-950">
                            Continue to shipping
                            Saving…
                        </button>
                    </form>
                @elseif ($checkout->shipping_address_json === null)

Complete your contact information first.

                @endif

                    <button type="button" wire:click="setActiveStep(3)" @disabled($activeStep < 3) class="flex min-w-0 items-start gap-3 text-left focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-900" aria-controls="checkout-shipping-panel" aria-expanded="{{ $activeStep === 3 ? 'true' : 'false' }}">
                        shipping_rate_id !== null || ! $this->requiresShipping() ? 'bg-green-600 text-white' : 'bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }} text-sm font-bold">{{ ($checkout->shipping_rate_id !== null || ! $this->requiresShipping()) && $activeStep !== 3 ? '✓' : '3' }}

                            Shipping method
                            @if (! $this->requiresShipping())
                                No shipping required
                            @elseif ($checkout->shippingRate && $activeStep !== 3)
                                {{ $checkout->shippingRate->name }}
                            @endif

                    </button>
                    @if ($activeStep > 3 && $this->requiresShipping())
                        <button type="button" wire:click="setActiveStep(3)" class="shrink-0 text-sm font-semibold text-blue-600 underline underline-offset-4 focus:outline-none focus:ring-2 focus:ring-blue-600">Edit</button>
                    @endif

                @if ($activeStep === 3)
                    <form id="checkout-shipping-panel" wire:submit="chooseShipping" class="p-5 sm:p-6">
                        @if ($rates->isEmpty())

No shipping methods are available for your address.

Please verify your address or contact us.

                        @else
                            <fieldset>
                                <legend class="text-base font-semibold text-zinc-950 dark:text-white">Select a shipping method *</legend>

                                    @foreach ($rates as $rate)
                                        id }}" class="flex cursor-pointer items-center gap-4 rounded-xl border p-4 transition has-[:checked]:border-blue-600 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-950/20 {{ (int) $shippingRateId === (int) $rate->id ? 'border-blue-600 bg-blue-50 dark:bg-blue-950/20' : 'border-zinc-200 dark:border-zinc-700' }}">
                                            <input id="shipping-rate-{{ $rate->id }}" type="radio" wire:model="shippingRateId" value="{{ $rate->id }}" class="h-4 w-4 border-zinc-300 text-blue-600 focus:ring-2 focus:ring-blue-600 dark:border-zinc-700">

                                                {{ $rate->name }}

                                                    @if ($rate->estimated_days_min && $rate->estimated_days_max)
                                                        {{ $rate->estimated_days_min }}–{{ $rate->estimated_days_max }} business days
                                                    @else
                                                        Delivery time shown at dispatch
                                                    @endif

                                            {{ $this->formatMoney($rate->price_amount, $rate->currency) }}

                                    @endforeach

                            </fieldset>
                            @error('shippingRateId')
{{ $message }}
@enderror
                            <button type="submit" wire:loading.attr="disabled" class="mt-6 inline-flex min-h-11 items-center justify-center rounded-full bg-blue-600 px-5 py-3 font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:opacity-60 dark:focus:ring-offset-zinc-950">
                                Continue to payment
                                Saving…
                            </button>
                        @endif

formatted_output:
Warning: truncated output (original token count: 11794)
Total output lines: 669

 M app/Http/Controllers/Api/AdminController.php
 M app/Http/Controllers/Api/StorefrontAnalyticsController.php
 M app/Http/Controllers/Api/StorefrontCheckoutController.php
 M app/Jobs/CleanupAbandonedCarts.php
 M app/Jobs/DeliverWebhook.php
 M app/Livewire/Admin/Analytics/Index.php
 M app/Livewire/Admin/Apps/Index.php
 M app/Livewire/Admin/Auth/Login.php
 M app/Livewire/Admin/Collections/Create.php
 M app/Livewire/Admin/Collections/Edit.php
 M app/Livewire/Admin/Collections/Index.php
 M app/Livewire/Admin/Customers/Index.php
 M app/Livewire/Admin/Customers/Show.php
 M app/Livewire/Admin/Developers/Index.php
 M app/Livewire/Admin/Discounts/Form.php
 M app/Livewire/Admin/Discounts/Index.php
 M app/Livewire/Admin/Inventory/Index.php
 M app/Livewire/Admin/Navigation/Index.php
 M app/Livewire/Admin/Orders/Index.php
 M app/Livewire/Admin/Orders/Show.php
 M app/Livewire/Admin/Pages/Create.php
 M app/Livewire/Admin/Pages/Edit.php
 M app/Livewire/Admin/Pages/Index.php
 M app/Livewire/Admin/Search/Settings.php
 M app/Livewire/Admin/Settings/General.php
 M app/Livewire/Admin/Settings/Shipping.php
 M app/Livewire/Admin/Settings/Taxes.php
 M app/Livewire/Admin/Themes/Editor.php
 M app/Livewire/Admin/Themes/Index.php
 M app/Livewire/Storefront/Account/Auth/Login.php
 M app/Livewire/Storefront/Account/Auth/Register.php
 M app/Livewire/Storefront/Cart/Show.php
 M app/Livewire/Storefront/Checkout/Confirmation.php
 M app/Livewire/Storefront/Checkout/Show.php
 M app/Livewire/Storefront/Products/Show.php
 M app/Models/Collection.php
 M app/Models/Customer.php
 M app/Models/NavigationMenu.php
 M app/Models/Refund.php
 M app/Models/SearchQuery.php
 M app/Models/Theme.php
 M app/Models/User.php
 M app/Providers/AppServiceProvider.php
 M app/Services/AnalyticsService.php
 M app/Services/CartService.php
 M app/Services/FulfillmentService.php
 M app/Services/OrderService.php
 M app/Services/PaymentService.php
 M app/Services/ProductService.php
 M app/Services/RefundService.php
 M app/Services/ShippingCalculator.php
 M app/Services/TaxCalculator.php
 M app/Services/VariantMatrixService.php
 M boost.json
 M bootstrap/app.php
 M config/auth.php
 M config/logging.php
 M config/session.php
 M config/shop.php
 M database/factories/CartFactory.php
 M database/factories/CollectionFactory.php
 M database/factories/CustomerFactory.php
 M database/factories/DiscountFactory.php
 M database/factories/InventoryItemFactory.php
 M database/factories/OrderFactory.php
 M database/factories/ProductFactory.php
 M database/factories/ProductVariantFactory.php
 M database/factories/StoreDomainFactory.php
 M database/factories/StoreFactory.php
 M database/factories/UserFactory.php
 M database/seeders/DatabaseSeeder.php
 M database/seeders/OrganizationSeeder.php
 M database/seeders/ShopSeeder.php
 M database/seeders/StoreDomainSeeder.php
 M database/seeders/StoreSeeder.php
 M database/seeders/StoreSettingsSeeder.php
 M resources/views/layouts/admin.blade.php
 M resources/views/layouts/storefront.blade.php
 M resources/views/livewire/admin/analytics/index.blade.php
 M resources/views/livewire/admin/apps/index.blade.php
 M resources/views/livewire/admin/auth/login.blade.php
 M resources/views/livewire/admin/collections/create.blade.php
 M resources/views/livewire/admin/collections/edit.blade.php
 M resources/views/livewire/admin/collections/index.blade.php
 M resources/views/livewire/admin/customers/index.blade.php
 M resources/views/livewire/admin/customers/show.blade.php
 M resources/views/livewire/admin/developers/index.blade.php
 M resources/views/livewire/admin/discounts/form.blade.php
 M resources/views/livewire/admin/discounts/index.blade.php
 M resources/views/livewire/admin/inventory/index.blade.php
 M resources/views/livewire/admin/navigation/index.blade.php
 M resources/views/livewire/admin/orders/index.blade.php
 M resources/views/livewire/admin/orders/show.blade.php
 M resources/views/livewire/admin/pages/create.blade.php
 M resources/views/livewire/admin/pages/edit.blade.php
 M resources/views/livewire/admin/pages/index.blade.php
 M resources/views/livewire/admin/search/settings.blade.php
 M resources/views/livewire/admin/settings/general.blade.php
 M resources/views/livewire/admin/settings/shipping.blade.php
 M resources/views/livewire/admin/settings/taxes.blade.php
 M resources/views/livewire/admin/themes/editor.blade.php
 M resources/views/livewire/admin/themes/index.blade.php
 M resources/views/livewire/storefront/account/auth/login.blade.php
 M resources/views/livewire/storefront/account/auth/register.blade.php
 M resources/views/livewire/storefront/cart/show.blade.php
 M resources/views/livewire/storefront/checkout/confirmation.blade.php
 M resources/views/livewire/storefront/checkout/show.blade.php
 M resources/views/livewire/storefront/products/show.blade.php
 M routes/api.php
 M routes/web.php
?? .playwright-mcp/
?? app/Auth/StoreScopedPasswordBrokerManager.php
?? app/Auth/StoreScopedTokenRepository.php
?? app/Contracts/TaxProvider.php
?? app/Http/Controllers/Api/StorefrontOrderController.php
?? app/Http/Controllers/Api/StorefrontSearchController.php
?? app/Http/Middleware/EnsureApiAbility.php
?? app/Livewire/Admin/Collections/Form.php
?? app/Livewire/Admin/Pages/Form.php
?? app/Livewire/Admin/Settings/Domains.php
?? app/Livewire/Storefront/CartDrawer.php
?? app/Services/AuditLogger.php
?? app/Services/Tax/
?? app/ValueObjects/TaxCalculationRequest.php
?? config/cors.php
?? config/sanctum.php
?? database/factories/AnalyticsDailyFactory.php
?? database/factories/AnalyticsEventFactory.php
?? database/factories/CartLineFactory.php
?? database/factories/CheckoutFactory.php
?? database/factories/CustomerAddressFactory.php
?? database/factories/FulfillmentFactory.php
?? database/factories/FulfillmentLineFactory.php
?? database/factories/NavigationItemFactory.php
?? database/factories/NavigationMenuFactory.php
?? database/factories/OrderLineFactory.php
?? database/factories/PageFactory.php
?? database/factories/PaymentFactory.php
?? database/factories/ProductMediaFactory.php
?? database/factories/ProductOptionFactory.php
?? database/factories/ProductOptionValueFactory.php
?? database/factories/RefundFactory.php
?? database/factories/SearchSettingFactory.php
?? database/factories/ShippingRateFactory.php
?? database/factories/ShippingZoneFactory.php
?? database/factories/TaxSettingsFactory.php
?? database/factories/ThemeFactory.php
?? database/factories/ThemeSettingFactory.php
?? database/migrations/2026_08_20_224526_add_line_allocations_to_refunds_table.php
?? database/migrations/2026_08_20_230144_add_remaining_spec_contract_fields.php
?? database/seeders/AnalyticsSeeder.php
?? database/seeders/CollectionSeeder.php
?? database/seeders/CustomerSeeder.php
?? database/seeders/DiscountSeeder.php
?? database/seeders/NavigationSeeder.php
?? database/seeders/OrderSeeder.php
?? database/seeders/PageSeeder.php
?? database/seeders/ProductSeeder.php
?? database/seeders/SearchSettingsSeeder.php
?? database/seeders/ShippingSeeder.php
?? database/seeders/StoreUserSeeder.php
?? database/seeders/TaxSettingsSeeder.php
?? database/seeders/ThemeSeeder.php
?? database/seeders/UserSeeder.php
?? resources/views/livewire/admin/collections/form.blade.php
?? resources/views/livewire/admin/pages/form.blade.php
?? resources/views/livewire/storefront/cart-drawer.blade.php
?? tests/Feature/ApiTokenTest.php
?? tests/Feature/SeedDataTest.php
<?php

namespace App\Livewire\Storefront\Checkout;

use App\Enums\CheckoutStatus;
use App\Enums\PaymentMethod;
use App\Exceptions\InvalidDiscountException;
use App\Models\Checkout as CheckoutModel;
use App\Models\CustomerAddress;
use App\Services\CheckoutService;
use App\Services\DiscountService;
use App\Services\PaymentService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use Carbon\Carbon;
use Livewire\Component;
use Throwable;

class Show extends Component
{
    public CheckoutModel $checkout;

    public string $email = '';

    /** @var array<string, string> */
    public array $shippingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    /** @var array<string, string> */
    public array $billingAddress = [
        'first_name' => '',
        'last_name' => '',
        'address1' => '',
        'address2' => '',
        'city' => '',
        'state' => '',
        'country_code' => 'DE',
        'postal_code' => '',
        'phone' => '',
    ];

    public bool $billingSameAsShipping = true;

    public ?int $savedAddressId = null;

    public ?int $shippingRateId = null;

    public string $paymentMethod = PaymentMethod::CreditCard->value;

    public string $cardNumber = '4242424242424242';

    public string $cardholderName = '';

    public string $cardExpiry = '';

    public string $cardCvc = '';

    public string $discountCode = '';

    public string $message = '';

    public bool $processing = false;

    public int $activeStep = 1;

    public bool $showOrderSummary = false;

    public function mount(int $checkoutId): void
    {
        $this->checkout = CheckoutModel::query()
            ->with(['cart.lines.variant.product.media', 'shippingRate'])
            ->findOrFail($checkoutId);

        $customerId = auth('customer')->id();
        $sessionCartIds = array_filter([
            session('cart_id'),
            session('cart_id_'.app('current_store')->getKey()),
        ]);

        abort_unless(
            ($customerId !== null && (int) $this->checkout->customer_id === (int) $customerId)
                || ($customerId === null && in_array($this->checkout->cart_id, $sessionCartIds, true)),
            404,
        );

        $this->email = (string) $this->checkout->email;
        $this->discountCode = (string) ($this->checkout->discount_code ?? '');
        $this->shippingRateId = $this->checkout->shipping_rate_id;
        $this->paymentMethod = (string) ($this->checkout->payment_method ?? PaymentMethod::CreditCard->value);

        if ($this->checkout->shipping_address_json !== null) {
            $this->shippingAddress = array_merge($this->shippingAddress, $this->checkout->shipping_address_json);
        }

        if ($this->checkout->billing_address_json !== null) {
            $this->billingAddress = array_merge($this->billingAddress, $this->checkout->billing_address_json);
            $this->billingSameAsShipping = $this->billingAddress === $this->shippingAddress;
        }

        $this->activeStep = match ($this->checkout->status) {
            CheckoutStatus::Started => 1,
            CheckoutStatus::Addressed => 3,
            CheckoutStatus::ShippingSelected, CheckoutStatus::PaymentSelected, CheckoutStatus::PaymentPending, CheckoutStatus::Completed => 4,
            default => 1,
        };
    }

    public function saveContact(): void
    {
        $this->validate([
            'email' => ['required', 'email', 'max:255'],
        ], [
            'email.required' => 'Enter an email address to receive your order confirmation.',
        ]);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout->refresh();
        $this->message = 'Contact information saved.';
        $this->activeStep = 2;
    }

    public function saveAddress(CheckoutService $checkouts, PricingEngine $pricing): void
    {
        $this->normalizeAddresses();

        $rules = [
            'email' => ['required', 'email', 'max:255'],
            ...$this->addressRules('shippingAddress'),
        ];

        if (! $this->billingSameAsShipping) {
            $rules = [...$rules, ...$this->addressRules('billingAddress')];
        }

        $this->validate($rules);

        $this->checkout->update(['email' => $this->email]);
        $this->checkout = $checkouts->setAddress(
            $this->checkout->refresh(),
            $this->shippingAddress,
            $this->billingAddress,
            $this->billingSameAsShipping,
        );

        if ($this->requiresShipping()) {
            $this->checkout->update([
                'shipping_rate_id' => null,
                'shipping_method_id' => null,
                'status' => CheckoutStatus::Addressed,
            ]);
            $this->checkout = $this->checkout->refresh();
            $pricing->calculate($this->checkout);
            $this->checkout = $this->checkout->refresh();
            $this->shippingRateId = null;
            $this->activeStep = 3;
        } else {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, 0);
            $this->shippingRateId = null;
            $this->activeStep = 4;
        }

        $this->message = 'Address saved.';
    }

    public function selectSavedAddress(int|string|null $addressId): void
    {
        $customerId = auth('customer')->id();

        if ($customerId === null || $addressId === null || $addressId === '') {
            $this->savedAddressId = null;

            return;
        }

        $address = CustomerAddress::query()
            ->whereKey((int) $addressId)
            ->where('customer_id', $customerId)
            ->firstOrFail();

        $this->savedAddressId = $address->getKey();
        $this->shippingAddress = array_merge($this->shippingAddress, $address->address_json ?? []);

        if ($this->billingSameAsShipping) {
            $this->billingAddress = $this->shippingAddress;
        }
    }

    public function updatedBillingSameAsShipping(bool $same): void
    {
        if ($same) {
            $this->billingAddress = $this->shippingAddress;
        }
    }

    public function chooseShipping(CheckoutService $checkouts): void
    {
        if (! $this->requiresShipping()) {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, 0);
            $this->activeStep = 4;

            return;
        }

        $this->validate(['shippingRateId' => ['required', 'integer']]);

        try {
            $this->checkout = $checkouts->setShippingMethod($this->checkout, $this->shippingRateId);
            $this->message = 'Shipping method saved.';
            $this->activeStep = 4;
        } catch (Throwable $exception) {
            $this->addError('shippingRateId', $exception->getMessage());
        }
    }

    public function applyDiscount(DiscountService $discounts, PricingEngine $pricing): void
    {
        $this->validate(['discountCode' => ['required', 'string', 'max:64']]);

        try {
            $code = trim($this->discountCode);
            $discount = $discounts->validate($code, app('current_store'), $this->checkout->cart);
            $this->checkout->cart->update(['discount_code' => $discount->code]);
            $this->checkout->update(['discount_code' => $discount->code]);
            $this->checkout = $this->checkout->refresh();
            $pricing->calculate($this->checkout);
            $this->checkout = $this->checkout->refresh();
            $this->checkout->load(['cart.lines.variant.product.media', 'shippingRate']);
            $this->discountCode = $discount->code;
            $this->message = 'Discount applied.';
        } catch (InvalidDiscountException $exception) {
            $this->addError('discountCode', $exception->getMessage());

Secure checkout

Complete your order

Your information is used only to process and deliver this order.

        <button type="button" wire:click="toggleOrderSummary" class="flex min-h-14 w-full items-center justify-between rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left dark:border-zinc-800 dark:bg-zinc-900" aria-controls="mobile-order-summary" aria-expanded="{{ $showOrderSummary ? 'true' : 'false' }}">

                {{ $showOrderSummary ? 'Hide order summary' : 'Show order summary' }}
                {{ $this->formatMoney($totals['total'] ?? 0, $totals['currency'] ?? null) }}

            {{ $showOrderSummary ? '⌃' : '⌄' }}
        </button>

                    <button type="button" wire:click="setActiveStep(1)" class="flex min-w-0 items-start gap-3 text-left focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 dark:focus:ring-offset-zinc-900" aria-controls="checkout-contact-panel" aria-expanded="{{ $activeStep === 1 ? 'true' : 'false' }}">
                         1 ? 'bg-green-600 text-white' : 'bg-blue-600 text-white' }} text-sm font-bold">{{ $activeStep > 1 ? '✓' : '1' }}

                            Contact information
                            @if ($activeStep > 1)
                                {{ $checkout->email }}
                            @endif

                    </button>
                    @if ($activeStep > 1)
                        <button type="button" wire:click="setActiveStep(1)" class="shrink-0 text-sm font-semibold text-blue-600 underline underline-offset-4 focus:outline-none focus:ring-2 focus:ring-blue-600">Edit</button>
                    @endif

                @if ($activeStep === 1)
                    <form id="checkout-contact-panel" wire:submit="saveContact" class="space-y-4 p-5 sm:p-6">

                            Email address *
                            <input id="checkout-email" type="email" wire:model.blur="email" autocomplete="email" required aria-describedby="checkout-email-help checkout-email-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 text-zinc-950 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

We’ll send your order confirmation here.

                            @error('email')

{{ $message }}

                            @enderror

Already have an account? Log in

                        <button type="submit" wire:loading.attr="disabled" cl…1794 tokens truncated…n-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                                    City *
                                    <input id="shipping-city" type="text" wire:model.blur="shippingAddress.city" autocomplete="address-level2" required aria-describedby="shipping-city-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.city')
{{ $message }}
@enderror

                                    State / province *
                                    <input id="shipping-state" type="text" wire:model.blur="shippingAddress.state" autocomplete="address-level1" required aria-describedby="shipping-state-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.state')
{{ $message }}
@enderror

                                    Postal code *
                                    <input id="shipping-postal-code" type="text" wire:model.blur="shippingAddress.postal_code" autocomplete="postal-code" required aria-describedby="shipping-postal-code-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                    @error('shippingAddress.postal_code')
{{ $message }}
@enderror

                                    Country *
                                    <select id="shipping-country" wire:model.blur="shippingAddress.country_code" autocomplete="country" required aria-describedby="shipping-country-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        <option value="DE">Germany</option>
                                        <option value="AT">Austria</option>
                                        <option value="CH">Switzerland</option>
                                        <option value="GB">United Kingdom</option>
                                        <option value="US">United States</option>
                                    </select>
                                    @error('shippingAddress.country_code')
{{ $message }}
@enderror

                                    Phone (optional)
                                    <input id="shipping-phone" type="tel" wire:model.blur="shippingAddress.phone" autocomplete="tel" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                        </fieldset>

                            <input type="checkbox" wire:model.live="billingSameAsShipping" class="mt-1 h-4 w-4 rounded border-zinc-300 text-blue-600 focus:ring-2 focus:ring-blue-600 dark:border-zinc-700" />
                            Billing address is the same as shipping

                        @if (! $billingSameAsShipping)
                            <fieldset>
                                <legend class="text-base font-semibold text-zinc-950 dark:text-white">Billing address</legend>

                                        First name *
                                        <input id="billing-first-name" type="text" wire:model.blur="billingAddress.first_name" autocomplete="billing given-name" required aria-describedby="billing-first-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.first_name')
{{ $message }}
@enderror

                                        Last name *
                                        <input id="billing-last-name" type="text" wire:model.blur="billingAddress.last_name" autocomplete="billing family-name" required aria-describedby="billing-last-name-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.last_name')
{{ $message }}
@enderror

                                        Address line 1 *
                                        <input id="billing-address1" type="text" wire:model.blur="billingAddress.address1" autocomplete="billing address-line1" required aria-describedby="billing-address1-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.address1')
{{ $message }}
@enderror

                                        Address line 2 (optional)
                                        <input id="billing-address2" type="text" wire:model.blur="billingAddress.address2" autocomplete="billing address-line2" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">

                                        City *
                                        <input id="billing-city" type="text" wire:model.blur="billingAddress.city" autocomplete="billing address-level2" required aria-describedby="billing-city-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.city')
{{ $message }}
@enderror

                                        State / province *
                                        <input id="billing-state" type="text" wire:model.blur="billingAddress.state" autocomplete="billing address-level1" required aria-describedby="billing-state-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.state')
{{ $message }}
@enderror

                                        Postal code *
                                        <input id="billing-postal-code" type="text" wire:model.blur="billingAddress.postal_code" autocomplete="billing postal-code" required aria-describedby="billing-postal-code-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                        @error('billingAddress.postal_code')
{{ $message }}
@enderror

                                        Country *
                                        <select id="billing-country" wire:model.blur="billingAddress.country_code" autocomplete="billing country" required aria-describedby="billing-country-error" class="mt-2 min-h-11 w-full rounded-lg border border-zinc-300 bg-white px-3 py-2.5 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600 dark:border-zinc-700 dark:bg-zinc-950 dark:text-white">
                                            <option value="DE">Germany</option>
                                            <option value="AT">Austria</option>
                                            <option value="CH">Switzerland</option>
                                            <option value="GB">United Kingdom</option>
                                            <option value="US">United States</option>
                                        </select>
                                        @error('billingAddress.country_code')
{{ $message }}
@enderror

                            </fieldset>
                        @endif

                        <button type="submit" wire:loading.attr="disabled" class="inline-flex min-h-11 items-center justify-center rounded-full bg-blue-600 px-5 py-3 font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:opacity-60 dark:focus:ring-offset-zinc-950">
                            Continue to shipping
                            Saving…
                        </button>
                    </form>
                @elseif ($checkout->shipping_address_json === null)

Complete your contact information first.

                @endif

                    <button type="button" wire:click="setActiveStep(3)" @disabled($activeStep < 3) class="flex min-w-0 items-start gap-3 text-left focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 dark:focus:ring-offset-zinc-900" aria-controls="checkout-shipping-panel" aria-expanded="{{ $activeStep === 3 ? 'true' : 'false' }}">
                        shipping_rate_id !== null || ! $this->requiresShipping() ? 'bg-green-600 text-white' : 'bg-zinc-200 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }} text-sm font-bold">{{ ($checkout->shipping_rate_id !== null || ! $this->requiresShipping()) && $activeStep !== 3 ? '✓' : '3' }}

                            Shipping method
                            @if (! $this->requiresShipping())
                                No shipping required
                            @elseif ($checkout->shippingRate && $activeStep !== 3)
                                {{ $checkout->shippingRate->name }}
                            @endif

                    </button>
                    @if ($activeStep > 3 && $this->requiresShipping())
                        <button type="button" wire:click="setActiveStep(3)" class="shrink-0 text-sm font-semibold text-blue-600 underline underline-offset-4 focus:outline-none focus:ring-2 focus:ring-blue-600">Edit</button>
                    @endif

                @if ($activeStep === 3)
                    <form id="checkout-shipping-panel" wire:submit="chooseShipping" class="p-5 sm:p-6">
                        @if ($rates->isEmpty())

No shipping methods are available for your address.

Please verify your address or contact us.

                        @else
                            <fieldset>
                                <legend class="text-base font-semibold text-zinc-950 dark:text-white">Select a shipping method *</legend>

                                    @foreach ($rates as $rate)
                                        id }}" class="flex cursor-pointer items-center gap-4 rounded-xl border p-4 transition has-[:checked]:border-blue-600 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-950/20 {{ (int) $shippingRateId === (int) $rate->id ? 'border-blue-600 bg-blue-50 dark:bg-blue-950/20' : 'border-zinc-200 dark:border-zinc-700' }}">
                                            <input id="shipping-rate-{{ $rate->id }}" type="radio" wire:model="shippingRateId" value="{{ $rate->id }}" class="h-4 w-4 border-zinc-300 text-blue-600 focus:ring-2 focus:ring-blue-600 dark:border-zinc-700">

                                                {{ $rate->name }}

                                                    @if ($rate->estimated_days_min && $rate->estimated_days_max)
                                                        {{ $rate->estimated_days_min }}–{{ $rate->estimated_days_max }} business days
                                                    @else
                                                        Delivery time shown at dispatch
                                                    @endif

                                            {{ $this->formatMoney($rate->price_amount, $rate->currency) }}

                                    @endforeach

                            </fieldset>
                            @error('shippingRateId')
{{ $message }}
@enderror
                            <button type="submit" wire:loading.attr="disabled" class="mt-6 inline-flex min-h-11 items-center justify-center rounded-full bg-blue-600 px-5 py-3 font-semibold text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2 disabled:opacity-60 dark:focus:ring-offset-zinc-950">
                                Continue to payment
                                Saving…
                            </button>
                        @endif