# Inventory Module — Architecture Map

> **Last updated:** 2026-06-10
> **Status:** Living document — incrementally updated as new information is discovered

---

## 1. Purpose & Responsibilities

The Inventory module is the **comprehensive stock and asset management** module. It manages:
- Inventory items (SKU, pricing, categorization, barcode)
- Warehouses (locations, keepers, capacity)
- Stock movements (single source of truth via StockService)
- Stock levels per warehouse (item_warehouse_stock)
- Batch/lot tracking with FEFO (First Expiry, First Out)
- Inter-warehouse stock transfers (with approval workflow)
- Stock audits (full, partial, spot check) with variance adjustment
- Purchase orders (full lifecycle: draft → submit → approve → receive)
- Suppliers (CRUD, credit limits, performance metrics)
- Fixed asset register with depreciation (straight-line, declining balance)
- Asset custody tracking (assignments to employees)
- Bill of Materials (BOM) — composite items with component deduction
- Opening balance initialization
- Configurable movement types via MovementEngineService
- Low stock alerts
- Credit limit enforcement (customer and supplier)
- Reports (stock balance, movements, expiry alert, low stock, depreciation, audit variance, supplier history)

It does **NOT** directly manage:
- Sales transactions (Sales module sells; Inventory deducts stock)
- Procurement workflow beyond PO (Procurement module handles GRNs, vendor invoices, returns)
- Accounting entries (Accounting listens to events)
- Payment processing (Payments module)

---

## 2. Directory & File Structure

```
app/Modules/Inventory/
├── bootstrap.php                              # Permissions + sidebar menu (no events)
├── Routes.php                                 # 56 web routes
├── provides.php                               # Registers StockManagerInterface
├── StockManagerAdapter.php                    # Interface adapter for StockService
├── Controllers/
│   ├── ItemController.php                    # Item CRUD + JSON search
│   ├── CategoryController.php                # Category CRUD
│   ├── WarehouseController.php               # Warehouse CRUD
│   ├── StockMovementController.php           # Manual stock movements
│   ├── StockTransferController.php           # Inter-warehouse transfers
│   ├── StockAuditController.php              # Stock audit lifecycle
│   ├── SupplierController.php                # Supplier CRUD
│   ├── PurchaseOrderController.php           # PO lifecycle
│   ├── AssetController.php                   # Asset register + depreciation
│   ├── AssetCustodyController.php            # Asset custody transfers
│   ├── BomController.php                     # Bill of Materials CRUD
│   ├── OpeningBalanceController.php          # Initial stock setup
│   └── InventoryReportController.php         # 7 report endpoints
├── Models/
│   ├── InventoryItem.php                     # Item model
│   ├── ItemCategory.php                      # Category model
│   ├── ItemWarehouseStock.php                # Stock level per warehouse
│   ├── ItemBatch.php                         # Batch/lot model
│   ├── Warehouse.php                         # Warehouse model
│   ├── StockMovement.php                     # Movement model
│   ├── StockTransfer.php                     # Transfer header model
│   ├── StockTransferItem.php                 # Transfer line items
│   ├── StockAudit.php                        # Audit header model
│   ├── StockAuditItem.php                    # Audit line items
│   ├── Supplier.php                          # Supplier model
│   ├── PurchaseOrder.php                     # PO header model
│   ├── PurchaseOrderItem.php                 # PO line items
│   ├── AssetRegister.php                     # Fixed asset model
│   └── DepreciationEntry.php                 # Depreciation entry model
├── Services/
│   ├── StockService.php                      # CORE: moveStock() — single entry for ALL stock changes
│   ├── BatchService.php                      # Batch CRUD + FEFO allocation
│   ├── BomService.php                        # BOM CRUD + component deduction
│   ├── StockTransferService.php              # Transfer lifecycle (create/approve/receive)
│   ├── StockAuditService.php                 # Audit lifecycle (create/count/approve)
│   ├── PurchaseOrderService.php              # PO lifecycle (create/submit/approve/receive/cancel)
│   ├── DepreciationService.php               # Monthly depreciation calculations
│   ├── AssetCustodyService.php               # Asset custody transfer logic
│   ├── CreditLimitService.php                # Customer/supplier credit limit checks
│   ├── MovementEngineService.php             # Configurable movement types with GL posting
│   ├── InventoryNumberGenerator.php          # Sequential number generation
│   ├── OpeningBalanceService.php             # Initial stock entry
│   └── StockService.php                      # (repeated for emphasis — the core service)
└── Views/
    ├── items/
    │   ├── index.php, form.php, show.php
    ├── categories/
    │   ├── index.php, form.php
    ├── warehouses/
    │   ├── index.php, form.php, show.php
    ├── stock/
    │   ├── movements.php, movement_form.php
    │   ├── transfers.php, transfer_form.php, transfer_show.php
    ├── audits/
    │   ├── index.php, form.php, show.php, count.php
    ├── suppliers/
    │   ├── index.php, form.php, show.php
    ├── purchase_orders/
    │   ├── index.php, form.php, show.php, receive.php
    ├── assets/
    │   ├── index.php, show.php
    │   ├── custody_index.php, custody_transfer.php
    ├── bom/
    │   ├── index.php, form.php, show.php
    ├── opening_balance/
    │   ├── index.php, form.php
    └── reports/
        ├── stock_balance.php
        ├── movements.php
        ├── expiry_alert.php
        ├── low_stock.php
        ├── depreciation.php
        ├── audit_variance.php
        └── supplier_history.php
```

---

## 3. Database Schema (Production — Source of Truth)

### 3.1 `inventory_items` Table (2 rows as of 2026-06-10)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| category_id | bigint unsigned | YES | MUL | FK to item_categories |
| sku | varchar(50) | NO | UNI | Stock keeping unit |
| barcode | varchar(100) | YES | MUL | Barcode (optional) |
| name_ar | varchar(300) | NO | | Arabic name |
| name_en | varchar(300) | YES | | English name |
| description_ar | text | YES | | |
| unit_of_measure | varchar(30) | NO | | Default: 'piece' |
| tracking_type | varchar(20) | NO | MUL | 'standard' or 'expiry' |
| cost_price | decimal(15,2) | NO | | Default: 0.00 |
| sale_price_member | decimal(15,2) | NO | | Member price |
| sale_price_nonmember | decimal(15,2) | NO | | Non-member price |
| sale_price_player | decimal(15,2) | YES | | Player price (optional) |
| tax_rate | decimal(5,2) | NO | | Default: 0.00 |
| is_sellable | tinyint(1) | NO | MUL | Default: 1 |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| image_document_id | bigint unsigned | YES | MUL | FK to documents |
| notes | text | YES | | |
| is_archived | tinyint(1) | NO | | Default: 0 |
| created_at | timestamp | NO | | |
| updated_at | timestamp | NO | | |

**Tracking types:**
- `standard` — simple quantity tracking
- `expiry` — batch/lot tracking with FEFO allocation

### 3.2 `warehouses` Table (6 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(20) | NO | UNI | |
| name_ar | varchar(200) | NO | | |
| name_en | varchar(200) | YES | | |
| warehouse_type | varchar(30) | NO | MUL | Default: 'general' |
| location_ar | varchar(300) | YES | | |
| keeper_employee_id | bigint unsigned | YES | MUL | FK to employees |
| capacity_units | int unsigned | YES | | |
| branch_id | bigint unsigned | YES | MUL | FK to branches |
| phone | varchar(30) | YES | | |
| is_active | tinyint(1) | NO | MUL | Default: 1 |
| is_archived | tinyint(1) | NO | | Default: 0 |

### 3.3 `item_warehouse_stock` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| quantity | decimal(15,3) | NO | MUL | Current stock level |
| min_level | decimal(15,3) | YES | | Low stock alert threshold |
| max_level | decimal(15,3) | YES | | Maximum stock level |
| last_counted_at | timestamp | YES | | Last physical count |
| updated_at | timestamp | NO | | |

**Key invariant:** This is the source of truth for available quantity. Updated by StockService on every movement.

### 3.4 `stock_movements` Table (2 rows)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| movement_type | varchar(30) | NO | MUL | Type code |
| direction | enum('in','out') | NO | MUL | Stock direction |
| quantity | decimal(15,3) | NO | | Always positive |
| unit_cost | decimal(15,2) | YES | | Cost per unit |
| total_cost | decimal(15,2) | YES | | qty * unit_cost |
| batch_id | bigint unsigned | YES | MUL | FK to item_batches |
| reference_type | varchar(50) | YES | MUL | Source document type |
| reference_id | bigint unsigned | YES | | Source document ID |
| transfer_id | bigint unsigned | YES | MUL | FK to stock_transfers |
| notes | text | YES | | |
| movement_date | date | NO | MUL | |
| branch_id | bigint unsigned | YES | | |
| created_at | timestamp | NO | | |
| created_by | bigint unsigned | YES | | |

**Movement types:** purchase_in, sale_out, transfer_in, transfer_out, return_in, adjustment_in, adjustment_out, consumed_out, opening_balance

### 3.5 `item_batches` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| batch_number | varchar(100) | NO | MUL | Lot/batch identifier |
| expiry_date | date | NO | MUL | Expiration date |
| quantity | decimal(15,3) | NO | | Current batch quantity |
| initial_quantity | decimal(15,3) | NO | | Original quantity received |
| unit_cost | decimal(15,2) | YES | | Cost at receipt |
| received_date | date | NO | | |
| status | varchar(20) | NO | MUL | 'active' or 'depleted' |

### 3.6 `purchase_orders` Table (1 row)

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| requisition_id | bigint unsigned | YES | MUL | FK to purchase_requisitions |
| po_number | varchar(50) | NO | UNI | Format: PO-YYYYMMDD-NNNNN |
| supplier_id | bigint unsigned | NO | MUL | FK to suppliers |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| status | varchar(20) | NO | MUL | Default: 'draft' |
| order_date | date | NO | MUL | |
| expected_delivery | date | YES | | |
| subtotal | decimal(15,2) | NO | | |
| tax_amount | decimal(15,2) | NO | | 14% VAT |
| discount_amount | decimal(15,2) | NO | | |
| total_amount | decimal(15,2) | NO | | Grand total |
| currency_code | varchar(3) | YES | | Default: 'EGP' |
| exchange_rate | decimal(15,6) | YES | | |
| payment_terms | varchar(100) | YES | | |
| submitted_by | bigint unsigned | YES | | |
| approved_by | bigint unsigned | YES | | |
| approved_at | timestamp | YES | | |

**PO Status lifecycle:** draft → submitted → approved → partially_received → received | cancelled

### 3.7 `purchase_order_items` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| purchase_order_id | bigint unsigned | NO | MUL | FK to purchase_orders |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| quantity_ordered | decimal(15,3) | NO | | |
| quantity_received | decimal(15,3) | NO | | Default: 0.000 |
| unit_cost | decimal(15,2) | NO | | |
| line_total | decimal(15,2) | NO | | |

### 3.8 `stock_transfers` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| transfer_number | varchar(50) | NO | UNI | Format: TRF-YYYYMMDD-NNNNN |
| from_warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| to_warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| status | varchar(20) | NO | MUL | Default: 'draft' |
| requested_by | bigint unsigned | YES | | |
| approved_by | bigint unsigned | YES | | |
| received_by | bigint unsigned | YES | | |
| transfer_date | date | NO | MUL | |

**Transfer status lifecycle:** pending_approval → approved → received (or in_transit → received)

### 3.9 `stock_audits` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| audit_number | varchar(50) | NO | UNI | |
| warehouse_id | bigint unsigned | NO | MUL | |
| audit_type | varchar(20) | NO | MUL | 'full', 'partial', 'spot_check' |
| category_id | bigint unsigned | YES | MUL | For partial audits |
| status | varchar(20) | NO | MUL | |
| total_items | int unsigned | NO | | |
| items_counted | int unsigned | NO | | |
| variance_count | int unsigned | NO | | |

**Audit status lifecycle:** in_progress → pending_approval → approved

### 3.10 `suppliers` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| code | varchar(30) | NO | UNI | |
| name_ar | varchar(300) | NO | MUL | |
| contact_person | varchar(200) | YES | | |
| phone | varchar(30) | YES | | |
| email | varchar(200) | YES | | |
| tax_number | varchar(50) | YES | | |
| commercial_register | varchar(50) | YES | | |
| payment_terms | varchar(100) | YES | | |
| credit_limit | decimal(15,2) | NO | | Default: 0.00 |
| credit_balance | decimal(15,2) | NO | | Default: 0.00 |
| rating | tinyint unsigned | YES | | 1-5 |
| total_orders | int unsigned | NO | | Performance metric |
| on_time_delivery_count | int unsigned | NO | | |
| quality_acceptance_count | int unsigned | NO | | |
| average_lead_days | decimal(5,1) | NO | | |
| last_order_date | date | YES | | |

### 3.11 `asset_register` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| item_id | bigint unsigned | NO | MUL | FK to inventory_items |
| warehouse_id | bigint unsigned | NO | MUL | FK to warehouses |
| asset_tag | varchar(50) | NO | UNI | Unique asset tag |
| serial_number | varchar(100) | YES | | |
| purchase_date | date | NO | MUL | |
| purchase_cost | decimal(15,2) | NO | | Original cost |
| useful_life_months | int unsigned | NO | | Default: 60 (5 years) |
| salvage_value | decimal(15,2) | NO | | Default: 0.00 |
| depreciation_method | varchar(30) | NO | MUL | 'straight_line' or 'declining_balance' |
| declining_rate | decimal(5,2) | YES | | Annual rate for declining balance |
| accumulated_depreciation | decimal(15,2) | NO | | Running total |
| book_value | decimal(15,2) | NO | | purchase_cost - accumulated_depreciation |
| status | varchar(20) | NO | MUL | 'active', 'disposed', etc. |
| custodian_employee_id | int unsigned | YES | | Current custodian |
| custody_date | date | YES | | |
| last_depreciation_date | date | YES | | |

### 3.12 `depreciation_entries` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| asset_id | bigint unsigned | NO | MUL | FK to asset_register |
| period_month | varchar(7) | NO | MUL | YYYY-MM format |
| depreciation_amount | decimal(15,2) | NO | | Monthly amount |
| accumulated_total | decimal(15,2) | NO | | Running total |
| book_value_after | decimal(15,2) | NO | | Book value after this entry |
| method_used | varchar(30) | NO | | Method at time of calculation |
| is_manual | tinyint(1) | NO | | Default: 0 |

### 3.13 `item_categories` Table

| Column | Type | Nullable | Key | Notes |
|--------|------|----------|-----|-------|
| id | bigint unsigned | NO | PRI | auto_increment |
| parent_id | bigint unsigned | YES | MUL | Self-referencing (tree structure) |
| code | varchar(30) | NO | UNI | |
| name_ar | varchar(200) | NO | | |
| name_en | varchar(200) | YES | | |
| sort_order | int | NO | | Default: 0 |
| is_active | tinyint(1) | NO | MUL | |

---

## 4. Core Business Flows

### 4.1 Stock Movement (Central — ALL stock changes go through this)

```
StockService::moveStock(data) [SINGLE ENTRY POINT]
1. Validate: item exists, warehouse active, quantity > 0, direction valid
2. For OUT: verify sufficient stock (getAvailableQuantity)
3. Begin transaction
   - SELECT FOR UPDATE on item_warehouse_stock (concurrency lock)
   - INSERT into stock_movements
   - Upsert item_warehouse_stock (+ for in, - for out)
   - If batch OUT: deduct from item_batches, mark 'depleted' if qty <= 0
4. Commit
5. Dispatch 'inventory.stock_moved' event
6. For OUT: check low stock alert threshold
```

### 4.2 Purchase Order Lifecycle

```
1. POST /inventory/purchase-orders (PurchaseOrderService::createPO)
   - Create PO with status='draft', calculate totals + 14% tax
   - Generate PO number

2. POST .../submit (submitPO) → status='submitted'
3. POST .../approve (approvePO) → status='approved'

4. POST .../receive (receivePO)
   - For each received item:
     - Update quantity_received on PO item
     - If tracking_type='expiry': create batch via BatchService
     - Call StockService::moveStock(purchase_in)
   - Determine new status: 'received' (all items) or 'partially_received'
   - Dispatch 'inventory.po_received'

5. POST .../cancel (cancelPO) → status='cancelled' (from draft/submitted only)
```

### 4.3 Stock Transfer

```
1. POST /inventory/transfers (StockTransferService::createTransfer)
   - Validate: from != to, items not empty
   - status='pending_approval'

2. POST .../approve → status='approved'

3. POST .../receive (StockTransferService::receive)
   - For each item:
     - StockService::moveStock(transfer_out, from_warehouse)
     - StockService::moveStock(transfer_in, to_warehouse)
   - status='received'
   - Dispatch 'inventory.transfer_completed'
```

### 4.4 Stock Audit

```
1. POST /inventory/audits (StockAuditService::createAudit)
   - Populate audit items from item_warehouse_stock
   - For spot_check: random 20-item subset
   - For partial: filtered by category_id
   - status='in_progress'

2. POST .../count (saveCounts)
   - Record physical_quantity for each audit item
   - Calculate variance + variance_cost
   - status='pending_approval'

3. POST .../approve (approveAudit)
   - For each item with variance != 0:
     - StockService::moveStock(adjustment_in or adjustment_out)
   - status='approved'
   - Dispatch 'inventory.audit_completed'
```

### 4.5 Monthly Depreciation

```
DepreciationService::runMonthlyDepreciation(periodMonth)
- Iterates all active assets
- Skips: already deprecated for period, book_value <= salvage_value
- Calculates:
  - straight_line: (purchase_cost - salvage_value) / useful_life_months
  - declining_balance: book_value * annual_rate / 12
- Caps depreciation so book_value never goes below salvage_value
- Creates depreciation_entries, updates asset_register
- Dispatches 'inventory.depreciation_run'
```

### 4.6 BOM Component Deduction (called by Sales)

```
BomService::deductComponents(bomId, quantity, warehouseId)
- Calculate multiplier: quantity / bom.output_quantity
- For each component (non-optional):
  - requiredQty = component.quantity * multiplier
  - wasteQty = requiredQty * waste_percentage / 100
  - totalRequired = requiredQty + wasteQty
  - Pre-check: verify all components have sufficient stock
  - StockService::moveStock(consumed_out)
- Dispatches 'bom.components_deducted'
```

---

## 5. Events Dispatched

| Event | Payload | Dispatched From |
|-------|---------|-----------------|
| `inventory.stock_moved` | `{movement_id, item_id, warehouse_id, direction, quantity, movement_type}` | StockService::moveStock |
| `inventory.low_stock_alert` | `{item_id, warehouse_id, quantity, min_level}` | StockService::moveStock (after OUT) |
| `inventory.po_received` | `{po_id, warehouse_id, status}` | PurchaseOrderService::receivePO |
| `inventory.transfer_completed` | `{transfer_id, from_warehouse_id, to_warehouse_id}` | StockTransferService::receive |
| `inventory.audit_completed` | `{audit_id, warehouse_id, variance_count}` | StockAuditService::approveAudit |
| `inventory.depreciation_run` | `{period_month, assets_count}` | DepreciationService::runMonthlyDepreciation |
| `bom.created` | `{bom_id, item_id}` | BomService::create |
| `bom.components_deducted` | `{bom_id, quantity, warehouse_id}` | BomService::deductComponents |
| `movement.executed` | `{type_code, document_number, data}` | MovementEngineService::executeMovement |
| `movement.post_gl` | `{movement_type, debit_account, credit_account, amount, description}` | MovementEngineService (if posts_to_gl) |

---

## 6. Events Consumed

The Inventory bootstrap.php does not register any event listeners. However, the module is consumed heavily by others.

---

## 7. Cross-Module Dependencies

### 7.1 Inventory IMPORTS FROM:

| Module | What it uses |
|--------|-------------|
| Accounting | AccountStatementService (via MovementEngineService for AR/AP posting) |
| Members | members table (credit_limit, credit_balance) via CreditLimitService |

### 7.2 Other modules that IMPORT FROM Inventory:

| Module | What it imports |
|--------|----------------|
| Sales | StockService, BatchService, BomService, CreditLimitService, Warehouse model, InventoryItem model |
| Procurement | StockService (GRN receiving), InventoryItem model, Warehouse model |
| ActivitySubscriptions | ActivitySubscriptionController references inventory |
| Accounting | AccountCodes references inventory accounts, StatementIntegrationService references inventory |

### 7.3 Interface Registration

The module registers `StockManagerInterface` via `provides.php` and implements it with `StockManagerAdapter`. This allows other modules to depend on the interface without directly coupling to StockService.

---

## 8. Permissions

| Key | Description |
|-----|-------------|
| inventory.view | View items and categories |
| inventory.manage | Create/edit items and categories |
| warehouse.view | View warehouses |
| warehouse.manage | Create/edit warehouses |
| stock.view | View stock movements |
| stock.move | Create stock movements |
| stock.approve_transfer | Approve inter-warehouse transfers |
| stock.audit | Conduct stock audits |
| stock.approve_adjustment | Approve audit variance adjustments |
| purchase.view | View purchase orders |
| purchase.create | Create purchase orders |
| purchase.approve | Approve purchase orders |
| supplier.view | View suppliers |
| supplier.manage | Create/edit suppliers |
| asset.view | View assets |
| asset.manage | Manage assets and depreciation |
| report.inventory | Access inventory reports |
| inventory.bom.view | View bill of materials |
| inventory.bom.manage | Manage bill of materials |
| inventory.opening.manage | Manage opening balances |

---

## 9. Number Generation

| Type | Format | Example |
|------|--------|---------|
| PO | PO-YYYYMMDD-NNNNN | PO-20260610-00001 |
| Transfer | TRF-YYYYMMDD-NNNNN | TRF-20260610-00001 |
| Audit | AUD-YYYYMMDD-NNNNN | AUD-20260610-00001 |

All via `InventoryNumberGenerator` service using MAX() queries.

---

## 10. Movement Types (MovementEngineService)

The system supports configurable movement types stored in `movement_types` table:

| Property | Description |
|----------|-------------|
| code | Unique type identifier |
| direction | 'in', 'out', or 'neutral' |
| affects_stock | Whether to call StockService |
| requires_approval | Whether approval workflow required |
| posts_to_gl | Whether to post accounting entries |
| creates_ar | Whether to create accounts receivable |
| creates_ap | Whether to create accounts payable |
| auto_number_prefix | Number prefix for documents |

This provides an extensible framework for adding new movement types without code changes.

---

## 11. High-Risk Areas

### 11.1 StockService::moveStock() (CRITICAL — single entry point)
- ALL stock changes MUST go through this method
- Uses SELECT FOR UPDATE for concurrency control
- If this method has a bug, ALL stock data is affected
- Validates stock availability before OUT movements — throws RuntimeException if insufficient
- Stock can go negative if the check passes but another transaction intervenes (narrow window)

### 11.2 Batch FEFO Allocation
- BatchService::allocateBatch() selects batches ordered by expiry_date ASC
- If batch quantity < required, splits across multiple batches
- If total batch stock < required, throws exception — but this should already be caught by stock level check
- Depleted batches marked automatically — no manual cleanup needed

### 11.3 Purchase Order Receiving
- Partial receiving is supported (quantity_received incrementally updated)
- Each receive creates stock movements — if receive is done twice for same PO item, stock doubles
- No explicit idempotency check on PO item receiving beyond comparing qty_received to qty_ordered

### 11.4 Stock Audit Variance Approval
- Approval creates adjustment movements that modify actual stock
- Once approved, cannot be reversed (no "unapprove" mechanism)
- Large variances could significantly impact financial reporting

### 11.5 Depreciation
- Monthly run is idempotent (checks for existing entry in same period)
- But if run twice in same period after manual intervention, could create duplicates
- Book value capped at salvage value — ensures non-negative depreciation

### 11.6 Credit Limit Check
- Only enforced at point of sale for members
- credit_balance on members table must stay in sync with actual transactions
- recalculateCustomerBalance() available but must be called manually if drift occurs

---

## 12. Known Patterns & Gotchas

1. **StockService is the ONLY way to change stock**: Never INSERT into stock_movements or UPDATE item_warehouse_stock directly
2. **Tracking type drives behavior**: 'expiry' items require batch handling; 'standard' items are simpler
3. **Three pricing tiers**: Each item has sale_price_member, sale_price_nonmember, sale_price_player
4. **Warehouse type**: 'general' by default — supports other types for categorization
5. **Asset register is inventory-linked**: asset_register.item_id references inventory_items (assets are inventory items too)
6. **BOM waste factor**: waste_percentage on each component accounts for material loss during production
7. **Spot check limits**: spot_check audits limited to random 20-item sample
8. **Supplier performance metrics**: total_orders, on_time_delivery_count, quality_acceptance_count tracked for supplier evaluation
9. **Only 2 inventory items in production**: System recently deployed; minimal real data
10. **Multi-currency on PO**: purchase_orders has currency_code and exchange_rate for foreign purchases

---

## 13. Configuration Dependencies

| Config | Purpose |
|--------|---------|
| movement_types table | Configurable movement type definitions |
| movement_specifications table | Per-type specifications (GL accounts, etc.) |
| item_warehouse_stock.min_level | Low stock alert thresholds |
| item_warehouse_stock.max_level | Maximum stock levels |

---

## Appendix: Files to Check Before Modifying Inventory

1. `app/Modules/Sales/Services/SaleService.php` — Calls StockService, BatchService, BomService
2. `app/Modules/Sales/Services/RefundService.php` — Calls StockService for return_in
3. `app/Modules/Procurement/Services/GoodsReceivingService.php` — Calls StockService for receiving
4. `app/Modules/Procurement/Services/ReturnToVendorService.php` — Calls StockService for returns
5. `app/Modules/Accounting/Services/AccountingIntegrationService.php` — Inventory-related GL posting
6. `app/Modules/Accounting/Services/StatementIntegrationService.php` — Customer/supplier statements
7. `app/Core/Contracts/StockManagerInterface.php` — Interface contract
