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:
2026-01-01 18:05:44 +01:00
parent abeacbe25a
commit 871f52da80
4 changed files with 649 additions and 1 deletions

View 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