# Phase 10: Missing Features & Schema Additions

## Objective
Cover all features visible in the screenshots that don't exist yet in the codebase and weren't fully addressed in previous phases. Also addresses cross-cutting concerns discovered during the deep audit.

---

## Audit Results

After cross-referencing every screenshot with the existing codebase:

| Feature | Screenshot | Current State |
|---|---|---|
| Product SKU | Product Detail ("SKU: WP-DRK-02-57") | No `sku` field exists |
| Product Barcode | Product Detail ("5391029384757") | No `barcode` field exists |
| Product Rating | Product Detail (4.6 stars) | No rating system |
| Product Reviews/Feedback | Product Detail ("58 verified comments") | No product review model |
| Product Badges | Product Detail ("TOP PERFORMER"), Products list ("TOP RATED") | No badge system |
| Stock Refill Logging | Activity Feed ("Stock Refill Logged +12 units") | No vendor-facing stock endpoint |
| Order Refunds (products) | Dashboard Risk tab ("9 Refund requests") | Only booking refunds exist |
| Product Active Toggle | Products list (toggle switch) | No `is_active` field |

---

## Cross-Cutting Concerns Discovered

| Concern | Finding | Impact |
|---|---|---|
| **Stock validation on checkout** | Stock is NOT checked before marking order as PAID | Risk of overselling — must add stock check |
| **Cart expiration** | No cleanup of stale WAITING_PAYMENT orders | Ghost carts accumulate forever |
| **Order notes** | No notes field on Order | Vendor can't add context ("gift wrapping", etc.) |
| **Pagination format** | Existing API uses Laravel standard (data/links/meta), NOT data/totalItems/totalPages | Phase 3, 6, 8 docs referenced wrong format — must align |
| **Currency handling** | Product currency falls back to Place currency then USD | Multi-currency carts are possible — need validation |
| **Pusher events** | Only chat events are broadcast, not order events | Real-time cart sync between tabs would be nice-to-have |
| **Order source** | `employee_id` tracks vendor orders, `ai_assistant_id` tracks chatbot | Already sufficient — no new field needed |
| **Rate limiting** | API has 60 req/min limit | Dashboard endpoints should be fine within this |
| **Audit trail** | Spatie Activity Log already on Order, Product, WalletTransaction | Stock adjustments will auto-log via `LogsActivity` trait |

---

## Tasks

### 10.1 SKU Field

**Migration:** Combined with 10.2 and 10.3 below.

Add to `ProductVariation` model:
- `$fillable`: add `sku`
- Unique constraint per place (composite: place_id via product + sku)

### 10.2 Barcode Field

Add to `ProductVariation` model:
- `$fillable`: add `barcode`
- Used by barcode scanner lookup endpoint (Phase 2)

### 10.3 Product Active Toggle

Add to `Product` model:
- `$fillable`: add `is_active`
- `$casts`: `'is_active' => 'boolean'`
- Scope: `scopeActive($query)` → `$query->where('is_active', true)`

### 10.4 Product Review/Feedback System

Powers the "Latest Feedback" section on product detail and the "Rating" in inventory snapshot.

**Model:** `app/Models/ProductReview.php`

**Migration:**
```php
Schema::create('product_reviews', function (Blueprint $table) {
    $table->id();
    $table->uuid('public_id')->unique();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->foreignId('order_id')->nullable()->constrained()->nullOnDelete();
    $table->tinyInteger('rating'); // 1-5
    $table->text('comment')->nullable();
    $table->boolean('is_verified_buyer')->default(false);
    $table->boolean('is_approved')->default(true);
    $table->timestamps();
    $table->softDeletes();
});
```

**Relations:**
- `Product` hasMany `ProductReview`
- `User` hasMany `ProductReview`
- `ProductReview` belongsTo `Product`, `User`, `Order`

**Computed on Product:**
- `average_rating` → Average of approved reviews' rating
- `reviews_count` → Count of approved reviews

**API Endpoints:**
- `GET /v1/places/{place}/shop/products/{product}/reviews` — List reviews (shop manager reads)
- `POST /v1/products/{product}/reviews` — Create review (customer endpoint, separate from shop manager)

**Filament:**
- `ReviewsRelationManager` on ProductResource

### 10.5 Product Badges (Computed)

No migration needed. Badges are computed from sales data:

```php
public function getBadgeAttribute(): ?string
{
    // TOP PERFORMER: Product is in top 10% by revenue this month
    // TOP RATED: Product has rating >= 4.5 with 10+ reviews
    // LOW STOCK: Stock below threshold
    // NEW: Created within last 7 days
}
```

Implement as a service method or model accessor, not a stored field.

### 10.6 Stock Adjustment/Refill Endpoint

**Controller:** `app/Http/Controllers/Api/Employee/ShopManager/AdjustStockController.php`

- `POST /v1/places/{place}/shop/products/{product}/stock`
- Body:
```json
{
  "variation_id": "variation_public_id",
  "quantity": 12,
  "type": "refill",
  "note": "Protein bars restocked"
}
```
- Creates a new `ProductVariationStock` entry
- Spatie Activity Log auto-records the change (trait already on ProductVariationStock)
- Activity feed reads from `activity_log` table

**Action:** `app/Actions/ShopManager/AdjustProductStockAction.php`

**StockAdjustmentType enum:**
```php
enum StockAdjustmentType: string
{
    case REFILL = 'refill';
    case CORRECTION = 'correction';
    case RETURN = 'return';
    case DAMAGED = 'damaged';
}
```

### 10.7 Order Refund Flow (Product Orders)

The Risk tab shows "Refund requests — After-payment support cases". Currently only booking refunds exist.

**Approach:** Keep it simple — **don't change OrderStatus**. Order stays `PAID`. Refund = wallet credit-back transaction, same pattern as existing booking refunds.

**Controller:** `app/Http/Controllers/Api/Employee/ShopManager/RefundOrderController.php`

- `POST /v1/places/{place}/shop/orders/{order}/refund`
- Body:
```json
{
  "items": [
    { "order_item_id": "public_id", "reason": "damaged" }
  ],
  "full_refund": false
}
```

**Action:** `app/Actions/ShopManager/RefundProductOrderAction.php`

Logic:
- Credit the customer's wallet for the refunded items' value
- Use `WalletPaymentType::REFUND` on the wallet transaction
- For STRIPE payments: also trigger Stripe refund via API
- For WALLET/POS/CASH/CRYPTO: wallet credit is sufficient
- Set `refunded_at` timestamp on order for dashboard tracking
- **Do NOT add new OrderStatus values** — order remains PAID

**Migration:** Add `refund_reason`, `refunded_at` to orders table. No need for refund_amount/currency since the wallet transaction already records that.

### 10.8 Stock Validation & Decrement

Currently stock is NOT validated or decremented when an order is paid.

**Constraint pipe:** `app/Http/Controllers/Api/Pipes/EnsureProductStockAvailable.php`

Register in `OrderItem::checkOrderableConstraints()` pipeline:
- **Only runs for Product orderables** (bookable services have no stock — no impact on existing flows)
- Checks `ProductVariationStock` has enough quantity
- If insufficient, throw `InsufficientStockException`

**Listener:** `app/Listeners/DecrementProductStockWhenOrderPaid.php`

- Listen to `OrderPaid` event
- For each OrderItem where orderable is Product:
  - Decrement stock quantity
  - **Soft failure:** if stock is already 0 (race condition), auto-refund the customer's wallet in credits for that item amount, log a warning. **Never break the listener chain.**
  - If stock crosses `low_stock_threshold`, dispatch `VendorLowStockNotification` (push)

Register in `EventServiceProvider`.

### 10.9 Cart Expiration Cleanup

Stale WAITING_PAYMENT orders accumulate indefinitely. Add a scheduled cleanup.

**Command:** `app/Console/Commands/CleanExpiredCartsCommand.php`

```php
// Signature: shop:clean-expired-carts
// Delete orders in WAITING_PAYMENT older than 24 hours (or configurable TTL)
// Only for vendor-created orders (employee_id IS NOT NULL)
// Log count of cleaned orders
```

**Register in Kernel.php:**
```php
$schedule->command('shop:clean-expired-carts')->dailyAt('03:00');
```

### 10.10 Order Notes Field

**Migration:** Add to orders table:
```php
$table->text('notes')->nullable();
```

Add to `Order` model `$fillable`.

Used by vendor to add context like "Gift wrapping requested", "Customer wants receipt emailed to spouse", etc.

**Update `CreateCartController`** and **`UpdateCartController`** to accept optional `notes` param.

### 10.11 Pagination Format Alignment

The existing API uses **Laravel standard pagination** (data/links/meta), not custom data/totalItems/totalPages.

**All shop manager list endpoints must follow the existing pattern:**
```json
{
  "data": [...],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "last_page": 5, "per_page": 15, "total": 72 }
}
```

Use `->paginate($perPage)->appends($request->query())` on all list endpoints.

> **Update Phase 3, 6, 8 docs** to reference this format instead of data/totalItems/totalPages.

### 10.12 Currency Validation on Cart

Products can have different currencies (product currency → place currency → USD fallback). A cart should not mix currencies.

**Validation in `UpdateCartController` / `AddCartItemController`:**
- When adding an item to a cart, check that its currency matches the cart's currency (determined by first item or by Place currency)
- If mismatch, return 422 with explicit error message

### 10.13 Dashboard Risk Section (Simplified)

Since we're NOT tracking failed payments (webhook flow handles this cleanly), the Risk tab in the dashboard should focus on what we DO have:

```json
{
  "risk": {
    "refund_requests": 9,
    "refund_change": -3,
    "refund_label": "After-payment support cases",
    "refund_sparkline": [12, 11, 10, 10, 9, 9, 9]
  }
}
```

Remove the "Failed payments / Declined" metric from the `checkout-snapshot` response. The mobile app can hide that card or show it as "0" if the field is absent.

---

## Notifications to Add

| Notification | Trigger | Channels | Template |
|---|---|---|---|
| `VendorLowStockNotification` | Stock drops below threshold (in `DecrementProductStockWhenOrderPaid`) | Expo push | "Low stock: {product} has {qty} units left" |
| `CustomerRefundNotification` | Order refunded by vendor | Sendinblue email | "Your order #{number} has been refunded" |
| `CustomerInvoiceNotification` | Invoice generated (Phase 5) | Sendinblue email | Invoice PDF attached |

---

## Combined Migration Strategy

Consolidate into 3 migrations:

**Migration 1:** `YYYY_MM_DD_HHMMSS_add_shop_manager_product_fields.php`
- `products.is_active` (boolean, default true)
- `product_variations.sku` (string, nullable, indexed)
- `product_variations.barcode` (string, nullable, indexed)
- `product_variations.low_stock_threshold` (unsigned int, default 5)

**Migration 2:** `YYYY_MM_DD_HHMMSS_add_shop_manager_order_fields.php`
- `orders.payment_details` (json, nullable)
- `orders.notes` (text, nullable)
- `orders.refund_reason` (string, nullable)
- `orders.refunded_at` (timestamp, nullable)

**Migration 3:** `YYYY_MM_DD_HHMMSS_create_shop_manager_tables.php`
- `invoices` table (Phase 5)
- `product_reviews` table

---

## Files Created/Modified

| Action | File |
|---|---|
| Create | `database/migrations/..._add_shop_manager_product_fields.php` |
| Create | `database/migrations/..._add_shop_manager_order_fields.php` |
| Create | `database/migrations/..._create_shop_manager_tables.php` |
| Create | `app/Models/ProductReview.php` |
| Modify | `app/Models/Product.php` (is_active, reviews relation, rating accessor, badge accessor, scopeActive) |
| Modify | `app/Models/ProductVariation.php` (sku, barcode, low_stock_threshold, reorder status) |
| Modify | `app/Models/Order.php` (payment_details, notes, refund_reason, refunded_at) |
| Create | `app/Enums/StockAdjustmentType.php` |
| Create | `app/Http/Controllers/Api/Employee/ShopManager/AdjustStockController.php` |
| Create | `app/Http/Controllers/Api/Employee/ShopManager/RefundOrderController.php` |
| Create | `app/Actions/ShopManager/AdjustProductStockAction.php` |
| Create | `app/Actions/ShopManager/RefundProductOrderAction.php` |
| Create | `app/Http/Controllers/Api/Pipes/EnsureProductStockAvailable.php` |
| Create | `app/Listeners/DecrementProductStockWhenOrderPaid.php` |
| Create | `app/Console/Commands/CleanExpiredCartsCommand.php` |
| Create | `app/Notifications/VendorLowStockNotification.php` |
| Create | `app/Notifications/CustomerRefundNotification.php` |
| Create | `app/Filament/Resources/ProductResource/RelationManagers/ReviewsRelationManager.php` |
| Modify | `app/Providers/EventServiceProvider.php` (register stock decrement listener) |
| Modify | `app/Console/Kernel.php` (register cart cleanup command) |
| Modify | `routes/api.php` |
| Create | `tests/Feature/Controllers/ShopManager/StockAdjustmentTest.php` |
| Create | `tests/Feature/Controllers/ShopManager/RefundTest.php` |
| Create | `tests/Feature/Controllers/ShopManager/StockValidationTest.php` |
| Create | `tests/Feature/Commands/CleanExpiredCartsTest.php` |

---

## Acceptance Criteria

- [ ] SKU and barcode fields exist and are searchable
- [ ] Product active toggle works in both API and Filament
- [ ] Product reviews can be listed with average rating
- [ ] Badges are computed correctly from sales/review data
- [ ] Stock can be adjusted from vendor app
- [ ] Stock is validated before checkout (Products only, not bookable services)
- [ ] Stock is decremented after payment (soft failure: wallet credit-back if race condition)
- [ ] Low stock push notification fires when threshold is crossed
- [ ] Refunds credit the customer's wallet (order stays PAID, no OrderStatus change)
- [ ] Stripe refunds also trigger via Stripe API
- [ ] Stale carts are cleaned up daily
- [ ] Order notes are saved and returned
- [ ] Pagination format matches existing Laravel standard
- [ ] Cart rejects mixed-currency items
- [ ] Risk dashboard shows refund data only (no failed payments)
- [ ] All new fields appear in Filament resources
- [ ] All tests pass
- [ ] `./gitCheck.sh` passes
