# Stock Management Integration

**Created:** January 1, 2026
**Status:** Implemented

## Overview

This document describes the automatic inventory synchronization between orders and stock levels. When order status changes, inventory is automatically updated to maintain accurate stock counts.

## Architecture

### Services Involved

```
OrderService                     OrderInventoryService
    │                                    │
    ├─ update_order_status() ──────────► handle_status_change()
    │                                    │
    │                                    ├─► reserve_for_order()
    │                                    ├─► fulfill_order()
    │                                    └─► release_order_reservation()
    │                                            │
    │                                            ▼
    │                               InventoryService
    │                                    │
    │                                    ├─► reserve_inventory()
    │                                    ├─► fulfill_reservation()
    │                                    └─► release_reservation()
```

### Key Files

| File | Purpose |
|------|---------|
| `app/services/order_inventory_service.py` | Orchestrates order-inventory operations |
| `app/services/order_service.py` | Calls inventory hooks on status change |
| `app/services/inventory_service.py` | Low-level inventory operations |

## Status Change Inventory Actions

| Status Transition | Inventory Action | Description |
|-------------------|------------------|-------------|
| Any → `processing` | Reserve | Reserves stock for order items |
| Any → `shipped` | Fulfill | Deducts from stock and releases reservation |
| Any → `cancelled` | Release | Returns reserved stock to available |

## Inventory Operations

### Reserve Inventory

When an order status changes to `processing`:

1. For each order item:
   - Find inventory record with available quantity
   - Increase `reserved_quantity` by item quantity
   - Log the reservation

2. Placeholder products (unmatched Letzshop items) are skipped

### Fulfill Inventory

When an order status changes to `shipped`:

1. For each order item:
   - Decrease `quantity` by item quantity (stock consumed)
   - Decrease `reserved_quantity` accordingly
   - Log the fulfillment

### Release Reservation

When an order is `cancelled`:

1. For each order item:
   - Decrease `reserved_quantity` (stock becomes available again)
   - Total `quantity` remains unchanged
   - Log the release

## Error Handling

Inventory operations use **soft failure** - if inventory cannot be updated:

1. Warning is logged
2. Order status update continues
3. Inventory can be manually adjusted

This ensures orders are never blocked by inventory issues while providing visibility into any problems.

## Edge Cases

### Placeholder Products

Letzshop orders may contain unmatched GTINs that map to placeholder products. These are identified by:
- GTIN `0000000000000`
- Product linked to placeholder MarketplaceProduct

Inventory operations skip placeholder products since they have no real stock.

### Missing Inventory

If a product has no inventory record:
- Operation is skipped with `skip_missing=True`
- Item is logged in `skipped_items` list
- No error is raised

### Multi-Location Inventory

The service finds the first location with available stock:
```python
def _find_inventory_location(db, product_id, store_id):
    return (
        db.query(Inventory)
        .filter(
            Inventory.product_id == product_id,
            Inventory.store_id == store_id,
            Inventory.quantity > Inventory.reserved_quantity,
        )
        .first()
    )
```

## Usage Example

### Automatic (Via Order Status Update)

```python
from app.services.order_service import order_service
from models.schema.order import OrderUpdate

# Update order status - inventory is handled automatically
order = order_service.update_order_status(
    db=db,
    store_id=store_id,
    order_id=order_id,
    order_update=OrderUpdate(status="processing")
)
# Inventory is now reserved for this order
```

### Direct (Manual Operations)

```python
from app.services.order_inventory_service import order_inventory_service

# Reserve inventory for an order
result = order_inventory_service.reserve_for_order(
    db=db,
    store_id=store_id,
    order_id=order_id,
    skip_missing=True
)
print(f"Reserved: {result['reserved_count']}, Skipped: {len(result['skipped_items'])}")

# Fulfill when shipped
result = order_inventory_service.fulfill_order(
    db=db,
    store_id=store_id,
    order_id=order_id
)

# Release if cancelled
result = order_inventory_service.release_order_reservation(
    db=db,
    store_id=store_id,
    order_id=order_id
)
```

## Inventory Model

```python
class Inventory:
    quantity: int          # Total stock
    reserved_quantity: int # Reserved for pending orders

    @property
    def available_quantity(self):
        return self.quantity - self.reserved_quantity
```

## Logging

All inventory operations are logged:

```
INFO: Reserved 2 units of product 123 for order ORD-1-20260101-ABC123
INFO: Order ORD-1-20260101-ABC123: reserved 3 items, skipped 1
INFO: Fulfilled 2 units of product 123 for order ORD-1-20260101-ABC123
WARNING: Order ORD-1-20260101-ABC123 inventory operation failed: No inventory found
```

## Audit Trail (Phase 2)

All inventory operations are logged to the `inventory_transactions` table.

### Transaction Types

| Type | Description |
|------|-------------|
| `reserve` | Stock reserved for order |
| `fulfill` | Reserved stock consumed (shipped) |
| `release` | Reserved stock released (cancelled) |
| `adjust` | Manual adjustment (+/-) |
| `set` | Set to exact quantity |
| `import` | Initial import/sync |
| `return` | Stock returned from customer |

### Transaction Record

```python
class InventoryTransaction:
    id: int
    store_id: int
    product_id: int
    inventory_id: int | None
    transaction_type: TransactionType
    quantity_change: int       # Positive = add, negative = remove
    quantity_after: int        # Snapshot after transaction
    reserved_after: int        # Snapshot after transaction
    location: str | None
    warehouse: str | None
    order_id: int | None       # Link to order if applicable
    order_number: str | None
    reason: str | None         # Human-readable reason
    created_by: str | None     # User/system identifier
    created_at: datetime
```

### Example Transaction Query

```python
from models.database import InventoryTransaction, TransactionType

# Get all transactions for an order
transactions = db.query(InventoryTransaction).filter(
    InventoryTransaction.order_id == order_id
).order_by(InventoryTransaction.created_at).all()

# Get recent stock changes for a product
recent = db.query(InventoryTransaction).filter(
    InventoryTransaction.product_id == product_id,
    InventoryTransaction.store_id == store_id,
).order_by(InventoryTransaction.created_at.desc()).limit(10).all()
```

## Partial Shipments (Phase 3)

Orders can be partially shipped, allowing stores to ship items as they become available.

### Status Flow

```
pending → processing → partially_shipped → shipped → delivered
                    ↘                     ↗
                      → shipped (if all items shipped at once)
```

### OrderItem Tracking

Each order item has a `shipped_quantity` field:

```python
class OrderItem:
    quantity: int           # Total ordered
    shipped_quantity: int   # Units shipped so far

    @property
    def remaining_quantity(self):
        return self.quantity - self.shipped_quantity

    @property
    def is_fully_shipped(self):
        return self.shipped_quantity >= self.quantity
```

### API Endpoints

#### Get Shipment Status

```http
GET /api/v1/store/orders/{order_id}/shipment-status
```

Returns item-level shipment status:
```json
{
    "order_id": 123,
    "order_number": "ORD-1-20260101-ABC123",
    "order_status": "partially_shipped",
    "is_fully_shipped": false,
    "is_partially_shipped": true,
    "shipped_item_count": 1,
    "total_item_count": 3,
    "total_shipped_units": 2,
    "total_ordered_units": 5,
    "items": [
        {
            "item_id": 1,
            "product_name": "Widget A",
            "quantity": 2,
            "shipped_quantity": 2,
            "remaining_quantity": 0,
            "is_fully_shipped": true
        },
        {
            "item_id": 2,
            "product_name": "Widget B",
            "quantity": 3,
            "shipped_quantity": 0,
            "remaining_quantity": 3,
            "is_fully_shipped": false
        }
    ]
}
```

#### Ship Individual Item

```http
POST /api/v1/store/orders/{order_id}/items/{item_id}/ship
Content-Type: application/json

{
    "quantity": 2  // Optional - defaults to remaining quantity
}
```

Response:
```json
{
    "order_id": 123,
    "item_id": 1,
    "fulfilled_quantity": 2,
    "shipped_quantity": 2,
    "remaining_quantity": 0,
    "is_fully_shipped": true
}
```

### Automatic Status Updates

When shipping items:
1. If some items are shipped → status becomes `partially_shipped`
2. If all items are fully shipped → status becomes `shipped`

### Service Usage

```python
from app.services.order_inventory_service import order_inventory_service

# Ship partial quantity of an item
result = order_inventory_service.fulfill_item(
    db=db,
    store_id=store_id,
    order_id=order_id,
    item_id=item_id,
    quantity=2,  # Ship 2 units
)

# Get shipment status
status = order_inventory_service.get_shipment_status(
    db=db,
    store_id=store_id,
    order_id=order_id,
)
```

## Future Enhancements

1. **Multi-Location Selection** - Choose which location to draw from
2. **Backorder Support** - Handle orders when stock is insufficient
3. **Return Processing** - Increase stock when orders are returned
