The agent is deep into reviewing services. Let me wait for it to finish.


<task-notification>
<task-id>a680109</task-id>
<status>completed</status>
<summary>Agent "Fresh agent code review" completed</summary>
<result>All checks pass. Here is the complete review report.

---

# Code Review Report

## CRITICAL Issues (Fixed)

### 1. Order Confirmation IDOR - Any user can view any order
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Confirmation.php`, line 17
**Severity:** CRITICAL

The `Confirmation` component fetched orders using `withoutGlobalScopes()` without any store or user ownership check. Any visitor who can guess or enumerate order numbers (which are sequential like #1001, #1002...) could view the full details of any order from any store, including line items, payment details, and addresses.

**Fix applied:** Added store scoping so orders are only visible if they belong to the current store.

---

### 2. Discount minimum purchase rule never works
**File:** `/Users/fabianwesner/Herd/shop/app/Services/DiscountService.php`, line 43
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Discounts/Form.php`, line 85/98
**Severity:** CRITICAL

The admin form saves the minimum purchase amount under the key `min_purchase_amount` in `rules_json`, but `DiscountService::validate()` checks for the key `minimum_purchase`. This means the minimum purchase validation is completely broken -- any discount with a minimum purchase requirement will never enforce it.

**Fix applied:** Updated `DiscountService::validate()` to check both `minimum_purchase` and `min_purchase_amount` keys, ensuring backwards compatibility.

---

### 3. Admin fulfillment bypasses FulfillmentService
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Orders/Show.php`, lines 45-68
**Severity:** CRITICAL

The `createFulfillment()` method created `Fulfillment` and `FulfillmentLine` records directly, bypassing `FulfillmentService`. This caused:
- No financial status guard -- unpaid orders could be fulfilled
- No check for already-fulfilled lines -- double fulfillment was possible
- No `OrderFulfilled` event dispatch -- webhooks and downstream listeners never fired
- Fulfillment status was hardcoded to `Fulfilled` regardless of whether all lines were actually fulfilled

**Fix applied:** Rewrote to use `FulfillmentService::create()` with proper error handling.

---

### 4. Admin refund bypasses RefundService
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Orders/Show.php`, lines 77-97
**Severity:** CRITICAL

The `processRefund()` method created `Refund` records directly, bypassing `RefundService`. This caused:
- No refund amount validation -- could refund more than the payment amount
- Order `financial_status` was never updated (stayed at `paid` after refund)
- No `OrderRefunded` event dispatch -- webhooks and downstream listeners never fired
- No provider refund call -- only a local record was created, the mock payment provider was never notified

**Fix applied:** Rewrote to use `RefundService::create()` with proper error handling and null payment check.

---

### 5. Checkout IDOR via checkoutId manipulation
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Checkout/Index.php`, line 230-233
**Severity:** CRITICAL

The `getCheckout()` method used `Checkout::withoutGlobalScopes()->findOrFail($this->checkoutId)` without any store or session validation. Since `checkoutId` is a public Livewire property, a malicious user could modify it to access and complete another store's or user's checkout.

**Fix applied:** Added store scoping to the checkout retrieval query.

---

### 6. Customer registration missing email uniqueness validation
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Storefront/Account/Auth/Register.php`, line 33
**Severity:** HIGH

The registration form did not validate email uniqueness per store. If a customer tried to register with an already-existing email, it would hit the database unique constraint and throw an ugly unhandled exception.

**Fix applied:** Added `Rule::unique('customers', 'email')->where('store_id', $storeId)` to the validation rules.

---

## MEDIUM Issues (Reported Only)

### 7. XSS via unescaped HTML output
**File:** `/Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/products/show.blade.php`, line 111
**File:** `/Users/fabianwesner/Herd/shop/resources/views/livewire/storefront/pages/show.blade.php`, line 6

Product `description_html` and page `body_html` are rendered with `{!! !!}` (raw HTML). While this is expected for rich text content entered by store admins, there is no HTML sanitization on input. If an admin account is compromised, or if a future feature allows user-contributed HTML, this becomes an XSS vector.

**Suggested fix:** Sanitize HTML on input using a library like `mews/purifier` or Laravel's `Str::of($html)->sanitizeHtml()` (if available), or at minimum sanitize on output.

---

### 8. Order number generation race condition
**File:** `/Users/fabianwesner/Herd/shop/app/Services/OrderService.php`, lines 136-150

`generateOrderNumber()` reads the max order number and increments it without a lock. Under concurrent checkouts, two orders could receive the same number. SQLite's single-writer model mitigates this somewhat, but this is a concern for future database migrations.

**Suggested fix:** Use a database sequence, or wrap in `lockForUpdate()` within the existing transaction.

---

### 9. Refund with restock=true restocks ALL lines regardless of refund amount
**File:** `/Users/fabianwesner/Herd/shop/app/Services/RefundService.php`, lines 52-59

When `$restock=true`, all order lines are restocked at their full quantity, even for a partial refund (e.g., refunding 50% of a $100 order still restocks everything). This could lead to phantom inventory.

**Suggested fix:** Accept line-level restock quantities, or only allow restock on full refunds.

---

### 10. Cart version increment is not atomic
**File:** `/Users/fabianwesner/Herd/shop/app/Services/CartService.php`, lines 72, 89, 102, 115, 127

`$cart->update(['cart_version' => $cart->cart_version + 1])` reads the version from the model's in-memory state and writes it back. Under concurrent requests, two operations could read the same version and both write `version + 1`, losing an increment. Should use `$cart->increment('cart_version')` instead.

---

### 11. Admin account routes lack store.resolve middleware
**File:** `/Users/fabianwesner/Herd/shop/routes/web.php`, lines 91-108

The storefront account routes (login, register, orders, addresses) do not apply the `store.resolve` middleware. This means `app('current_store')` may not be bound when these components try to use it. The customer login component has a safety check via `CustomerUserProvider`, but the register component accesses `app('current_store')` directly which could return null.

---

### 12. Dashboard date filters not validated
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Dashboard.php`, lines 27-29

The `$dateFrom` and `$dateTo` properties are public strings that are used directly in `whereBetween`. While Livewire's type coercion provides some protection, these values are not validated as dates and not sanitized. Malformed date strings would not cause SQL injection (parameterized queries), but could cause unexpected query results.

---

## LOW Issues (Reported Only)

### 13. N+1 queries in Admin Customers Show
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Customers/Show.php`, line 22

`$this->customer->load(['orders', 'addresses'])` is called on every render. This loads all orders without pagination, which could be slow for high-volume customers.

---

### 14. ShippingCalculator weight-based rate loads cart lines on every call
**File:** `/Users/fabianwesner/Herd/shop/app/Services/ShippingCalculator.php`, line 101

`$cart->load('lines.variant')` is called inside `calculateWeight()`. If the cart lines are already loaded (likely since PricingEngine loaded them), this triggers an unnecessary query.

---

### 15. Product handle regenerated on every edit
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Products/Form.php`, line 106

When editing a product, the handle is regenerated from the title with a random suffix (`Str::slug($this->title).'-'.Str::random(4)`), which breaks existing URLs/bookmarks for that product. The handle should be preserved unless explicitly changed.

---

### 16. Collection handle also regenerated on edit
**File:** `/Users/fabianwesner/Herd/shop/app/Livewire/Admin/Collections/Form.php`, line 62

Same issue as products -- the handle is regenerated on every save.

---

### 17. Storefront account routes do not use store.resolve
**File:** `/Users/fabianwesner/Herd/shop/routes/web.php`, line 91

Customer account routes (login, register, etc.) are not wrapped in `store.resolve` middleware. The `CustomerUserProvider` handles store scoping for login, but registration and other components access `app('current_store')` directly.

---

## Summary of Changes Made

| File | Change |
|------|--------|
| `app/Livewire/Storefront/Checkout/Confirmation.php` | Added store scoping to prevent order IDOR |
| `app/Services/DiscountService.php` | Fixed minimum purchase key mismatch (accepts both keys) |
| `app/Livewire/Admin/Orders/Show.php` | Rewrote to use FulfillmentService and RefundService instead of direct model creation |
| `app/Livewire/Storefront/Checkout/Index.php` | Added store scoping to checkout retrieval |
| `app/Livewire/Storefront/Account/Auth/Register.php` | Added email uniqueness validation per store |
| `resources/views/livewire/admin/orders/show.blade.php` | Added error display in fulfillment and refund modals |
| `tests/Feature/Admin/OrderManagementTest.php` | Updated test to use `paid()` factory state (fulfillment requires paid order) |

## Verification Results

- **Tests:** 335 passed, 730 assertions, 0 failures
- **PHPStan:** 0 errors at max level
- **Pint:** Pass (no formatting issues)</result>
<usage>total_tokens: 139460
tool_uses: 100
duration_ms: 319025</usage>
</task-notification>
Full transcript available at: /private/tmp/claude-501/-Users-fabianwesner-Herd-shop/tasks/a680109.output