# Phase 12-14 — Inventory, Supply Chain, Kits & Point of Sale

## Goal

Complete equipment lifecycle from purchasing through to selling at reception. Unified POS that handles subscriptions, products, kits, and payments in a single transaction.

---

# PART A: PRODUCTS & CATEGORIES

## Entity: `product_categories`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| academy_id | FK organizations | |
| parent_id | FK product_categories | Nullable — tree structure |
| name | string | |
| name_ar | string | |
| slug | string | Unique per academy |
| description | text | Nullable |
| sort_order | integer | Default 0 |
| is_active | boolean | Default true |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `products`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| category_id | FK product_categories | Nullable |
| name | string | |
| name_ar | string | |
| sku | string(30) | UNIQUE per academy |
| barcode | string(50) | Nullable |
| description | text | Nullable |
| unit_of_measure | string(10) | CHECK: piece, pair, set, kg, meter, liter, box |
| brand | string | Nullable |
| size | string(20) | Nullable |
| color | string(20) | Nullable |
| base_cost | bigint | Purchase cost in piasters |
| selling_price | bigint | Default selling price (before pricing engine adjustments) |
| min_stock_level | integer | Default 0 — reorder alert threshold |
| reorder_quantity | integer | Nullable — suggested order quantity |
| track_inventory | boolean | Default true — some items (services) don't need tracking |
| is_sellable | boolean | Default true — can be sold at POS |
| is_purchasable | boolean | Default true — can be ordered from suppliers |
| is_active | boolean | Default true |
| photo_path | string | Nullable |
| weight_grams | integer | Nullable |
| dimensions | jsonb | Nullable — {length, width, height, unit} |
| metadata | jsonb | Default '{}' |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

---

# PART B: SUPPLIERS & PURCHASING

## Entity: `suppliers`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| name | string | |
| name_ar | string | Nullable |
| contact_name | string | Nullable |
| email | string | Nullable |
| phone | string(20) | Nullable |
| address | text | Nullable |
| tax_number | string | Nullable |
| payment_terms | string | Nullable — "Net 30", "COD" |
| notes | text | Nullable |
| is_active | boolean | Default true |
| total_purchases | bigint | Default 0 — denormalized |
| last_purchase_at | timestamp | Nullable |
| rating | decimal(3,2) | Nullable — 1.00-5.00 |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `purchase_orders`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| supplier_id | FK suppliers | |
| branch_id | FK branches | Receiving branch |
| order_number | string(30) | UNIQUE per academy |
| status | string(20) | CHECK: draft, submitted, confirmed, partially_received, received, cancelled |
| order_date | date | |
| expected_delivery_date | date | Nullable |
| actual_delivery_date | date | Nullable |
| subtotal | bigint | |
| tax_amount | bigint | Default 0 |
| shipping_cost | bigint | Default 0 |
| total_amount | bigint | |
| payment_status | string(20) | CHECK: unpaid, partially_paid, paid |
| notes | text | Nullable |
| created_by | FK users | |
| approved_by | FK users | Nullable |
| received_by | FK users | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `purchase_order_items`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| purchase_order_id | FK purchase_orders | |
| product_id | FK products | |
| quantity_ordered | integer | |
| quantity_received | integer | Default 0 |
| unit_cost | bigint | Cost per unit in piasters |
| total_cost | bigint | |
| notes | string | Nullable |
| created_at | timestamp | |

---

# PART C: WAREHOUSES & INVENTORY

## Entity: `warehouses`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| branch_id | FK branches | |
| name | string | "Main Warehouse", "Reception Store", "Football Equipment Room" |
| name_ar | string | |
| code | string(10) | Unique per academy |
| type | string(20) | CHECK: warehouse, store, room, virtual |
| is_default | boolean | Default false — one per branch |
| is_active | boolean | Default true |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `inventory_levels`

Current stock per product per warehouse.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| product_id | FK products | |
| warehouse_id | FK warehouses | |
| quantity_on_hand | integer | Current physical stock |
| quantity_reserved | integer | Default 0 — reserved for pending orders |
| quantity_available | integer | on_hand - reserved (computed/denormalized) |
| last_counted_at | timestamp | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |
| UNIQUE | (product_id, warehouse_id) | |

## Entity: `inventory_movements`

EVERY stock change is recorded. Never modify inventory_levels directly — always through a movement.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| product_id | FK products | |
| warehouse_id | FK warehouses | |
| type | string(20) | CHECK: see below |
| direction | string(3) | CHECK: in, out |
| quantity | integer | Always positive |
| quantity_before | integer | Stock level before this movement |
| quantity_after | integer | Stock level after |
| unit_cost | bigint | Nullable — cost at time of movement |
| reference_type | string | Nullable polymorphic — PurchaseOrder, POSTransaction, StockCount, Transfer |
| reference_id | bigint | Nullable |
| source_warehouse_id | FK warehouses | Nullable — for transfers |
| destination_warehouse_id | FK warehouses | Nullable — for transfers |
| reason | string | Nullable — explanation |
| batch_number | string | Nullable — for batch tracking |
| expiry_date | date | Nullable — for perishables |
| created_by | FK users | |
| created_at | timestamp | |

### Movement Types
- `purchase_received` — goods received from supplier (direction: in)
- `sale` — sold at POS or invoiced (direction: out)
- `transfer_out` — sent to another warehouse (direction: out)
- `transfer_in` — received from another warehouse (direction: in)
- `return_to_stock` — customer return (direction: in)
- `return_to_supplier` — defective return (direction: out)
- `adjustment_up` — manual increase (found extra, count correction) (direction: in)
- `adjustment_down` — manual decrease (loss, damage, expiry) (direction: out)
- `damage` — written off as damaged (direction: out)
- `loss` — missing/stolen (direction: out)
- `consumption` — used internally (not sold) — cleaning supplies, office supplies (direction: out)
- `kit_assembly` — components consumed to assemble kit (direction: out)
- `kit_disassembly` — kit broken back into components (direction: in)
- `initial_stock` — opening balance (direction: in)

---

# PART D: STOCK COUNTS (INVENTORY AUDIT)

## Entity: `stock_counts`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| warehouse_id | FK warehouses | |
| name | string | "Monthly Count - June 2026" |
| count_type | string(20) | CHECK: full, partial, cycle, surprise |
| status | string(20) | CHECK: planned, in_progress, completed, approved, cancelled |
| planned_date | date | |
| started_at | timestamp | Nullable |
| completed_at | timestamp | Nullable |
| total_items_counted | integer | Default 0 |
| total_discrepancies | integer | Default 0 |
| total_variance_value | bigint | Default 0 — total piaster value of discrepancies |
| counted_by | FK users | Nullable |
| approved_by | FK users | Nullable |
| approved_at | timestamp | Nullable |
| notes | text | Nullable |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `stock_count_items`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| stock_count_id | FK stock_counts | |
| product_id | FK products | |
| system_quantity | integer | What the system thinks we have |
| counted_quantity | integer | Nullable — what was actually counted |
| discrepancy | integer | Nullable — counted - system (positive=over, negative=short) |
| discrepancy_value | bigint | Nullable — discrepancy × unit cost |
| reason | string | Nullable — explanation for discrepancy |
| adjustment_approved | boolean | Default false |
| adjusted_at | timestamp | Nullable |
| notes | string | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

### Stock Count Business Rules
- **Full count:** every product in the warehouse is counted
- **Partial count:** only selected categories or products
- **Cycle count:** rotating subsets counted regularly (e.g., Category A week 1, B week 2)
- **Surprise count:** unannounced, no planning phase
- Approval workflow: discrepancies above threshold require manager approval before adjustment
- When approved: system creates `adjustment_up` or `adjustment_down` inventory movements
- Frequency configurable per academy: daily (high-value items), weekly, monthly, quarterly

---

# PART E: KIT MANAGEMENT

## Entity: `kits`

A virtual product composed of real products.

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| name | string | "Football Starter Kit" |
| name_ar | string | |
| sku | string(30) | UNIQUE per academy |
| description | text | Nullable |
| selling_price | bigint | Kit price (may differ from sum of components) |
| component_cost | bigint | Sum of component costs (denormalized) |
| photo_path | string | Nullable |
| season | string | Nullable — "Summer 2026" |
| is_required | boolean | Default false — some programs require buying a kit |
| required_for_program_id | FK training_programs | Nullable — if required, which program |
| allow_individual_purchase | boolean | Default true — can components be bought separately |
| is_active | boolean | Default true |
| available_from | date | Nullable |
| available_until | date | Nullable |
| max_sold | integer | Nullable — limited edition |
| total_sold | integer | Default 0 |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | |

## Entity: `kit_items`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| kit_id | FK kits | |
| product_id | FK products | |
| quantity | integer | Default 1 |
| is_optional | boolean | Default false — false=always included, true=customer chooses |
| substitute_product_id | FK products | Nullable — alternative if this product is out of stock |
| sort_order | integer | Default 0 |

### Kit Business Rules
- Selling a kit: reduces stock of EACH component product by their quantity
- Kit availability: check stock of ALL required (non-optional) components. If ANY component is out of stock → kit unavailable
- Substitute handling: if primary component out of stock but substitute available → offer substitute
- Kit pricing: kit.selling_price may be LESS than sum of individual products (bundle discount)
- Kit + Pricing Engine: kit can go through pricing rules too (family discount on kits, etc.)
- Kit disassembly: rarely needed, but possible — puts components back in stock

---

# PART F: POINT OF SALE

## Entity: `pos_sessions`

A POS session represents one operator's sales shift (similar to cash_sessions but sales-focused).

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| branch_id | FK branches | |
| user_id | FK users | Operator |
| cash_session_id | FK cash_sessions | Linked to financial cash session |
| status | string(20) | CHECK: open, closed |
| transactions_count | integer | Default 0 |
| total_sales | bigint | Default 0 |
| opened_at | timestamp | |
| closed_at | timestamp | Nullable |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `pos_transactions`

One complete POS transaction (can contain multiple items).

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| uuid | uuid UNIQUE | |
| academy_id | FK organizations | |
| pos_session_id | FK pos_sessions | |
| participant_id | FK participants | Nullable — who is this for |
| guardian_id | FK guardians | Nullable — who is paying |
| transaction_number | string(30) | Auto-generated receipt number |
| subtotal | bigint | |
| discount_amount | bigint | Default 0 |
| tax_amount | bigint | Default 0 |
| total_amount | bigint | |
| payment_method | string(20) | CHECK: cash, card, wallet, split, other |
| payment_status | string(20) | CHECK: paid, pending, refunded |
| invoice_id | FK invoices | Generated invoice for this transaction |
| coupon_code | string(20) | Nullable — if coupon applied |
| notes | text | Nullable |
| created_by | FK users | |
| created_at | timestamp | |
| updated_at | timestamp | |

## Entity: `pos_transaction_items`

| Column | Type | Rules |
|--------|------|-------|
| id | bigint PK | |
| pos_transaction_id | FK pos_transactions | |
| item_type | string(20) | CHECK: product, kit, program, service |
| item_id | bigint | Polymorphic ID |
| item_name | string | Denormalized for receipt |
| quantity | integer | Default 1 |
| unit_price | bigint | Price at time of sale |
| discount_amount | bigint | Default 0 |
| total_price | bigint | |
| metadata | jsonb | Default '{}' — size, color, variant details |
| created_at | timestamp | |

### POS Transaction Flow

```
1. Receptionist opens POS screen
2. Scans participant QR/searches by name/phone/national_id
3. System shows participant profile + outstanding balance + active programs
4. Receptionist adds items to cart:
   a. Training Program subscription → creates enrollment
   b. Product → reduces inventory
   c. Kit → reduces inventory for each component
   d. Service (private session, camp fee) → creates booking
   e. Payment on existing invoice → links to invoice
5. System applies pricing rules automatically (family discount visible in real-time)
6. Receptionist can manually apply coupon code
7. Cart shows: subtotal, discounts applied, tax, total
8. Payment:
   a. Single method: cash / card / wallet
   b. Split: part wallet + part cash (or any combination)
9. On confirm:
   a. Invoice created with line items
   b. Payment recorded
   c. Inventory movements created for products/kits
   d. Enrollment created for programs
   e. Receipt printed/emailed
   f. Cash session updated
10. Receipt shows: academy name, participant name, items, pricing breakdown, payment info, receipt number
```

### POS Edge Cases
- **Participant not registered:** Quick-registration from POS (name + phone minimum)
- **Product out of stock:** Warning but can override (sell what you have physically)
- **Wallet insufficient:** Show wallet balance, offer to pay difference with cash/card
- **Split payment:** 500 from wallet, 300 cash → two payment records on same invoice
- **Return at POS:** Creates refund + inventory return movement (item goes back to stock)
- **Program already enrolled:** Block duplicate enrollment, show warning
- **Suspended participant:** Block new purchases, allow payment of outstanding balance only
- **No POS session open:** Operator must open session first
- **Offline mode (future):** Queue transactions locally, sync when connected

---

## Scheduled Jobs (Inventory)

| Job | Frequency | Action |
|-----|-----------|--------|
| `LowStockAlerts` | Daily | Products below min_stock_level → notify admin |
| `ReorderSuggestions` | Weekly | Generate suggested purchase orders based on consumption rate |
| `ExpiredStockCheck` | Daily | Products with expiry_date passed → alert, mark for disposal |
| `StockCountReminders` | Monthly | If scheduled count is due → notify responsible user |
| `RecalculateInventoryLevels` | Weekly | Integrity check: sum all movements vs current level |

---

## Events Dispatched

| Event | Trigger | Consumers |
|-------|---------|-----------|
| `ProductLowStock` | Quantity below min | Admin notification, reorder suggestion |
| `PurchaseOrderReceived` | Goods received | Inventory update, AP invoice |
| `POSTransactionCompleted` | Sale finished | Inventory movement, invoice creation |
| `StockCountCompleted` | Count approved | Adjustments applied |
| `KitSold` | Kit in POS | Component stock reduced |
