# Shop Manager — Comprehensive Test Plan

All tests use **Pest 2.6** and go in `tests/Feature/Controllers/ShopManager/`.

---

## Phase 1: Authorization & Middleware

**File:** `tests/Feature/Controllers/ShopManager/AuthorizationTest.php`

### Access Control
- [ ] Shop manager can access `/v1/places/{place}/shop/*` routes → 200
- [ ] Regular employee without shop_manager role → 403
- [ ] Customer user → 403
- [ ] Coach user → 403
- [ ] Unauthenticated user → 401
- [ ] Shop manager at Place A accessing Place B → 403
- [ ] Shop manager at Place A accessing Place A → 200
- [ ] Deactivated employee (is_active=false) → 403

---

## Phase 2: Products

**File:** `tests/Feature/Controllers/ShopManager/ProductsTest.php`

### List Products
- [ ] Returns paginated results for the place
- [ ] Pagination follows Laravel format (data/links/meta)
- [ ] `page` and `per_page` params work
- [ ] Search by name returns matching products
- [ ] Search is case-insensitive
- [ ] Filter by category returns only products in that category
- [ ] Only returns products for this place (not other places)
- [ ] Only returns `is_active = true` products by default
- [ ] Returns all products when `include_inactive=true` param is passed
- [ ] Includes stock count per product
- [ ] Includes total sold count per product
- [ ] Sort by name works (asc/desc)
- [ ] Empty results return empty data array, not error

### Product Detail
- [ ] Returns full product with variation pricing
- [ ] Returns sales performance (weekly units, daily breakdown)
- [ ] Returns inventory snapshot (stock, reorder status, barcode, rating)
- [ ] Returns latest feedback with comment count
- [ ] Returns computed badge (TOP PERFORMER / TOP RATED / null)
- [ ] Product from another place → 404
- [ ] Inactive product is still accessible by detail (for vendor review)

### Barcode Lookup
- [ ] Valid barcode returns correct product + variation
- [ ] Unknown barcode → 404
- [ ] Barcode from another place → 404
- [ ] Empty barcode → 422

### Toggle Active
- [ ] Toggle active→inactive works, returns updated product
- [ ] Toggle inactive→active works
- [ ] Toggling product not in place → 404

---

## Phase 3: Cart & Customers

**File:** `tests/Feature/Controllers/ShopManager/CartTest.php`

### List Carts
- [ ] Returns only WAITING_PAYMENT orders created by this employee
- [ ] Does NOT return other employees' carts
- [ ] Does NOT return PAID orders
- [ ] Each cart includes customer info + item count + total
- [ ] Empty list returns empty data array

### Create Cart
- [ ] Creates order with WAITING_PAYMENT status
- [ ] Sets employee_id and place_id correctly
- [ ] Returns cart with customer info
- [ ] Invalid customer_id → 422
- [ ] Customer from another system (valid UUID but not a user) → 422

### Show Cart
- [ ] Returns full item details (product, variation, quantity, prices)
- [ ] Returns both unit_price and line_total per item
- [ ] Returns cart total
- [ ] Cart from another employee → 403 (or 404)
- [ ] PAID order → still accessible (read-only)

### Add Item
- [ ] Add by product_id + variation_id works
- [ ] Add by barcode works
- [ ] Adding duplicate product+variation increments quantity
- [ ] Adding product from another place → 422
- [ ] Adding inactive product → 422
- [ ] Quantity defaults to 1 if not provided
- [ ] Quantity must be positive integer → 422 for 0 or negative
- [ ] Adding item to PAID order → 422

### Update Item Quantity
- [ ] Quantity update works (+/-)
- [ ] Quantity = 0 removes the item
- [ ] Negative quantity → 422
- [ ] Item not in this cart → 404

### Remove Item
- [ ] Removes item from cart
- [ ] Cart total recalculates
- [ ] Removing last item leaves empty cart (doesn't delete cart)
- [ ] Item not in this cart → 404

### Update Cart (Full)
- [ ] Full item list replaces existing items
- [ ] Empty items list clears the cart
- [ ] PAID order → 422

### Change Customer
- [ ] Customer change works, updates order.user_id
- [ ] Invalid customer_id → 422
- [ ] PAID order → 422

### Delete Cart
- [ ] Deletes order and all items
- [ ] PAID order cannot be deleted → 422

### Currency Validation
- [ ] Adding item with different currency than existing items → 422
- [ ] First item sets the cart currency

**File:** `tests/Feature/Controllers/ShopManager/CustomerTest.php`

### Search Customers
- [ ] Search by name returns matching users
- [ ] Search by email returns matching users
- [ ] Search by phone returns matching users
- [ ] Search is case-insensitive
- [ ] Returns paginated results
- [ ] Empty search returns all customers (paginated)
- [ ] Returns avatar URL

### Create Customer
- [ ] Creates user with UserType::CUSTOMER
- [ ] Creates wallet for new user
- [ ] Returns user with wallet info
- [ ] Duplicate email → 422
- [ ] Missing required fields (firstname, lastname, email) → 422
- [ ] Invalid email format → 422
- [ ] Invalid phone format → 422

---

## Phase 4: Payments

**File:** `tests/Feature/Controllers/ShopManager/PaymentTest.php`

### POS Payment
- [ ] With receipt photo → marks order PAID, payment_provider = POS
- [ ] Without receipt photo → 422
- [ ] Non-image file → 422
- [ ] File exceeding 10MB → 422
- [ ] Receipt photo is stored on order media collection
- [ ] terminal_id is optional and stored in payment_details
- [ ] OrderPaid event is dispatched
- [ ] Already PAID order → 422

### Cash Payment
- [ ] With valid amounts + photo → marks order PAID, payment_provider = CASH
- [ ] amount_received < order total → 422
- [ ] amount_returned != (amount_received - total) → 422 (or server calculates)
- [ ] Without cash photo → 422
- [ ] Non-image file → 422
- [ ] Negative amounts → 422
- [ ] Zero amount_received → 422
- [ ] Cash photo is stored on order media collection
- [ ] payment_details JSON stores amount_received, amount_returned, currency
- [ ] OrderPaid event is dispatched

### Stripe Payment (Vendor Context)
- [ ] Creates PaymentIntent for customer's order
- [ ] Returns client_secret for mobile SDK
- [ ] Sets payment_provider = STRIPE
- [ ] Empty cart → 422

### Wallet Payment (Vendor Context)
- [ ] Deducts from customer's wallet
- [ ] Insufficient wallet balance → 400 (InsufficientWalletBalanceException)
- [ ] Sets payment_provider = WALLET
- [ ] OrderPaid event is dispatched

### Crypto Payment (Vendor Context)
- [ ] Creates NowPayments invoice
- [ ] Returns payment URL
- [ ] Sets payment_provider = NOWPAYMENTS

### Cross-Cutting Payment Tests
- [ ] Cannot pay an already PAID order (all 5 methods)
- [ ] Cannot pay an empty cart
- [ ] Payment on cart from another employee → 403
- [ ] OrderPaid event fires for ALL payment types (POS, CASH, STRIPE, WALLET, CRYPTO)

---

## Phase 5: Invoices

**File:** `tests/Feature/Controllers/ShopManager/InvoiceTest.php`

### Auto-Generation
- [ ] Invoice is created when vendor order is paid (POS)
- [ ] Invoice is created when vendor order is paid (CASH)
- [ ] Invoice is created when vendor order is paid (STRIPE)
- [ ] Invoice is created when vendor order is paid (WALLET)
- [ ] Invoice is NOT created for non-vendor orders (no employee_id)
- [ ] Invoice generation is queued (Job dispatched, not inline)

### Invoice Data
- [ ] Invoice number is sequential per place (INV-2026-0001, INV-2026-0002)
- [ ] Invoice number is unique across the system
- [ ] Line items match order items at time of payment
- [ ] Customer snapshot is correct (name, email)
- [ ] Place snapshot is correct (name, address, VAT)
- [ ] Total matches order total
- [ ] VAT/tax breakdown is correct

### Invoice Immutability
- [ ] Invoice snapshot doesn't change if order is later refunded

### Download
- [ ] Returns PDF file (content-type: application/pdf)
- [ ] 404 for order without invoice
- [ ] Invoice from another place → 403

### Send to Alternate Email
- [ ] Sends invoice to provided email
- [ ] Invalid email → 422
- [ ] Missing email → 422
- [ ] Invoice not generated yet → 404
- [ ] Notification is dispatched (check via Notification::fake)

---

## Phase 6: Dashboard

**File:** `tests/Feature/Controllers/ShopManager/DashboardTest.php`

### Revenue
- [ ] Returns total for today
- [ ] Returns total for 7d
- [ ] Returns total for 30d
- [ ] Custom date range works (from/to params)
- [ ] Percentage change vs previous period is correct
- [ ] Chart has one data point per day in range
- [ ] Only counts PAID orders at this place
- [ ] Empty period returns 0 total, 0% change, empty chart
- [ ] Invalid period param → 422 or defaults gracefully

### Checkout Snapshot
- [ ] Checkout section: correct completed count + % change
- [ ] Basket section: correct average value + % change
- [ ] Risk section: correct refund count (orders with refunded_at)
- [ ] Sparkline arrays have correct length (7 data points for 7 intervals)
- [ ] Empty data returns zeros, not errors

### Sales Performance
- [ ] Revenue metric groups by day of week (MON-SUN)
- [ ] Orders metric counts orders per day
- [ ] Basket metric averages order total per day
- [ ] Current day is flagged (is_today = true)
- [ ] Average line is calculated correctly
- [ ] 7D/14D/30D periods work
- [ ] Empty data returns zero-filled days

### Best Sellers
- [ ] Returns top product by quantity sold
- [ ] limit=1 returns single item (for highlight card)
- [ ] limit=20 returns multiple items (for "See All")
- [ ] Includes sold_this_week, price, category, badge
- [ ] Product with no sales is not returned

### Recent Activity
- [ ] Merges sales, feedback, and stock events
- [ ] Sorted by timestamp DESC
- [ ] Respects limit param
- [ ] Each entry has type, title, subtitle, time_ago
- [ ] Empty activity returns empty array

### Top Categories
- [ ] Returns categories with % of sales
- [ ] Percentages sum to ~100%
- [ ] Trend labels are computed (rising/stable/best)
- [ ] Categories with 0 sales are excluded
- [ ] Empty data returns empty array

### Caching
- [ ] Response is served from cache on second request
- [ ] Cache key includes place_id + period

---

## Phase 7: Inventory

**File:** `tests/Feature/Controllers/ShopManager/InventoryTest.php`

### Inventory Alerts
- [ ] Returns products with stock <= low_stock_threshold
- [ ] Out-of-stock (stock=0) products are included
- [ ] Products above threshold are NOT included
- [ ] Products exactly at threshold ARE included
- [ ] Ordered by stock ASC (most critical first)
- [ ] Empty inventory returns { low_stock_count: 0, items: [] }
- [ ] Custom threshold per variation is respected
- [ ] Default threshold (5) applies when not set
- [ ] Only products for this place

### Reorder Status
- [ ] stock=0 → "out_of_stock"
- [ ] 0 < stock <= threshold → "low"
- [ ] threshold < stock <= threshold*2 → "warning"
- [ ] stock > threshold*2 → "healthy"

---

## Phase 8: Orders History

**File:** `tests/Feature/Controllers/ShopManager/OrdersHistoryTest.php`

### List Orders
- [ ] Returns only this employee's orders at this place
- [ ] Does NOT return other employees' orders
- [ ] Pagination follows Laravel format (data/links/meta)
- [ ] Date filter: today returns today's orders only
- [ ] Date filter: 7d returns last 7 days
- [ ] Date filter: 30d returns last 30 days
- [ ] Date filter: all returns everything
- [ ] Customer filter returns only that customer's orders
- [ ] Type filter works (product, service, etc.)
- [ ] Combined filters work (date + customer + type)
- [ ] Order number is first 6 chars of public_id
- [ ] type_label reflects dominant orderable type
- [ ] Status badge is included
- [ ] Total amount is correct

### Order Detail
- [ ] Returns full order with all items
- [ ] Returns payment info (provider, details)
- [ ] Returns invoice link (if invoice exists)
- [ ] Returns customer info
- [ ] Order from another employee → 403

---

## Phase 9: Filament Admin

**File:** `tests/Feature/Filament/ShopManagerFilamentTest.php`

### EmployeeResource
- [ ] SHOP_MANAGER role appears in role options
- [ ] Can assign SHOP_MANAGER role to employee
- [ ] SHOP_MANAGER displays correctly in table

### ProductResource
- [ ] is_active toggle appears in form
- [ ] SKU field appears in variation form
- [ ] barcode field appears in variation form
- [ ] low_stock_threshold field appears in variation form
- [ ] Stock status badge shows in variation table

### OrderResource
- [ ] POS payment details display on view page
- [ ] CASH payment details display (amounts received/returned)
- [ ] Receipt photo displays for POS orders
- [ ] Cash photo displays for CASH orders
- [ ] POS/CASH appear in payment provider filter

### InvoiceResource
- [ ] List page shows invoices with search
- [ ] Filter by place works
- [ ] Filter by date range works
- [ ] View page shows full invoice details
- [ ] PDF download action works
- [ ] Resend action sends notification
- [ ] Cannot create invoices manually (no create page)

---

## Phase 10: Schema & Cross-Cutting

**File:** `tests/Feature/Controllers/ShopManager/StockValidationTest.php`

### Stock Check Before Payment
- [ ] Payment blocked when product stock is 0 → InsufficientStockException
- [ ] Payment blocked when requested qty > available stock
- [ ] Payment allowed when stock is sufficient
- [ ] Stock check only applies to Product orderables (not services, courses, etc.)
- [ ] Multiple items: stock checked per item independently
- [ ] Mixed order (product + bookable): only product stock is checked

### Stock Decrement After Payment
- [ ] Stock decremented after successful payment
- [ ] Quantity decremented matches ordered quantity
- [ ] Race condition: stock=0 at decrement time → wallet credit-back + warning log
- [ ] Race condition: does NOT throw exception, does NOT break listener chain
- [ ] Low stock notification dispatched when threshold crossed

**File:** `tests/Feature/Controllers/ShopManager/StockAdjustmentTest.php`

### Adjust Stock
- [ ] Refill adds stock (creates ProductVariationStock)
- [ ] Correction type works
- [ ] Return type works
- [ ] Damaged type (negative adjustment) works
- [ ] Variation must belong to product → 422
- [ ] Product must belong to place → 422
- [ ] Quantity must be positive (except damaged) → 422
- [ ] Activity log entry is created
- [ ] Note is saved in activity log properties

**File:** `tests/Feature/Controllers/ShopManager/RefundTest.php`

### Refund Order
- [ ] Full refund credits entire order amount to wallet
- [ ] Partial refund credits only specified items
- [ ] WalletPaymentType::REFUND is used on wallet transaction
- [ ] Stripe orders also trigger Stripe API refund
- [ ] POS/CASH orders: wallet credit only (no external refund)
- [ ] refunded_at timestamp is set on order
- [ ] refund_reason is saved on order
- [ ] Order status stays PAID (not changed to REFUNDED)
- [ ] Cannot refund an unpaid order (WAITING_PAYMENT) → 422
- [ ] Cannot refund same item twice → 422
- [ ] CustomerRefundNotification is dispatched
- [ ] Non-shop-manager → 403

**File:** `tests/Feature/Controllers/ShopManager/ProductReviewTest.php`

### List Reviews
- [ ] Returns reviews for the product
- [ ] Only returns approved reviews
- [ ] Includes rating, comment, author name, verified badge
- [ ] Paginated
- [ ] Empty reviews returns empty array

### Product Rating (Computed)
- [ ] average_rating is correct (average of approved reviews)
- [ ] reviews_count is correct
- [ ] Product with no reviews has null rating and 0 count

**File:** `tests/Feature/Commands/CleanExpiredCartsTest.php`

### Cart Cleanup
- [ ] Deletes vendor carts (employee_id IS NOT NULL) older than 24h
- [ ] Does NOT delete customer carts (employee_id IS NULL)
- [ ] Does NOT delete PAID orders
- [ ] Does NOT delete carts newer than 24h
- [ ] Deletes associated order items
- [ ] Command runs without error when no stale carts exist
- [ ] Logs count of cleaned carts

---

## Test Utilities

### Common Setup (use in beforeEach or dataset)

```php
// Create a place
$place = Place::factory()->create();

// Create shop manager employee
$user = User::factory()->create(['type' => UserType::EMPLOYEE]);
$employee = Employee::factory()->create(['user_id' => $user->id]);
$employee->places()->attach($place);
$employee->assignRole('shop_manager');

// Auth token
$token = $user->createToken('test', [User::ABILITY_FULL])->plainTextToken;

// Create a customer
$customer = User::factory()->create(['type' => UserType::CUSTOMER]);
Wallet::factory()->create(['user_id' => $customer->id]);

// Create a product with stock
$product = Product::factory()->create(['place_id' => $place->id, 'is_active' => true]);
$variation = ProductVariation::factory()->create(['product_id' => $product->id]);
ProductVariationStock::factory()->create(['product_variation_id' => $variation->id, 'quantity' => 50]);
```

### Assertion Helpers

```php
// Assert Laravel pagination format
->assertJsonStructure(['data', 'links' => ['first', 'last', 'prev', 'next'], 'meta' => ['current_page', 'last_page', 'per_page', 'total']])

// Assert public_id used (never id)
->assertJsonMissing(['id' => $model->id])
->assertJsonFragment(['id' => $model->public_id])
```

---

## Total Test Count

| Phase | File | Tests |
|---|---|---|
| 1 | AuthorizationTest | 8 |
| 2 | ProductsTest | 23 |
| 3 | CartTest + CustomerTest | 37 + 11 = 48 |
| 4 | PaymentTest | 33 |
| 5 | InvoiceTest | 22 |
| 6 | DashboardTest | 37 |
| 7 | InventoryTest | 14 |
| 8 | OrdersHistoryTest | 18 |
| 9 | ShopManagerFilamentTest | 17 |
| 10 | StockValidation + StockAdjustment + Refund + Review + Cleanup | 14 + 9 + 12 + 6 + 7 = 48 |
| **Total** | | **~268 tests** |
