# DOMAIN_MODEL.md — NexStock Domain Model

## Entity Relationship Overview

```mermaid
erDiagram
    BUSINESS ||--o{ BRANCH : has
    BUSINESS ||--o{ USER : has
    BUSINESS ||--o{ PRODUCT : has
    BUSINESS ||--o{ CATEGORY : has
    BUSINESS ||--o{ BRAND : has
    BUSINESS ||--o{ UNIT : has
    BUSINESS ||--o{ SUPPLIER : has
    BUSINESS ||--o{ CUSTOMER : has
    BUSINESS ||--o{ EXPENSE_CATEGORY : has
    BUSINESS ||--|| SUBSCRIPTION : has

    BRANCH ||--o{ SALE : processes
    BRANCH ||--o{ PURCHASE : receives
    BRANCH ||--o{ INVENTORY_BALANCE : tracks
    BRANCH ||--o{ CASH_SHIFT : manages
    BRANCH ||--o{ EXPENSE : records

    PRODUCT ||--o{ PRODUCT_VARIANT : has
    PRODUCT }o--|| CATEGORY : belongs_to
    PRODUCT }o--o| BRAND : belongs_to
    PRODUCT }o--|| UNIT : measured_in
    PRODUCT }o--o| SUPPLIER : preferred

    PRODUCT_VARIANT ||--o{ INVENTORY_MOVEMENT : tracked_by
    PRODUCT_VARIANT ||--o{ INVENTORY_BALANCE : has
    PRODUCT_VARIANT ||--o{ SALE_LINE : sold_as
    PRODUCT_VARIANT ||--o{ PURCHASE_LINE : purchased_as

    SALE ||--o{ SALE_LINE : contains
    SALE ||--o{ SALE_PAYMENT : paid_by
    SALE }o--o| CUSTOMER : sold_to
    SALE }o--|| USER : sold_by
    SALE }o--|| BRANCH : at

    PURCHASE ||--o{ PURCHASE_LINE : contains
    PURCHASE }o--|| SUPPLIER : from
    PURCHASE }o--|| USER : created_by

    SALE ||--o{ SALE_RETURN : returned_from
    SALE_RETURN ||--o{ SALE_RETURN_LINE : contains

    INVENTORY_MOVEMENT }o--|| PRODUCT_VARIANT : for
    INVENTORY_MOVEMENT }o--|| BRANCH : at

    CASH_SHIFT ||--o{ CASH_MOVEMENT : contains
    CASH_SHIFT }o--|| USER : managed_by

    EXPENSE }o--|| EXPENSE_CATEGORY : categorized_as
    EXPENSE }o--|| BRANCH : at

    USER ||--o{ USER_PERMISSION : has
    USER }o--|| BUSINESS : belongs_to

    PLAN ||--o{ PLAN_ENTITLEMENT : grants
    SUBSCRIPTION }o--|| PLAN : on
    SUBSCRIPTION }o--|| BUSINESS : for
```

## Core Entities

### Business (Tenant)

The top-level tenant entity. Everything business-related belongs to a Business.

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| name | string | Business name |
| slug | string | URL-friendly identifier |
| currency_code | string(3) | ISO 4217 currency (e.g., TZS, USD, KES) |
| timezone | string | IANA timezone (e.g., Africa/Dar_es_Salaam) |
| tax_name | string | Tax label (e.g., "VAT") |
| tax_rate | decimal(5,2) | Default tax rate percentage |
| tax_enabled | boolean | Whether tax is applied by default |
| logo_path | string | Path to business logo |
| phone | string | Business phone |
| email | string | Business email |
| address | text | Business address |
| onboarding_completed | boolean | Whether setup wizard is done |
| onboarding_step | integer | Current onboarding step |
| status | enum | active, suspended, cancelled |
| settings | json | Flexible settings (receipt header, etc.) |
| created_at | timestamp | |
| updated_at | timestamp | |

### Branch

A physical business location. V1 focuses on single branch, architecture supports multiple.

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Branch name (e.g., "Main Store") |
| phone | string | Branch phone |
| email | string | Branch email |
| address | text | Branch address |
| is_default | boolean | Default branch for the business |
| is_active | boolean | Whether branch is operational |
| created_at | timestamp | |
| updated_at | timestamp | |

### User

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Full name |
| email | string | Login email (unique globally) |
| password | string | Hashed password |
| phone | string | Phone number |
| role | enum | owner, employee |
| branch_id | uuid | FK → branches (default branch) |
| is_active | boolean | Can this user log in? |
| two_factor_secret | text | 2FA secret (encrypted) |
| two_factor_confirmed_at | timestamp | When 2FA was confirmed |
| email_verified_at | timestamp | |
| last_login_at | timestamp | |
| created_at | timestamp | |
| updated_at | timestamp | |

### UserPermission

Configurable permissions for employees.

| Field | Type | Description |
|-------|------|-------------|
| id | bigint | Primary key |
| user_id | uuid | FK → users |
| permission | string | Permission key (e.g., 'sell', 'view_products', 'adjust_stock') |
| created_at | timestamp | |

### Category

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Category name |
| description | text | Optional description |
| parent_id | uuid | FK → categories (for nesting) |
| sort_order | integer | Display order |
| is_active | boolean | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Brand

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Brand name |
| is_active | boolean | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Unit

Unit of measurement (piece, kg, liter, box, dozen, etc.)

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Unit name (e.g., "Piece") |
| abbreviation | string | Short form (e.g., "pc") |
| is_default | boolean | Default unit for new products |
| created_at | timestamp | |
| updated_at | timestamp | |

### Product

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Product name |
| description | text | Product description |
| category_id | uuid | FK → categories |
| brand_id | uuid | FK → brands (nullable) |
| unit_id | uuid | FK → units |
| preferred_supplier_id | uuid | FK → suppliers (nullable) |
| tax_rate | decimal(5,2) | Product-specific tax rate (null = use business default) |
| reorder_level | decimal(12,4) | Minimum stock threshold |
| image_path | string | Product image path |
| has_variants | boolean | Whether product uses variants |
| is_active | boolean | Available for sale |
| is_archived | boolean | Hidden from active listings |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | Soft delete |

### ProductVariant

Represents a specific sellable/stockable version of a product. Every product has at least one variant (the "default" variant for simple products).

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| product_id | uuid | FK → products |
| name | string | Variant name (e.g., "Red / Large") or product name for default |
| sku | string | Stock Keeping Unit (unique per tenant) |
| barcode | string | Barcode (unique per tenant, nullable) |
| purchase_cost | decimal(12,4) | Last known purchase cost |
| selling_price | decimal(12,4) | Selling price |
| is_default | boolean | True for simple (non-variant) products |
| is_active | boolean | |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | Soft delete |

**Design Decision — Why Every Product Has a Variant:**
Rather than having some products tracked directly and some via variants, every product has at least one `ProductVariant` (the default). This means the inventory engine, sales, and purchases all work with `product_variant_id` consistently. For simple products (no variants), the default variant is auto-created and the user never sees "variant" terminology.

### Supplier

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Supplier name |
| phone | string | |
| email | string | |
| address | text | |
| notes | text | |
| is_active | boolean | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Customer

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Customer name |
| phone | string | |
| email | string | |
| address | text | |
| notes | text | |
| created_at | timestamp | |
| updated_at | timestamp | |

---

## Transaction Entities

### Sale

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| user_id | uuid | FK → users (who made the sale) |
| customer_id | uuid | FK → customers (nullable) |
| receipt_number | string | Sequential per-tenant receipt number |
| subtotal | decimal(14,4) | Sum of line totals before tax |
| tax_amount | decimal(14,4) | Total tax |
| discount_amount | decimal(14,4) | Total discount |
| total | decimal(14,4) | Final total |
| notes | text | |
| status | enum | completed, held, voided |
| completed_at | timestamp | When the sale was finalized |
| idempotency_key | string | Prevents duplicate submissions |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | Soft delete |

### SaleLine

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| sale_id | uuid | FK → sales |
| product_variant_id | uuid | FK → product_variants |
| product_name | string | Snapshot of product name at time of sale |
| sku | string | Snapshot of SKU |
| quantity | decimal(12,4) | Quantity sold |
| unit_price | decimal(12,4) | Price per unit at sale time |
| cost_price | decimal(12,4) | WAC at sale time (for profit calc) |
| discount_amount | decimal(12,4) | Discount on this line |
| tax_rate | decimal(5,2) | Tax rate applied |
| tax_amount | decimal(14,4) | Tax on this line |
| line_total | decimal(14,4) | Final line total (qty × price - discount + tax) |
| created_at | timestamp | |

### SalePayment

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| sale_id | uuid | FK → sales |
| payment_method | enum | cash, card, bank_transfer, mobile_money |
| amount | decimal(14,4) | Amount paid via this method |
| reference | string | Payment reference (nullable) |
| created_at | timestamp | |

### SaleReturn

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| sale_id | uuid | FK → sales (original sale) |
| branch_id | uuid | FK → branches |
| user_id | uuid | FK → users |
| return_number | string | Sequential return number |
| total_refund | decimal(14,4) | Total refund amount |
| refund_method | enum | cash, card, bank_transfer, mobile_money, exchange |
| reason | text | Reason for return |
| created_at | timestamp | |

### SaleReturnLine

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| sale_return_id | uuid | FK → sale_returns |
| sale_line_id | uuid | FK → sale_lines (original line) |
| product_variant_id | uuid | FK → product_variants |
| quantity | decimal(12,4) | Quantity returned |
| unit_price | decimal(12,4) | Original sale price |
| refund_amount | decimal(14,4) | Refund for this line |
| created_at | timestamp | |

### Purchase

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| supplier_id | uuid | FK → suppliers |
| user_id | uuid | FK → users (creator) |
| purchase_number | string | Sequential purchase number |
| subtotal | decimal(14,4) | Sum of line totals |
| tax_amount | decimal(14,4) | Total tax on purchase |
| total | decimal(14,4) | Final total |
| status | enum | draft, ordered, partially_received, received, cancelled |
| notes | text | |
| purchase_date | date | Date of purchase |
| expected_date | date | Expected delivery (nullable) |
| created_at | timestamp | |
| updated_at | timestamp | |
| deleted_at | timestamp | Soft delete |

### PurchaseLine

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| purchase_id | uuid | FK → purchases |
| product_variant_id | uuid | FK → product_variants |
| quantity_ordered | decimal(12,4) | Quantity ordered |
| quantity_received | decimal(12,4) | Quantity received so far |
| unit_cost | decimal(12,4) | Cost per unit |
| tax_rate | decimal(5,2) | Tax rate |
| tax_amount | decimal(14,4) | Tax on this line |
| line_total | decimal(14,4) | Total for this line |
| created_at | timestamp | |
| updated_at | timestamp | |

### PurchaseReceiving

Records each receiving event for a purchase (supports partial receiving).

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| purchase_id | uuid | FK → purchases |
| user_id | uuid | FK → users (who received) |
| received_at | timestamp | When stock was received |
| notes | text | |
| created_at | timestamp | |

### PurchaseReceivingLine

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| purchase_receiving_id | uuid | FK → purchase_receivings |
| purchase_line_id | uuid | FK → purchase_lines |
| product_variant_id | uuid | FK → product_variants |
| quantity_received | decimal(12,4) | Quantity received in this batch |
| created_at | timestamp | |

---

## Inventory Entities

### InventoryMovement

The immutable ledger of all stock changes. This is the **source of truth** for inventory history.

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| product_variant_id | uuid | FK → product_variants |
| type | enum | See movement types below |
| quantity | decimal(12,4) | Signed quantity (+/-) |
| unit_cost | decimal(12,4) | Cost per unit for this movement |
| total_cost | decimal(14,4) | quantity × unit_cost |
| balance_after | decimal(12,4) | Running balance after this movement |
| average_cost_after | decimal(12,4) | WAC after this movement |
| reference_type | string | Polymorphic (Sale, Purchase, SaleReturn, etc.) |
| reference_id | uuid | Polymorphic FK |
| notes | text | Reason / description |
| user_id | uuid | FK → users (who performed the action) |
| posted_at | datetime | When the movement was posted |
| created_at | timestamp | |

**Movement Types:**
| Type | Quantity Sign | Trigger |
|------|--------------|---------|
| `opening_balance` | + | Initial stock setup |
| `purchase_received` | + | Purchase receiving |
| `sale` | - | Sale completion |
| `customer_return` | + | Customer returns product |
| `supplier_return` | - | Return stock to supplier |
| `adjustment_add` | + | Manual stock increase |
| `adjustment_remove` | - | Manual stock decrease |
| `damage` | - | Damaged goods write-off |
| `loss` | - | Lost/stolen goods |
| `transfer_out` | - | Transfer to another branch |
| `transfer_in` | + | Transfer from another branch |

### InventoryBalance

Denormalized snapshot of current stock state per product-variant per branch.

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| product_variant_id | uuid | FK → product_variants |
| quantity_on_hand | decimal(12,4) | Current physical stock |
| quantity_reserved | decimal(12,4) | Reserved (held sales, etc.) |
| quantity_available | decimal(12,4) | on_hand - reserved |
| average_cost | decimal(12,4) | Weighted average cost |
| total_value | decimal(14,4) | quantity_on_hand × average_cost |
| last_movement_at | timestamp | Last stock change |
| created_at | timestamp | |
| updated_at | timestamp | |

**Unique constraint:** `(tenant_id, branch_id, product_variant_id)`

---

## Cash & Expense Entities

### CashShift

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| user_id | uuid | FK → users |
| opening_amount | decimal(14,4) | Starting cash |
| expected_amount | decimal(14,4) | Calculated expected cash at close |
| counted_amount | decimal(14,4) | Actual counted cash (nullable until closed) |
| variance | decimal(14,4) | counted - expected |
| status | enum | open, closed |
| opened_at | timestamp | |
| closed_at | timestamp | |
| notes | text | |
| created_at | timestamp | |
| updated_at | timestamp | |

### CashMovement

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| cash_shift_id | uuid | FK → cash_shifts |
| type | enum | cash_sale, cash_refund, cash_in, cash_out |
| amount | decimal(14,4) | Signed amount (+/-) |
| description | string | Reason |
| reference_type | string | Polymorphic (Sale, SaleReturn, etc.) |
| reference_id | uuid | Polymorphic FK |
| user_id | uuid | FK → users |
| created_at | timestamp | |

### ExpenseCategory

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| name | string | Category name |
| is_active | boolean | |
| created_at | timestamp | |
| updated_at | timestamp | |

### Expense

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| branch_id | uuid | FK → branches |
| expense_category_id | uuid | FK → expense_categories |
| amount | decimal(14,4) | Expense amount |
| date | date | Expense date |
| description | text | Description |
| payment_method | enum | cash, card, bank_transfer, mobile_money |
| reference | string | Receipt/reference number |
| user_id | uuid | FK → users |
| created_at | timestamp | |
| updated_at | timestamp | |

---

## Platform / SaaS Entities

### Plan

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| name | string | Plan name (Starter, Business, etc.) |
| slug | string | URL identifier |
| description | text | |
| price_monthly | decimal(10,2) | Monthly price |
| price_yearly | decimal(10,2) | Yearly price |
| is_active | boolean | Available for new subscriptions |
| sort_order | integer | Display order |
| created_at | timestamp | |
| updated_at | timestamp | |

### PlanEntitlement

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| plan_id | uuid | FK → plans |
| feature | string | Feature key (e.g., 'multi_branch', 'export_reports') |
| limit | integer | Numeric limit (null = unlimited) |
| created_at | timestamp | |

### Subscription

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses |
| plan_id | uuid | FK → plans |
| status | enum | trial, active, past_due, cancelled, expired |
| trial_ends_at | timestamp | Trial expiry |
| starts_at | timestamp | Subscription start |
| ends_at | timestamp | Current period end |
| cancelled_at | timestamp | When cancelled |
| grace_ends_at | timestamp | Grace period after expiry |
| created_at | timestamp | |
| updated_at | timestamp | |

---

## Audit Entity

### AuditLog

| Field | Type | Description |
|-------|------|-------------|
| id | uuid | Primary key |
| tenant_id | uuid | FK → businesses (nullable for platform events) |
| user_id | uuid | FK → users |
| action | string | What happened (e.g., 'product.updated', 'sale.completed') |
| auditable_type | string | Polymorphic model type |
| auditable_id | uuid | Polymorphic model ID |
| old_values | json | Previous state (for updates) |
| new_values | json | New state |
| ip_address | string | |
| user_agent | string | |
| metadata | json | Additional context |
| created_at | timestamp | |

---

## Notification Entity

### Notification (uses Laravel's built-in notifications table)

Standard Laravel `notifications` table with `tenant_id` added for tenant scoping.

---

## Enums

```php
enum UserRole: string {
    case Owner = 'owner';
    case Employee = 'employee';
}

enum SaleStatus: string {
    case Completed = 'completed';
    case Held = 'held';
    case Voided = 'voided';
}

enum PurchaseStatus: string {
    case Draft = 'draft';
    case Ordered = 'ordered';
    case PartiallyReceived = 'partially_received';
    case Received = 'received';
    case Cancelled = 'cancelled';
}

enum PaymentMethod: string {
    case Cash = 'cash';
    case Card = 'card';
    case BankTransfer = 'bank_transfer';
    case MobileMoney = 'mobile_money';
}

enum InventoryMovementType: string {
    case OpeningBalance = 'opening_balance';
    case PurchaseReceived = 'purchase_received';
    case Sale = 'sale';
    case CustomerReturn = 'customer_return';
    case SupplierReturn = 'supplier_return';
    case AdjustmentAdd = 'adjustment_add';
    case AdjustmentRemove = 'adjustment_remove';
    case Damage = 'damage';
    case Loss = 'loss';
    case TransferOut = 'transfer_out';
    case TransferIn = 'transfer_in';
}

enum CashMovementType: string {
    case CashSale = 'cash_sale';
    case CashRefund = 'cash_refund';
    case CashIn = 'cash_in';
    case CashOut = 'cash_out';
}

enum CashShiftStatus: string {
    case Open = 'open';
    case Closed = 'closed';
}

enum BusinessStatus: string {
    case Active = 'active';
    case Suspended = 'suspended';
    case Cancelled = 'cancelled';
}

enum SubscriptionStatus: string {
    case Trial = 'trial';
    case Active = 'active';
    case PastDue = 'past_due';
    case Cancelled = 'cancelled';
    case Expired = 'expired';
}
```

## Key Design Decisions

### 1. ProductVariant as the Unit of Tracking
Every product has at least one `ProductVariant`. The inventory system, sales, and purchases all reference `product_variant_id`. This avoids branching logic between "simple" and "variant" products throughout the codebase.

### 2. Signed Quantity in Movements
Inventory movements use signed quantities (positive for additions, negative for deductions). This simplifies balance calculation: `SUM(quantity)` over movements equals the current balance.

### 3. Snapshot Data in Sales
Sale lines store product names, SKUs, and prices at the time of sale. Later product changes never alter historical receipts.

### 4. Separate Return Entities
Returns are separate entities that reference original sales. We never modify completed sale records.

### 5. Receiving as Separate Entity
Purchase receiving is tracked separately from the purchase itself, supporting partial receiving and providing a clear audit trail of when stock actually arrived.

### 6. Decimal Precision
All monetary values use `decimal(14,4)` for precision. Quantities use `decimal(12,4)` to support fractional units (kg, liters). Tax rates use `decimal(5,2)`.
