command: sed -n '1,360p' 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…3280 chars truncated…nt 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;
    }

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

        $code = trim($checkout->discount_code);
        $discount = Discount::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->whereRaw('lower(code) = ?', [mb_strtolower($code)])
            ->first();

        return [[
            'discount_id' => $discount?->getKey(),
            'code' => $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)])


output:
Chunk ID: 5ea03c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3320
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;
    }

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

        $code = trim($checkout->discount_code);
        $discount = Discount::withoutGlobalScopes()
            ->where('store_id', $checkout->store_id)
            ->whereRaw('lower(code) = ?', [mb_strtolower($code)])
            ->first();

        return [[
            'discount_id' => $discount?->getKey(),
            'code' => $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)])


command: sed -n '1,260p' tests/Feature/Orders/OrderServiceTest.php && sed -n '1,280p' tests/Feature/Checkout/PricingServicesTest.php

OrderServiceTest.php

type: read

PricingServicesTest.php

aggregated_output:
<?php

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Enums\FinancialStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Exceptions\PaymentFailedException;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;

uses(RefreshDatabase::class);

function orderCompletionStore(int $start = 1001): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    StoreSettings::query()->create([
        'store_id' => $store->getKey(),
        'settings_json' => [
            'order_number_prefix' => '#',
            'order_number_start' => $start,
        ],
    ]);

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

    return $store;
}

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

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => $stock,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function orderCompletionShippingRate(Store $store): ShippingRate
{
    $zone = ShippingZone::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('name', 'Germany')
        ->first();

    if (! $zone instanceof ShippingZone) {
        $zone = ShippingZone::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
        ]);
    }

    $rate = ShippingRate::withoutGlobalScopes()
        ->where('zone_id', $zone->getKey())
        ->where('name', 'Standard')
        ->first();

    if ($rate instanceof ShippingRate) {
        return $rate;
    }

    return ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
}

/**
 * @return array{0: Checkout, 1: ProductVariant, 2: \App\Models\Cart}
 */
function orderCompletionCheckout(Store $store, string $paymentMethod = 'credit_card', ?string $discountCode = null): array
{
    $variant = orderCompletionVariant($store);
    $rate = orderCompletionShippingRate($store);
    $cart = app(CartService::class)->create($store);

    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => 'DE',
            'postal_code' => '10115',
        ],
    ]);
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());

    if ($discountCode !== null) {
        $checkout->forceFill(['discount_code' => $discountCode])->save();
        app(PricingEngine::class)->calculate($checkout);
    }

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $paymentMethod);

    return [$checkout, $variant, $cart];
}

test('checkout completion creates a paid order and commits inventory', function () {
    $store = orderCompletionStore();
    $discount = Discount::factory()
        ->fixed(500)
        ->create([
            'store_id' => $store->getKey(),
            'code' => 'SAVE500',
        ]);
    [$checkout, $variant, $cart] = orderCompletionCheckout($store, discountCode: 'SAVE500');

    Event::fake();

    $order = app(CheckoutService::class)->completeCheckout($checkout, [
        'card_number' => '4242 4242 4242 4242',
    ]);

    expect($order->order_number)->toBe('#1001')
        ->and($order->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->total_amount)->toBe(4999)
      …9367 chars truncated…ricingEngine::class)->calculate($checkout);

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

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

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

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

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

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

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

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

    $usedCart = app(CartService::class)->create($store, $customer);
    app(CartService::class)->addLine($usedCart, $variant->getKey(), 1);

    try {
        app(DiscountService::class)->validate('once', $store, $usedCart);

        $this->fail('Expected one-per-customer discount validation to fail.');
    } catch (InvalidDiscountException $exception) {
        expect($exception->reasonCode)->toBe('discount_usage_limit_reached');
    }

    $otherCart = app(CartService::class)->create($store, $otherCustomer);
    app(CartService::class)->addLine($otherCart, $variant->getKey(), 1);

    expect(app(DiscountService::class)->validate('ONCE', $store, $otherCart)->is($discount))->toBeTrue();
});

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)

status: completed


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

use App\Enums\CartStatus;
use App\Enums\CheckoutStatus;
use App\Enums\FinancialStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Exceptions\PaymentFailedException;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\InventoryItem;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Product;
use App\Models\ProductVariant;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\Store;
use App\Models\StoreSettings;
use App\Models\TaxSettings;
use App\Services\CartService;
use App\Services\CheckoutService;
use App\Services\PricingEngine;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;

uses(RefreshDatabase::class);

function orderCompletionStore(int $start = 1001): Store
{
    $store = Store::factory()->create();
    app()->instance('current_store', $store);

    StoreSettings::query()->create([
        'store_id' => $store->getKey(),
        'settings_json' => [
            'order_number_prefix' => '#',
            'order_number_start' => $start,
        ],
    ]);

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

    return $store;
}

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

    InventoryItem::withoutGlobalScopes()
        ->where('variant_id', $variant->getKey())
        ->update([
            'quantity_on_hand' => $stock,
            'quantity_reserved' => 0,
        ]);

    return $variant->refresh();
}

function orderCompletionShippingRate(Store $store): ShippingRate
{
    $zone = ShippingZone::withoutGlobalScopes()
        ->where('store_id', $store->getKey())
        ->where('name', 'Germany')
        ->first();

    if (! $zone instanceof ShippingZone) {
        $zone = ShippingZone::withoutGlobalScopes()->create([
            'store_id' => $store->getKey(),
            'name' => 'Germany',
            'countries_json' => ['DE'],
            'regions_json' => [],
        ]);
    }

    $rate = ShippingRate::withoutGlobalScopes()
        ->where('zone_id', $zone->getKey())
        ->where('name', 'Standard')
        ->first();

    if ($rate instanceof ShippingRate) {
        return $rate;
    }

    return ShippingRate::withoutGlobalScopes()->create([
        'zone_id' => $zone->getKey(),
        'name' => 'Standard',
        'type' => 'flat',
        'config_json' => ['amount' => 499],
        'is_active' => true,
    ]);
}

/**
 * @return array{0: Checkout, 1: ProductVariant, 2: \App\Models\Cart}
 */
function orderCompletionCheckout(Store $store, string $paymentMethod = 'credit_card', ?string $discountCode = null): array
{
    $variant = orderCompletionVariant($store);
    $rate = orderCompletionShippingRate($store);
    $cart = app(CartService::class)->create($store);

    app(CartService::class)->addLine($cart, $variant->getKey(), 2);

    $checkout = app(CheckoutService::class)->createFromCart($cart);
    $checkout = app(CheckoutService::class)->setAddress($checkout, [
        'email' => 'buyer@example.test',
        'shipping_address' => [
            'first_name' => 'Test',
            'last_name' => 'Buyer',
            'address1' => 'Main Street 1',
            'city' => 'Berlin',
            'country' => 'DE',
            'postal_code' => '10115',
        ],
    ]);
    $checkout = app(CheckoutService::class)->setShippingMethod($checkout, $rate->getKey());

    if ($discountCode !== null) {
        $checkout->forceFill(['discount_code' => $discountCode])->save();
        app(PricingEngine::class)->calculate($checkout);
    }

    $checkout = app(CheckoutService::class)->selectPaymentMethod($checkout, $paymentMethod);

    return [$checkout, $variant, $cart];
}

test('checkout completion creates a paid order and commits inventory', function () {
    $store = orderCompletionStore();
    $discount = Discount::factory()
        ->fixed(500)
        ->create([
            'store_id' => $store->getKey(),
            'code' => 'SAVE500',
        ]);
    [$checkout, $variant, $cart] = orderCompletionCheckout($store, discountCode: 'SAVE500');

    Event::fake();

    $order = app(CheckoutService::class)->completeCheckout($checkout, [
        'card_number' => '4242 4242 4242 4242',
    ]);

    expect($order->order_number)->toBe('#1001')
        ->and($order->status)->toBe(OrderStatus::Paid)
        ->and($order->financial_status)->toBe(FinancialStatus::Paid)
        ->and($order->total_amount)->toBe(4999)
        ->and($order->lines)->toHaveCount(1)
        ->and($order->lines->first()->title_snapshot)->toContain($variant->product->title)
        ->and($order->lines->first()->discount_allocations_json)->toBe([[
            'discount_id' => $discount->getKey(),
            'code' => 'SAVE500',
            'amount' => 500,
        ]])
        ->and($order->payments)->toHaveCount(1)
        ->and($order->payments->first()->status)->toBe(PaymentStatus::Captured)
        ->and($order->payments->first()->raw_json_encrypted['success'])->toBeTrue()
        ->and($cart->refresh()->status)->toBe(CartStatus::Converted)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::Completed)
        ->and($discount->refresh()->usage_count)->toBe(1);

    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($inventory->quantity_on_hand)->toBe(8)
        ->and($inventory->quantity_reserved)->toBe(0);

    Event::assertDispatched(OrderCreated::class, fn (OrderCreated $event): bool => $event->order->is($order));
    Event::assertDispatched(OrderPaid::class, fn (OrderPaid $event): bool => $event->order->is($order));
});

test('checkout completion is idempotent for the same checkout', function () {
    $store = orderCompletionStore();
    [$checkout, $variant] = orderCompletionCheckout($store);

    $order = app(CheckoutService::class)->completeCheckout($checkout, [
        'card_number' => '4242 4242 4242 4242',
    ]);
    $sameOrder = app(CheckoutService::class)->completeCheckout($checkout->refresh(), [
        'card_number' => '4000 0000 0000 0002',
    ]);

    expect($sameOrder->is($order))->toBeTrue()
        ->and(Order::withoutGlobalScopes()->count())->toBe(1)
        ->and(Payment::query()->count())->toBe(1)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_on_hand)->toBe(8)
        ->and(InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->first()?->quantity_reserved)->toBe(0);
});

test('bank transfer completion creates a pending order and keeps inventory reserved', function () {
    $store = orderCompletionStore();
    [$checkout, $variant] = orderCompletionCheckout($store, paymentMethod: 'bank_transfer');

    Event::fake();

    $order = app(CheckoutService::class)->completeCheckout($checkout);

    expect($order->status)->toBe(OrderStatus::Pending)
        ->and($order->financial_status)->toBe(FinancialStatus::Pending)
        ->and($order->payments->first()->status)->toBe(PaymentStatus::Pending)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::Completed);

    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect($inventory->quantity_on_hand)->toBe(10)
        ->and($inventory->quantity_reserved)->toBe(2);

    Event::assertDispatched(OrderCreated::class);
    Event::assertNotDispatched(OrderPaid::class);
});

test('payment failures release reserved inventory and allow checkout retry', function () {
    $store = orderCompletionStore();
    [$checkout, $variant] = orderCompletionCheckout($store);

    expect(fn () => app(CheckoutService::class)->completeCheckout($checkout, [
        'card_number' => '4000 0000 0000 0002',
    ]))->toThrow(PaymentFailedException::class);

    $inventory = InventoryItem::withoutGlobalScopes()->where('variant_id', $variant->getKey())->firstOrFail();

    expect(Order::withoutGlobalScopes()->count())->toBe(0)
        ->and($inventory->quantity_on_hand)->toBe(10)
        ->and($inventory->quantity_reserved)->toBe(0)
        ->and($checkout->refresh()->status)->toBe(CheckoutStatus::ShippingSelected)
        ->and($checkout->payment_method)->toBeNull();
});

test('order numbers increment sequentially per store', function () {
    $store = orderCompletionStore(start: 5001);
    [$firstCheckout] = orderCompletionCheckout($store);
    [$secondCheckout] = orderCompletionCheckout($store);

    $firstOrder = app(CheckoutService::class)->completeCheckout($firstCheckout, [
        'card_number' => '4242 4242 4242 4242',
    ]);
    $secondOrder = app(CheckoutService::class)->completeCheckout($secondCheckout, [
        'card_number' => '4242 4242 4242 4242',
    ]);

    expect($firstOrder->order_number)->toBe('#5001')
        ->and($secondOrder->order_number)->toBe('#5002');
});
<?php

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

uses(RefreshDatabase::class);

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

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

    return $store;
}

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

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

    return $variant->refresh();
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    $usedCart = app(CartService::class)->create($store, $customer);
    app(CartService::class)->addLine($usedCart, $variant->getKey(), 1);

    try {
        app(DiscountService::class)->validate('once', $store, $usedCart);

        $this->fail('Expected one-per-customer discount validation to fail.');
    } catch (InvalidDiscountException $exception) {
        expect($exception->reasonCode)->toBe('discount_usage_limit_reached');
    }

    $otherCart = app(CartService::class)->create($store, $otherCustomer);
    app(CartService::class)->addLine($otherCart, $variant->getKey(), 1);

    expect(app(DiscountService::class)->validate('ONCE', $store, $otherCart)->is($discount))->toBeTrue();
});

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)


command: rg -n "discount_allocations|automatic|DiscountService|discount" app tests database specs/05-BUSINESS-LOGIC.md

type: search

cmd: rg -n 'discount_allocations|automatic|DiscountService|discount' app tests database specs/05-BUSINESS-LOGIC.md

query: discount_allocations|automatic|DiscountService|discount

path: app

aggregated_output:
specs/05-BUSINESS-LOGIC.md:12:| Percentage storage | Basis points where noted (e.g., 1900 = 19.00%) or whole-number percentage in `discounts.value_amount` (e.g., 15 = 15%) |
specs/05-BUSINESS-LOGIC.md:71:The trait applies a global query scope to every model that uses it. The scope adds a `WHERE store_id = <current_store_id>` clause to all queries automatically.
specs/05-BUSINESS-LOGIC.md:75:- On the model's `creating` event, automatically set `store_id` from the current store context (defense-in-depth).
specs/05-BUSINESS-LOGIC.md:80:products, collections, customers, orders, carts, checkouts, discounts, shipping_zones, themes, pages, navigation_menus, analytics_events, analytics_daily, webhook_subscriptions, inventory_items, search_queries.
specs/05-BUSINESS-LOGIC.md:94:| Manage discounts | Yes | Yes | Yes | No |
specs/05-BUSINESS-LOGIC.md:203:Each `product_variant` has exactly one `inventory_item` record (created automatically when the variant is created).
specs/05-BUSINESS-LOGIC.md:300:8. Set `line_discount_amount = 0` (recalculated by discount engine during checkout).
specs/05-BUSINESS-LOGIC.md:301:9. Calculate: `line_total_amount = line_subtotal_amount - line_discount_amount`.
specs/05-BUSINESS-LOGIC.md:330:line_discount_amount = 0  (updated by discount engine during checkout)
specs/05-BUSINESS-LOGIC.md:331:line_total_amount    = line_subtotal_amount - line_discount_amount
specs/05-BUSINESS-LOGIC.md:361:    Apply discount rules (Section 7)
specs/05-BUSINESS-LOGIC.md:364:    subtotal - total_discount"]
specs/05-BUSINESS-LOGIC.md:368:    Calculated on discounted line amounts
specs/05-BUSINESS-LOGIC.md:371:    discounted_subtotal + shipping + tax_total"]
specs/05-BUSINESS-LOGIC.md:380:| `subtotal` | integer (cents) | Sum of all line subtotals before discount |
specs/05-BUSINESS-LOGIC.md:381:| `discount` | integer (cents) | Total discount amount |
specs/05-BUSINESS-LOGIC.md:385:| `total` | integer (cents) | Final amount: discounted_subtotal + shipping + taxTotal |
specs/05-BUSINESS-LOGIC.md:398:- Each line's discount allocation, tax, and total are rounded independently.
specs/05-BUSINESS-LOGIC.md:401:- Any remainder from proportional discount allocation is added to the last qualifying line (largest-remainder method).
specs/05-BUSINESS-LOGIC.md:406:- Recalculate on every significant state change: address change (may affect tax/shipping zones), shipping method change, discount code change.
specs/05-BUSINESS-LOGIC.md:550:       - tax_lines_json, discount_allocations_json
specs/05-BUSINESS-LOGIC.md:562:    9. IF checkout has a discount code:
specs/05-BUSINESS-LOGIC.md:563:       INCREMENT discount's usage_count by 1
specs/05-BUSINESS-LOGIC.md:604:**Enum:** `App\Enums\DiscountType` - values: `code`, `automatic`
specs/05-BUSINESS-LOGIC.md:608:| `code` | Customer enters a code at checkout. Stored in `discounts.code`, matched case-insensitively. |
specs/05-BUSINESS-LOGIC.md:609:| `automatic` | Applied automatically when rule conditions are met. Evaluated on every pricing calculation. |
specs/05-BUSINESS-LOGIC.md:612:- Only one code discount per checkout.
specs/05-BUSINESS-LOGIC.md:613:- Multiple automatic discounts can stack (each applied sequentially to the remaining undiscounted amount).
specs/05-BUSINESS-LOGIC.md:619:| `draft` | Created but not yet active. Not visible to customers, not applicable to carts. | Admin creates discount -> `draft`. Admin activates -> `active`. |
specs/05-BUSINESS-LOGIC.md:622:| `disabled` | Manually deactivated by admin. Not applicable to carts. | Admin disables an `active` discount. Admin can re-enable -> `active`. |
specs/05-BUSINESS-LOGIC.md:627:- `active` -> `expired` (automatic)
specs/05-BUSINESS-LOGIC.md:636:| `percent` | `discount = applicable_subtotal * value_amount / 100` | `value_amount` stores whole percentage (e.g., 15 = 15%) |
specs/05-BUSINESS-LOGIC.md:637:| `fixed` | `discount = value_amount` (cents) | Cannot exceed the applicable subtotal |
specs/05-BUSINESS-LOGIC.md:638:| `free_shipping` | Override shipping amount to 0 | No monetary discount on items. `value_amount` is ignored. |
specs/05-BUSINESS-LOGIC.md:642:Rules are stored in `discounts.rules_json` with the following structure:
specs/05-BUSINESS-LOGIC.md:651:**Applicability logic:** When both `applicable_product_ids` and `applicable_collection_ids` are set, a product qualifies if it matches **either** condition (union, not intersection). When both are null or empty, the discount applies to the entire order.
specs/05-BUSINESS-LOGIC.md:655:**Service:** `App\Services\DiscountService`
specs/05-BUSINESS-LOGIC.md:661:    A[Input: discount code + cart] --> B{Code exists in store?
specs/05-BUSINESS-LOGIC.md:663:    B -->|No| C["Error: discount_not_found"]
specs/05-BUSINESS-LOGIC.md:665:    D -->|No| E["Error: discount_expired"]
specs/05-BUSINESS-LOGIC.md:667:    F -->|No| G["Error: discount_not_yet_active"]
specs/05-BUSINESS-LOGIC.md:669:    H -->|No| I["Error: discount_expired"]
specs/05-BUSINESS-LOGIC.md:671:    J -->|Ye…34359 chars truncated…unt instanceof Discount) {
app/Livewire/Admin/Discounts/Index.php:60:    public function discounts(): LengthAwarePaginator
app/Livewire/Admin/Discounts/Index.php:80:    public function valueLabel(Discount $discount): string
app/Livewire/Admin/Discounts/Index.php:82:        return match ($discount->value_type) {
app/Livewire/Admin/Discounts/Index.php:83:            DiscountValueType::Percent => $discount->value_amount.'%',
app/Livewire/Admin/Discounts/Index.php:84:            DiscountValueType::Fixed => Money::format($discount->value_amount, $this->storeCurrency),
app/Livewire/Admin/Discounts/Index.php:89:    public function effectiveStatus(Discount $discount): string
app/Livewire/Admin/Discounts/Index.php:91:        if ($discount->status === DiscountStatus::Disabled) {
app/Livewire/Admin/Discounts/Index.php:95:        if ($discount->starts_at->isFuture()) {
app/Livewire/Admin/Discounts/Index.php:99:        if ($discount->status === DiscountStatus::Expired || ($discount->ends_at !== null && $discount->ends_at->isPast())) {
app/Livewire/Admin/Discounts/Index.php:103:        return $discount->status->value;
app/Livewire/Admin/Discounts/Index.php:118:        return view('livewire.admin.discounts.index', [
app/Livewire/Admin/Discounts/Index.php:119:            'discounts' => $this->discounts(),
app/Services/OrderService.php:89:                    'discount_amount' => (int) data_get($totals, 'discount', 0),
app/Services/OrderService.php:265:                'discount_allocations_json' => $this->discountAllocations($checkout, $line),
app/Services/OrderService.php:283:     * @return array<int, array{discount_id: int|null, code: string, amount: int}>
app/Services/OrderService.php:285:    private function discountAllocations(Checkout $checkout, CartLine $line): array
app/Services/OrderService.php:287:        if ($checkout->discount_code === null || $line->line_discount_amount <= 0) {
app/Services/OrderService.php:291:        $code = trim($checkout->discount_code);
app/Services/OrderService.php:292:        $discount = Discount::withoutGlobalScopes()
app/Services/OrderService.php:298:            'discount_id' => $discount?->getKey(),
app/Services/OrderService.php:300:            'amount' => $line->line_discount_amount,
app/Services/OrderService.php:352:        $code = trim((string) $checkout->discount_code);
database/migrations/2026_05_04_000410_create_order_lines_table.php:25:            $table->text('discount_allocations_json')->default('[]');
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:82:            'discount_code' => trim((string) $request->validated('code')) ?: null,
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:90:            $checkout->forceFill(['discount_code' => null])->save();
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:102:        abort_if($checkout->discount_code === null, 404);
app/Http/Controllers/Api/Storefront/V1/CheckoutController.php:104:        $checkout->forceFill(['discount_code' => null])->save();
database/migrations/2026_05_03_225405_create_cart_lines_table.php:21:            $table->integer('line_discount_amount')->default(0);
app/Models/Store.php:124:    public function discounts(): HasMany
app/Http/Resources/Admin/V1/OrderResource.php:33:            'discount_amount' => $this->discount_amount,
app/Models/Checkout.php:29:        'discount_code',
app/Models/Order.php:34:        'discount_amount',
app/Models/Order.php:53:        'discount_amount' => 0,
app/Models/Order.php:126:            'discount_amount' => 'integer',
app/Http/Resources/Storefront/V1/OrderResource.php:30:            'discount_amount' => $this->discount_amount,
database/migrations/2026_05_04_000408_create_orders_table.php:26:            $table->integer('discount_amount')->default(0);
database/migrations/2026_05_03_225427_create_discounts_table.php:14:        Schema::create('discounts', function (Blueprint $table) {
database/migrations/2026_05_03_225427_create_discounts_table.php:17:            $table->enum('type', ['code', 'automatic'])->default('code');
database/migrations/2026_05_03_225427_create_discounts_table.php:41:        Schema::dropIfExists('discounts');
app/Http/Resources/Storefront/V1/CheckoutResource.php:28:            'discount_code' => $this->discount_code,
app/Http/Resources/Storefront/V1/CartResource.php:29:                'discount' => $this->lines->sum('line_discount_amount'),
app/Http/Resources/Storefront/V1/CartLineResource.php:23:            'line_discount_amount' => $this->line_discount_amount,
app/Models/CartLine.php:26:        'line_discount_amount',
app/Models/CartLine.php:37:        'line_discount_amount' => 0,
app/Models/CartLine.php:87:            'line_discount_amount' => 'integer',
database/migrations/2026_05_03_225409_create_checkouts_table.php:25:            $table->string('discount_code')->nullable();
app/Models/OrderLine.php:30:        'discount_allocations_json',
app/Models/OrderLine.php:75:            'discount_allocations_json' => 'array',