Move 39 documentation files from top-level docs/ into each module's docs/ folder, accessible via symlinks from docs/modules/. Create data-model.md files for 10 modules with full schema documentation. Replace originals with redirect stubs. Remove empty guide stubs. Modules migrated: tenancy, billing, loyalty, marketplace, orders, messaging, cms, catalog, inventory, hosting, prospecting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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:
-
For each order item:
- Find inventory record with available quantity
- Increase
reserved_quantityby item quantity - Log the reservation
-
Placeholder products (unmatched Letzshop items) are skipped
Fulfill Inventory
When an order status changes to shipped:
- For each order item:
- Decrease
quantityby item quantity (stock consumed) - Decrease
reserved_quantityaccordingly - Log the fulfillment
- Decrease
Release Reservation
When an order is cancelled:
- For each order item:
- Decrease
reserved_quantity(stock becomes available again) - Total
quantityremains unchanged - Log the release
- Decrease
Error Handling
Inventory operations use soft failure - if inventory cannot be updated:
- Warning is logged
- Order status update continues
- 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_itemslist - No error is raised
Multi-Location Inventory
The service finds the first location with available stock:
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)
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)
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
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
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
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:
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/store/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/store/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:
- If some items are shipped → status becomes
partially_shipped - 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,
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
- Multi-Location Selection - Choose which location to draw from
- Backorder Support - Handle orders when stock is insufficient
- Return Processing - Increase stock when orders are returned