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>
This commit is contained in:
196
docs/implementation/stock-management-integration.md
Normal file
196
docs/implementation/stock-management-integration.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# 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, 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)
|
||||
|
||||
```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,
|
||||
vendor_id=vendor_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,
|
||||
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
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user