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:
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user