Files
orion/docs/implementation/stock-management-integration.md
Samir Boulahtit 871f52da80 feat: add automatic stock synchronization for orders
Implements order-inventory integration that automatically manages stock
when order status changes:
- processing: reserves inventory for order items
- shipped: fulfills (deducts) from stock
- cancelled: releases reserved inventory

Creates OrderInventoryService to orchestrate operations between
OrderService and InventoryService. Placeholder products (unmatched
Letzshop items) are skipped. Inventory errors are logged but don't
block order status updates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 18:05:44 +01:00

5.7 KiB

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:

def _find_inventory_location(db, product_id, vendor_id):
    return (
        db.query(Inventory)
        .filter(
            Inventory.product_id == product_id,
            Inventory.vendor_id == vendor_id,
            Inventory.quantity > Inventory.reserved_quantity,
        )
        .first()
    )

Usage Example

Automatic (Via Order Status Update)

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,
    vendor_id=vendor_id,
    order_id=order_id,
    order_update=OrderUpdate(status="processing")
)
# Inventory is now reserved for this order

Direct (Manual Operations)

from app.services.order_inventory_service import order_inventory_service

# Reserve inventory for an order
result = order_inventory_service.reserve_for_order(
    db=db,
    vendor_id=vendor_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,
    vendor_id=vendor_id,
    order_id=order_id
)

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

Inventory Model

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

Future Enhancements

  1. Inventory Transaction Log - Audit trail for all stock movements
  2. Multi-Location Selection - Choose which location to draw from
  3. Backorder Support - Handle orders when stock is insufficient
  4. Return Processing - Increase stock when orders are returned