text:
Script completed
Wall time 0.1 seconds
Output:

text:
<?php

namespace App\Http\Controllers\Api;

use App\Enums\ProductStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreateFulfillmentRequest;
use App\Http\Requests\CreateRefundRequest;
use App\Http\Requests\StoreCollectionRequest;
use App\Http\Requests\StoreDiscountRequest;
use App\Http\Requests\StorePageRequest;
use App\Http\Requests\StoreProductRequest;
use App\Http\Requests\StoreShippingRateRequest;
use App\Http\Requests\StoreShippingZoneRequest;
use App\Http\Requests\StoreThemeRequest;
use App\Http\Requests\UpdateCollectionRequest;
use App\Http\Requests\UpdateDiscountRequest;
use App\Http\Requests\UpdatePageRequest;
use App\Http\Requests\UpdateProductRequest;
use App\Http\Requests\UpdateShippingZoneRequest;
use App\Http\Requests\UpdateTaxSettingsRequest;
use App\Http\Requests\UpdateThemeSettingsRequest;
use App\Models\Collection;
use App\Models\Customer;
use App\Models\Discount;
use App\Models\Order;
use App\Models\Page;
use App\Models\Product;
use App\Models\ShippingRate;
use App\Models\ShippingZone;
use App\Models\TaxSettings;
use App\Models\Theme;
use App\Services\FulfillmentService;
use App\Services\ProductService;
use App\Services\RefundService;
use App\Services\SearchService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class AdminController extends Controller
{
    public function products(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validate(['status' => ['nullable', 'in:draft,active,archived'], 'query' => ['nullable', 'string'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);
        $products = Product::withoutGlobalScopes()->where('store_id', $storeId)->with(['variants.inventory', 'collections'])->when($data['status'] ?? null, fn ($query, string $status) => $query->where('status', $status))->when($data['query'] ?? null, fn ($query, string $queryText) => $query->where(function ($nested) use ($queryText): void {
            $nested->where('title', 'like', '%'.$queryText.'%')->orWhere('vendor', 'like', '%'.$queryText.'%')->orWhereHas('variants', fn ($variants) => $variants->where('sku', 'like', '%'.$queryText.'%'));
        }))->latest('updated_at')->paginate($data['per_page'] ?? 25);

        return $this->paginated($products);
    }

    public function storeProduct(StoreProductRequest $request, int $storeId, ProductService $products): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $product = $products->create(app('current_store'), [...$data, 'status' => ProductStatus::from($data['status'] ?? ProductStatus::Draft->value)]);

        return response()->json(['data' => $product->load(['variants.inventory', 'variants.optionValues.option', 'options.values', 'media', 'collections'])->toArray()], 201);
    }

    public function showProduct(int $storeId, int $productId): JsonResponse
    {
        $this->assertStore($storeId);
        $product = Product::withoutGlobalScopes()->where('store_id', $storeId)->with(['variants.inventory', 'variants.optionValues.option', 'options.values', 'media', 'collections'])->findOrFail($productId);

        return response()->json(['data' => $product->toArray()]);
    }

    public function updateProduct(UpdateProductRequest $request, int $storeId, int $productId, ProductService $products): JsonResponse
    {
        $this->assertStore($storeId);
        $product = Product::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($productId);
        $data = $request->validated();

        return response()->json(['data' => $products->update($product, $data)->load(['variants.inventory', 'variants.optionValues.option', 'options.values', 'media', 'collections'])->toArray()]);
    }

    public function deleteProduct(int $storeId, int $productId, ProductService $products): JsonResponse
    {
        $this->assertStore($storeId);
        $product = Product::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($productId);
        $products->transitionStatus($product, ProductStatus::Archived);

        return response()->json(['message' => 'Product archived']);
    }

    public function collections(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validate(['query' => ['nullable', 'string'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);
        $collections = Collection::withoutGlobalScopes()->where('store_id', $storeId)->withCount('products')->when($data['query'] ?? null, fn ($query, string $queryText) => $query->where('title', 'like', '%'.$queryText.'%'))->latest()->paginate($data['per_page'] ?? 25);

        return $this->paginated($collections);
    }

    public function storeCollection(StoreCollectionRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $collection = Collection::withoutGlobalScopes()->create(['store_id' => $storeId, 'title' => $data['title'], 'handle' => $data['handle'] ?? Str::slug($data['title']), 'description' => $data['description_html'] ?? null, 'status' => $data['status'] ?? 'active']);
        $this->syncCollectionProducts($collection, $data['product_ids'] ?? [], $storeId);

        return response()->json(['data' => $collection->load('products')->toArray()], 201);
    }

    public function updateCollection(UpdateCollectionRequest $request, int $storeId, int $collectionId): JsonResponse
    {
        $this->assertStore($storeId);
        $collection = Collection::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($collectionId);
        $data = $request->validated();
        $collection->update(array_filter(['title' => $data['title'] ?? null, 'description' => $data['description_html'] ?? null, 'status' => $data['status'] ?? null], fn ($value): bool => $value !== null));

        if (array_key_exists('product_ids', $data)) {
            $this->syncCollectionProducts($collection, $data['product_ids'], $storeId);
        }

        return response()->json(['data' => $collection->load('products')->toArray()]);
    }

    public function deleteCollection(int $storeId, int $collectionId): JsonResponse
    {
        $this->assertStore($storeId);
        Collection::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($collectionId)->delete();

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

    public function orders(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validate(['status' => ['nullable', 'string'], 'financial_status' => ['nullable', 'string'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);
        $orders = Order::withoutGlobalScopes()->where('store_id', $storeId)->with('customer')->when($data['status'] ?? null, fn ($query, string $status) => $query->where('status', $status))->when($data['financial_status'] ?? null, fn ($query, string $status) => $query->where('financial_status', $status))->latest('placed_at')->paginate($data['per_page'] ?? 25);

        return $this->paginated($orders);
    }

    public function showOrder(int $storeId, int $orderId): JsonResponse
    {
        $this->assertStore($storeId);
        $order = Order::withoutGlobalScopes()->where('store_id', $storeId)->with(['customer', 'lines', 'payments', 'refunds', 'fulfillments.lines'])->findOrFail($orderId);

        return response()->json(['data' => $order->toArray()]);
    }

    public function customers(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validate(['query' => ['nullable', 'string'], 'per_page' => ['nullable', 'integer', 'min:1', 'max:100']]);
        $customers = Customer::withoutGlobalScopes()->where('store_id', $storeId)->when($data['query'] ?? null, fn ($query, string $queryText) => $query->where(function ($nested) use ($queryText): void {
            $nested->where('email', 'like', '%'.$queryText.'%')->orWhere('first_name', 'like', '%'.$queryText.'%')->orWhere('last_name', 'like', '%'.$queryText.'%');
        }))->latest()->paginate($data['per_page'] ?? 25);

        return $this->paginated($customers);
    }

    public function discounts(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);

        return $this->paginated(Discount::withoutGlobalScopes()->where('store_id', $storeId)->latest()->paginate($request->integer('per_page', 25)));
    }

    public function storeDiscount(StoreDiscountRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $discount = Discount::withoutGlobalScopes()->create([...$data, 'store_id' => $storeId, 'status' => $data['status'] ?? 'active', 'usage_count' => 0]);

        return response()->json(['data' => $discount], 201);
    }

    public function updateDiscount(UpdateDiscountRequest $request, int $storeId, int $discountId): JsonResponse
    {
        $this->assertStore($storeId);
        $discount = Discount::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($discountId);
        $discount->update($request->validated());

        return response()->json(['data' => $discount->refresh()]);
    }

    public function deleteDiscount(int $storeId, int $discountId): JsonResponse
    {
        $this->assertStore($storeId);
        Discount::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($discountId)->delete();

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

    public function fulfillOrder(CreateFulfillmentRequest $request, int $storeId, int $orderId, FulfillmentService $fulfillments): JsonResponse
    {
        $this->assertStore($storeId);
        $order = Order::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($orderId);
        $data = $request->validated();
        $fulfillment = $fulfillments->create($order, $data['lines'], array_filter(['tracking_company' => $data['tracking_company'] ?? null, 'tracking_number' => $data['tracking_number'] ?? null, 'tracking_url' => $data['tracking_url'] ?? null]));

        return response()->json(['data' => $fulfillment->toArray()], 201);
    }

    public function refundOrder(CreateRefundRequest $request, int $storeId, int $orderId, RefundService $refunds): JsonResponse
    {
        $this->assertStore($storeId);
        $order = Order::withoutGlobalScopes()->where('store_id', $storeId)->with('payments')->findOrFail($orderId);
        $data = $request->validated();
        $payment = $order->payments->firstWhere('id', $data['payment_id'] ?? null) ?? $order->payments->firstWhere('status', 'captured');
        abort_unless($payment !== null, 422, 'No captured payment is available.');
        $refund = $refunds->create($order, $payment, $data['lines'] ?? $data['amount'] ?? null, $data['reason'] ?? null, (bool) ($data['restock'] ?? false), $data['lines'] ?? []);

        return response()->json(['data' => $refund->toArray()], 201);
    }

    public function shippingZones(int $storeId): JsonResponse
    {
        $this->assertStore($storeId);

        return response()->json(['data' => ShippingZone::withoutGlobalScopes()->where('store_id', $storeId)->with('rates')->get()]);
    }

    public function storeShippingZone(StoreShippingZoneRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();

        return response()->json(['data' => ShippingZone::withoutGlobalScopes()->create([...$data, 'store_id' => $storeId])], 201);
    }

    public function updateShippingZone(UpdateShippingZoneRequest $request, int $storeId, int $zoneId): JsonResponse
    {
        $this->assertStore($storeId);
        $zone = ShippingZone::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($zoneId);
        $zone->update($request->validated());

        return response()->json(['data' => $zone->refresh()]);
    }

    public function storeShippingRate(StoreShippingRateRequest $request, int $storeId, int $zoneId): JsonResponse
    {
        $this->assertStore($storeId);
        ShippingZone::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($zoneId);
        $data = $request->validated();

        return response()->json(['data' => ShippingRate::create([...$data, 'shipping_zone_id' => $zoneId, 'is_active' => $data['is_active'] ?? true])], 201);
    }

    public function taxSettings(int $storeId): JsonResponse
    {
        $this->assertStore($storeId);

        return response()->json(['data' => TaxSettings::withoutGlobalScopes()->firstOrCreate(['store_id' => $storeId])]);
    }

    public function updateTaxSettings(UpdateTaxSettingsRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $settings = TaxSettings::withoutGlobalScopes()->updateOrCreate(['store_id' => $storeId], $data);

        return response()->json(['data' => $settings]);
    }

    public function pages(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);

        return $this->paginated(Page::withoutGlobalScopes()->where('store_id', $storeId)->latest()->paginate($request->integer('per_page', 25)));
    }

    public function storePage(StorePageRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $page = Page::withoutGlobalScopes()->create([...$data, 'store_id' => $storeId, 'handle' => $data['handle'] ?? Str::slug($data['title']), 'published_at' => $data['status'] === 'published' ? now() : null]);

        return response()->json(['data' => $page], 201);
    }

    public function updatePage(UpdatePageRequest $request, int $storeId, int $pageId): JsonResponse
    {
        $this->assertStore($storeId);
        $page = Page::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($pageId);
        $data = $request->validated();
        if (($data['status'] ?? null) === 'published') {
            $data['published_at'] = $page->published_at ?? now();
        }
        $page->update($data);

        return response()->json(['data' => $page->refresh()]);
    }

    public function deletePage(int $storeId, int $pageId): JsonResponse
    {
        $this->assertStore($storeId);
        Page::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($pageId)->delete();

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

    public function storeTheme(StoreThemeRequest $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $data = $request->validated();
        $theme = Theme::withoutGlobalScopes()->create(['store_id' => $storeId, 'name' => $data['name'], 'version' => $data['version'] ?? null, 'status' => 'draft']);
        $theme->settings()->create(['settings_json' => $data['settings'] ?? []]);

        return response()->json(['data' => $theme->load('settings')], 201);
    }

    public function publishTheme(int $storeId, int $themeId): JsonResponse
    {
        $this->assertStore($storeId);
        $theme = Theme::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($themeId);
        Theme::withoutGlobalScopes()->where('store_id', $storeId)->update(['status' => 'draft']);
        $theme->update(['status' => 'published']);

        return response()->json(['data' => $theme->refresh()]);
    }

    public function updateThemeSettings(UpdateThemeSettingsRequest $request, int $storeId, int $themeId): JsonResponse
    {
        $this->assertStore($storeId);
        $theme = Theme::withoutGlobalScopes()->where('store_id', $storeId)->findOrFail($themeId);
        $theme->settings()->updateOrCreate(['theme_id' => $theme->getKey()], ['settings_json' => $request->validated()['settings']]);

        return response()->json(['data' => $theme->refresh()->load('settings')]);
    }

    public function reindex(SearchService $search, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        Product::withoutGlobalScopes()->where('store_id', $storeId)->each(fn (Product $product) => $search->syncProduct($product));

        return response()->json(['status' => 'completed']);
    }

    public function searchStatus(int $storeId): JsonResponse
    {
        $this->assertStore($storeId);

        return response()->json(['status' => 'ready', 'product_count' => Product::withoutGlobalScopes()->where('store_id', $storeId)->count()]);
    }

    public function analyticsSummary(Request $request, int $storeId): JsonResponse
    {
        $this->assertStore($storeId);
        $days = \App\Models\AnalyticsDaily::withoutGlobalScopes()->where('store_id', $storeId)->whereBetween('date', [$request->input('from', now()->subDays(29)->toDateString()), $request->input('to', now()->toDateString())])->get();

        return response()->json(['data' => ['visits' => (int) $days->sum('visits_count'), 'orders' => (int) $days->sum('orders_count'), 'revenue_amount' => (int) $days->sum('revenue_amount'), 'checkout_completed' => (int) $days->sum('checkout_completed_count'), 'days' => $days]]);
    }

    private function assertStore(int $storeId): void
    {
        abort_unless((int) app('current_store')->getKey() === $storeId, 404);
    }

    private function syncCollectionProducts(Collection $collection, array $productIds, int $storeId): void
    {
        $validIds = Product::withoutGlobalScopes()->where('store_id', $storeId)->whereIn('id', $productIds)->pluck('id')->all();
        $collection->products()->sync(array_fill_keys($validIds, ['position' => 0]));
    }

    private function paginated(LengthAwarePaginator $paginator): JsonResponse
    {
        return response()->json(['data' => $paginator->items(), 'meta' => ['current_page' => $paginator->currentPage(), 'per_page' => $paginator->perPage(), 'total' => $paginator->total(), 'last_page' => $paginator->lastPage()]]);
    }
}
}
```

**Error responses:**
- `403` - Insufficient permissions
- `422` - Invalid date range

---

#### POST /api/admin/v1/stores/{storeId}/exports/orders

Queue an order export job. The export runs asynchronously and produces a downloadable CSV.

**Request body:**

```json
{
  "format": "csv",
  "filters": {
    "status": "paid",
    "created_after": "2026-01-01T00:00:00Z",
    "created_before": "2026-02-11T23:59:59Z"
  }
}
```

| Field | Type | Required | Validation |
|-------|------|----------|------------|
| `format` | string | No | One of: `csv`. Default: `csv`. |
| `filters` | object | No | Same filter options as GET /orders. |

**Success response:** `202 Accepted`

```json
{
  "export_id": 1,
  "status": "queued",
  "created_at": "2026-02-11T10:00:00Z"
}
```

---

#### GET /api/admin/v1/stores/{storeId}/exports/{exportId}

Check export status and get download URL.

**Success response:** `200 OK`

```json
{
  "data": {
    "id": 1,
    "status": "completed",
    "format": "csv",
    "row_count": 156,
    "download_url": "https://s3.example.com/...presigned...",
    "download_expires_at": "2026-02-11T11:00:00Z",
    "created_at": "2026-02-11T10:00:00Z",
    "completed_at": "2026-02-11T10:01:30Z"
  }
}
```

**Export status values:** `queued`, `processing`, `completed`, `failed`.

**Error responses:**
- `403` - Insufficient permissions
- `404` - Export not found

---

## 4. Webhook Receiver Endpoints

> **Note:** No inbound webhook endpoints are needed. All payment processing happens in-process via the Mock PSP -- there are no external payment provider callbacks. The webhook infrastructure defined in this section is reserved for **outbound** app webhooks (see Section 5, OAuth / App Ecosystem).

237-
238-| Operation | Trigger | Effect on `quantity_on_hand` | Effect on `quantity_reserved` | Pre-check |
239-|---|---|---|---|---|
240-| **Reserve** | Checkout enters `payment_selected` | No change | +quantity | If policy=deny, verify available >= quantity. Throw `InsufficientInventoryException` if not. |
241-| **Release** | Checkout expires or is abandoned | No change | -quantity | None |
242:| **Commit** | Payment confirmed, order created | -quantity | -quantity | None (reservation already validated) |
243-| **Restock** | Refund processed with restock flag | +quantity | No change | None |
244-
245-### 3.4 Concurrency
246-
247-SQLite operates in single-writer mode with WAL journaling. Database transactions provide sufficient isolation. No optimistic locking or advisory locks are needed.
--
590-
591-| Event | Dispatched When |
592-|---|---|
593-| `CheckoutAddressed` | Address saved on checkout |
594-| `CheckoutShippingSelected` | Shipping method selected |
595:| `CheckoutCompleted` | Payment confirmed and order created |
596-| `CheckoutExpired` | Checkout expired due to inactivity |
597-
598----
599-
600-## 7. Discount Engine
--
1348-
1349-Stores (or app installations) subscribe to event types via `webhook_subscriptions`.
1350-
1351-**Supported event types:**
1352-
1353:| Event Type | Triggered By |
1354-|---|---|
1355:| `order.created` | OrderCreated event |
1356-| `order.paid` | Payment captured successfully |
1357-| `order.fulfilled` | All order lines fulfilled |
1358-| `order.refunded` | Refund processed |
1359-| `product.created` | New product saved |
1360-| `product.updated` | Product modified |
1361-| `product.deleted` | Product deleted or archived |
1362:| `checkout.completed` | Checkout successfully completed |
1363-
1364-### 13.2 Delivery Process
1365-
1366-**Listener:** `App\Listeners\DispatchWebhooks` - registered to listen for all domain events that have webhook counterparts.
1367-
--
1391-**Required HTTP headers on delivery:**
1392-
1393-| Header | Value |
1394-|---|---|
1395-| `X-Platform-Signature` | HMAC-SHA256 hex digest of the payload |
1396:| `X-Platform-Event` | Event type string (e.g., `order.created`) |
1397-| `X-Platform-Delivery-Id` | Unique UUID for this delivery attempt |
1398-| `X-Platform-Timestamp` | Unix timestamp of the event |
1399-| `Content-Type` | `application/json` |
1400-
1401-### 13.3 Retry Logic
--
1455-
1456-**Event fields:**
1457-
1458-| Field | Type | Required | Description |
1459-|---|---|---|---|
1460:| `type` | string | Yes | One of: `page_view`, `product_view`, `add_to_cart`, `checkout_started`, `checkout_completed` |
1461-| `session_id` | string | Yes | Client session identifier |
1462-| `client_event_id` | string | Yes | Client-generated unique ID for deduplication |
1463-| `occurred_at` | string (ISO-8601) | Yes | When the event occurred on the client |
1464-| `properties` | object | No | Event-specific data (product_id, variant_id, etc.) |
1465-
--
1476-
1477-**Aggregated metrics:**
1478-
1479-| Metric | Calculation |
1480-|---|---|
1481:| `orders_count` | Count of `checkout_completed` events for the day |
1482:| `revenue_amount` | Sum of order totals from `checkout_completed` event properties |
1483-| `aov_amount` | `revenue_amount / orders_count` (integer division, 0 if no orders) |
1484-| `visits_count` | Count of distinct `session_id` values with `page_view` events |
1485-| `add_to_cart_count` | Count of `add_to_cart` events |
1486-| `checkout_started_count` | Count of `checkout_started` events |
1487-
--
1589-
1590-Summary of all domain events and their listeners/side effects:
1591-
1592-| Event | Listeners / Side Effects |
1593-|---|---|
1594:| `OrderCreated` | Send confirmation email, dispatch webhooks (`order.created`), log analytics event |
1595-| `OrderRefunded` | Send refund notification email, dispatch webhooks (`order.refunded`), optionally restock |
1596-| `CheckoutAddressed` | Recalculate pricing |
1597-| `CheckoutShippingSelected` | Recalculate pricing |
1598:| `CheckoutCompleted` | Dispatch webhooks (`checkout.completed`) |
1599-| `CheckoutExpired` | Release reserved inventory |
1600-| `ProductStatusChanged` | Sync FTS5 index if transitioning to/from active |
1601-| `ProductCreated` | Sync FTS5 index, dispatch webhooks (`product.created`) |
1602-| `ProductUpdated` | Sync FTS5 index, dispatch webhooks (`product.updated`) |
1603-| `ProductDeleted` | Remove from FTS5 index, dispatch webhooks (`product.deleted`) |


exec

status: completed

input:
const result = await tools.multi_agent_v1__wait_agent({
  targets: [
    "01a021ae-45c6-7540-8912-baba502a2606",
    "01a021ae-4641-7d70-97fa-a13af2f3ed44",
    "01a021ae-46e6-7403-b288-76b3738f772b"
  ],
  timeout_ms: 30000
});
text(result);


output:
Script running with cell ID 1110
Wall time 11.0 seconds
Output:


text:
Script completed
Wall time 17.3 seconds
Output:


status: completed

input:
const r = await tools.exec_command({cmd:"for f in app/Events/*.php app/Jobs/DeliverWebhook.php app/Services/WebhookService.php app/Models/WebhookSubscription.php app/Models/WebhookDelivery.php app/Services/OrderService.php app/Services/RefundService.php app/Services/FulfillmentService.php app/Observers/ProductObserver.php; do echo \"--- $f\"; sed -n '1,260p' \"$f\"; done","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":40000});
text(r.output);


type: CommandExecution

command: for f in app/Events/*.php app/Jobs/DeliverWebhook.php app/Services/WebhookService.php app/Models/WebhookSubscription.php app/Models/WebhookDelivery.php app/Services/OrderService.php app/Services/RefundService.php app/Services/FulfillmentService.php app/Observers/ProductObserver.php; do echo "--- $f"; sed -n '1,260p' "$f"; done

type: unknown

status: completed

stdout:
--- app/Events/FulfillmentDelivered.php
<?php

namespace App\Events;

use App\Models\Fulfillment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class FulfillmentDelivered
{
    use Dispatchable, SerializesModels;

    public function __construct(public Fulfillment $fulfillment) {}
}
--- app/Events/FulfillmentShipped.php
<?php

namespace App\Events;

use App\Models\Fulfillment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class FulfillmentShipped
{
    use Dispatchable, SerializesModels;

    public function __construct(public Fulfillment $fulfillment) {}
}
--- app/Events/OrderCancelled.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderCancelled
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderCreated.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderCreated
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderFulfilled.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderFulfilled
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderPaid.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPaid
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderRefunded.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderRefunded
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/ProductStatusChanged.php
<?php

namespace App\Events;

use App\Enums\ProductStatus;
use App\Models\Product;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProductStatusChanged
{
    use Dispatchable, SerializesModels;

    public function __construct(public Product $product, public ProductStatus $from, public ProductStatus $to) {}
}
--- app/Jobs/DeliverWebhook.php
<?php

namespace App\Jobs;

use App\Models\WebhookDelivery;
use App\Services\WebhookService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Throwable;

class DeliverWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, SerializesModels;

    public int $tries = 6;

    public function __construct(public WebhookDelivery $delivery) {}

    /** @return array<int, int> */
    public function backoff(): array
    {
        return [60, 300, 1800, 7200, 43200];
    }

    public function handle(WebhookService $webhooks): void
    {
        $this->delivery->load('subscription');
        $payload = json_encode($this->delivery->payload, JSON_THROW_ON_ERROR);
        $subscription = $this->delivery->subscription;
        $timestamp = (string) now()->timestamp;
        $response = Http::withHeaders([
            'Content-Type' => 'application/json',
            'X-Platform-Signature' => $webhooks->sign($timestamp.'.'.$payload, $subscription->signing_secret_encrypted),
            'X-Platform-Event' => $this->delivery->event,
            'X-Platform-Delivery-Id' => (string) $this->delivery->getKey(),
            'X-Platform-Timestamp' => $timestamp,
        ])->timeout(10)->post($subscription->target_url, $this->delivery->payload);

        $this->delivery->increment('attempts');
        $this->delivery->increment('attempt_count');
        $this->delivery->update([
            'response_status' => $response->status(),
            'response_code' => $response->status(),
            'response_body' => mb_substr($response->body(), 0, 10000),
            'response_body_snippet' => mb_substr($response->body(), 0, 1000),
            'last_attempt_at' => now(),
        ]);

        if ($response->successful()) {
            $subscription->update(['consecutive_failures' => 0]);
            $this->delivery->update(['status' => 'delivered', 'delivered_at' => now(), 'next_attempt_at' => null]);

            return;
        }

        $subscription->increment('consecutive_failures');

        if ($subscription->fresh()->consecutive_failures >= 5) {
            $subscription->update(['status' => 'paused']);
        }

        throw new \RuntimeException('Webhook delivery failed with HTTP '.$response->status());
    }

    public function failed(?Throwable $exception): void
    {
        $this->delivery->update(['status' => 'failed', 'next_attempt_at' => null]);
    }
}
--- app/Services/WebhookService.php
<?php

namespace App\Services;

use App\Jobs\DeliverWebhook;
use App\Models\Store;
use App\Models\WebhookSubscription;

class WebhookService
{
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where(function ($query) use ($eventType): void {
                $query->where('event', $eventType)->orWhere('event_type', $eventType);
            })
            ->where('status', 'active')
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($eventType, $payload): void {
                $delivery = $subscription->deliveries()->create(['event' => $eventType, 'event_id' => (string) str()->uuid(), 'payload' => $payload, 'attempts' => 0, 'attempt_count' => 0, 'next_attempt_at' => now()]);
                DeliverWebhook::dispatch($delivery);
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }
}
--- app/Models/WebhookSubscription.php
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class WebhookSubscription extends Model
{
    use BelongsToStore;

    protected $fillable = ['store_id', 'event', 'event_type', 'target_url', 'app_installation_id', 'signing_secret_encrypted', 'status', 'consecutive_failures'];

    protected $hidden = ['signing_secret_encrypted'];

    protected function casts(): array
    {
        return ['signing_secret_encrypted' => 'encrypted'];
    }

    protected static function booted(): void
    {
        static::saving(function (WebhookSubscription $subscription): void {
            $subscription->event_type = $subscription->event;
        });
    }

    public function deliveries(): HasMany
    {
        return $this->hasMany(WebhookDelivery::class);
    }
}
--- app/Models/WebhookDelivery.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class WebhookDelivery extends Model
{
    protected $fillable = ['webhook_subscription_id', 'subscription_id', 'event', 'event_id', 'payload', 'status', 'attempts', 'attempt_count', 'response_status', 'response_code', 'response_body', 'response_body_snippet', 'delivered_at', 'last_attempt_at', 'next_attempt_at'];

    protected function casts(): array
    {
        return ['payload' => 'array', 'delivered_at' => 'datetime', 'last_attempt_at' => 'datetime', 'next_attempt_at' => 'datetime'];
    }

    protected static function booted(): void
    {
        static::creating(function (WebhookDelivery $delivery): void {
            $delivery->subscription_id ??= $delivery->webhook_subscription_id;
            $delivery->attempt_count ??= $delivery->attempts ?? 0;
            $delivery->event_id ??= (string) str()->uuid();
        });
    }

    public function subscription(): BelongsTo
    {
        return $this->belongsTo(WebhookSubscription::class, 'webhook_subscription_id');
    }
}
--- app/Services/OrderService.php
<?php

namespace App\Services;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\Order;
use App\Models\Store;
use App\ValueObjects\PaymentResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function __construct(private readonly InventoryService $inventory, private readonly AuditLogger $audit) {}

    public function createFromCheckout(Checkout $checkout, ?PaymentResult $paymentResult = null): Order
    {
        return DB::transaction(function () use ($checkout, $paymentResult): Order {
            $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.inventory', 'shippingRate']);
            $existing = Order::withoutGlobalScopes()->where('checkout_id', $checkout->getKey())->first();

            if ($existing !== null) {
                return $existing->load(['lines', 'payments']);
            }

            $totals = $checkout->totals_json ?? ['subtotal' => 0, 'discount' => 0, 'shipping' => 0, 'tax' => 0, 'total' => 0, 'currency' => $checkout->cart->currency];
            $discount = $checkout->discount_code === null ? null : Discount::withoutGlobalScopes()->where('store_id', $checkout->store_id)->whereRaw('lower(code) = ?', [strtolower($checkout->discount_code)])->first();
            $status = $paymentResult?->status === PaymentStatus::Captured ? FinancialStatus::Paid : FinancialStatus::Pending;
            $taxByLine = $this->allocateTaxLines($checkout->cart->lines, $totals['tax_lines'] ?? []);
            $order = Order::withoutGlobalScopes()->create([
                'store_id' => $checkout->store_id,
                'customer_id' => $checkout->customer_id,
                'checkout_id' => $checkout->getKey(),
                'order_number' => $this->generateOrderNumber(Store::findOrFail($checkout->store_id)),
                'currency' => $totals['currency'],
                'status' => $status === FinancialStatus::Paid ? OrderStatus::Paid : OrderStatus::Pending,
                'financial_status' => $status,
                'fulfillment_status' => 'unfulfilled',
                'payment_method' => $checkout->payment_method,
                'email' => $checkout->email,
                'shipping_address_json' => $checkout->shipping_address_json,
                'billing_address_json' => $checkout->billing_address_json,
                'subtotal_amount' => $totals['subtotal'],
                'discount_amount' => $totals['discount'] ?? 0,
                'shipping_amount' => $totals['shipping'] ?? 0,
                'tax_amount' => $totals['tax'] ?? 0,
                'total_amount' => $totals['total'],
                'placed_at' => now(),
            ]);

            foreach ($checkout->cart->lines as $line) {
                $order->lines()->create([
                    'product_id' => $line->variant->product_id,
                    'variant_id' => $line->variant_id,
                    'product_title' => $line->variant->product->title,
                    'title_snapshot' => $line->variant->product->title.' · '.$line->variant->title,
                    'variant_title' => $line->variant->title,
                    'sku' => $line->variant->sku,
                    'sku_snapshot' => $line->variant->sku,
                    'quantity' => $line->quantity,
                    'unit_price_amount' => $line->unit_price_amount,
                    'line_subtotal_amount' => $line->line_subtotal_amount,
                    'line_discount_amount' => $line->line_discount_amount,
                    'line_total_amount' => $line->line_total_amount,
                    'tax_lines_json' => $taxByLine[$line->getKey()] ?? [],
                    'discount_allocations_json' => $discount === null || $line->line_discount_amount < 1 ? [] : [['discount_id' => $discount->getKey(), 'amount' => $line->line_discount_amount]],
                ]);

                if ($paymentResult?->status === PaymentStatus::Captured && $line->variant->inventory !== null) {
                    $this->inventory->commit($line->variant->inventory, $line->quantity);
                }
            }

            $checkout->update(['status' => 'completed']);
            $checkout->cart->update(['status' => 'converted']);
            OrderCreated::dispatch($order);
            $this->audit->record('order.created', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);
            $order->load(['lines', 'payments']);

            if ($status === FinancialStatus::Paid) {
                OrderPaid::dispatch($order);
                $this->audit->record('order.paid', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);

                if ($checkout->cart->lines->every(fn ($line): bool => ! $line->variant->requires_shipping)) {
                    $fulfillment = $order->fulfillments()->create(['status' => 'delivered', 'fulfilled_at' => now(), 'delivered_at' => now()]);

                    foreach ($order->lines as $line) {
                        $fulfillment->lines()->create(['order_line_id' => $line->getKey(), 'quantity' => $line->quantity]);
                    }

                    $order->update(['status' => OrderStatus::Fulfilled, 'fulfillment_status' => FulfillmentStatus::Fulfilled]);
                }
            }

            return $order;
        });
    }

    public function generateOrderNumber(Store $store): string
    {
        $lastNumber = Order::withoutGlobalScopes()->where('store_id', $store->getKey())->selectRaw("max(cast(replace(order_number, '#', '') as integer)) as value")->value('value');

        return (string) config('shop.order_prefix', '#').(max(1000, (int) $lastNumber) + 1);
    }

    public function cancel(Order $order, string $reason): void
    {
        if ($order->fulfillment_status !== FulfillmentStatus::Unfulfilled) {
            throw new \LogicException('Fulfilled orders cannot be cancelled.');
        }

        DB::transaction(function () use ($order, $reason): void {
            $wasPending = $order->financial_status === FinancialStatus::Pending;
            $order->load('lines.variant.inventory')->update(['status' => OrderStatus::Cancelled, 'financial_status' => $order->financial_status === FinancialStatus::Pending ? FinancialStatus::Voided : $order->financial_status, 'metadata' => array_merge($order->metadata ?? [], ['cancellation_reason' => $reason])]);

            foreach ($order->lines as $line) {
                if ($line->variant?->inventory !== null && $wasPending) {
                    $this->inventory->release($line->variant->inventory, $line->quantity);
                }
            }

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

            OrderCancelled::dispatch($order->refresh());
            $this->audit->record('order.cancelled', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number, 'reason' => $reason]);
        });
    }

    public function confirmPayment(Order $order): void
    {
        if ($order->payment_method !== 'bank_transfer' || $order->financial_status !== FinancialStatus::Pending) {
            throw new \LogicException('Only pending bank transfer orders can be confirmed.');
        }

        DB::transaction(function () use ($order): void {
            $order->load('lines.variant.inventory')->update(['financial_status' => FinancialStatus::Paid, 'status' => OrderStatus::Paid]);

            foreach ($order->lines as $line) {
                if ($line->variant?->inventory !== null) {
                    $this->inventory->commit($line->variant->inventory, $line->quantity);
                }
            }

            $order->payments()->where('status', PaymentStatus::Pending)->update(['status' => PaymentStatus::Captured]);
            OrderPaid::dispatch($order->refresh());
            $this->audit->record('order.paid', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);

            if ($order->lines->every(fn ($line): bool => ! $line->variant?->requires_shipping)) {
                $this->autoFulfillDigitalOrder($order->refresh());
            }
        });
    }

    private function autoFulfillDigitalOrder(Order $order): void
    {
        if ($order->fulfillments()->exists()) {
            return;
        }

        $fulfillment = $order->fulfillments()->create(['status' => 'delivered', 'fulfilled_at' => now(), 'delivered_at' => now()]);
        foreach ($order->lines as $line) {
            $fulfillment->lines()->create(['order_line_id' => $line->getKey(), 'quantity' => $line->quantity]);
        }
        $order->update(['status' => OrderStatus::Fulfilled, 'fulfillment_status' => FulfillmentStatus::Fulfilled]);
    }

    /**
     * @param  \Illuminate\Support\Collection<int, \App\Models\CartLine>  $lines
     * @param  array<int, array{title?: string, rate?: int, amount?: int}>  $taxLines
     * @return array<int, array<int, array<string, mixed>>>
     */
    private function allocateTaxLines(Collection $lines, array $taxLines): array
    {
        $base = max(1, (int) $lines->sum('line_total_amount'));
        $allocations = $lines->mapWithKeys(fn ($line): array => [$line->getKey() => []])->all();

        foreach ($taxLines as $taxLine) {
            $remaining = (int) ($taxLine['amount'] ?? 0);
            $lineCount = $lines->count();

            foreach ($lines->values() as $index => $line) {
                $amount = $index === $lineCount - 1
                    ? $remaining
                    : intdiv((int) ($taxLine['amount'] ?? 0) * (int) $line->line_total_amount, $base);
                $remaining -= $amount;

                if ($amount > 0) {
                    $allocations[$line->getKey()][] = [...$taxLine, 'amount' => $amount];
                }
            }
        }

        return $allocations;
    }
}
--- app/Services/RefundService.php
<?php

namespace App\Services;

use App\Contracts\PaymentProvider;
use App\Enums\FinancialStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderRefunded;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Refund;
use Illuminate\Support\Facades\DB;

class RefundService
{
    public function __construct(private readonly PaymentProvider $provider, private readonly InventoryService $inventory, private readonly AuditLogger $audit) {}

    /**
     * @param  int|array<int, int>|null  $amount
     * @param  array<int, int>  $lines
     */
    public function create(Order $order, Payment $payment, int|array|null $amount = null, ?string $reason = null, bool $restock = false, array $lines = []): Refund
    {
        if ((int) $payment->order_id !== (int) $order->getKey() || $payment->status !== PaymentStatus::Captured) {
            throw new \InvalidArgumentException('The payment is not refundable for this order.');
        }

        if (is_array($amount)) {
            $lines = $amount;
            $amount = null;
        }

        $refunded = (int) $order->refunds()->where('status', 'processed')->sum('amount');
        $order->loadMissing('lines');
        $restockLines = $lines;
        $previousLineQuantities = [];
        foreach ($order->refunds()->where('status', 'processed')->get(['lines_json']) as $previousRefund) {
            foreach ($previousRefund->lines_json ?? [] as $lineId => $quantity) {
                $previousLineQuantities[(string) $lineId] = ($previousLineQuantities[(string) $lineId] ?? 0) + (int) $quantity;
            }
        }

        if ($lines !== []) {
            $amount = 0;

            foreach ($lines as $lineId => $quantity) {
                $orderLine = $order->lines->firstWhere('id', (int) $lineId);

                if ($orderLine === null || $quantity < 1 || $quantity > $orderLine->quantity) {
                    throw new \InvalidArgumentException('The refund quantity is invalid.');
                }

                if (($previousLineQuantities[(string) $lineId] ?? 0) + $quantity > $orderLine->quantity) {
                    throw new \InvalidArgumentException('The refund quantity exceeds the remaining refundable quantity.');
                }

                $unitAmount = intdiv($orderLine->line_total_amount, $orderLine->quantity);
                $amount += $unitAmount * $quantity;
            }
        }

        $amount ??= $payment->amount - $refunded;

        if ($amount < 1 || $refunded + $amount > $payment->amount) {
            throw new \InvalidArgumentException('Refund amount exceeds the captured payment.');
        }

        if ($restock && $restockLines === [] && $refunded + $amount >= $payment->amount) {
            $restockLines = $order->lines->mapWithKeys(fn ($line): array => [$line->getKey() => $line->quantity])->all();
        }

        return DB::transaction(function () use ($order, $payment, $amount, $reason, $restock, $restockLines, $lines): Refund {
            $result = $this->provider->refund($payment, $amount);
            $refund = $order->refunds()->create(['payment_id' => $payment->getKey(), 'amount' => $amount, 'status' => $result->successful ? 'processed' : 'failed', 'provider_refund_id' => $result->reference, 'reason' => $reason, 'restock' => $restock, 'lines_json' => $lines !== [] ? $lines : null]);

            if ($result->successful) {
                $totalRefunded = (int) $order->refunds()->where('status', 'processed')->sum('amount');
                $order->update([
                    'financial_status' => $totalRefunded >= $payment->amount ? FinancialStatus::Refunded : FinancialStatus::PartiallyRefunded,
                    'status' => $totalRefunded >= $payment->amount ? 'refunded' : $order->status,
                ]);
                $payment->update(['status' => $totalRefunded >= $payment->amount ? PaymentStatus::Refunded : $payment->status]);

                if ($restock && $restockLines !== []) {
                    $order->load('lines.variant.inventory');
                    foreach ($restockLines as $lineId => $quantity) {
                        $line = $order->lines->firstWhere('id', (int) $lineId);

                        if ($line->variant?->inventory !== null) {
                            $this->inventory->restock($line->variant->inventory, (int) $quantity);
                        }
                    }
                }

                OrderRefunded::dispatch($order->refresh());
                $this->audit->record('order.refunded', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number, 'amount' => $amount]);
            }

            return $refund;
        });
    }
}
--- app/Services/FulfillmentService.php
<?php

namespace App\Services;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Events\FulfillmentDelivered;
use App\Events\FulfillmentShipped;
use App\Events\OrderFulfilled;
use App\Exceptions\FulfillmentGuardException;
use App\Models\Fulfillment;
use App\Models\Order;
use Illuminate\Support\Facades\DB;

class FulfillmentService
{
    public function create(Order $order, array $lines, ?array $tracking = null): Fulfillment
    {
        if (! in_array($order->financial_status, [FinancialStatus::Paid, FinancialStatus::PartiallyRefunded], true)) {
            throw new FulfillmentGuardException;
        }

        return DB::transaction(function () use ($order, $lines, $tracking): Fulfillment {
            $order->load(['lines', 'fulfillments.lines']);
            $fulfilledQuantities = $order->fulfillments->flatMap->lines->groupBy('order_line_id')->map(fn ($items): int => (int) $items->sum('quantity'));
            $requestedQuantities = [];

            foreach ($lines as $line) {
                $orderLine = $order->lines->firstWhere('id', $line['order_line_id']);
                $quantity = (int) ($line['quantity'] ?? 0);
                $requestedQuantities[$line['order_line_id']] = ($requestedQuantities[$line['order_line_id']] ?? 0) + $quantity;

                if ($orderLine === null || $quantity < 1 || $orderLine->quantity < $requestedQuantities[$line['order_line_id']] + (int) ($fulfilledQuantities[$orderLine->id] ?? 0)) {
                    throw new \InvalidArgumentException('The fulfillment quantity exceeds the unfulfilled quantity.');
                }
            }

            $fulfillment = $order->fulfillments()->create(array_merge($tracking ?? [], ['status' => 'pending']));

            foreach ($lines as $line) {
                $fulfillment->lines()->create(['order_line_id' => $line['order_line_id'], 'quantity' => $line['quantity']]);
            }

            $this->refreshOrderStatus($order->refresh());

            return $fulfillment->load('lines.orderLine');
        });
    }

    public function markAsShipped(Fulfillment $fulfillment, ?array $tracking = null): void
    {
        if ($fulfillment->status !== 'pending') {
            throw new \LogicException('Only pending fulfillments can be shipped.');
        }

        $fulfillment->update(array_merge($tracking ?? [], ['status' => 'shipped', 'shipped_at' => now(), 'fulfilled_at' => now()]));
        FulfillmentShipped::dispatch($fulfillment->refresh());
        $this->refreshOrderStatus($fulfillment->load('order')->order);
    }

    public function markAsDelivered(Fulfillment $fulfillment): void
    {
        if ($fulfillment->status !== 'shipped') {
            throw new \LogicException('Only shipped fulfillments can be delivered.');
        }

        $fulfillment->update(['status' => 'delivered', 'delivered_at' => now()]);
        $fulfillment->load('order');
        FulfillmentDelivered::dispatch($fulfillment);

        if ($fulfillment->order !== null) {
            $this->refreshOrderStatus($fulfillment->order);
        }
    }

    private function refreshOrderStatus(Order $order): void
    {
        $order->load(['lines', 'fulfillments.lines']);
        $fulfilledQuantities = [];

        foreach ($order->fulfillments->whereIn('status', ['shipped', 'delivered']) as $fulfillment) {
            foreach ($fulfillment->lines as $line) {
                $fulfilledQuantities[$line->order_line_id] = ($fulfilledQuantities[$line->order_line_id] ?? 0) + $line->quantity;
            }
        }

        $fulfilled = collect($order->lines)->every(fn ($line): bool => ($fulfilledQuantities[$line->id] ?? 0) >= $line->quantity);
        $partial = $fulfilledQuantities !== [];
        $order->update(['fulfillment_status' => $fulfilled ? FulfillmentStatus::Fulfilled : ($partial ? FulfillmentStatus::Partial : FulfillmentStatus::Unfulfilled)]);

        if ($fulfilled) {
            $order->update(['status' => OrderStatus::Fulfilled]);
            OrderFulfilled::dispatch($order->refresh());
        } elseif ($order->status === OrderStatus::Fulfilled) {
            $order->update(['status' => OrderStatus::Paid]);
        }
    }
}
--- app/Observers/ProductObserver.php
<?php

namespace App\Observers;

use App\Models\Product;
use App\Services\SearchService;

class ProductObserver
{
    public function created(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function updated(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function deleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }
}


text:
Script completed
Wall time 0.1 seconds
Output:

text:
--- app/Events/FulfillmentDelivered.php
<?php

namespace App\Events;

use App\Models\Fulfillment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class FulfillmentDelivered
{
    use Dispatchable, SerializesModels;

    public function __construct(public Fulfillment $fulfillment) {}
}
--- app/Events/FulfillmentShipped.php
<?php

namespace App\Events;

use App\Models\Fulfillment;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class FulfillmentShipped
{
    use Dispatchable, SerializesModels;

    public function __construct(public Fulfillment $fulfillment) {}
}
--- app/Events/OrderCancelled.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderCancelled
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderCreated.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderCreated
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderFulfilled.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderFulfilled
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderPaid.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderPaid
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/OrderRefunded.php
<?php

namespace App\Events;

use App\Models\Order;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class OrderRefunded
{
    use Dispatchable, SerializesModels;

    public function __construct(public Order $order) {}
}
--- app/Events/ProductStatusChanged.php
<?php

namespace App\Events;

use App\Enums\ProductStatus;
use App\Models\Product;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class ProductStatusChanged
{
    use Dispatchable, SerializesModels;

    public function __construct(public Product $product, public ProductStatus $from, public ProductStatus $to) {}
}
--- app/Jobs/DeliverWebhook.php
<?php

namespace App\Jobs;

use App\Models\WebhookDelivery;
use App\Services\WebhookService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Throwable;

class DeliverWebhook implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, SerializesModels;

    public int $tries = 6;

    public function __construct(public WebhookDelivery $delivery) {}

    /** @return array<int, int> */
    public function backoff(): array
    {
        return [60, 300, 1800, 7200, 43200];
    }

    public function handle(WebhookService $webhooks): void
    {
        $this->delivery->load('subscription');
        $payload = json_encode($this->delivery->payload, JSON_THROW_ON_ERROR);
        $subscription = $this->delivery->subscription;
        $timestamp = (string) now()->timestamp;
        $response = Http::withHeaders([
            'Content-Type' => 'application/json',
            'X-Platform-Signature' => $webhooks->sign($timestamp.'.'.$payload, $subscription->signing_secret_encrypted),
            'X-Platform-Event' => $this->delivery->event,
            'X-Platform-Delivery-Id' => (string) $this->delivery->getKey(),
            'X-Platform-Timestamp' => $timestamp,
        ])->timeout(10)->post($subscription->target_url, $this->delivery->payload);

        $this->delivery->increment('attempts');
        $this->delivery->increment('attempt_count');
        $this->delivery->update([
            'response_status' => $response->status(),
            'response_code' => $response->status(),
            'response_body' => mb_substr($response->body(), 0, 10000),
            'response_body_snippet' => mb_substr($response->body(), 0, 1000),
            'last_attempt_at' => now(),
        ]);

        if ($response->successful()) {
            $subscription->update(['consecutive_failures' => 0]);
            $this->delivery->update(['status' => 'delivered', 'delivered_at' => now(), 'next_attempt_at' => null]);

            return;
        }

        $subscription->increment('consecutive_failures');

        if ($subscription->fresh()->consecutive_failures >= 5) {
            $subscription->update(['status' => 'paused']);
        }

        throw new \RuntimeException('Webhook delivery failed with HTTP '.$response->status());
    }

    public function failed(?Throwable $exception): void
    {
        $this->delivery->update(['status' => 'failed', 'next_attempt_at' => null]);
    }
}
--- app/Services/WebhookService.php
<?php

namespace App\Services;

use App\Jobs\DeliverWebhook;
use App\Models\Store;
use App\Models\WebhookSubscription;

class WebhookService
{
    public function dispatch(Store $store, string $eventType, array $payload): void
    {
        WebhookSubscription::withoutGlobalScopes()
            ->where('store_id', $store->getKey())
            ->where(function ($query) use ($eventType): void {
                $query->where('event', $eventType)->orWhere('event_type', $eventType);
            })
            ->where('status', 'active')
            ->get()
            ->each(function (WebhookSubscription $subscription) use ($eventType, $payload): void {
                $delivery = $subscription->deliveries()->create(['event' => $eventType, 'event_id' => (string) str()->uuid(), 'payload' => $payload, 'attempts' => 0, 'attempt_count' => 0, 'next_attempt_at' => now()]);
                DeliverWebhook::dispatch($delivery);
            });
    }

    public function sign(string $payload, string $secret): string
    {
        return hash_hmac('sha256', $payload, $secret);
    }

    public function verify(string $payload, string $signature, string $secret): bool
    {
        return hash_equals($this->sign($payload, $secret), $signature);
    }
}
--- app/Models/WebhookSubscription.php
<?php

namespace App\Models;

use App\Models\Concerns\BelongsToStore;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class WebhookSubscription extends Model
{
    use BelongsToStore;

    protected $fillable = ['store_id', 'event', 'event_type', 'target_url', 'app_installation_id', 'signing_secret_encrypted', 'status', 'consecutive_failures'];

    protected $hidden = ['signing_secret_encrypted'];

    protected function casts(): array
    {
        return ['signing_secret_encrypted' => 'encrypted'];
    }

    protected static function booted(): void
    {
        static::saving(function (WebhookSubscription $subscription): void {
            $subscription->event_type = $subscription->event;
        });
    }

    public function deliveries(): HasMany
    {
        return $this->hasMany(WebhookDelivery::class);
    }
}
--- app/Models/WebhookDelivery.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class WebhookDelivery extends Model
{
    protected $fillable = ['webhook_subscription_id', 'subscription_id', 'event', 'event_id', 'payload', 'status', 'attempts', 'attempt_count', 'response_status', 'response_code', 'response_body', 'response_body_snippet', 'delivered_at', 'last_attempt_at', 'next_attempt_at'];

    protected function casts(): array
    {
        return ['payload' => 'array', 'delivered_at' => 'datetime', 'last_attempt_at' => 'datetime', 'next_attempt_at' => 'datetime'];
    }

    protected static function booted(): void
    {
        static::creating(function (WebhookDelivery $delivery): void {
            $delivery->subscription_id ??= $delivery->webhook_subscription_id;
            $delivery->attempt_count ??= $delivery->attempts ?? 0;
            $delivery->event_id ??= (string) str()->uuid();
        });
    }

    public function subscription(): BelongsTo
    {
        return $this->belongsTo(WebhookSubscription::class, 'webhook_subscription_id');
    }
}
--- app/Services/OrderService.php
<?php

namespace App\Services;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderCancelled;
use App\Events\OrderCreated;
use App\Events\OrderPaid;
use App\Models\Checkout;
use App\Models\Discount;
use App\Models\Order;
use App\Models\Store;
use App\ValueObjects\PaymentResult;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;

class OrderService
{
    public function __construct(private readonly InventoryService $inventory, private readonly AuditLogger $audit) {}

    public function createFromCheckout(Checkout $checkout, ?PaymentResult $paymentResult = null): Order
    {
        return DB::transaction(function () use ($checkout, $paymentResult): Order {
            $checkout->load(['cart.lines.variant.product', 'cart.lines.variant.inventory', 'shippingRate']);
            $existing = Order::withoutGlobalScopes()->where('checkout_id', $checkout->getKey())->first();

            if ($existing !== null) {
                return $existing->load(['lines', 'payments']);
            }

            $totals = $checkout->totals_json ?? ['subtotal' => 0, 'discount' => 0, 'shipping' => 0, 'tax' => 0, 'total' => 0, 'currency' => $checkout->cart->currency];
            $discount = $checkout->discount_code === null ? null : Discount::withoutGlobalScopes()->where('store_id', $checkout->store_id)->whereRaw('lower(code) = ?', [strtolower($checkout->discount_code)])->first();
            $status = $paymentResult?->status === PaymentStatus::Captured ? FinancialStatus::Paid : FinancialStatus::Pending;
            $taxByLine = $this->allocateTaxLines($checkout->cart->lines, $totals['tax_lines'] ?? []);
            $order = Order::withoutGlobalScopes()->create([
                'store_id' => $checkout->store_id,
                'customer_id' => $checkout->customer_id,
                'checkout_id' => $checkout->getKey(),
                'order_number' => $this->generateOrderNumber(Store::findOrFail($checkout->store_id)),
                'currency' => $totals['currency'],
                'status' => $status === FinancialStatus::Paid ? OrderStatus::Paid : OrderStatus::Pending,
                'financial_status' => $status,
                'fulfillment_status' => 'unfulfilled',
                'payment_method' => $checkout->payment_method,
                'email' => $checkout->email,
                'shipping_address_json' => $checkout->shipping_address_json,
                'billing_address_json' => $checkout->billing_address_json,
                'subtotal_amount' => $totals['subtotal'],
                'discount_amount' => $totals['discount'] ?? 0,
                'shipping_amount' => $totals['shipping'] ?? 0,
                'tax_amount' => $totals['tax'] ?? 0,
                'total_amount' => $totals['total'],
                'placed_at' => now(),
            ]);

            foreach ($checkout->cart->lines as $line) {
                $order->lines()->create([
                    'product_id' => $line->variant->product_id,
                    'variant_id' => $line->variant_id,
                    'product_title' => $line->variant->product->title,
                    'title_snapshot' => $line->variant->product->title.' · '.$line->variant->title,
                    'variant_title' => $line->variant->title,
                    'sku' => $line->variant->sku,
                    'sku_snapshot' => $line->variant->sku,
                    'quantity' => $line->quantity,
                    'unit_price_amount' => $line->unit_price_amount,
                    'line_subtotal_amount' => $line->line_subtotal_amount,
                    'line_discount_amount' => $line->line_discount_amount,
                    'line_total_amount' => $line->line_total_amount,
                    'tax_lines_json' => $taxByLine[$line->getKey()] ?? [],
                    'discount_allocations_json' => $discount === null || $line->line_discount_amount < 1 ? [] : [['discount_id' => $discount->getKey(), 'amount' => $line->line_discount_amount]],
                ]);

                if ($paymentResult?->status === PaymentStatus::Captured && $line->variant->inventory !== null) {
                    $this->inventory->commit($line->variant->inventory, $line->quantity);
                }
            }

            $checkout->update(['status' => 'completed']);
            $checkout->cart->update(['status' => 'converted']);
            OrderCreated::dispatch($order);
            $this->audit->record('order.created', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);
            $order->load(['lines', 'payments']);

            if ($status === FinancialStatus::Paid) {
                OrderPaid::dispatch($order);
                $this->audit->record('order.paid', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);

                if ($checkout->cart->lines->every(fn ($line): bool => ! $line->variant->requires_shipping)) {
                    $fulfillment = $order->fulfillments()->create(['status' => 'delivered', 'fulfilled_at' => now(), 'delivered_at' => now()]);

                    foreach ($order->lines as $line) {
                        $fulfillment->lines()->create(['order_line_id' => $line->getKey(), 'quantity' => $line->quantity]);
                    }

                    $order->update(['status' => OrderStatus::Fulfilled, 'fulfillment_status' => FulfillmentStatus::Fulfilled]);
                }
            }

            return $order;
        });
    }

    public function generateOrderNumber(Store $store): string
    {
        $lastNumber = Order::withoutGlobalScopes()->where('store_id', $store->getKey())->selectRaw("max(cast(replace(order_number, '#', '') as integer)) as value")->value('value');

        return (string) config('shop.order_prefix', '#').(max(1000, (int) $lastNumber) + 1);
    }

    public function cancel(Order $order, string $reason): void
    {
        if ($order->fulfillment_status !== FulfillmentStatus::Unfulfilled) {
            throw new \LogicException('Fulfilled orders cannot be cancelled.');
        }

        DB::transaction(function () use ($order, $reason): void {
            $wasPending = $order->financial_status === FinancialStatus::Pending;
            $order->load('lines.variant.inventory')->update(['status' => OrderStatus::Cancelled, 'financial_status' => $order->financial_status === FinancialStatus::Pending ? FinancialStatus::Voided : $order->financial_status, 'metadata' => array_merge($order->metadata ?? [], ['cancellation_reason' => $reason])]);

            foreach ($order->lines as $line) {
                if ($line->variant?->inventory !== null && $wasPending) {
                    $this->inventory->release($line->variant->inventory, $line->quantity);
                }
            }

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

            OrderCancelled::dispatch($order->refresh());
            $this->audit->record('order.cancelled', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number, 'reason' => $reason]);
        });
    }

    public function confirmPayment(Order $order): void
    {
        if ($order->payment_method !== 'bank_transfer' || $order->financial_status !== FinancialStatus::Pending) {
            throw new \LogicException('Only pending bank transfer orders can be confirmed.');
        }

        DB::transaction(function () use ($order): void {
            $order->load('lines.variant.inventory')->update(['financial_status' => FinancialStatus::Paid, 'status' => OrderStatus::Paid]);

            foreach ($order->lines as $line) {
                if ($line->variant?->inventory !== null) {
                    $this->inventory->commit($line->variant->inventory, $line->quantity);
                }
            }

            $order->payments()->where('status', PaymentStatus::Pending)->update(['status' => PaymentStatus::Captured]);
            OrderPaid::dispatch($order->refresh());
            $this->audit->record('order.paid', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number]);

            if ($order->lines->every(fn ($line): bool => ! $line->variant?->requires_shipping)) {
                $this->autoFulfillDigitalOrder($order->refresh());
            }
        });
    }

    private function autoFulfillDigitalOrder(Order $order): void
    {
        if ($order->fulfillments()->exists()) {
            return;
        }

        $fulfillment = $order->fulfillments()->create(['status' => 'delivered', 'fulfilled_at' => now(), 'delivered_at' => now()]);
        foreach ($order->lines as $line) {
            $fulfillment->lines()->create(['order_line_id' => $line->getKey(), 'quantity' => $line->quantity]);
        }
        $order->update(['status' => OrderStatus::Fulfilled, 'fulfillment_status' => FulfillmentStatus::Fulfilled]);
    }

    /**
     * @param  \Illuminate\Support\Collection<int, \App\Models\CartLine>  $lines
     * @param  array<int, array{title?: string, rate?: int, amount?: int}>  $taxLines
     * @return array<int, array<int, array<string, mixed>>>
     */
    private function allocateTaxLines(Collection $lines, array $taxLines): array
    {
        $base = max(1, (int) $lines->sum('line_total_amount'));
        $allocations = $lines->mapWithKeys(fn ($line): array => [$line->getKey() => []])->all();

        foreach ($taxLines as $taxLine) {
            $remaining = (int) ($taxLine['amount'] ?? 0);
            $lineCount = $lines->count();

            foreach ($lines->values() as $index => $line) {
                $amount = $index === $lineCount - 1
                    ? $remaining
                    : intdiv((int) ($taxLine['amount'] ?? 0) * (int) $line->line_total_amount, $base);
                $remaining -= $amount;

                if ($amount > 0) {
                    $allocations[$line->getKey()][] = [...$taxLine, 'amount' => $amount];
                }
            }
        }

        return $allocations;
    }
}
--- app/Services/RefundService.php
<?php

namespace App\Services;

use App\Contracts\PaymentProvider;
use App\Enums\FinancialStatus;
use App\Enums\PaymentStatus;
use App\Events\OrderRefunded;
use App\Models\Order;
use App\Models\Payment;
use App\Models\Refund;
use Illuminate\Support\Facades\DB;

class RefundService
{
    public function __construct(private readonly PaymentProvider $provider, private readonly InventoryService $inventory, private readonly AuditLogger $audit) {}

    /**
     * @param  int|array<int, int>|null  $amount
     * @param  array<int, int>  $lines
     */
    public function create(Order $order, Payment $payment, int|array|null $amount = null, ?string $reason = null, bool $restock = false, array $lines = []): Refund
    {
        if ((int) $payment->order_id !== (int) $order->getKey() || $payment->status !== PaymentStatus::Captured) {
            throw new \InvalidArgumentException('The payment is not refundable for this order.');
        }

        if (is_array($amount)) {
            $lines = $amount;
            $amount = null;
        }

        $refunded = (int) $order->refunds()->where('status', 'processed')->sum('amount');
        $order->loadMissing('lines');
        $restockLines = $lines;
        $previousLineQuantities = [];
        foreach ($order->refunds()->where('status', 'processed')->get(['lines_json']) as $previousRefund) {
            foreach ($previousRefund->lines_json ?? [] as $lineId => $quantity) {
                $previousLineQuantities[(string) $lineId] = ($previousLineQuantities[(string) $lineId] ?? 0) + (int) $quantity;
            }
        }

        if ($lines !== []) {
            $amount = 0;

            foreach ($lines as $lineId => $quantity) {
                $orderLine = $order->lines->firstWhere('id', (int) $lineId);

                if ($orderLine === null || $quantity < 1 || $quantity > $orderLine->quantity) {
                    throw new \InvalidArgumentException('The refund quantity is invalid.');
                }

                if (($previousLineQuantities[(string) $lineId] ?? 0) + $quantity > $orderLine->quantity) {
                    throw new \InvalidArgumentException('The refund quantity exceeds the remaining refundable quantity.');
                }

                $unitAmount = intdiv($orderLine->line_total_amount, $orderLine->quantity);
                $amount += $unitAmount * $quantity;
            }
        }

        $amount ??= $payment->amount - $refunded;

        if ($amount < 1 || $refunded + $amount > $payment->amount) {
            throw new \InvalidArgumentException('Refund amount exceeds the captured payment.');
        }

        if ($restock && $restockLines === [] && $refunded + $amount >= $payment->amount) {
            $restockLines = $order->lines->mapWithKeys(fn ($line): array => [$line->getKey() => $line->quantity])->all();
        }

        return DB::transaction(function () use ($order, $payment, $amount, $reason, $restock, $restockLines, $lines): Refund {
            $result = $this->provider->refund($payment, $amount);
            $refund = $order->refunds()->create(['payment_id' => $payment->getKey(), 'amount' => $amount, 'status' => $result->successful ? 'processed' : 'failed', 'provider_refund_id' => $result->reference, 'reason' => $reason, 'restock' => $restock, 'lines_json' => $lines !== [] ? $lines : null]);

            if ($result->successful) {
                $totalRefunded = (int) $order->refunds()->where('status', 'processed')->sum('amount');
                $order->update([
                    'financial_status' => $totalRefunded >= $payment->amount ? FinancialStatus::Refunded : FinancialStatus::PartiallyRefunded,
                    'status' => $totalRefunded >= $payment->amount ? 'refunded' : $order->status,
                ]);
                $payment->update(['status' => $totalRefunded >= $payment->amount ? PaymentStatus::Refunded : $payment->status]);

                if ($restock && $restockLines !== []) {
                    $order->load('lines.variant.inventory');
                    foreach ($restockLines as $lineId => $quantity) {
                        $line = $order->lines->firstWhere('id', (int) $lineId);

                        if ($line->variant?->inventory !== null) {
                            $this->inventory->restock($line->variant->inventory, (int) $quantity);
                        }
                    }
                }

                OrderRefunded::dispatch($order->refresh());
                $this->audit->record('order.refunded', $order, ['store_id' => $order->store_id, 'order_number' => $order->order_number, 'amount' => $amount]);
            }

            return $refund;
        });
    }
}
--- app/Services/FulfillmentService.php
<?php

namespace App\Services;

use App\Enums\FinancialStatus;
use App\Enums\FulfillmentStatus;
use App\Enums\OrderStatus;
use App\Events\FulfillmentDelivered;
use App\Events\FulfillmentShipped;
use App\Events\OrderFulfilled;
use App\Exceptions\FulfillmentGuardException;
use App\Models\Fulfillment;
use App\Models\Order;
use Illuminate\Support\Facades\DB;

class FulfillmentService
{
    public function create(Order $order, array $lines, ?array $tracking = null): Fulfillment
    {
        if (! in_array($order->financial_status, [FinancialStatus::Paid, FinancialStatus::PartiallyRefunded], true)) {
            throw new FulfillmentGuardException;
        }

        return DB::transaction(function () use ($order, $lines, $tracking): Fulfillment {
            $order->load(['lines', 'fulfillments.lines']);
            $fulfilledQuantities = $order->fulfillments->flatMap->lines->groupBy('order_line_id')->map(fn ($items): int => (int) $items->sum('quantity'));
            $requestedQuantities = [];

            foreach ($lines as $line) {
                $orderLine = $order->lines->firstWhere('id', $line['order_line_id']);
                $quantity = (int) ($line['quantity'] ?? 0);
                $requestedQuantities[$line['order_line_id']] = ($requestedQuantities[$line['order_line_id']] ?? 0) + $quantity;

                if ($orderLine === null || $quantity < 1 || $orderLine->quantity < $requestedQuantities[$line['order_line_id']] + (int) ($fulfilledQuantities[$orderLine->id] ?? 0)) {
                    throw new \InvalidArgumentException('The fulfillment quantity exceeds the unfulfilled quantity.');
                }
            }

            $fulfillment = $order->fulfillments()->create(array_merge($tracking ?? [], ['status' => 'pending']));

            foreach ($lines as $line) {
                $fulfillment->lines()->create(['order_line_id' => $line['order_line_id'], 'quantity' => $line['quantity']]);
            }

            $this->refreshOrderStatus($order->refresh());

            return $fulfillment->load('lines.orderLine');
        });
    }

    public function markAsShipped(Fulfillment $fulfillment, ?array $tracking = null): void
    {
        if ($fulfillment->status !== 'pending') {
            throw new \LogicException('Only pending fulfillments can be shipped.');
        }

        $fulfillment->update(array_merge($tracking ?? [], ['status' => 'shipped', 'shipped_at' => now(), 'fulfilled_at' => now()]));
        FulfillmentShipped::dispatch($fulfillment->refresh());
        $this->refreshOrderStatus($fulfillment->load('order')->order);
    }

    public function markAsDelivered(Fulfillment $fulfillment): void
    {
        if ($fulfillment->status !== 'shipped') {
            throw new \LogicException('Only shipped fulfillments can be delivered.');
        }

        $fulfillment->update(['status' => 'delivered', 'delivered_at' => now()]);
        $fulfillment->load('order');
        FulfillmentDelivered::dispatch($fulfillment);

        if ($fulfillment->order !== null) {
            $this->refreshOrderStatus($fulfillment->order);
        }
    }

    private function refreshOrderStatus(Order $order): void
    {
        $order->load(['lines', 'fulfillments.lines']);
        $fulfilledQuantities = [];

        foreach ($order->fulfillments->whereIn('status', ['shipped', 'delivered']) as $fulfillment) {
            foreach ($fulfillment->lines as $line) {
                $fulfilledQuantities[$line->order_line_id] = ($fulfilledQuantities[$line->order_line_id] ?? 0) + $line->quantity;
            }
        }

        $fulfilled = collect($order->lines)->every(fn ($line): bool => ($fulfilledQuantities[$line->id] ?? 0) >= $line->quantity);
        $partial = $fulfilledQuantities !== [];
        $order->update(['fulfillment_status' => $fulfilled ? FulfillmentStatus::Fulfilled : ($partial ? FulfillmentStatus::Partial : FulfillmentStatus::Unfulfilled)]);

        if ($fulfilled) {
            $order->update(['status' => OrderStatus::Fulfilled]);
            OrderFulfilled::dispatch($order->refresh());
        } elseif ($order->status === OrderStatus::Fulfilled) {
            $order->update(['status' => OrderStatus::Paid]);
        }
    }
}
--- app/Observers/ProductObserver.php
<?php

namespace App\Observers;

use App\Models\Product;
use App\Services\SearchService;

class ProductObserver
{
    public function created(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function updated(Product $product): void
    {
        app(SearchService::class)->syncProduct($product);
    }

    public function deleted(Product $product): void
    {
        app(SearchService::class)->removeProduct($product->getKey());
    }
}


status: completed

input:
const r = await tools.exec_command({cmd:"rg -n \"class Checkout|CheckoutAddress|CheckoutShipping|CheckoutCompleted|CheckoutExpired|dispatch\\(\" app | head -300; sed -n '1,180p' app/Models/Checkout.php; sed -n '1,150p' app/Http/Controllers/Api/StorefrontCheckoutController.php 2>/dev/null || true; rg -n \"checkout\" app/Livewire/Checkout app/Services/CheckoutService.php app/Http/Controllers/Api | head -200","workdir":"/Users/wesner/Herd/shop","yield_time_ms":10000,"max_output_tokens":20000});
text(r.output);