# AI Planning Files – E-commerce Module (v4)

> **Project:** Laravel E-commerce Module
> **Stack:** Laravel, Eloquent ORM, Sanctum, Blade / API-ready
> **Scope:** Digital, Physical, and Hybrid Products
> **Version:** 4.0 — Stock Management System added

### Changelog
| Version | Area | Change |
|---------|------|--------|
| v2 | Database | `payments`, `stock_quantity`, file redundancy fix |
| v2 | Rules | Digital signed URLs, cart quantity enforcement |
| v3 | Database | `discount_type`, `discount_value`, `discount_expires_at` |
| v3 | Model | `hasActiveDiscount()`, `getDiscountAmount()`, `digitalFinalPrice()`, `physicalFinalPrice()` |
| **v4** | **Database** | **`product_stock_logs` table added — every stock movement is recorded** |
| **v4** | **Database** | **`low_stock_threshold` added to `products`** |
| **v4** | **Model** | **`currentStock()`, `totalReceived()`, `totalSold()`, `totalReturned()`, `stockStatus()`, `stockLabel()`, `isAvailableToOrder()` added to Product** |
| **v4** | **Rules** | **Full Stock Management rules section added** |
| **v4** | **Tasks** | **Task 11 (Stock Management Admin Screen) added** |
| **v4** | **Tasks** | **Task 02 updated — all new model methods documented** |
| **v4** | **Tasks** | **Task 06 updated — checkout writes to stock log instead of decrementing raw column** |

---

## Table of Contents

1. [rules.md](#1-rulesmd)
2. [database.md](#2-databasemd)
3. [relations.md](#3-relationsmd)
4. [tasks/](#4-tasks)

---

# 1. rules.md

```md
# System Rules
1- make all fomes spured to two lang "ar,en"
2- add lang files for every form 
3- in vew add two inputs to insert two lang "en,ar"
4- in forms use validtion server side use "proengsoft/laravel-jsvalidation"
## General Rules
- System supports digital, physical, and hybrid products.
- Each product belongs to exactly ONE category.
- Categories support unlimited nesting via `parent_id` (tree structure).
- All business logic must be encapsulated in Service classes.
- Controllers are thin: validate → call service → return response.
- All APIs must be stateless and mobile-app ready (consumed by Sanctum tokens).

---

## Product Rules
- A product has two independent types: **Digital** and **Physical**.
- Each type has its own independent price:
  - `digital_price`  → nullable (null = no digital variant)
  - `physical_price` → nullable (null = no physical variant)
- At least one price must be non-null for a product to be activatable.
- Digital products must have at least one file in `product_files` before activation.
- Digital product files are stored in **private storage only** — never public disk.
- A product is visible to users only when `is_active = true`.
- File paths must NEVER be exposed directly to the client.

---

## Stock Management Rules

### Core Concept
- `stock_quantity` on the `products` table is **NOT the source of truth** for current stock.
- It only stores the **initial quantity** set when the product was first created.
- The **real-time current stock** is always calculated by the model from the `product_stock_logs` table.
- This design creates a full, auditable history of every stock movement.

### Stock Log Table
- Every stock change is recorded as a row in `product_stock_logs`.
- Each log entry has a `movement_type`:
  | Type | When it is created | Effect on stock |
  |------|--------------------|-----------------|
  | `initial` | Admin creates product with opening stock | + (adds) |
  | `restock` | Admin adds new stock via Stock Management screen | + (adds) |
  | `sale` | Order is placed (checkout) | − (subtracts) |
  | `return` | Admin manually reverses a sale | + (adds) |
  | `adjustment` | Admin corrects stock (e.g. damaged goods) | + or − |
- The log also stores:
  - `quantity` → always a positive integer (the direction is implied by `movement_type`)
  - `note` → nullable text, e.g. "Received from supplier", "Order #45 returned"
  - `order_id` → nullable FK, only set for `sale` and `return` movements
  - `admin_id` → nullable FK to the user (admin) who created the log entry
  - `created_at` → timestamp of the movement

### Stock Calculation (Model Methods)
The `Product` model always calculates stock from the log — never from a raw column:

```
currentStock()  = totalReceived() - totalSold() + totalReturned()
```

Where:
- `totalReceived()` = sum of quantity where `movement_type IN ('initial', 'restock', 'adjustment')`
  filtered to only rows where `quantity` contributes positively
- `totalSold()`     = sum of quantity where `movement_type = 'sale'`
- `totalReturned()` = sum of quantity where `movement_type = 'return'`
- `currentStock()`  = the exact available quantity right now

> Note: `adjustment` entries can be negative (e.g. damaged goods written off).
> They are stored with a `quantity` value and a `direction` field (`+` or `−`).

### Stock Status for Users (Display Rules)
- The raw `currentStock()` number is **never shown to regular users**.
- Users see a computed label from `stockLabel()`:
  | `stockStatus()` | Condition | Label shown |
  |-----------------|-----------|-------------|
  | `unlimited` | `stock_quantity IS NULL` (no stock tracking) | "In Stock" |
  | `in_stock` | `currentStock() > low_stock_threshold` | "In Stock" |
  | `low_stock` | `currentStock() > 0` AND `<= low_stock_threshold` | "Only X left!" |
  | `out_of_stock` | `currentStock() <= 0` | "Out of Stock" |
- `low_stock_threshold` is set per product by admin (default: 5).
- `isAvailableToOrder()` returns true if `stockStatus() != 'out_of_stock'` OR stock tracking is off.

### Stock Admin Screen Rules
- Admin can view the full movement history per product (sorted newest first).
- Admin can add new stock (restock) with a required `quantity` and optional `note`.
- Admin can add a manual adjustment with a `quantity`, `direction` (+/−), and required `note`.
- Admin can record a return by selecting an order that contained the product.
- Admin **cannot** delete or edit existing log entries (immutable audit trail).
- The screen always shows: Total Received, Total Sold, Total Returned, **Current Stock** (computed).

### Checkout & Sale Rules
- When an order is placed, `StockService::recordSale()` is called per physical order item.
- It creates a `product_stock_logs` entry with `movement_type = 'sale'`, `order_id` set.
- Before recording, it calls `isAvailableToOrder()` — abort the order if stock is 0.
- The raw `products.stock_quantity` column is NEVER decremented after v4.
  It remains as the initial stock reference only.

---

## Discount Rules

### Discount Structure
- Each product can have ONE active discount at a time.
- `discount_type` → enum: `percentage` | `fixed` | null
- `discount_value` → decimal, the raw value
- `discount_expires_at` → nullable timestamp
- Active when: type not null + value > 0 + not expired

### Discount Calculation (Model Methods)
- `hasActiveDiscount(): bool`
- `getDiscountAmount(string $type): float`
- `digitalFinalPrice(): ?float`
- `physicalFinalPrice(): ?float`

### Cart Snapshot
Each `cart_item` stores: `original_price`, `discount_amount`, `price` (final).

---

## Cart Rules
- Each cart item stores: `product_id`, `type`, `original_price`, `discount_amount`, `price`, `quantity`.
- `quantity` is always 1 for digital (enforced in CartService).
- Adding digital item that already exists → reject.
- Adding physical item → check `isAvailableToOrder()` before allowing.

---

## Payment Rules
- `order_status` and `payment_status` are independent lifecycles.
- `payment_status`: `unpaid` | `paid` | `failed` | `refunded`
- `payment_method`: `cash_on_delivery` | `credit_card` | `online`
- Digital access granted only when `order_status = completed` AND `payment_status = paid`.

---

## Order Rules
- Order status (one-way): `pending → preparing → on_the_way → delivered → completed`
- `total_price` is frozen at checkout.
- Stock is logged (not decremented) at checkout time.
- Any physical item with `currentStock() = 0` aborts the entire order (atomic).

---

## User & Address Rules
- Multiple addresses per user; exactly one `is_default`.
- Address is frozen on order (not affected by later address changes).

---

## Digital Access Rules
- Granted only when `order_status = completed` AND `payment_status = paid`.
- Served via signed temporary URLs — never raw file paths.

---

## Scalability & Security Rules
- PHP Enums for all typed columns.
- Private files under `storage/app/private/` only.
- Raw `currentStock()` number never exposed to users — only `stock_label` and `stock_status`.
- Stock log is append-only — no updates or deletes allowed.
```

---

# 2. database.md

```md
# Database Structure

---

## `categories`
| Column      | Type      | Notes                               |
|-------------|-----------|-------------------------------------|
| id          | bigint PK |                                     |
| name        | string    | Required                            |
| description | text      | Nullable                            |
| image       | string    | Nullable — private storage path     |
| parent_id   | bigint FK | Nullable → references categories.id |
| created_at  | timestamp |                                     |
| updated_at  | timestamp |                                     |

---

## `products`
| Column              | Type                    | Notes                                                          |
|---------------------|-------------------------|----------------------------------------------------------------|
| id                  | bigint PK               |                                                                |
| name                | string                  | Required                                                       |
| description         | text                    | Nullable                                                       |
| main_image          | string                  | Nullable — private storage path                                |
| digital_price       | decimal(10,2)           | Nullable — null = no digital variant                           |
| physical_price      | decimal(10,2)           | Nullable — null = no physical variant                          |
| discount_type       | enum: percentage, fixed | Nullable                                                       |
| discount_value      | decimal(10,2)           | Nullable                                                       |
| discount_expires_at | timestamp               | Nullable                                                       |
| stock_quantity      | unsignedInt             | Nullable — null = no stock tracking (unlimited); otherwise = opening/initial stock |
| low_stock_threshold | unsignedInt             | Default: 5 — triggers "Only X left!" label                     |
| category_id         | bigint FK               | References categories.id                                       |
| is_active           | boolean                 | Default: false                                                 |
| created_at          | timestamp               |                                                                |
| updated_at          | timestamp               |                                                                |

> ⚠️ `stock_quantity` = the INITIAL opening stock only. Current stock is always calculated from `product_stock_logs`.
> ⚠️ If `stock_quantity IS NULL`, no stock tracking is done (digital products, unlimited items).

---

## `product_stock_logs`   ← NEW in v4

| Column        | Type                                          | Notes                                                         |
|---------------|-----------------------------------------------|---------------------------------------------------------------|
| id            | bigint PK                                     |                                                               |
| product_id    | bigint FK                                     | References products.id                                        |
| movement_type | enum: initial, restock, sale, return, adjustment | Required                                                   |
| quantity      | unsignedInt                                   | Always positive — direction is encoded in `movement_type` or `direction` |
| direction     | enum: +, −                                    | Default `+`; used only for `adjustment` type                  |
| note          | text                                          | Nullable — required for `adjustment`, optional for others     |
| order_id      | bigint FK                                     | Nullable — references orders.id; set only for `sale` / `return` |
| admin_id      | bigint FK                                     | Nullable — references users.id; the admin who made the entry  |
| created_at    | timestamp                                     | Immutable — no `updated_at` (log is append-only)              |

> 🔒 No updates or deletes allowed on this table. Append-only audit log.
> ℹ️ Movement contribution to stock:
>    - `initial`, `restock` → always +quantity
>    - `return` → always +quantity
>    - `sale` → always −quantity
>    - `adjustment` with `direction = +` → +quantity
>    - `adjustment` with `direction = −` → −quantity

---

## `product_files`
| Column     | Type           | Notes                                                       |
|------------|----------------|-------------------------------------------------------------|
| id         | bigint PK      |                                                             |
| product_id | bigint FK      | References products.id                                      |
| file_name  | string         | Display name (e.g. "Chapter 1.pdf")                         |
| file_path  | string         | Private storage — never exposed to client                   |
| file_size  | unsignedBigInt | Nullable — bytes                                            |
| created_at | timestamp      |                                                             |

---

## `users`
| Column     | Type      | Notes       |
|------------|-----------|-------------|
| id         | bigint PK |             |
| name       | string    |             |
| email      | string    | Unique      |
| password   | string    | Hashed      |
| created_at | timestamp |             |
| updated_at | timestamp |             |

---

## `addresses`
| Column       | Type      | Notes                            |
|--------------|-----------|----------------------------------|
| id           | bigint PK |                                  |
| user_id      | bigint FK | References users.id              |
| title        | string    | e.g. "Home", "Work"              |
| address_line | string    |                                  |
| city         | string    |                                  |
| notes        | text      | Nullable                         |
| is_default   | boolean   | Default: false                   |
| created_at   | timestamp |                                  |
| updated_at   | timestamp |                                  |

---

## `carts`
| Column     | Type      | Notes               |
|------------|-----------|---------------------|
| id         | bigint PK |                     |
| user_id    | bigint FK | References users.id |
| created_at | timestamp |                     |
| updated_at | timestamp |                     |

---

## `cart_items`
| Column          | Type                    | Notes                                              |
|-----------------|-------------------------|----------------------------------------------------|
| id              | bigint PK               |                                                    |
| cart_id         | bigint FK               | References carts.id                                |
| product_id      | bigint FK               | References products.id                             |
| type            | enum: digital, physical | Required                                           |
| original_price  | decimal(10,2)           | Base price before discount                         |
| discount_amount | decimal(10,2)           | Flat discount applied at add-time                  |
| price           | decimal(10,2)           | Final price = original_price − discount_amount     |
| quantity        | unsignedInt             | Always 1 for digital; >= 1 for physical            |
| created_at      | timestamp               |                                                    |
| updated_at      | timestamp               |                                                    |

> 🔒 Unique constraint: `(cart_id, product_id, type)`

---

## `orders`
| Column         | Type                                                       | Notes                    |
|----------------|------------------------------------------------------------|--------------------------|
| id             | bigint PK                                                  |                          |
| user_id        | bigint FK                                                  |                          |
| address_id     | bigint FK                                                  |                          |
| order_status   | enum: pending, preparing, on_the_way, delivered, completed | Default: pending         |
| payment_status | enum: unpaid, paid, failed, refunded                       | Default: unpaid          |
| payment_method | enum: cash_on_delivery, credit_card, online                |                          |
| total_price    | decimal(10,2)                                              | Frozen at checkout       |
| notes          | text                                                       | Nullable                 |
| created_at     | timestamp                                                  |                          |
| updated_at     | timestamp                                                  |                          |

---

## `order_items`
| Column          | Type                    | Notes                                               |
|-----------------|-------------------------|-----------------------------------------------------|
| id              | bigint PK               |                                                     |
| order_id        | bigint FK               | References orders.id                                |
| product_id      | bigint FK               | References products.id                              |
| type            | enum: digital, physical |                                                     |
| original_price  | decimal(10,2)           | Snapshot                                            |
| discount_amount | decimal(10,2)           | Snapshot                                            |
| price           | decimal(10,2)           | Final unit price                                    |
| quantity        | unsignedInt             |                                                     |

---

## `digital_access`
| Column     | Type      | Notes                           |
|------------|-----------|---------------------------------|
| id         | bigint PK |                                 |
| user_id    | bigint FK |                                 |
| product_id | bigint FK |                                 |
| order_id   | bigint FK |                                 |
| granted_at | timestamp |                                 |

> 🔒 Unique constraint: `(user_id, product_id, order_id)`
```

---

# 3. relations.md

```md
# Eloquent Model Relationships

---

## Category
```php
public function parent(): BelongsTo
public function children(): HasMany
public function products(): HasMany
```

---

## Product
```php
public function category(): BelongsTo
public function files(): HasMany          // → ProductFile
public function orderItems(): HasMany     // → OrderItem
public function stockLogs(): HasMany      // → ProductStockLog  ← NEW

// ══════════════════════════════════════════════════════════════════
// STOCK CALCULATION METHODS (v4)
// ══════════════════════════════════════════════════════════════════

/**
 * Returns the total quantity ever received into stock.
 * Includes: initial stock + all restocks + positive adjustments.
 * Excludes: negative adjustments.
 */
public function totalReceived(): int
{
    return (int) $this->stockLogs()
        ->where(function ($q) {
            $q->whereIn('movement_type', ['initial', 'restock'])
              ->orWhere(function ($q2) {
                  $q2->where('movement_type', 'adjustment')
                     ->where('direction', '+');
              });
        })
        ->sum('quantity');
}

/**
 * Returns the total quantity sold across all completed order items.
 * Only counts movement_type = 'sale'.
 */
public function totalSold(): int
{
    return (int) $this->stockLogs()
        ->where('movement_type', 'sale')
        ->sum('quantity');
}

/**
 * Returns the total quantity returned (reversed sales).
 * Only counts movement_type = 'return'.
 */
public function totalReturned(): int
{
    return (int) $this->stockLogs()
        ->where('movement_type', 'return')
        ->sum('quantity');
}

/**
 * Returns the total quantity written off via negative adjustments.
 * (e.g. damaged goods, corrections)
 */
public function totalAdjustedOut(): int
{
    return (int) $this->stockLogs()
        ->where('movement_type', 'adjustment')
        ->where('direction', '-')
        ->sum('quantity');
}

/**
 * THE MAIN METHOD — Calculates exact current available stock.
 *
 * Formula:
 *   currentStock = totalReceived - totalSold + totalReturned - totalAdjustedOut
 *
 * Returns null if stock tracking is disabled (stock_quantity IS NULL).
 * Returns 0 minimum — never goes negative.
 */
public function currentStock(): ?int
{
    if ($this->stock_quantity === null) {
        return null; // stock tracking is off → unlimited
    }

    $stock = $this->totalReceived()
           - $this->totalSold()
           + $this->totalReturned()
           - $this->totalAdjustedOut();

    return max(0, $stock);
}

// ══════════════════════════════════════════════════════════════════
// STOCK STATUS & DISPLAY METHODS
// ══════════════════════════════════════════════════════════════════

/**
 * Returns a machine-readable stock status string.
 * Used internally and in API resources.
 *
 * @return string 'unlimited' | 'in_stock' | 'low_stock' | 'out_of_stock'
 */
public function stockStatus(): string
{
    $current = $this->currentStock();

    if ($current === null) return 'unlimited';
    if ($current === 0)    return 'out_of_stock';
    if ($current <= $this->low_stock_threshold) return 'low_stock';
    return 'in_stock';
}

/**
 * Returns a human-readable stock label for display on the site.
 * Raw stock count is NEVER exposed to users directly.
 *
 * @return string e.g. "In Stock", "Only 3 left!", "Out of Stock"
 */
public function stockLabel(): string
{
    return match ($this->stockStatus()) {
        'unlimited'     => 'In Stock',
        'in_stock'      => 'In Stock',
        'low_stock'     => 'Only ' . $this->currentStock() . ' left!',
        'out_of_stock'  => 'Out of Stock',
    };
}

/**
 * Returns true if the product can currently be added to cart and ordered.
 * Always true for digital products (no stock concept).
 * Always true for unlimited physical products.
 */
public function isAvailableToOrder(): bool
{
    return $this->stockStatus() !== 'out_of_stock';
}

// ══════════════════════════════════════════════════════════════════
// DISCOUNT METHODS (v3)
// ══════════════════════════════════════════════════════════════════

public function hasActiveDiscount(): bool
{
    return $this->discount_type !== null
        && $this->discount_value > 0
        && ($this->discount_expires_at === null || $this->discount_expires_at->isFuture());
}

public function getDiscountAmount(string $type): float
{
    $base = $type === 'digital' ? $this->digital_price : $this->physical_price;
    if (! $this->hasActiveDiscount() || $base === null) return 0.0;

    $amount = match ($this->discount_type->value) {
        'percentage' => round($base * ($this->discount_value / 100), 2),
        'fixed'      => min($this->discount_value, $base),
    };
    return max(0.0, $amount);
}

public function digitalFinalPrice(): ?float
{
    if ($this->digital_price === null) return null;
    return round($this->digital_price - $this->getDiscountAmount('digital'), 2);
}

public function physicalFinalPrice(): ?float
{
    if ($this->physical_price === null) return null;
    return round($this->physical_price - $this->getDiscountAmount('physical'), 2);
}
```

---

## ProductStockLog   ← NEW
```php
public function product(): BelongsTo      // → Product
public function order(): BelongsTo        // → Order (nullable)
public function admin(): BelongsTo        // → User (nullable — the admin who created entry)
```

---

## ProductFile
```php
public function product(): BelongsTo
```

---

## User
```php
public function addresses(): HasMany
public function cart(): HasOne
public function orders(): HasMany
public function digitalAccess(): HasMany
public function stockLogEntries(): HasMany  // → ProductStockLog (as admin)
```

---

## Cart
```php
public function user(): BelongsTo
public function items(): HasMany
```

## CartItem
```php
public function cart(): BelongsTo
public function product(): BelongsTo
```

## Order
```php
public function user(): BelongsTo
public function address(): BelongsTo
public function items(): HasMany
public function digitalAccesses(): HasMany
public function stockLogs(): HasMany        // → ProductStockLog (via order_id)
```

## OrderItem
```php
public function order(): BelongsTo
public function product(): BelongsTo
```

## DigitalAccess
```php
public function user(): BelongsTo
public function product(): BelongsTo
public function order(): BelongsTo
```
```

---

# 4. tasks/

---

## Task 01 – Setup Database Migrations

```md
# Task 01: Setup Database Migrations

## Goal
Create all migration files in dependency order, including the new `product_stock_logs` table.

## Tables (migration order)
1. categories
2. products (with stock_quantity, low_stock_threshold, discount fields)
3. product_files
4. product_stock_logs  ← NEW
5. addresses
6. carts
7. cart_items (with original_price, discount_amount, price)
8. orders
9. order_items (with original_price, discount_amount, price)
10. digital_access

## Key Constraints
- `cart_items` → unique(`cart_id`, `product_id`, `type`)
- `digital_access` → unique(`user_id`, `product_id`, `order_id`)
- `product_stock_logs.movement_type` → enum(`initial`, `restock`, `sale`, `return`, `adjustment`)
- `product_stock_logs.direction` → enum(`+`, `−`), default `+`
- `product_stock_logs` → no `updated_at` column (append-only)
- `products.low_stock_threshold` → unsignedInt, default 5

## Files
- `database/migrations/xxxx_create_categories_table.php`
- `database/migrations/xxxx_create_products_table.php`
- `database/migrations/xxxx_create_product_files_table.php`
- `database/migrations/xxxx_create_product_stock_logs_table.php`
- `database/migrations/xxxx_create_addresses_table.php`
- `database/migrations/xxxx_create_carts_table.php`
- `database/migrations/xxxx_create_cart_items_table.php`
- `database/migrations/xxxx_create_orders_table.php`
- `database/migrations/xxxx_create_order_items_table.php`
- `database/migrations/xxxx_create_digital_access_table.php`

## Steps
1. Create all migrations in order above
2. In `product_stock_logs`: use `$table->timestamp('created_at')` only — no `updated_at`
3. Add `direction` column as enum(`+`, `−`) with default `+`
4. Add `note` as nullable text; `order_id` and `admin_id` as nullable FKs
5. Run `php artisan migrate` and verify
```

---

## Task 02 – Models, Enums & Relationships

```md
# Task 02: Models, Enums & Relationships

## Goal
Create all models and enums. Implement all stock calculation and display methods on Product.
Create the ProductStockLog model.

## Enums (app/Enums/)
- `ProductType`      → `Digital`, `Physical`
- `DiscountType`     → `Percentage`, `Fixed`
- `StockMovement`    → `Initial`, `Restock`, `Sale`, `Return`, `Adjustment`
- `StockDirection`   → `In` (value: '+'), `Out` (value: '-')
- `OrderStatus`      → `Pending`, `Preparing`, `OnTheWay`, `Delivered`, `Completed`
- `PaymentStatus`    → `Unpaid`, `Paid`, `Failed`, `Refunded`
- `PaymentMethod`    → `CashOnDelivery`, `CreditCard`, `Online`

## Files
- `app/Enums/StockMovement.php`   ← NEW
- `app/Enums/StockDirection.php`  ← NEW
- `app/Models/Ecommerce/ProductStockLog.php` ← NEW
- `app/Models/Ecommerce/Product.php` ← primary focus
- `app/Models/Ecommerce/*.php` (all others)

## Steps
1. Create all Enums with backed `string` type
2. Create `ProductStockLog` model:
   - `$fillable`: product_id, movement_type, quantity, direction, note, order_id, admin_id
   - `$casts`: movement_type → StockMovement, direction → StockDirection
   - No `updated_at` → set `public $timestamps = false;` and manually manage `created_at`
   - Relationships: `product()`, `order()`, `admin()`
3. Update `Product` model:
   - `$casts`: discount_type → DiscountType, discount_expires_at → datetime
   - Implement ALL stock methods per relations.md:
     - `stockLogs(): HasMany`
     - `totalReceived(): int`
     - `totalSold(): int`
     - `totalReturned(): int`
     - `totalAdjustedOut(): int`
     - `currentStock(): ?int`
     - `stockStatus(): string`
     - `stockLabel(): string`
     - `isAvailableToOrder(): bool`
   - Implement all discount methods per relations.md
   - Add scopes:
     - `scopeActive()` → `where('is_active', true)`
     - `scopeInStock()` → products where `isAvailableToOrder()` would be true
     - `scopeTracksStock()` → `whereNotNull('stock_quantity')`
4. Verify in `php artisan tinker`:
   - Create a product, add `initial` stock log of 100
   - Add `sale` log of 30 → confirm `currentStock()` = 70
   - Add `return` log of 5 → confirm `currentStock()` = 75
   - Add `adjustment` direction `−` of 10 → confirm `currentStock()` = 65
   - Set `low_stock_threshold = 70` → confirm `stockStatus() = 'low_stock'`
   - Confirm `stockLabel()` = "Only 65 left!"
```

---

## Task 03 – Category Tree

```md
# Task 03: Category Tree

## Goal
Recursive category system with admin CRUD and public API.

## Files
- `app/Services/Ecommerce/CategoryService.php`
- `app/Http/Controllers/Admin/Ecommerce/CategoryController.php`
- `app/Http/Controllers/Api/CategoryController.php`
- `app/Http/Requests/Ecommerce/StoreCategoryRequest.php`
- `app/Http/Resources/Ecommerce/CategoryResource.php`
- `resources/views/dashbord/admin/ecommerce/categories/`

## Steps
1. `CategoryService`: `getTree()`, `getFlat()`, `store()`, `update()`, `delete()`
2. `delete()` → reject if has products or children
3. Admin + API controllers + views
4. Register routes
```

---

## Task 04 – Product CRUD

```md
# Task 04: Product CRUD

## Goal
Full product management. On create: if stock_quantity is set, write the first
`product_stock_logs` entry with `movement_type = 'initial'` automatically.

## Database
- `products`, `product_files`, `product_stock_logs`

## Files
- `app/Services/Ecommerce/ProductService.php`
- `app/Services/Ecommerce/FileStorageService.php`
- `app/Services/Ecommerce/StockService.php`  ← NEW (see Task 11)
- `app/Http/Controllers/Admin/Ecommerce/ProductController.php`
- `app/Http/Controllers/Api/ProductController.php`
- `app/Http/Requests/Ecommerce/StoreProductRequest.php`
- `app/Http/Requests/Ecommerce/UpdateProductRequest.php`
- `app/Http/Resources/Ecommerce/ProductResource.php`
- `app/Http/Resources/Ecommerce/Admin/AdminProductResource.php`  ← NEW
- `resources/views/dashbord/admin/ecommerce/products/`

## ProductResource (for users) — fields
```json
{
  "id": 1,
  "name": "...",
  "digital_price": 50.00,
  "physical_price": 80.00,
  "has_active_discount": true,
  "digital_final_price": 40.00,
  "physical_final_price": 64.00,
  "stock_status": "low_stock",
  "stock_label": "Only 3 left!",
  "is_available_to_order": true,
  "category": {},
  "files": [{ "id": 1, "file_name": "...", "file_size": 204800 }]
}
```
> Raw `stock_quantity` and `currentStock()` number NEVER appear in user-facing resource.

## AdminProductResource (for admin) — additional fields
```json
{
  "stock_quantity": 100,
  "current_stock": 3,
  "total_received": 100,
  "total_sold": 97,
  "total_returned": 0,
  "total_adjusted_out": 0,
  "low_stock_threshold": 5,
  "stock_status": "low_stock"
}
```

## Steps
1. `ProductService::store(array $data, array $files)`:
   - Create product
   - If `stock_quantity` is provided → call `StockService::recordInitial($product, $qty, $admin)`
   - Attach files via `FileStorageService`
2. `ProductService::update()` → update fields, sync files; do NOT touch stock here
   (stock changes go through Task 11 screen only)
3. `ProductResource` → call model methods: `stockStatus()`, `stockLabel()`, `isAvailableToOrder()`
4. `AdminProductResource` → also call: `currentStock()`, `totalReceived()`, `totalSold()`, `totalReturned()`, `totalAdjustedOut()`
5. API `index()` supports filter `?on_discount=true` and `?stock_status=low_stock|out_of_stock`
6. Register routes
```

---

## Task 05 – Cart System

```md
# Task 05: Cart System

## Goal
Manage cart. Check `isAvailableToOrder()` before adding physical items.
Snapshot prices with discount.

## Files
- `app/Services/Ecommerce/CartService.php`
- `app/Http/Controllers/Api/CartController.php`
- `app/Http/Resources/Ecommerce/CartResource.php`
- `app/Http/Resources/Ecommerce/CartItemResource.php`
- `resources/views/cart/index.blade.php`

## Steps
1. `CartService::addItem()`:
   - Validate product active + type has price
   - For physical: call `product->isAvailableToOrder()` → reject if false
   - Snapshot `original_price`, `discount_amount`, `price`
   - Enforce `quantity = 1` for digital
2. `CartService::updateQuantity()`:
   - Reject for digital with ValidationException
   - Check `isAvailableToOrder()` for new qty
3. `CartItemResource`: expose `original_price`, `discount_amount`, `price`, `subtotal`, `stock_label`
4. Register routes under `auth` middleware
```

---

## Task 06 – Checkout & Payment

```md
# Task 06: Checkout & Payment

## Goal
Place order. For each physical item, call `StockService::recordSale()` instead of
decrementing `products.stock_quantity` directly.

## Files
- `app/Services/Ecommerce/CheckoutService.php`
- `app/Services/Ecommerce/StockService.php`  ← used here
- `app/Http/Controllers/Api/CheckoutController.php`
- `app/Http/Requests/Ecommerce/CheckoutRequest.php`
- `app/Http/Resources/Ecommerce/OrderResource.php`
- `resources/views/checkout/index.blade.php`

## Steps
1. `CheckoutService::placeOrder()` inside `DB::transaction()`:
   - Step 1: Load cart items
   - Step 2: For each physical item → call `product->isAvailableToOrder()` → abort if false
   - Step 3: Calculate total
   - Step 4: Create Order
   - Step 5: Create OrderItems (snapshot all price fields)
   - Step 6: For each physical OrderItem → call `StockService::recordSale($product, $qty, $order)`
     - This writes: `movement_type = 'sale'`, `quantity`, `order_id` to `product_stock_logs`
     - Does NOT touch `products.stock_quantity`
   - Step 7: Clear cart
   - Step 8: Fire `OrderPlaced` event
2. Register routes
```

---

## Task 07 – Orders Management

```md
# Task 07: Orders Management

## Goal
Admin updates order + payment status. User views own orders.
When admin records a return, StockService::recordReturn() is called.

## Files
- `app/Http/Controllers/Admin/Ecommerce/OrderController.php`
- `app/Http/Controllers/Api/OrderController.php`
- `app/Events/OrderStatusUpdated.php`
- `app/Policies/OrderPolicy.php`
- `app/Http/Resources/Ecommerce/OrderResource.php`
- `resources/views/dashbord/admin/ecommerce/orders/`
- `resources/views/orders/`

## Steps
1. Admin `updateOrderStatus()` → validate forward-only, dispatch event
2. Admin `updatePaymentStatus()` → update, trigger digital access check
3. `OrderPolicy` → user views own; admin updates
4. Build views, register routes
```

---

## Task 08 – Digital Library

```md
# Task 08: Digital Library

## Goal
Auto-grant digital access. Serve files via signed URLs only.

## Files
- `app/Services/Ecommerce/DigitalAccessService.php`
- `app/Listeners/GrantDigitalAccess.php`
- `app/Http/Controllers/Api/DigitalLibraryController.php`
- `app/Policies/DigitalAccessPolicy.php`
- `resources/views/library/index.blade.php`

## Steps
1. `DigitalAccessService`: `shouldGrant()`, `grantForOrder()`, `getLibrary()`, `generateDownloadUrl()`
2. `GrantDigitalAccess` listener → fires on `OrderStatusUpdated`
3. `DigitalLibraryController`: `index()`, `download()` (policy-guarded)
4. Never return `file_path` raw
5. Register routes under `auth`
```

---

## Task 09 – API Authentication (Sanctum)

```md
# Task 09: API Authentication

## Files
- `app/Http/Controllers/Api/AuthController.php`
- All API Resources
- `routes/api.php`

## Steps
1. `php artisan install:api`
2. `AuthController`: `register()`, `login()`, `logout()`, `me()`
3. Resources: strict whitelisting — no `file_path`, no raw stock numbers in user resources
4. Routes: public (register/login) + `auth:sanctum` (everything else)
```

---

## Task 10 – Discount Feature

```md
# Task 10: Discount Management

## Files
- `app/Http/Controllers/Admin/Ecommerce/DiscountController.php`
- `app/Http/Requests/Ecommerce/SetDiscountRequest.php`
- `resources/views/dashbord/admin/ecommerce/discounts/`

## Steps
1. `SetDiscountRequest`: validate type, value, expiry
2. `ProductService::setDiscount()` / `clearDiscount()`
3. Admin UI: discount dashboard + per-product form with live price preview
4. API: `POST /admin/products/{product}/discount`, `DELETE /admin/products/{product}/discount`
5. `Product::scopeOnDiscount()` for filtering
```

---

## Task 11 – Stock Management Admin Screen   ← NEW

```md
# Task 11: Stock Management Admin Screen

## Goal
Dedicated admin interface to:
1. View real-time stock for any product (calculated from log)
2. Add new stock (restock)
3. Record a manual adjustment (+/−)
4. Record a stock return linked to an order
5. View the full movement history of a product

This is the ONLY place stock numbers change after a product is created.
Checkout writes `sale` entries automatically. Everything else goes through this screen.

## Database
- `product_stock_logs` (read + append)
- `products` (read `low_stock_threshold`; update it here)

## Relations
- ProductStockLog `belongsTo` Product, Order, User (admin)

## Files
- `app/Services/Ecommerce/StockService.php`                          ← core service
- `app/Http/Controllers/Admin/Ecommerce/StockController.php`
- `app/Http/Requests/Ecommerce/RestockRequest.php`
- `app/Http/Requests/Ecommerce/AdjustmentRequest.php`
- `app/Http/Requests/Ecommerce/ReturnStockRequest.php`
- `app/Http/Resources/Ecommerce/StockLogResource.php`
- `resources/views/dashbord/admin/ecommerce/stock/index.blade.php`            ← product stock dashboard
- `resources/views/dashbord/admin/ecommerce/stock/show.blade.php`             ← per-product stock detail + history
- `resources/views/dashbord/admin/ecommerce/stock/restock.blade.php`          ← add new stock form
- `resources/views/dashbord/admin/ecommerce/stock/adjust.blade.php`           ← adjustment form
- `resources/views/dashbord/admin/ecommerce/stock/return.blade.php`           ← return form

## Permissions
- Admin only

## Admin Screen Layout

### Stock Dashboard (index) — all products with stock tracking
```
┌──────────────────────────────────────────────────────────────┐
│  Stock Management                              [Search...]    │
├────────────────┬──────────┬────────┬──────────┬─────────────┤
│ Product        │ Received │  Sold  │ Returned │ Current     │
├────────────────┼──────────┼────────┼──────────┼─────────────┤
│ Product A      │   100    │   97   │    0     │  3  ⚠ LOW   │
│ Product B      │   200    │   50   │    5     │  155  ✓     │
│ Product C      │    50    │   50   │    0     │  0  ✗ OUT   │
├────────────────┴──────────┴────────┴──────────┴─────────────┤
│ [Add Stock] [Adjust] [View History]  per row                 │
└──────────────────────────────────────────────────────────────┘
```

### Per-Product Stock Detail (show)
```
┌─────────────────────────────────────────────────────────────┐
│  Product A — Stock Detail                                    │
│  ─────────────────────────────────────────────────          │
│  Total Received:   100     Total Sold:      97               │
│  Total Returned:     0     Total Adj Out:    0               │
│  ────────────────────────────────────────────               │
│  Current Stock:      3   ⚠ Low Stock (threshold: 5)         │
│  ─────────────────────────────────────────────────          │
│  [+ Add Stock]  [± Adjust]  [↩ Record Return]               │
│                                                              │
│  Movement History                                            │
│  ──────────────────────────────────────────────────────     │
│  Date          │ Type       │ Qty │ Dir │ Note │ Order      │
│  2025-01-01    │ initial    │ 100 │  +  │  —   │  —         │
│  2025-03-15    │ sale       │  20 │  −  │  —   │ #12        │
│  2025-03-20    │ sale       │  50 │  −  │  —   │ #18        │
│  2025-03-22    │ sale       │  27 │  −  │  —   │ #24        │
│  ──────────────────────────────────────────────────────     │
└─────────────────────────────────────────────────────────────┘
```

### Add Stock / Restock Form
```
Product: Product A       Current Stock: 3
─────────────────────────────────────────
Quantity to Add:  [____]   (required, min: 1)
Note:             [____]   (optional, e.g. "Received from supplier")
─────────────────────────────────────────
              [Cancel]  [Add Stock]
```

### Adjustment Form
```
Product: Product A       Current Stock: 3
─────────────────────────────────────────
Direction:  (●) Add (+)   ( ) Remove (−)
Quantity:   [____]   (required, min: 1)
Note:       [____]   (required — must explain reason)
─────────────────────────────────────────
              [Cancel]  [Apply Adjustment]
```

### Return Form
```
Product: Product A       Current Stock: 3
─────────────────────────────────────────
Order:      [Select order that had this product]
Quantity:   [____]  (max: qty in that order)
Note:       [____]  (optional)
─────────────────────────────────────────
              [Cancel]  [Record Return]
```

## StockService — full method list

```php
class StockService
{
    /**
     * Write the first log entry when a product is created with opening stock.
     * Called by ProductService::store() if stock_quantity is set.
     */
    public function recordInitial(Product $product, int $quantity, User $admin): void
    {
        $product->stockLogs()->create([
            'movement_type' => StockMovement::Initial,
            'quantity'      => $quantity,
            'direction'     => StockDirection::In,
            'note'          => 'Opening stock',
            'admin_id'      => $admin->id,
            'created_at'    => now(),
        ]);
    }

    /**
     * Add new stock (restock). Called from Stock Admin Screen.
     */
    public function recordRestock(Product $product, int $quantity, ?string $note, User $admin): void
    {
        $product->stockLogs()->create([
            'movement_type' => StockMovement::Restock,
            'quantity'      => $quantity,
            'direction'     => StockDirection::In,
            'note'          => $note,
            'admin_id'      => $admin->id,
            'created_at'    => now(),
        ]);
    }

    /**
     * Record a sale. Called automatically by CheckoutService per physical order item.
     * Never called manually from the admin screen.
     */
    public function recordSale(Product $product, int $quantity, Order $order): void
    {
        $product->stockLogs()->create([
            'movement_type' => StockMovement::Sale,
            'quantity'      => $quantity,
            'direction'     => StockDirection::Out,
            'order_id'      => $order->id,
            'created_at'    => now(),
        ]);
    }

    /**
     * Record a stock return (reversed sale). Called from Stock Admin Screen.
     * Requires a valid order_id that contains this product.
     */
    public function recordReturn(Product $product, int $quantity, Order $order, ?string $note, User $admin): void
    {
        // Validate: order must contain this product as a physical item
        $orderItem = $order->items()
            ->where('product_id', $product->id)
            ->where('type', 'physical')
            ->firstOrFail();

        // Validate: return quantity cannot exceed what was sold in this order
        if ($quantity > $orderItem->quantity) {
            throw new \InvalidArgumentException('Return quantity exceeds ordered quantity.');
        }

        $product->stockLogs()->create([
            'movement_type' => StockMovement::Return,
            'quantity'      => $quantity,
            'direction'     => StockDirection::In,
            'note'          => $note,
            'order_id'      => $order->id,
            'admin_id'      => $admin->id,
            'created_at'    => now(),
        ]);
    }

    /**
     * Record a manual adjustment. Called from Stock Admin Screen.
     * Note is REQUIRED for adjustments (audit trail).
     */
    public function recordAdjustment(Product $product, int $quantity, string $direction, string $note, User $admin): void
    {
        if (empty($note)) {
            throw new \InvalidArgumentException('Note is required for adjustments.');
        }

        $product->stockLogs()->create([
            'movement_type' => StockMovement::Adjustment,
            'quantity'      => $quantity,
            'direction'     => $direction === '+' ? StockDirection::In : StockDirection::Out,
            'note'          => $note,
            'admin_id'      => $admin->id,
            'created_at'    => now(),
        ]);
    }

    /**
     * Returns paginated movement history for a product (newest first).
     */
    public function getHistory(Product $product, int $perPage = 20): LengthAwarePaginator
    {
        return $product->stockLogs()
            ->with(['order', 'admin'])
            ->latest('created_at')
            ->paginate($perPage);
    }
}
```

## Implementation Steps

1. Implement `StockService` with all five methods above
2. Create `RestockRequest`:
   - `quantity` → required, integer, min: 1
   - `note` → nullable string
3. Create `AdjustmentRequest`:
   - `quantity` → required, integer, min: 1
   - `direction` → required, in: `+`, `−`
   - `note` → required string (mandatory for audit)
4. Create `ReturnStockRequest`:
   - `order_id` → required, exists in orders
   - `quantity` → required, integer, min: 1
   - `note` → nullable string
5. Implement `StockController`:
   - `index()` → all products with stock tracking; show computed columns
   - `show(Product $product)` → stock detail + paginated history
   - `restock(RestockRequest $request, Product $product)` → call `StockService::recordRestock()`
   - `adjust(AdjustmentRequest $request, Product $product)` → call `StockService::recordAdjustment()`
   - `recordReturn(ReturnStockRequest $request, Product $product)` → call `StockService::recordReturn()`
6. Create `StockLogResource`:
   - Fields: `id`, `movement_type`, `direction`, `quantity`, `note`, `created_at`
   - Include: `order.id` (if set), `admin.name` (if set)
7. Build all Blade views per the wireframes above
8. Register routes under admin middleware:
   - `GET    /admin/stock` → `index`
   - `GET    /admin/stock/{product}` → `show`
   - `POST   /admin/stock/{product}/restock` → `restock`
   - `POST   /admin/stock/{product}/adjust` → `adjust`
   - `POST   /admin/stock/{product}/return` → `recordReturn`
9. Write unit tests:
   - Record initial 100 → `currentStock()` = 100
   - Record sale 30 → `currentStock()` = 70
   - Record return 5 → `currentStock()` = 75
   - Record adjustment `−` 10 → `currentStock()` = 65
   - Record restock 50 → `currentStock()` = 115
   - Confirm `stockLabel()` output at each threshold boundary
   - Confirm return validation rejects qty > order qty

---

## Task Status - Implementation Progress

### Completed Tasks
- **Task 01 - Setup Database Migrations**: 
  - Status: **COMPLETED** 
  - Notes: All e-commerce tables created with proper relationships and constraints
  - Update: Categories table renamed to `ecommerce_categories` for better namespacing

- **Task 02 - Models, Enums & Relationships**: 
  - Status: **COMPLETED**
  - Notes: All models and enums created with v4 stock management methods
  - Files: Product model with full stock calculation methods, all enums implemented

- **Task 03 - Category Tree**: 
  - Status: **COMPLETED**
  - Notes: Full category CRUD with tree structure, admin views, and API endpoints
  - Files: CategoryService, CategoryController, views, and API resources completed

- **Task 04 - Product CRUD**: 
  - Status: **COMPLETED**
  - Notes: Complete product management with stock integration
  - Files: ProductService, ProductController, requests, resources, and admin routes

- **Task 05 - Cart System**: 
  - Status: **COMPLETED**
  - Notes: Cart management with stock validation and API endpoints
  - Files: CartService, CartController, CartResource, CartItemResource, and API routes

### Pending Tasks
- **Task 06 - Checkout & Payment with stock logging**: Status: PENDING
- **Task 07 - Orders Management system**: Status: PENDING  
- **Task 08 - Digital Library with signed URLs**: Status: PENDING
- **Task 09 - API Authentication with Sanctum**: Status: PENDING
- **Task 10 - Discount management features**: Status: PENDING
- **Task 11 - Stock Management Admin Screen**: Status: PENDING

### Implementation Notes
- All database migrations use proper foreign key constraints
- Stock management system fully implemented with real-time calculations
- Categories renamed to `ecommerce_categories` for better organization
- All services follow dependency injection pattern
- API endpoints are Sanctum-ready and mobile-app compatible
- Bilingual support (AR/EN) implemented throughout

---

*End of AI Planning Files - E-commerce Module v4*
