cmd: rg "DiscountService|automatic" tests/Feature app/Services -n


command: sed -n '1,260p' app/Services/PricingEngine.php

PricingEngine.php

type: read

aggregated_output:
<?php

namespace App\Services;

use App\Exceptions\InvalidCheckoutTransitionException;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\ShippingRate;
use App\Models\TaxSettings;
use App\ValueObjects\DiscountResult;
use App\ValueObjects\PricingResult;
use Illuminate\Support\Facades\DB;

class PricingEngine
{
    public function __construct(
        private readonly DiscountService $discounts,
        private readonly ShippingCalculator $shipping,
        private readonly TaxCalculator $taxes,
    ) {}

    public function calculate(Checkout $checkout): PricingResult
    {
        return DB::transaction(function () use ($checkout): PricingResult {
            $checkout = Checkout::withoutGlobalScopes()
                ->with(['cart.lines'])
                ->whereKey($checkout->getKey())
                ->firstOrFail();
            $cart = $checkout->cart;

            $this->resetLineAmounts($checkout);

            $lines = CartLine::withoutGlobalScopes()
                ->where('cart_id', $cart->getKey())
                ->orderBy('id')
                ->get();
            $subtotal = $lines->sum('line_subtotal_amount');
            $discountResult = new DiscountResult(0, []);

            if ($checkout->discount_code) {
                $discount = $this->discounts->validate($checkout->discount_code, $checkout->store, $cart);
                $discountResult = $this->discounts->applyToCart($cart, $discount);
                $lines = CartLine::withoutGlobalScopes()
                    ->where('cart_id', $cart->getKey())
                    ->orderBy('id')
                    ->get();
            }

            $shippingAmount = $this->shippingAmount($checkout, $discountResult);
            $taxSettings = $this->taxSettings($checkout);
            $taxResult = $this->taxes->calculateForAmounts(
                $lines->pluck('line_total_amount')->map(fn (int $amount): int => $amount)->all(),
                $shippingAmount,
                $taxSettings,
                $checkout->shipping_address_json ?? [],
            );
            $discountedSubtotal = $subtotal - $discountResult->amount;
            $total = $taxSettings->prices_include_tax
                ? $discountedSubtotal + $shippingAmount
                : $discountedSubtotal + $shippingAmount + $taxResult->totalAmount;

            $result = new PricingResult(
                subtotal: $subtotal,
                discount: $discountResult->amount,
                shipping: $shippingAmount,
                taxLines: $taxResult->taxLines,
                taxTotal: $taxResult->totalAmount,
                total: $total,
                currency: $cart->currency,
            );

            $checkout->forceFill([
                'tax_provider_snapshot_json' => $taxResult->toArray(),
                'totals_json' => $result->toArray(),
            ])->save();

            return $result;
        });
    }

    private function resetLineAmounts(Checkout $checkout): void
    {
        CartLine::withoutGlobalScopes()
            ->where('cart_id', $checkout->cart_id)
            ->get()
            ->each(function (CartLine $line): void {
                $subtotal = $line->unit_price_amount * $line->quantity;

                $line->forceFill([
                    'line_subtotal_amount' => $subtotal,
                    'line_discount_amount' => 0,
                    'line_total_amount' => $subtotal,
                ])->save();
            });
    }

    private function shippingAmount(Checkout $checkout, DiscountResult $discountResult): int
    {
        if (! $this->shipping->requiresShipping($checkout->cart)) {
            return 0;
        }

        if ($checkout->shipping_method_id === null) {
            return 0;
        }

        $rate = ShippingRate::withoutGlobalScopes()->find($checkout->shipping_method_id);

        if (! $rate instanceof ShippingRate) {
            throw InvalidCheckoutTransitionException::because('Shipping rate is not available.');
        }

        $amount = $this->shipping->calculate($rate, $checkout->cart);

        if ($amount === null) {
            throw InvalidCheckoutTransitionException::because('Shipping rate is not available for this cart.');
        }

        return $discountResult->freeShipping ? 0 : $amount;
    }

    private function taxSettings(Checkout $checkout): TaxSettings
    {
        return TaxSettings::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->first() ?? new TaxSettings([
                'store_id' => $checkout->store_id,
                'mode' => 'manual',
                'provider' => 'none',
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 0,
                    'shipping_taxable' => false,
                ],
            ]);
    }
}

status: completed


command: sed -n '1,260p' tests/Feature/Checkout/PricingEngineTest.php

PricingEngineTest.php

aggregated_output: sed: tests/Feature/Checkout/PricingEngineTest.php: No such file or directory

status: failed


command: rg "DiscountService|automatic" tests/Feature app/Services -n

type: search

cmd: rg 'DiscountService|automatic' tests/Feature app/Services -n

query: DiscountService|automatic

path: Feature

aggregated_output:
app/Services/DiscountService.php:18:class DiscountService
app/Services/PricingEngine.php:17:        private readonly DiscountService $discounts,
tests/Feature/Admin/DiscountManagementTest.php:158:        ->set('type', 'automatic')


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

namespace App\Services;

use App\Exceptions\InvalidCheckoutTransitionException;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\ShippingRate;
use App\Models\TaxSettings;
use App\ValueObjects\DiscountResult;
use App\ValueObjects\PricingResult;
use Illuminate\Support\Facades\DB;

class PricingEngine
{
    public function __construct(
        private readonly DiscountService $discounts,
        private readonly ShippingCalculator $shipping,
        private readonly TaxCalculator $taxes,
    ) {}

    public function calculate(Checkout $checkout): PricingResult
    {
        return DB::transaction(function () use ($checkout): PricingResult {
            $checkout = Checkout::withoutGlobalScopes()
                ->with(['cart.lines'])
                ->whereKey($checkout->getKey())
                ->firstOrFail();
            $cart = $checkout->cart;

            $this->resetLineAmounts($checkout);

            $lines = CartLine::withoutGlobalScopes()
                ->where('cart_id', $cart->getKey())
                ->orderBy('id')
                ->get();
            $subtotal = $lines->sum('line_subtotal_amount');
            $discountResult = new DiscountResult(0, []);

            if ($checkout->discount_code) {
                $discount = $this->discounts->validate($checkout->discount_code, $checkout->store, $cart);
                $discountResult = $this->discounts->applyToCart($cart, $discount);
                $lines = CartLine::withoutGlobalScopes()
                    ->where('cart_id', $cart->getKey())
                    ->orderBy('id')
                    ->get();
            }

            $shippingAmount = $this->shippingAmount($checkout, $discountResult);
            $taxSettings = $this->taxSettings($checkout);
            $taxResult = $this->taxes->calculateForAmounts(
                $lines->pluck('line_total_amount')->map(fn (int $amount): int => $amount)->all(),
                $shippingAmount,
                $taxSettings,
                $checkout->shipping_address_json ?? [],
            );
            $discountedSubtotal = $subtotal - $discountResult->amount;
            $total = $taxSettings->prices_include_tax
                ? $discountedSubtotal + $shippingAmount
                : $discountedSubtotal + $shippingAmount + $taxResult->totalAmount;

            $result = new PricingResult(
                subtotal: $subtotal,
                discount: $discountResult->amount,
                shipping: $shippingAmount,
                taxLines: $taxResult->taxLines,
                taxTotal: $taxResult->totalAmount,
                total: $total,
                currency: $cart->currency,
            );

            $checkout->forceFill([
                'tax_provider_snapshot_json' => $taxResult->toArray(),
                'totals_json' => $result->toArray(),
            ])->save();

            return $result;
        });
    }

    private function resetLineAmounts(Checkout $checkout): void
    {
        CartLine::withoutGlobalScopes()
            ->where('cart_id', $checkout->cart_id)
            ->get()
            ->each(function (CartLine $line): void {
                $subtotal = $line->unit_price_amount * $line->quantity;

                $line->forceFill([
                    'line_subtotal_amount' => $subtotal,
                    'line_discount_amount' => 0,
                    'line_total_amount' => $subtotal,
                ])->save();
            });
    }

    private function shippingAmount(Checkout $checkout, DiscountResult $discountResult): int
    {
        if (! $this->shipping->requiresShipping($checkout->cart)) {
            return 0;
        }

        if ($checkout->shipping_method_id === null) {
            return 0;
        }

        $rate = ShippingRate::withoutGlobalScopes()->find($checkout->shipping_method_id);

        if (! $rate instanceof ShippingRate) {
            throw InvalidCheckoutTransitionException::because('Shipping rate is not available.');
        }

        $amount = $this->shipping->calculate($rate, $checkout->cart);

        if ($amount === null) {
            throw InvalidCheckoutTransitionException::because('Shipping rate is not available for this cart.');
        }

        return $discountResult->freeShipping ? 0 : $amount;
    }

    private function taxSettings(Checkout $checkout): TaxSettings
    {
        return TaxSettings::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->first() ?? new TaxSettings([
                'store_id' => $checkout->store_id,
                'mode' => 'manual',
                'provider' => 'none',
                'prices_include_tax' => false,
                'config_json' => [
                    'default_rate_bps' => 0,
                    'shipping_taxable' => false,
                ],
            ]);
    }
}


output:
Chunk ID: 5f24f4
Wall time: 0.0000 seconds
Process exited with code 1
Original token count: 20
Output:
sed: tests/Feature/Checkout/PricingEngineTest.php: No such file or directory


output:
Chunk ID: 4729c6
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 58
Output:
app/Services/DiscountService.php:18:class DiscountService
app/Services/PricingEngine.php:17:        private readonly DiscountService $discounts,
tests/Feature/Admin/DiscountManagementTest.php:158:        ->set('type', 'automatic')


cmd: rg "PricingEngine|discount" tests/Feature/Checkout tests/Feature/Cart tests/Feature/Orders -n


command: rg "PricingEngine|discount" tests/Feature/Checkout tests/Feature/Cart tests/Feature/Orders -n

type: search

cmd: rg 'PricingEngine|discount' tests/Feature/Checkout tests/Feature/Cart tests/Feature/Orders -n

query: PricingEngine|discount

path: Checkout

aggregated_output:
tests/Feature/Orders/OrderServiceTest.php:25:use App\Services\PricingEngine;
tests/Feature/Orders/OrderServiceTest.php:111:function orderCompletionCheckout(Store $store, string $paymentMethod = 'credit_card', ?string $discountCode = null): array
tests/Feature/Orders/OrderServiceTest.php:133:    if ($discountCode !== null) {
tests/Feature/Orders/OrderServiceTest.php:134:        $checkout->forceFill(['discount_code' => $discountCode])->save();
tests/Feature/Orders/OrderServiceTest.php:135:        app(PricingEngine::class)->calculate($checkout);
tests/Feature/Orders/OrderServiceTest.php:145:    $discount = Discount::factory()
tests/Feature/Orders/OrderServiceTest.php:151:    [$checkout, $variant, $cart] = orderCompletionCheckout($store, discountCode: 'SAVE500');
tests/Feature/Orders/OrderServiceTest.php:165:        ->and($order->lines->first()->discount_allocations_json)->toBe([['code' => 'SAVE500', 'amount' => 500]])
tests/Feature/Orders/OrderServiceTest.php:171:        ->and($discount->refresh()->usage_count)->toBe(1);
tests/Feature/Checkout/PricingServicesTest.php:13:use App\Services\PricingEngine;
tests/Feature/Checkout/PricingServicesTest.php:103:    $result = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:112:test('pricing engine applies percent and free shipping discounts', function () {
tests/Feature/Checkout/PricingServicesTest.php:133:        'discount_code' => 'save10',
tests/Feature/Checkout/PricingServicesTest.php:136:    $discounted = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:138:    expect($discounted->discount)->toBe(500)
tests/Feature/Checkout/PricingServicesTest.php:139:        ->and($discounted->shipping)->toBe(499)
tests/Feature/Checkout/PricingServicesTest.php:140:        ->and($discounted->total)->toBe(5949);
tests/Feature/Checkout/PricingServicesTest.php:142:    $checkout->forceFill(['discount_code' => 'FREESHIP'])->save();
tests/Feature/Checkout/PricingServicesTest.php:143:    $freeShipping = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:145:    expect($freeShipping->discount)->toBe(0)


output:
Chunk ID: 05d0df
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 549
Output:
tests/Feature/Orders/OrderServiceTest.php:25:use App\Services\PricingEngine;
tests/Feature/Orders/OrderServiceTest.php:111:function orderCompletionCheckout(Store $store, string $paymentMethod = 'credit_card', ?string $discountCode = null): array
tests/Feature/Orders/OrderServiceTest.php:133:    if ($discountCode !== null) {
tests/Feature/Orders/OrderServiceTest.php:134:        $checkout->forceFill(['discount_code' => $discountCode])->save();
tests/Feature/Orders/OrderServiceTest.php:135:        app(PricingEngine::class)->calculate($checkout);
tests/Feature/Orders/OrderServiceTest.php:145:    $discount = Discount::factory()
tests/Feature/Orders/OrderServiceTest.php:151:    [$checkout, $variant, $cart] = orderCompletionCheckout($store, discountCode: 'SAVE500');
tests/Feature/Orders/OrderServiceTest.php:165:        ->and($order->lines->first()->discount_allocations_json)->toBe([['code' => 'SAVE500', 'amount' => 500]])
tests/Feature/Orders/OrderServiceTest.php:171:        ->and($discount->refresh()->usage_count)->toBe(1);
tests/Feature/Checkout/PricingServicesTest.php:13:use App\Services\PricingEngine;
tests/Feature/Checkout/PricingServicesTest.php:103:    $result = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:112:test('pricing engine applies percent and free shipping discounts', function () {
tests/Feature/Checkout/PricingServicesTest.php:133:        'discount_code' => 'save10',
tests/Feature/Checkout/PricingServicesTest.php:136:    $discounted = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:138:    expect($discounted->discount)->toBe(500)
tests/Feature/Checkout/PricingServicesTest.php:139:        ->and($discounted->shipping)->toBe(499)
tests/Feature/Checkout/PricingServicesTest.php:140:        ->and($discounted->total)->toBe(5949);
tests/Feature/Checkout/PricingServicesTest.php:142:    $checkout->forceFill(['discount_code' => 'FREESHIP'])->save();
tests/Feature/Checkout/PricingServicesTest.php:143:    $freeShipping = app(PricingEngine::class)->calculate($checkout);
tests/Feature/Checkout/PricingServicesTest.php:145:    expect($freeShipping->discount)->toBe(0)


cmd: sed -n '1,230p' tests/Feature/Checkout/PricingServicesTest.php


command: sed -n '1,230p' tests/Feature/Checkout/PricingServicesTest.php

aggregated_output:
<?php

use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use App\Services\TaxCalculator;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

    return $store;
}

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

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

    return $variant->refresh();
}

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

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

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

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

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

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

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

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

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

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

test('shipping and tax calculators handle matching ranges and inclusive extraction', function () {
    $store = pricingStore();
    $physicalVariant = pricingVariant($store, requiresShipping: true);
    $digitalVariant = pricingVariant($store, requiresShipping: false);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $physicalVariant->getKey(), 2);
    app(CartService::class)->addLine($cart, $digitalVariant->getKey(), 1);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Berlin',
        'countries_json' => ['DE'],
        'regions_json' => ['DE-BE'],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Weight',
        'type' => 'weight',
        'config_json' => [
            'ranges' => [
                ['min_g' => 0, 'max_g' => 500, 'amount' => 499],
                ['min_g' => 501, 'max_g' => 2000, 'amount' => 899],
            ],
        ],
        'is_active' => true,
    ]);

    $rates = app(ShippingCalculator::class)->getAvailableRates($store, ['country' => 'DE', 'province_code' => 'DE-BE']);

    expect($rates->first()?->getKey())->toBe($rate->getKey())
        ->and(app(ShippingCalculator::class)->calculate($rate, $cart))->toBe(899)
        ->and(app(TaxCalculator::class)->extractInclusive(11900, 1900))->toBe(1900)
        ->and(app(TaxCalculator::class)->addExclusive(10000, 1900))->toBe(1900);
});


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

use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\PricingEngine;
use App\Services\ShippingCalculator;
use App\Services\TaxCalculator;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

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

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

    return $store;
}

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

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

    return $variant->refresh();
}

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

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

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

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

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

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

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

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

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

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

test('shipping and tax calculators handle matching ranges and inclusive extraction', function () {
    $store = pricingStore();
    $physicalVariant = pricingVariant($store, requiresShipping: true);
    $digitalVariant = pricingVariant($store, requiresShipping: false);
    $cart = app(CartService::class)->create($store);
    app(CartService::class)->addLine($cart, $physicalVariant->getKey(), 2);
    app(CartService::class)->addLine($cart, $digitalVariant->getKey(), 1);
    $zone = ShippingZone::withoutGlobalScopes()->create([
        'store_id' => $store->getKey(),
        'name' => 'Berlin',
        'countries_json' => ['DE'],
        'regions_json' => ['DE-BE'],
    ]);
    $rate = ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Weight',
        'type' => 'weight',
        'config_json' => [
            'ranges' => [
                ['min_g' => 0, 'max_g' => 500, 'amount' => 499],
                ['min_g' => 501, 'max_g' => 2000, 'amount' => 899],
            ],
        ],
        'is_active' => true,
    ]);

    $rates = app(ShippingCalculator::class)->getAvailableRates($store, ['country' => 'DE', 'province_code' => 'DE-BE']);

    expect($rates->first()?->getKey())->toBe($rate->getKey())
        ->and(app(ShippingCalculator::class)->calculate($rate, $cart))->toBe(899)
        ->and(app(TaxCalculator::class)->extractInclusive(11900, 1900))->toBe(1900)
        ->and(app(TaxCalculator::class)->addExclusive(10000, 1900))->toBe(1900);
});


cmd: sed -n '1,280p' app/Services/OrderService.php


command: sed -n '1,280p' app/Services/OrderService.php

aggregated_output:
<?php

namespace App\Services;

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidOrderOperationException;
use App\Exceptions\PaymentFailedException;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\StoreSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use ValueError;

class OrderService
{
    public function __construct(
        private readonly PaymentService $payments,
        private readonly InventoryService $inventory,
        private readonly PricingEngine $pricing,
        private readonly FulfillmentService $fulfillments,
    ) {}

    /**
     * Create an order from a checkout after payment has been selected.
     *
     * @param  array<string, mixed>  $paymentMethodData
     */
    public function createFromCheckout(Checkout $checkout, array $paymentMethodData = []): Order
    {
        try {
            return DB::transaction(function () use ($checkout, $paymentMethodData): Order {
                $checkout = $this->freshCheckout($checkout);
                $existingOrder = Order::withoutGlobalScopes()
                    ->where('checkout_id', $checkout->getKey())
                    ->first();

                if ($existingOrder instanceof Order) {
                    return $existingOrder->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);
                }

                if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                    throw InvalidCheckoutTransitionException::because("Checkout cannot transition from {$checkout->status->value}.");
                }

                if ($checkout->totals_json === null) {
                    $this->pricing->calculate($checkout);
                    $checkout = $this->freshCheckout($checkout);
                }

                $method = $this->paymentMethod($checkout);
                $paymentResult = $this->payments->charge($checkout, $method, $paymentMethodData);

                if (! $paymentResult->success) {
                    throw PaymentFailedException::fromResult($paymentResult);
                }

                $paidImmediately = $paymentResult->status === PaymentStatus::Captured;
                $totals = $checkout->totals_json ?? [];

                $order = Order::withoutGlobalScopes()->create([
                    'store_id' => $checkout->store_id,
                    'checkout_id' => $checkout->getKey(),
                    'customer_id' => $checkout->customer_id,
                    'order_number' => $this->nextOrderNumber($checkout->store),
                    'payment_method' => $method,
                    'status' => $paidImmediately ? OrderStatus::Paid : OrderStatus::Pending,
                    'financial_status' => $paidImmediately ? FinancialStatus::Paid : FinancialStatus::Pending,
                    'fulfillment_status' => FulfillmentStatus::Unfulfilled,
                    'currency' => (string) data_get($totals, 'currency', $checkout->cart->currency),
                    'subtotal_amount' => (int) data_get($totals, 'subtotal', 0),
                    'discount_amount' => (int) data_get($totals, 'discount', 0),
                    'shipping_amount' => (int) data_get($totals, 'shipping', 0),
                    'tax_amount' => (int) data_get($totals, 'tax', 0),
                    'total_amount' => (int) data_get($totals, 'total', 0),
                    'email' => $checkout->email,
                    'billing_address_json' => $checkout->billing_address_json,
                    'shipping_address_json' => $checkout->shipping_address_json,
                    'placed_at' => now(),
                ]);

                $this->createOrderLines($checkout, $order);

                $order->payments()->create([
                    'provider' => 'mock',
                    'method' => $method,
                    'provider_payment_id' => $paymentResult->referenceId,
                    'status' => $paymentResult->status,
                    'amount' => $order->total_amount,
                    'currency' => $order->currency,
                    'raw_json_encrypted' => $paymentResult->toArray(),
                ]);

                if ($paidImmediately) {
                    $this->commitReservedInventory($checkout);
                }

                $checkout->cart->forceFill([
                    'status' => CartStatus::Converted,
                ])->save();

                $checkout->forceFill([
                    'status' => CheckoutStat…527 chars truncated…         }

                return $order;
            });
        } catch (PaymentFailedException $exception) {
            $this->releaseFailedPaymentReservation($checkout);

            throw $exception;
        }
    }

    public function confirmBankTransferPayment(Order $order): Order
    {
        return DB::transaction(function () use ($order): Order {
            $order = $this->freshOrder($order);
            $this->assertPendingBankTransfer($order);

            $order->payments()
                ->where('status', PaymentStatus::Pending->value)
                ->update(['status' => PaymentStatus::Captured->value]);

            $this->commitReservedOrderInventory($order);

            $order->forceFill([
                'status' => OrderStatus::Paid,
                'financial_status' => FinancialStatus::Paid,
            ])->save();

            $this->fulfillments->autoFulfillDigital($order);

            $order = $order->refresh()->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);

            event(new OrderPaid($order));

            return $order;
        });
    }

    public function cancelUnpaidBankTransferOrder(Order $order): Order
    {
        return DB::transaction(function () use ($order): Order {
            $order = $this->freshOrder($order);
            $this->assertPendingBankTransfer($order);

            $this->releaseReservedOrderInventory($order);

            $order->payments()
                ->where('status', PaymentStatus::Pending->value)
                ->update(['status' => PaymentStatus::Failed->value]);

            $order->forceFill([
                'status' => OrderStatus::Cancelled,
                'financial_status' => FinancialStatus::Voided,
            ])->save();

            $order = $order->refresh()->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);

            event(new OrderCancelled($order));

            return $order;
        });
    }

    private function freshCheckout(Checkout $checkout): Checkout
    {
        return Checkout::withoutGlobalScopes()
            ->with(['cart', 'store'])
            ->whereKey($checkout->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function freshOrder(Order $order): Order
    {
        return Order::withoutGlobalScopes()
            ->with(['lines', 'payments', 'store.settings'])
            ->whereKey($order->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function paymentMethod(Checkout $checkout): PaymentMethod
    {
        try {
            return PaymentMethod::from((string) $checkout->payment_method);
        } catch (ValueError) {
            throw InvalidCheckoutTransitionException::because('Payment method is invalid.');
        }
    }

    private function nextOrderNumber(Store $store): string
    {
        $settings = StoreSettings::query()
            ->where('store_id', $store->getKey())
            ->first()
            ?->settings_json ?? [];
        $prefix = (string) data_get($settings, 'order_number_prefix', '#');
        $start = max(1, (int) data_get($settings, 'order_number_start', 1001));
        $maxExisting = Order::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->pluck('order_number')
            ->map(fn (string $orderNumber): ?int => $this->sequenceFromOrderNumber($orderNumber))
            ->filter()
            ->max();

        return $prefix.((int) max($start - 1, $maxExisting ?? 0) + 1);
    }

    private function sequenceFromOrderNumber(string $orderNumber): ?int
    {
        $digits = preg_replace('/\D+/', '', $orderNumber);

        return $digits === '' ? null : (int) $digits;
    }

    private function createOrderLines(Checkout $checkout, Order $order): void
    {
        $this->cartLines($checkout)->each(function (CartLine $line) use ($checkout, $order): void {
            $variant = $line->variant;

            $order->lines()->create([
                'product_id' => $variant?->product_id,
                'variant_id' => $variant?->getKey(),
                'title_snapshot' => $this->titleSnapshot($variant),
                'sku_snapshot' => $variant?->sku,
                'quantity' => $line->quantity,
                'unit_price_amount' => $line->unit_price_amount,
                'total_amount' => $line->line_total_amount,
                'tax_lines_json' => [],
                'discount_allocations_json' => $this->discountAllocations($checkout, $line),
            ]);
        });
    }

    private function titleSnapshot(?ProductVariant $variant): string
    {
        $title = $variant?->product?->title ?? 'Product';
        $optionValues = $variant?->optionValues
            ->sortBy(fn (ProductOptionValue $value): int => $value->option?->position ?? $value->position)
            ->pluck('value')
            ->filter()
            ->implode(' / ');

        return $optionValues ? "{$title} - {$optionValues}" : $title;
    }


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

namespace App\Services;

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentMethod;
use App\Enums\PaymentStatus;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Exceptions\InvalidCheckoutTransitionException;
use App\Exceptions\InvalidOrderOperationException;
use App\Exceptions\PaymentFailedException;
use App\Models\CartLine;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\OrderLine;
use App\Models\ProductOptionValue;
use App\Models\ProductVariant;
use App\Models\Store;
use App\Models\StoreSettings;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use ValueError;

class OrderService
{
    public function __construct(
        private readonly PaymentService $payments,
        private readonly InventoryService $inventory,
        private readonly PricingEngine $pricing,
        private readonly FulfillmentService $fulfillments,
    ) {}

    /**
     * Create an order from a checkout after payment has been selected.
     *
     * @param  array<string, mixed>  $paymentMethodData
     */
    public function createFromCheckout(Checkout $checkout, array $paymentMethodData = []): Order
    {
        try {
            return DB::transaction(function () use ($checkout, $paymentMethodData): Order {
                $checkout = $this->freshCheckout($checkout);
                $existingOrder = Order::withoutGlobalScopes()
                    ->where('checkout_id', $checkout->getKey())
                    ->first();

                if ($existingOrder instanceof Order) {
                    return $existingOrder->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);
                }

                if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                    throw InvalidCheckoutTransitionException::because("Checkout cannot transition from {$checkout->status->value}.");
                }

                if ($checkout->totals_json === null) {
                    $this->pricing->calculate($checkout);
                    $checkout = $this->freshCheckout($checkout);
                }

                $method = $this->paymentMethod($checkout);
                $paymentResult = $this->payments->charge($checkout, $method, $paymentMethodData);

                if (! $paymentResult->success) {
                    throw PaymentFailedException::fromResult($paymentResult);
                }

                $paidImmediately = $paymentResult->status === PaymentStatus::Captured;
                $totals = $checkout->totals_json ?? [];

                $order = Order::withoutGlobalScopes()->create([
                    'store_id' => $checkout->store_id,
                    'checkout_id' => $checkout->getKey(),
                    'customer_id' => $checkout->customer_id,
                    'order_number' => $this->nextOrderNumber($checkout->store),
                    'payment_method' => $method,
                    'status' => $paidImmediately ? OrderStatus::Paid : OrderStatus::Pending,
                    'financial_status' => $paidImmediately ? FinancialStatus::Paid : FinancialStatus::Pending,
                    'fulfillment_status' => FulfillmentStatus::Unfulfilled,
                    'currency' => (string) data_get($totals, 'currency', $checkout->cart->currency),
                    'subtotal_amount' => (int) data_get($totals, 'subtotal', 0),
                    'discount_amount' => (int) data_get($totals, 'discount', 0),
                    'shipping_amount' => (int) data_get($totals, 'shipping', 0),
                    'tax_amount' => (int) data_get($totals, 'tax', 0),
                    'total_amount' => (int) data_get($totals, 'total', 0),
                    'email' => $checkout->email,
                    'billing_address_json' => $checkout->billing_address_json,
                    'shipping_address_json' => $checkout->shipping_address_json,
                    'placed_at' => now(),
                ]);

                $this->createOrderLines($checkout, $order);

                $order->payments()->create([
                    'provider' => 'mock',
                    'method' => $method,
                    'provider_payment_id' => $paymentResult->referenceId,
                    'status' => $paymentResult->status,
                    'amount' => $order->total_amount,
                    'currency' => $order->currency,
                    'raw_json_encrypted' => $paymentResult->toArray(),
                ]);

                if ($paidImmediately) {
                    $this->commitReservedInventory($checkout);
                }

                $checkout->cart->forceFill([
                    'status' => CartStatus::Converted,
                ])->save();

                $checkout->forceFill([
                    'status' => CheckoutStatus::Completed,
                    'expires_at' => null,
                ])->save();

                $this->incrementDiscountUsage($checkout);

                if ($paidImmediately) {
                    $this->fulfillments->autoFulfillDigital($order);
                }

                $order = $order->refresh()->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);

                event(new OrderCreated($order));

                if ($paidImmediately) {
                    event(new OrderPaid($order));
                }

                return $order;
            });
        } catch (PaymentFailedException $exception) {
            $this->releaseFailedPaymentReservation($checkout);

            throw $exception;
        }
    }

    public function confirmBankTransferPayment(Order $order): Order
    {
        return DB::transaction(function () use ($order): Order {
            $order = $this->freshOrder($order);
            $this->assertPendingBankTransfer($order);

            $order->payments()
                ->where('status', PaymentStatus::Pending->value)
                ->update(['status' => PaymentStatus::Captured->value]);

            $this->commitReservedOrderInventory($order);

            $order->forceFill([
                'status' => OrderStatus::Paid,
                'financial_status' => FinancialStatus::Paid,
            ])->save();

            $this->fulfillments->autoFulfillDigital($order);

            $order = $order->refresh()->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);

            event(new OrderPaid($order));

            return $order;
        });
    }

    public function cancelUnpaidBankTransferOrder(Order $order): Order
    {
        return DB::transaction(function () use ($order): Order {
            $order = $this->freshOrder($order);
            $this->assertPendingBankTransfer($order);

            $this->releaseReservedOrderInventory($order);

            $order->payments()
                ->where('status', PaymentStatus::Pending->value)
                ->update(['status' => PaymentStatus::Failed->value]);

            $order->forceFill([
                'status' => OrderStatus::Cancelled,
                'financial_status' => FinancialStatus::Voided,
            ])->save();

            $order = $order->refresh()->load(['lines', 'payments', 'refunds', 'fulfillments.lines']);

            event(new OrderCancelled($order));

            return $order;
        });
    }

    private function freshCheckout(Checkout $checkout): Checkout
    {
        return Checkout::withoutGlobalScopes()
            ->with(['cart', 'store'])
            ->whereKey($checkout->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function freshOrder(Order $order): Order
    {
        return Order::withoutGlobalScopes()
            ->with(['lines', 'payments', 'store.settings'])
            ->whereKey($order->getKey())
            ->lockForUpdate()
            ->firstOrFail();
    }

    private function paymentMethod(Checkout $checkout): PaymentMethod
    {
        try {
            return PaymentMethod::from((string) $checkout->payment_method);
        } catch (ValueError) {
            throw InvalidCheckoutTransitionException::because('Payment method is invalid.');
        }
    }

    private function nextOrderNumber(Store $store): string
    {
        $settings = StoreSettings::query()
            ->where('store_id', $store->getKey())
            ->first()
            ?->settings_json ?? [];
        $prefix = (string) data_get($settings, 'order_number_prefix', '#');
        $start = max(1, (int) data_get($settings, 'order_number_start', 1001));
        $maxExisting = Order::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->pluck('order_number')
            ->map(fn (string $orderNumber): ?int => $this->sequenceFromOrderNumber($orderNumber))
            ->filter()
            ->max();

        return $prefix.((int) max($start - 1, $maxExisting ?? 0) + 1);
    }

    private function sequenceFromOrderNumber(string $orderNumber): ?int
    {
        $digits = preg_replace('/\D+/', '', $orderNumber);

        return $digits === '' ? null : (int) $digits;
    }

    private function createOrderLines(Checkout $checkout, Order $order): void
    {
        $this->cartLines($checkout)->each(function (CartLine $line) use ($checkout, $order): void {
            $variant = $line->variant;

            $order->lines()->create([
                'product_id' => $variant?->product_id,
                'variant_id' => $variant?->getKey(),
                'title_snapshot' => $this->titleSnapshot($variant),
                'sku_snapshot' => $variant?->sku,
                'quantity' => $line->quantity,
                'unit_price_amount' => $line->unit_price_amount,
                'total_amount' => $line->line_total_amount,
                'tax_lines_json' => [],
                'discount_allocations_json' => $this->discountAllocations($checkout, $line),
            ]);
        });
    }

    private function titleSnapshot(?ProductVariant $variant): string
    {
        $title = $variant?->product?->title ?? 'Product';
        $optionValues = $variant?->optionValues
            ->sortBy(fn (ProductOptionValue $value): int => $value->option?->position ?? $value->position)
            ->pluck('value')
            ->filter()
            ->implode(' / ');

        return $optionValues ? "{$title} - {$optionValues}" : $title;
    }


cmd: sed -n '280,430p' app/Services/OrderService.php


command: sed -n '280,430p' app/Services/OrderService.php

aggregated_output:
    }

    /**
     * @return array<int, array{code: string, amount: int}>
     */
    private function discountAllocations(Checkout $checkout, CartLine $line): array
    {
        if ($checkout->discount_code === null || $line->line_discount_amount <= 0) {
            return [];
        }

        return [[
            'code' => $checkout->discount_code,
            'amount' => $line->line_discount_amount,
        ]];
    }

    private function commitReservedInventory(Checkout $checkout): void
    {
        $this->cartLines($checkout)->each(function (CartLine $line): void {
            $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
        });
    }

    private function commitReservedOrderInventory(Order $order): void
    {
        $order->lines->each(function (OrderLine $line): void {
            if ($line->variant_id !== null) {
                $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            }
        });
    }

    private function releaseReservedOrderInventory(Order $order): void
    {
        $order->lines->each(function (OrderLine $line): void {
            if ($line->variant_id !== null) {
                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            }
        });
    }

    private function releaseFailedPaymentReservation(Checkout $checkout): void
    {
        DB::transaction(function () use ($checkout): void {
            $checkout = $this->freshCheckout($checkout);

            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                return;
            }

            $this->cartLines($checkout)->each(function (CartLine $line): void {
                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            });

            $checkout->forceFill([
                'status' => CheckoutStatus::ShippingSelected,
                'payment_method' => null,
                'expires_at' => null,
            ])->save();
        });
    }

    private function incrementDiscountUsage(Checkout $checkout): void
    {
        $code = trim((string) $checkout->discount_code);

        if ($code === '') {
            return;
        }

        Discount::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->whereRaw('lower(code) = ?', [mb_strtolower($code)])
            ->increment('usage_count');
    }

    private function assertPendingBankTransfer(Order $order): void
    {
        if ($order->payment_method !== PaymentMethod::BankTransfer || $order->financial_status !== FinancialStatus::Pending) {
            throw InvalidOrderOperationException::because('Order is not a pending bank transfer order.');
        }
    }

    private function inventoryItemForVariant(int $variantId): InventoryItem
    {
        return InventoryItem::withoutGlobalScopes()
            ->where('variant_id', $variantId)
            ->firstOrFail();
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Checkout $checkout): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->with([
                'variant' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.product' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.optionValues' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.optionValues.option' => fn ($query) => $query->withoutGlobalScopes(),
            ])
            ->where('cart_id', $checkout->cart_id)
            ->orderBy('id')
            ->get();
    }
}


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

    /**
     * @return array<int, array{code: string, amount: int}>
     */
    private function discountAllocations(Checkout $checkout, CartLine $line): array
    {
        if ($checkout->discount_code === null || $line->line_discount_amount <= 0) {
            return [];
        }

        return [[
            'code' => $checkout->discount_code,
            'amount' => $line->line_discount_amount,
        ]];
    }

    private function commitReservedInventory(Checkout $checkout): void
    {
        $this->cartLines($checkout)->each(function (CartLine $line): void {
            $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
        });
    }

    private function commitReservedOrderInventory(Order $order): void
    {
        $order->lines->each(function (OrderLine $line): void {
            if ($line->variant_id !== null) {
                $this->inventory->commit($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            }
        });
    }

    private function releaseReservedOrderInventory(Order $order): void
    {
        $order->lines->each(function (OrderLine $line): void {
            if ($line->variant_id !== null) {
                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            }
        });
    }

    private function releaseFailedPaymentReservation(Checkout $checkout): void
    {
        DB::transaction(function () use ($checkout): void {
            $checkout = $this->freshCheckout($checkout);

            if ($checkout->status !== CheckoutStatus::PaymentSelected) {
                return;
            }

            $this->cartLines($checkout)->each(function (CartLine $line): void {
                $this->inventory->release($this->inventoryItemForVariant($line->variant_id), $line->quantity);
            });

            $checkout->forceFill([
                'status' => CheckoutStatus::ShippingSelected,
                'payment_method' => null,
                'expires_at' => null,
            ])->save();
        });
    }

    private function incrementDiscountUsage(Checkout $checkout): void
    {
        $code = trim((string) $checkout->discount_code);

        if ($code === '') {
            return;
        }

        Discount::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->whereRaw('lower(code) = ?', [mb_strtolower($code)])
            ->increment('usage_count');
    }

    private function assertPendingBankTransfer(Order $order): void
    {
        if ($order->payment_method !== PaymentMethod::BankTransfer || $order->financial_status !== FinancialStatus::Pending) {
            throw InvalidOrderOperationException::because('Order is not a pending bank transfer order.');
        }
    }

    private function inventoryItemForVariant(int $variantId): InventoryItem
    {
        return InventoryItem::withoutGlobalScopes()
            ->where('variant_id', $variantId)
            ->firstOrFail();
    }

    /**
     * @return Collection<int, CartLine>
     */
    private function cartLines(Checkout $checkout): Collection
    {
        return CartLine::withoutGlobalScopes()
            ->with([
                'variant' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.product' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.optionValues' => fn ($query) => $query->withoutGlobalScopes(),
                'variant.optionValues.option' => fn ($query) => $query->withoutGlobalScopes(),
            ])
            ->where('cart_id', $checkout->cart_id)
            ->orderBy('id')
            ->get();
    }
}


cmd: sed -n '1,180p' app/ValueObjects/PricingResult.php && sed -n '1,120p' app/ValueObjects/DiscountResult.php


command: sed -n '1,180p' app/ValueObjects/PricingResult.php && sed -n '1,120p' app/ValueObjects/DiscountResult.php

DiscountResult.php

aggregated_output:
<?php

namespace App\ValueObjects;

final readonly class PricingResult
{
    /**
     * @param  array<int, TaxLine>  $taxLines
     */
    public function __construct(
        public int $subtotal,
        public int $discount,
        public int $shipping,
        public array $taxLines,
        public int $taxTotal,
        public int $total,
        public string $currency,
    ) {}

    /**
     * @return array{subtotal: int, discount: int, shipping: int, tax_lines: array<int, array{name: string, rate: int, amount: int}>, tax: int, total: int, currency: string}
     */
    public function toArray(): array
    {
        return [
            'subtotal' => $this->subtotal,
            'discount' => $this->discount,
            'shipping' => $this->shipping,
            'tax_lines' => array_map(fn (TaxLine $line): array => $line->toArray(), $this->taxLines),
            'tax' => $this->taxTotal,
            'total' => $this->total,
            'currency' => $this->currency,
        ];
    }
}
<?php

namespace App\ValueObjects;

final readonly class DiscountResult
{
    /**
     * @param  array<int, int>  $allocations
     */
    public function __construct(
        public int $amount,
        public array $allocations,
        public bool $freeShipping = false,
    ) {}
}


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

namespace App\ValueObjects;

final readonly class PricingResult
{
    /**
     * @param  array<int, TaxLine>  $taxLines
     */
    public function __construct(
        public int $subtotal,
        public int $discount,
        public int $shipping,
        public array $taxLines,
        public int $taxTotal,
        public int $total,
        public string $currency,
    ) {}

    /**
     * @return array{subtotal: int, discount: int, shipping: int, tax_lines: array<int, array{name: string, rate: int, amount: int}>, tax: int, total: int, currency: string}
     */
    public function toArray(): array
    {
        return [
            'subtotal' => $this->subtotal,
            'discount' => $this->discount,
            'shipping' => $this->shipping,
            'tax_lines' => array_map(fn (TaxLine $line): array => $line->toArray(), $this->taxLines),
            'tax' => $this->taxTotal,
            'total' => $this->total,
            'currency' => $this->currency,
        ];
    }
}
<?php

namespace App\ValueObjects;

final readonly class DiscountResult
{
    /**
     * @param  array<int, int>  $allocations
     */
    public function __construct(
        public int $amount,
        public array $allocations,
        public bool $freeShipping = false,
    ) {}
}


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


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


command: sed -n '1,120p' database/migrations/2026_05_04_000408_create_orders_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('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'order_number']);
            $table->unique('checkout_id');
            $table->index('store_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'financial_status']);
            $table->index(['store_id', 'fulfillment_status']);
            $table->index(['store_id', 'placed_at']);
        });
    }

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


command: sed -n '1,120p' database/migrations/2026_05_03_225400_create_carts_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('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('currency', 3)->default('USD');
            $table->unsignedInteger('cart_version')->default(1);
            $table->enum('status', ['active', 'converted', 'abandoned'])->default('active');
            $table->timestamps();

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

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


output:
Chunk ID: cbd8cd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 261
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('carts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('currency', 3)->default('USD');
            $table->unsignedInteger('cart_version')->default(1);
            $table->enum('status', ['active', 'converted', 'abandoned'])->default('active');
            $table->timestamps();

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

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


output:
Chunk ID: 7fd9a9
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 571
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('orders', function (Blueprint $table) {
            $table->id();
            $table->foreignId('store_id')->constrained()->cascadeOnDelete();
            $table->foreignId('checkout_id')->nullable()->constrained()->nullOnDelete();
            $table->foreignId('customer_id')->nullable()->constrained()->nullOnDelete();
            $table->string('order_number');
            $table->enum('payment_method', ['credit_card', 'paypal', 'bank_transfer']);
            $table->enum('status', ['pending', 'paid', 'fulfilled', 'cancelled', 'refunded'])->default('pending');
            $table->enum('financial_status', ['pending', 'authorized', 'paid', 'partially_refunded', 'refunded', 'voided'])->default('pending');
            $table->enum('fulfillment_status', ['unfulfilled', 'partial', 'fulfilled'])->default('unfulfilled');
            $table->string('currency', 3)->default('USD');
            $table->integer('subtotal_amount')->default(0);
            $table->integer('discount_amount')->default(0);
            $table->integer('shipping_amount')->default(0);
            $table->integer('tax_amount')->default(0);
            $table->integer('total_amount')->default(0);
            $table->string('email')->nullable();
            $table->text('billing_address_json')->nullable();
            $table->text('shipping_address_json')->nullable();
            $table->timestamp('placed_at')->nullable();
            $table->timestamps();

            $table->unique(['store_id', 'order_number']);
            $table->unique('checkout_id');
            $table->index('store_id');
            $table->index('customer_id');
            $table->index(['store_id', 'status']);
            $table->index(['store_id', 'financial_status']);
            $table->index(['store_id', 'fulfillment_status']);
            $table->index(['store_id', 'placed_at']);
        });
    }

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


message: I’m also invoking the Fortify/auth skill for the email-verification middleware fix because it changes how the existing `verified` admin middleware actually behaves on Livewire requests.

phase: commentary


type: message

role: assistant

text: I’m also invoking the Fortify/auth skill for the email-verification middleware fix because it changes how the existing `verified` admin middleware actually behaves on Livewire requests.


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


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

aggregated_output:
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
---

# Laravel Fortify Development

Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.

## Documentation

Use `search-docs` for detailed Laravel Fortify patterns and documentation.

## Usage

- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.

## Available Features

Enable in `config/fortify.php` features array:

- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes

> Use `search-docs` for feature configuration options and customization patterns.

## Setup Workflows

### Two-Factor Authentication Setup

```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```

> Use `search-docs` for TOTP implementation and recovery code handling patterns.

### Email Verification Setup

```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```

> Use `search-docs` for MustVerifyEmail implementation patterns.

### Password Reset Setup

```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```

> Use `search-docs` for custom password reset flow patterns.

### SPA Authentication Setup

```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```

> Use `search-docs` for integration and SPA authentication patterns.

## Best Practices

### Custom Authentication Logic

Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.

### Registration Customization

Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.

### Rate Limiting

Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.

## Key Endpoints

| Feature                | Method   | Endpoint                                    |
|------------------------|----------|---------------------------------------------|
| Login                  | POST     | `/login`                                    |
| Logout                 | POST     | `/logout`                                   |
| Register               | POST     | `/register`                                 |
| Password Reset Request | POST     | `/forgot-password`                          |
| Password Reset         | POST     | `/reset-password`                           |
| Email Verify Notice    | GET      | `/email/verify`                             |
| Resend Verification    | POST     | `/email/verification-notification`          |
| Password Confirm       | POST     | `/user/confirm-password`                    |
| Enable 2FA             | POST     | `/user/two-factor-authentication`           |
| Confirm 2FA            | POST     | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge          | POST     | `/two-factor-challenge`                     |
| Get QR Code            | GET      | `/user/two-factor-qr-code`                  |
| Recovery Codes         | GET/POST | `/user/two-factor-recovery-codes`           |


output:
Chunk ID: 955a8f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1256
Output:
---
name: developing-with-fortify
description: Laravel Fortify headless authentication backend development. Activate when implementing authentication features including login, registration, password reset, email verification, two-factor authentication (2FA/TOTP), profile updates, headless auth, authentication scaffolding, or auth guards in Laravel applications.
---

# Laravel Fortify Development

Fortify is a headless authentication backend that provides authentication routes and controllers for Laravel applications.

## Documentation

Use `search-docs` for detailed Laravel Fortify patterns and documentation.

## Usage

- **Routes**: Use `list-routes` with `only_vendor: true` and `action: "Fortify"` to see all registered endpoints
- **Actions**: Check `app/Actions/Fortify/` for customizable business logic (user creation, password validation, etc.)
- **Config**: See `config/fortify.php` for all options including features, guards, rate limiters, and username field
- **Contracts**: Look in `Laravel\Fortify\Contracts\` for overridable response classes (`LoginResponse`, `LogoutResponse`, etc.)
- **Views**: All view callbacks are set in `FortifyServiceProvider::boot()` using `Fortify::loginView()`, `Fortify::registerView()`, etc.

## Available Features

Enable in `config/fortify.php` features array:

- `Features::registration()` - User registration
- `Features::resetPasswords()` - Password reset via email
- `Features::emailVerification()` - Requires User to implement `MustVerifyEmail`
- `Features::updateProfileInformation()` - Profile updates
- `Features::updatePasswords()` - Password changes
- `Features::twoFactorAuthentication()` - 2FA with QR codes and recovery codes

> Use `search-docs` for feature configuration options and customization patterns.

## Setup Workflows

### Two-Factor Authentication Setup

```
- [ ] Add TwoFactorAuthenticatable trait to User model
- [ ] Enable feature in config/fortify.php
- [ ] Run migrations for 2FA columns
- [ ] Set up view callbacks in FortifyServiceProvider
- [ ] Create 2FA management UI
- [ ] Test QR code and recovery codes
```

> Use `search-docs` for TOTP implementation and recovery code handling patterns.

### Email Verification Setup

```
- [ ] Enable emailVerification feature in config
- [ ] Implement MustVerifyEmail interface on User model
- [ ] Set up verifyEmailView callback
- [ ] Add verified middleware to protected routes
- [ ] Test verification email flow
```

> Use `search-docs` for MustVerifyEmail implementation patterns.

### Password Reset Setup

```
- [ ] Enable resetPasswords feature in config
- [ ] Set up requestPasswordResetLinkView callback
- [ ] Set up resetPasswordView callback
- [ ] Define password.reset named route (if views disabled)
- [ ] Test reset email and link flow
```

> Use `search-docs` for custom password reset flow patterns.

### SPA Authentication Setup

```
- [ ] Set 'views' => false in config/fortify.php
- [ ] Install and configure Laravel Sanctum
- [ ] Use 'web' guard in fortify config
- [ ] Set up CSRF token handling
- [ ] Test XHR authentication flows
```

> Use `search-docs` for integration and SPA authentication patterns.

## Best Practices

### Custom Authentication Logic

Override authentication behavior using `Fortify::authenticateUsing()` for custom user retrieval or `Fortify::authenticateThrough()` to customize the authentication pipeline. Override response contracts in `AppServiceProvider` for custom redirects.

### Registration Customization

Modify `app/Actions/Fortify/CreateNewUser.php` to customize user creation logic, validation rules, and additional fields.

### Rate Limiting

Configure via `fortify.limiters.login` in config. Default configuration throttles by username + IP combination.

## Key Endpoints

| Feature                | Method   | Endpoint                                    |
|------------------------|----------|---------------------------------------------|
| Login                  | POST     | `/login`                                    |
| Logout                 | POST     | `/logout`                                   |
| Register               | POST     | `/register`                                 |
| Password Reset Request | POST     | `/forgot-password`                          |
| Password Reset         | POST     | `/reset-password`                           |
| Email Verify Notice    | GET      | `/email/verify`                             |
| Resend Verification    | POST     | `/email/verification-notification`          |
| Password Confirm       | POST     | `/user/confirm-password`                    |
| Enable 2FA             | POST     | `/user/two-factor-authentication`           |
| Confirm 2FA            | POST     | `/user/confirmed-two-factor-authentication` |
| 2FA Challenge          | POST     | `/two-factor-challenge`                     |
| Get QR Code            | GET      | `/user/two-factor-qr-code`                  |
| Recovery Codes         | GET/POST | `/user/two-factor-recovery-codes`           |


packages: laravel/fortify laravel/framework

queries: email verification MustVerifyEmail verified middleware

queries: Fortify emailVerification feature MustVerifyEmail


server: laravel-boost

tool: search-docs

type: text

text:
# Search Results

## Fortify Email Verification
Fortify | laravel/fortify@latest | Source: https://github.com/laravel/docs/blob/13.x/fortify.md

## Email Verification

After registration, you may wish for users to verify their email address before they continue accessing your application. To get started, ensure the `emailVerification` feature is enabled in your `fortify` configuration file's `features` array. Next, you should ensure that your `App\Models\User` class implements the `Illuminate\Contracts\Auth\MustVerifyEmail` interface.

Once these two setup steps have been completed, newly registered users will receive an email prompting them to verify their email address ownership. However, we need to inform Fortify how to display the email verification screen which informs the user that they need to go click the verification link in the email.

All of Fortify's view's rendering logic may be customized using the appropriate methods available via the `Laravel\Fortify\Fortify` class. Typically, you should call this method from the `boot` method of your application's `App\Providers\FortifyServiceProvider` class:

```php
use Laravel\Fortify\Fortify;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Fortify::verifyEmailView(function () {
        return view('auth.verify-email');
    });

    // ...
}
```

Fortify will take care of defining the route that displays this view when a user is redirected to the `/email/verify` endpoint by Laravel's built-in `verified` middleware.

Your `verify-email` template should include an informational message instructing the user to click the email verification link that was sent to their email address.

---

## Verification Routing The Email Verification Notice
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### The Email Verification Notice

As mentioned previously, a route should be defined that will return a view instructing the user to click the email verification link that was emailed to them by Laravel after registration. This view will be displayed to users when they try to access other parts of the application without verifying their email address first. Remember, the link is automatically emailed to the user as long as your `App\Models\User` model implements the `MustVerifyEmail` interface:

```php
Route::get('/email/verify', function () {
    return view('auth.verify-email');
})->middleware('auth')->name('verification.notice');
```

The route that returns the email verification notice should be named `verification.notice`. It is important that the route is assigned this exact name since the `verified` middleware [included with Laravel](#protecting-routes) will automatically redirect to this route name if a user has not verified their email address.

> [!NOTE]
> When manually implementing email verification, you are required to define the contents of the verification notice view yourself. If you would like scaffolding that includes all necessary authentication and verification views, check out the [Laravel application starter kits](/docs/{{version}}/starter-kits).

---

## Fortify Email Verification
Fortify | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/fortify.md

## Email Verification

After registration, you may wish for users to verify their email address before they continue accessing your application. To get started, ensure the `emailVerification` feature is enabled in your `fortify` configuration file's `features` array. Next, you should ensure that your `App\Models\User` class implements the `Illuminate\Contracts\Auth\MustVerifyEmail` interface.

Once these two setup steps have been completed, newly registered users will receive an email prompting them to verify their email address ownership. However, we need to inform Fortify how to display the email verification screen which informs the user that they need to go click the verification link in the email.

All of Fortify's view's rendering logic may be customized using the appropriate methods available via the `Laravel\Fortify\Fortify` class. Typically, you should call this method from the `boot` method of your application's `App\Providers\FortifyServiceProvider` class:

```php
use Laravel\Fortify\Fortify;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Fortify::verifyEmailView(function () {
        return view('auth.verify-email');
    });

    // ...
}
```

Fortify will take care of defining the route that displays this view when a user is redirected to the `/email/verify` endpoint by Laravel's built-in `verified` middleware.

Your `verify-email` template should include an informational message instructing the user to click the email verification link that was sent to their email address.

---

## Verification
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

# Email Verification

- [Introduction](#introduction)
    - [Model Preparation](#model-preparation)
    - [Database Preparation](#database-preparation)
- [Routing](#verification-routing)
    - [The Email Verification Notice](#the-email-verification-notice)
    - [The Email Verification Handler](#the-email-verification-handler)
    - [Resending the Verification Email](#resending-the-verification-email)
    - [Protecting Routes](#protecting-routes)
- [Customization](#customization)
- [Events](#events)

---

## Verification Routing The Email Verification Handler
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### The Email Verification Handler

Next, we need to define a route that will handle requests generated when the user clicks the email verification link that was emailed to them. This route should be named `verification.verify` and be assigned the `auth` and `signed` middlewares:

```php
use Illuminate\Foundation\Auth\EmailVerificationRequest;

Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) {
    $request->fulfill();

    return redirect('/home');
})->middleware(['auth', 'signed'])->name('verification.verify');
```

Before moving on, let's take a closer look at this route. First, you'll notice we are using an `EmailVerificationRequest` request type instead of the typical `Illuminate\Http\Request` instance. The `EmailVerificationRequest` is a [form request](/docs/{{version}}/validation#form-request-validation) that is included with Laravel. This request will automatically take care of validating the request's `id` and `hash` parameters.

Next, we can proceed directly to calling the `fulfill` method on the request. This method will call the `markEmailAsVerified` method on the authenticated user and dispatch the `Illuminate\Auth\Events\Verified` event. The `markEmailAsVerified` method is available to the default `App\Models\User` model via the `Illuminate\Foundation\Auth\User` base class. Once the user's email address has been verified, you may redirect them wherever you wish.

---

## Fortify Email Verification Protecting Routes
Fortify | laravel/fortify@latest | Source: https://github.com/laravel/docs/blob/13.x/fortify.md

### Protecting Routes

To specify that a route or group of routes requires that the user has verified their email address, you should attach Laravel's built-in `verified` middleware to the route. The `verified` middleware alias is automatically registered by Laravel and serves as an alias for the `Illuminate\Auth\Middleware\EnsureEmailIsVerified` middleware:

```php
Route::get('/dashboard', function () {
    // ...
})->middleware(['verified']);
```

---

## Verification Routing
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

## Routing

To properly implement email verification, three routes will need to be defined. First, a route will be needed to display a notice to the user that they should click the email verification link in the verification email that Laravel sent them after registration.

Second, a route will be needed to handle requests generated when the user clicks the email verification link in the email.

Third, a route will be needed to resend a verification link if the user accidentally loses the first verification link.

---

## Fortify Email Verification Resending Email Verification Links
Fortify | laravel/fortify@latest | Source: https://github.com/laravel/docs/blob/13.x/fortify.md

#### Resending Email Verification Links

If you wish, you may add a button to your application's `verify-email` template that triggers a POST request to the `/email/verification-notification` endpoint. When this endpoint receives a request, a new verification email link will be emailed to the user, allowing the user to get a new verification link if the previous one was accidentally deleted or lost.

If the request to resend the verification link email was successful, Fortify will redirect the user back to the `/email/verify` endpoint with a `status` session variable, allowing you to display an informational message to the user informing them the operation was successful. If the request was an XHR request, a 202 HTTP response will be returned:

```blade
@if (session('status') == 'verification-link-sent')
    <div class="mb-4 font-medium text-sm text-green-600">
        A new email verification link has been emailed to you!
    </div>
@endif
```

---

## Fortify Email Verification Resending Email Verification Links
Fortify | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/fortify.md

#### Resending Email Verification Links

If you wish, you may add a button to your application's `verify-email` template that triggers a POST request to the `/email/verification-notification` endpoint. When this endpoint receives a request, a new verification email link will be emailed to the user, allowing the user to get a new verification link if the previous one was accidentally deleted or lost.

If the request to resend the verification link email was successful, Fortify will redirect the user back to the `/email/verify` endpoint with a `status` session variable, allowing you to display an informational message to the user informing them the operation was successful. If the request was an XHR request, a 202 HTTP response will be returned:

```blade
@if (session('status') == 'verification-link-sent')
    <div class="mb-4 font-medium text-sm text-green-600">
        A new email verification link has been emailed to you!
    </div>
@endif
```

---

## Fortify Email Verification Protecting Routes
Fortify | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/fortify.md

### Protecting Routes

To specify that a route or group of routes requires that the user has verified their email address, you should attach Laravel's built-in `verified` middleware to the route. The `verified` middleware alias is automatically registered by Laravel and serves as an alias for the `Illuminate\Auth\Middleware\EnsureEmailIsVerified` middleware:

```php
Route::get('/dashboard', function () {
    // ...
})->middleware(['verified']);
```

---

## Verification Customization Verification Email Customization
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

#### Verification Email Customization

Although the default email verification notification should satisfy the requirements of most applications, Laravel allows you to customize how the email verification mail message is constructed.

To get started, pass a closure to the `toMailUsing` method provided by the `Illuminate\Auth\Notifications\VerifyEmail` notification. The closure will receive the notifiable model instance that is receiving the notification as well as the signed email verification URL that the user must visit to verify their email address. The closure should return an instance of `Illuminate\Notifications\Messages\MailMessage`. Typically, you should call the `toMailUsing` method from the `boot` method of your application's `AppServiceProvider` class:

```php
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Notifications\Messages\MailMessage;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    // ...

    VerifyEmail::toMailUsing(function (object $notifiable, string $url) {
        return (new MailMessage)
            ->subject('Verify Email Address')
            ->line('Click the button below to verify your email address.')
            ->action('Verify Email Address', $url);
    });
}
```

> [!NOTE]
> To learn more about mail notifications, please consult the [mail notification documentation](/docs/{{version}}/notifications#mail-notifications).

---

## Verification Routing Resending the Verification Email
Verification | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/verification.md

### Resending the Verification Email

Sometimes a user may misplace or accidentally delete the email address verification email. To accommodate this, you may wish to define a route to allow the user to request that the verification email be resent. You may then make a request to this route by placing a simple form submission button within your [verification notice view](#the-email-verification-notice):

```php
use Illuminate\Http\Request;

Route::post('/email/verification-notification', function (Request $request) {
    $request->user()->sendEmailVerificationNotification();

    return back()->with('message', 'Verification link sent!');
})->middleware(['auth', 'throttle:6,1'])->name('verification.send');
```

---

## Validation Available Validation Rules email
Validation | laravel/framework@12.x | Source: https://github.com/laravel/docs/blob/12.x/validation.md

#### email

The field under validation must be formatted as an email address. This validation rule utilizes the [egulias/email-validator](https://github.com/egulias/EmailValidator) package for validating the email address. By default, the `RFCValidation` validator is applied, but you can apply other validation styles as well:

```php
'email' => 'email:rfc,dns'
```

The example above will apply the `RFCValidation` and `DNSCheckValidation` validations. Here's a full list of validation styles you can apply:

- `rfc`: `RFCValidation` - Validate the email address according to [supported RFCs](https://github.com/egulias/EmailValidator?tab=readme-ov-file#supported-rfcs).
- `strict`: `NoRFCWarningsValidation` - Validate the email according to [supported RFCs](https://github.com/egulias/EmailValidator?tab=readme-ov-file#supported-rfcs), failing when warnings are found (e.g. trailing periods and multiple consecutive periods).
- `dns`: `DNSCheckValidation` - Ensure the email address's domain has a valid MX record.
- `spoof`: `SpoofCheckValidation` - Ensure the email address does not contain homograph or deceptive Unicode characters.
- `filter`: `FilterEmailValidation` - Ensure the email address is valid according to PHP's `filter_var` function.
- `filter_unicode`: `FilterEmailValidation::unicode()` - Ensure the email address is valid according to PHP's `filter_var` function, allowing some Unicode characters.

For convenience, email validation rules may be built using the fluent rule builder:

```php
use Illuminate\Validation\Rule;

$request->validate([
    'email' => [
        'required',
        Rule::email()
            ->rfcCompliant(strict: false)
            ->validateMxRecord()
            ->preventSpoofing()
    ],
]);
```

> [!WARNING]
> The `dns` and `spoof` validators require the PHP `intl` extension.

_[Truncated]_