# 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