Files
orion/docs/implementation/stock-management-integration.md
Samir Boulahtit 5a3f2bce57 feat: add partial shipment support (Phase 3)
- Add shipped_quantity field to OrderItem for tracking partial fulfillment
- Add partially_shipped order status for orders with partial shipments
- Add fulfill_item method for shipping individual items with quantities
- Add get_shipment_status method for detailed shipment tracking
- Add vendor API endpoints for partial shipment operations:
  - GET /orders/{id}/shipment-status - Get item-level shipment status
  - POST /orders/{id}/items/{item_id}/ship - Ship specific item quantity
- Automatic status updates: partially_shipped when some items shipped,
  shipped when all items fully shipped
- Migration to add shipped_quantity column with upgrade for existing data
- Update documentation with partial shipment usage examples

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

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

9.8 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()

Partial Shipments (Phase 3)

Orders can be partially shipped, allowing vendors 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:

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

GET /api/v1/vendor/orders/{order_id}/shipment-status

Returns item-level shipment status:

{
    "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

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

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

Response:

{
    "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

from app.services.order_inventory_service import order_inventory_service

# Ship partial quantity of an item
result = order_inventory_service.fulfill_item(
    db=db,
    vendor_id=vendor_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,
    vendor_id=vendor_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