Files
bakery-ia/services/inventory/app/services/internal_transfer_service.py
2025-11-30 09:12:40 +01:00

484 lines
18 KiB
Python

"""
Internal Transfer Service for Inventory Management
Handles inventory ownership changes during internal transfers
"""
import logging
from typing import Dict, Any, List
from datetime import datetime
from decimal import Decimal
import uuid
from shared.clients.tenant_client import TenantServiceClient
from shared.clients.inventory_client import InventoryServiceClient
logger = logging.getLogger(__name__)
class InternalTransferInventoryService:
"""
Service for handling inventory transfers during enterprise internal transfers
"""
def __init__(
self,
tenant_client: TenantServiceClient,
inventory_client: InventoryServiceClient
):
self.tenant_client = tenant_client
self.inventory_client = inventory_client
async def process_internal_delivery(
self,
parent_tenant_id: str,
child_tenant_id: str,
shipment_items: List[Dict[str, Any]],
shipment_id: str
) -> Dict[str, Any]:
"""
Process inventory ownership transfer when internal shipment is delivered
Args:
parent_tenant_id: Source tenant (central production)
child_tenant_id: Destination tenant (retail outlet)
shipment_items: List of items being transferred with quantities
shipment_id: ID of the shipment for reference
Returns:
Dict with transfer results
"""
try:
logger.info(
"Processing internal inventory transfer",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
shipment_id=shipment_id,
item_count=len(shipment_items)
)
# Process each item in the shipment
successful_transfers = []
failed_transfers = []
for item in shipment_items:
product_id = item.get('product_id')
quantity = Decimal(str(item.get('delivered_quantity', item.get('quantity', 0))))
if not product_id or quantity <= 0:
logger.warning(
"Skipping invalid transfer item",
product_id=product_id,
quantity=quantity
)
continue
try:
# Step 1: Deduct inventory from parent (central production)
parent_subtraction_result = await self._subtract_from_parent_inventory(
parent_tenant_id=parent_tenant_id,
product_id=product_id,
quantity=quantity,
shipment_id=shipment_id
)
# Step 2: Add inventory to child (retail outlet)
child_addition_result = await self._add_to_child_inventory(
child_tenant_id=child_tenant_id,
product_id=product_id,
quantity=quantity,
shipment_id=shipment_id
)
successful_transfers.append({
'product_id': product_id,
'quantity': float(quantity),
'parent_result': parent_subtraction_result,
'child_result': child_addition_result
})
logger.info(
"Internal inventory transfer completed",
product_id=product_id,
quantity=float(quantity),
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id
)
except Exception as item_error:
logger.error(
"Failed to process inventory transfer for item",
product_id=product_id,
quantity=float(quantity),
error=str(item_error),
exc_info=True
)
failed_transfers.append({
'product_id': product_id,
'quantity': float(quantity),
'error': str(item_error)
})
# Update shipment status in inventory records to reflect completed transfer
await self._mark_shipment_as_completed_in_inventory(
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
shipment_id=shipment_id
)
total_transferred = sum(item['quantity'] for item in successful_transfers)
result = {
'shipment_id': shipment_id,
'parent_tenant_id': parent_tenant_id,
'child_tenant_id': child_tenant_id,
'transfers_completed': len(successful_transfers),
'transfers_failed': len(failed_transfers),
'total_quantity_transferred': total_transferred,
'successful_transfers': successful_transfers,
'failed_transfers': failed_transfers,
'status': 'completed' if failed_transfers == 0 else 'partial_success',
'processed_at': datetime.utcnow().isoformat()
}
logger.info(
"Internal inventory transfer processing completed",
shipment_id=shipment_id,
successfully_processed=len(successful_transfers),
failed_count=len(failed_transfers)
)
return result
except Exception as e:
logger.error(
"Error processing internal inventory transfer",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
shipment_id=shipment_id,
error=str(e),
exc_info=True
)
raise
async def _subtract_from_parent_inventory(
self,
parent_tenant_id: str,
product_id: str,
quantity: Decimal,
shipment_id: str
) -> Dict[str, Any]:
"""
Subtract inventory from parent tenant (central production)
"""
try:
# Check current inventory level in parent
parent_stock = await self.inventory_client.get_product_stock(
tenant_id=parent_tenant_id,
product_id=product_id
)
current_stock = Decimal(str(parent_stock.get('available_quantity', 0)))
if current_stock < quantity:
raise ValueError(
f"Insufficient inventory in parent tenant {parent_tenant_id}. "
f"Required: {quantity}, Available: {current_stock}"
)
# Create stock movement record with negative quantity
stock_movement_data = {
'product_id': product_id,
'movement_type': 'INTERNAL_TRANSFER_OUT',
'quantity': float(-quantity), # Negative for outbound
'reference_type': 'internal_transfer',
'reference_id': shipment_id,
'source_tenant_id': parent_tenant_id,
'destination_tenant_id': parent_tenant_id, # Self-reference for tracking
'notes': f'Shipment to child tenant #{shipment_id}'
}
# Execute the stock movement
movement_result = await self.inventory_client.create_stock_movement(
tenant_id=parent_tenant_id,
movement_data=stock_movement_data
)
logger.info(
"Inventory subtracted from parent",
parent_tenant_id=parent_tenant_id,
product_id=product_id,
quantity=float(quantity),
movement_id=movement_result.get('id')
)
return {
'movement_id': movement_result.get('id'),
'quantity_subtracted': float(quantity),
'new_balance': float(current_stock - quantity),
'status': 'success'
}
except Exception as e:
logger.error(
"Error subtracting from parent inventory",
parent_tenant_id=parent_tenant_id,
product_id=product_id,
quantity=float(quantity),
error=str(e)
)
raise
async def _add_to_child_inventory(
self,
child_tenant_id: str,
product_id: str,
quantity: Decimal,
shipment_id: str
) -> Dict[str, Any]:
"""
Add inventory to child tenant (retail outlet)
"""
try:
# Create stock movement record with positive quantity
stock_movement_data = {
'product_id': product_id,
'movement_type': 'INTERNAL_TRANSFER_IN',
'quantity': float(quantity), # Positive for inbound
'reference_type': 'internal_transfer',
'reference_id': shipment_id,
'source_tenant_id': child_tenant_id, # Self-reference from parent
'destination_tenant_id': child_tenant_id,
'notes': f'Internal transfer from parent tenant shipment #{shipment_id}'
}
# Execute the stock movement
movement_result = await self.inventory_client.create_stock_movement(
tenant_id=child_tenant_id,
movement_data=stock_movement_data
)
logger.info(
"Inventory added to child",
child_tenant_id=child_tenant_id,
product_id=product_id,
quantity=float(quantity),
movement_id=movement_result.get('id')
)
return {
'movement_id': movement_result.get('id'),
'quantity_added': float(quantity),
'status': 'success'
}
except Exception as e:
logger.error(
"Error adding to child inventory",
child_tenant_id=child_tenant_id,
product_id=product_id,
quantity=float(quantity),
error=str(e)
)
raise
async def _mark_shipment_as_completed_in_inventory(
self,
parent_tenant_id: str,
child_tenant_id: str,
shipment_id: str
):
"""
Update inventory records to mark shipment as completed
"""
try:
# In a real implementation, this would update inventory tracking records
# to reflect that the internal transfer is complete
# For now, we'll just log that we're tracking this
logger.info(
"Marked internal transfer as completed in inventory tracking",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
shipment_id=shipment_id
)
except Exception as e:
logger.error(
"Error updating inventory completion status",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
shipment_id=shipment_id,
error=str(e)
)
# This is not critical enough to fail the entire operation
async def get_internal_transfer_history(
self,
parent_tenant_id: str,
child_tenant_id: str = None,
start_date: str = None,
end_date: str = None,
limit: int = 100
) -> List[Dict[str, Any]]:
"""
Get history of internal inventory transfers
Args:
parent_tenant_id: Parent tenant ID
child_tenant_id: Optional child tenant ID to filter by
start_date: Optional start date filter
end_date: Optional end date filter
limit: Max results to return
Returns:
List of internal transfer records
"""
try:
# Build filter conditions
filters = {
'reference_type': 'internal_transfer'
}
if child_tenant_id:
filters['destination_tenant_id'] = child_tenant_id
if start_date:
filters['created_after'] = start_date
if end_date:
filters['created_before'] = end_date
# Query inventory movements for internal transfers
parent_movements = await self.inventory_client.get_stock_movements(
tenant_id=parent_tenant_id,
filters=filters,
limit=limit
)
# Filter for outbound transfers (negative values)
outbound_transfers = [m for m in parent_movements if m.get('quantity', 0) < 0]
# Also get inbound transfers for the children if specified
all_transfers = outbound_transfers
if child_tenant_id:
child_movements = await self.inventory_client.get_stock_movements(
tenant_id=child_tenant_id,
filters=filters,
limit=limit
)
# Filter for inbound transfers (positive values)
inbound_transfers = [m for m in child_movements if m.get('quantity', 0) > 0]
all_transfers.extend(inbound_transfers)
# Sort by creation date (most recent first)
all_transfers.sort(key=lambda x: x.get('created_at', ''), reverse=True)
return all_transfers[:limit]
except Exception as e:
logger.error(
"Error getting internal transfer history",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
error=str(e)
)
raise
async def validate_internal_transfer_eligibility(
self,
parent_tenant_id: str,
child_tenant_id: str,
items: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""
Validate that internal transfer is possible (sufficient inventory, etc.)
Args:
parent_tenant_id: Parent tenant ID (supplier)
child_tenant_id: Child tenant ID (recipient)
items: List of items to transfer
Returns:
Dict with validation results
"""
try:
logger.info(
"Validating internal transfer eligibility",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
item_count=len(items)
)
validation_results = {
'eligible': True,
'errors': [],
'warnings': [],
'inventory_check': []
}
for item in items:
product_id = item.get('product_id')
quantity = Decimal(str(item.get('quantity', 0)))
if quantity <= 0:
validation_results['errors'].append({
'product_id': product_id,
'error': 'Quantity must be greater than 0',
'quantity': float(quantity)
})
continue
# Check if parent has sufficient inventory
try:
parent_stock = await self.inventory_client.get_product_stock(
tenant_id=parent_tenant_id,
product_id=product_id
)
available_quantity = Decimal(str(parent_stock.get('available_quantity', 0)))
if available_quantity < quantity:
validation_results['errors'].append({
'product_id': product_id,
'error': 'Insufficient inventory in parent tenant',
'available': float(available_quantity),
'requested': float(quantity)
})
else:
validation_results['inventory_check'].append({
'product_id': product_id,
'available': float(available_quantity),
'requested': float(quantity),
'sufficient': True
})
except Exception as stock_error:
logger.error(
"Error checking parent inventory for validation",
product_id=product_id,
error=str(stock_error)
)
validation_results['errors'].append({
'product_id': product_id,
'error': f'Error checking inventory: {str(stock_error)}'
})
# Overall eligibility based on errors
validation_results['eligible'] = len(validation_results['errors']) == 0
logger.info(
"Internal transfer validation completed",
eligible=validation_results['eligible'],
error_count=len(validation_results['errors'])
)
return validation_results
except Exception as e:
logger.error(
"Error validating internal transfer eligibility",
parent_tenant_id=parent_tenant_id,
child_tenant_id=child_tenant_id,
error=str(e)
)
raise