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>
This commit is contained in:
2026-01-01 18:12:09 +01:00
parent 871f52da80
commit 049e3319c3
5 changed files with 468 additions and 7 deletions

View File

@@ -9,6 +9,8 @@ This service orchestrates inventory operations for orders:
This is the critical link between the order and inventory systems
that ensures stock accuracy.
All operations are logged to the inventory_transactions table for audit trail.
"""
import logging
@@ -22,6 +24,7 @@ from app.exceptions import (
)
from app.services.inventory_service import inventory_service
from models.database.inventory import Inventory
from models.database.inventory_transaction import InventoryTransaction, TransactionType
from models.database.order import Order, OrderItem
from models.schema.inventory import InventoryReserve
@@ -84,6 +87,51 @@ class OrderInventoryService:
# Check if it's the placeholder product (GTIN 0000000000000)
return order_item.product.gtin == "0000000000000"
def _log_transaction(
self,
db: Session,
vendor_id: int,
product_id: int,
inventory: Inventory,
transaction_type: TransactionType,
quantity_change: int,
order: Order,
reason: str | None = None,
) -> InventoryTransaction:
"""
Create an inventory transaction record for audit trail.
Args:
db: Database session
vendor_id: Vendor ID
product_id: Product ID
inventory: Inventory record after the operation
transaction_type: Type of transaction
quantity_change: Change in quantity (positive = add, negative = remove)
order: Order associated with this transaction
reason: Optional reason for the transaction
Returns:
Created InventoryTransaction
"""
transaction = InventoryTransaction.create_transaction(
vendor_id=vendor_id,
product_id=product_id,
inventory_id=inventory.id if inventory else None,
transaction_type=transaction_type,
quantity_change=quantity_change,
quantity_after=inventory.quantity if inventory else 0,
reserved_after=inventory.reserved_quantity if inventory else 0,
location=inventory.location if inventory else None,
warehouse=inventory.warehouse if inventory else None,
order_id=order.id,
order_number=order.order_number,
reason=reason,
created_by="system",
)
db.add(transaction)
return transaction
def reserve_for_order(
self,
db: Session,
@@ -142,9 +190,23 @@ class OrderInventoryService:
location=location,
quantity=item.quantity,
)
inventory_service.reserve_inventory(db, vendor_id, reserve_data)
updated_inventory = inventory_service.reserve_inventory(
db, vendor_id, reserve_data
)
reserved_count += 1
# Log transaction for audit trail
self._log_transaction(
db=db,
vendor_id=vendor_id,
product_id=item.product_id,
inventory=updated_inventory,
transaction_type=TransactionType.RESERVE,
quantity_change=0, # Reserve doesn't change quantity, only reserved_quantity
order=order,
reason=f"Reserved for order {order.order_number}",
)
logger.info(
f"Reserved {item.quantity} units of product {item.product_id} "
f"for order {order.order_number}"
@@ -242,9 +304,23 @@ class OrderInventoryService:
location=location,
quantity=item.quantity,
)
inventory_service.fulfill_reservation(db, vendor_id, reserve_data)
updated_inventory = inventory_service.fulfill_reservation(
db, vendor_id, reserve_data
)
fulfilled_count += 1
# Log transaction for audit trail
self._log_transaction(
db=db,
vendor_id=vendor_id,
product_id=item.product_id,
inventory=updated_inventory,
transaction_type=TransactionType.FULFILL,
quantity_change=-item.quantity, # Negative because stock is consumed
order=order,
reason=f"Fulfilled for order {order.order_number}",
)
logger.info(
f"Fulfilled {item.quantity} units of product {item.product_id} "
f"for order {order.order_number}"
@@ -335,9 +411,23 @@ class OrderInventoryService:
location=inventory.location,
quantity=item.quantity,
)
inventory_service.release_reservation(db, vendor_id, reserve_data)
updated_inventory = inventory_service.release_reservation(
db, vendor_id, reserve_data
)
released_count += 1
# Log transaction for audit trail
self._log_transaction(
db=db,
vendor_id=vendor_id,
product_id=item.product_id,
inventory=updated_inventory,
transaction_type=TransactionType.RELEASE,
quantity_change=0, # Release doesn't change quantity, only reserved_quantity
order=order,
reason=f"Released for cancelled order {order.order_number}",
)
logger.info(
f"Released {item.quantity} units of product {item.product_id} "
f"for cancelled order {order.order_number}"