Files
orion/docs/implementation/stock-management-integration.md
Samir Boulahtit 049e3319c3 feat: add inventory transaction audit trail (Phase 2)
Adds complete audit trail for all stock movements:
- InventoryTransaction model with transaction types (reserve, fulfill,
  release, adjust, set, import, return)
- Alembic migration for inventory_transactions table
- Transaction logging in order_inventory_service for all order operations
- Captures quantity snapshots, order references, and timestamps

Each inventory operation now creates a transaction record for
accountability and debugging.

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

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

7.3 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

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

class InventoryTransaction:
    id: int
    vendor_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

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.vendor_id == vendor_id,
).order_by(InventoryTransaction.created_at.desc()).limit(10).all()

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
  4. Transaction API - Endpoint to view inventory history