New enterprise feature
This commit is contained in:
@@ -24,17 +24,14 @@ from shared.utils.demo_dates import adjust_date_for_demo, BASE_REFERENCE_DATE
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
|
||||
# Internal API key for service-to-service auth
|
||||
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
|
||||
DEMO_TENANT_PROFESSIONAL = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
|
||||
|
||||
def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)):
|
||||
"""Verify internal API key for service-to-service communication"""
|
||||
if x_internal_api_key != INTERNAL_API_KEY:
|
||||
from app.core.config import settings
|
||||
if x_internal_api_key != settings.INTERNAL_API_KEY:
|
||||
logger.warning("Unauthorized internal API access attempted")
|
||||
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
||||
return True
|
||||
|
||||
256
services/inventory/app/consumers/inventory_transfer_consumer.py
Normal file
256
services/inventory/app/consumers/inventory_transfer_consumer.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""
|
||||
Inventory Transfer Event Consumer
|
||||
Listens for completed internal transfers and handles inventory ownership transfer
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import structlog
|
||||
from typing import Dict, Any
|
||||
import json
|
||||
|
||||
from app.services.internal_transfer_service import InternalTransferInventoryService
|
||||
from shared.messaging.rabbitmq import RabbitMQClient
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class InventoryTransferEventConsumer:
|
||||
"""
|
||||
Consumer for inventory transfer events triggered by internal transfers
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
internal_transfer_service: InternalTransferInventoryService,
|
||||
rabbitmq_client: RabbitMQClient
|
||||
):
|
||||
self.internal_transfer_service = internal_transfer_service
|
||||
self.rabbitmq_client = rabbitmq_client
|
||||
self.is_running = False
|
||||
|
||||
async def start_consuming(self):
|
||||
"""
|
||||
Start consuming inventory transfer events
|
||||
"""
|
||||
logger.info("Starting inventory transfer event consumer")
|
||||
self.is_running = True
|
||||
|
||||
# Declare exchange and queue for internal transfer events
|
||||
await self.rabbitmq_client.declare_exchange("internal_transfers", "topic")
|
||||
await self.rabbitmq_client.declare_queue("inventory_service_internal_transfers")
|
||||
await self.rabbitmq_client.bind_queue_to_exchange(
|
||||
queue_name="inventory_service_internal_transfers",
|
||||
exchange_name="internal_transfers",
|
||||
routing_key="internal_transfer.completed"
|
||||
)
|
||||
|
||||
# Start consuming
|
||||
await self.rabbitmq_client.consume(
|
||||
queue_name="inventory_service_internal_transfers",
|
||||
callback=self.handle_internal_transfer_completed,
|
||||
auto_ack=False
|
||||
)
|
||||
|
||||
logger.info("Inventory transfer event consumer started")
|
||||
|
||||
async def handle_internal_transfer_completed(self, message):
|
||||
"""
|
||||
Handle internal transfer completed event
|
||||
This means a shipment has been delivered and inventory ownership should transfer
|
||||
"""
|
||||
try:
|
||||
event_data = json.loads(message.body.decode())
|
||||
logger.info("Processing internal transfer completed event", event_data=event_data)
|
||||
|
||||
# Extract data from the event
|
||||
shipment_id = event_data.get('shipment_id')
|
||||
parent_tenant_id = event_data.get('parent_tenant_id')
|
||||
child_tenant_id = event_data.get('child_tenant_id')
|
||||
items = event_data.get('items', [])
|
||||
|
||||
if not all([shipment_id, parent_tenant_id, child_tenant_id, items]):
|
||||
logger.error("Missing required data in internal transfer event", event_data=event_data)
|
||||
await message.nack(requeue=False) # Don't retry invalid messages
|
||||
return
|
||||
|
||||
# Process the inventory transfer for each item
|
||||
transfer_results = []
|
||||
errors = []
|
||||
|
||||
for item in items:
|
||||
product_id = item.get('product_id')
|
||||
delivered_quantity = item.get('delivered_quantity')
|
||||
|
||||
if not all([product_id, delivered_quantity]):
|
||||
errors.append({
|
||||
'error': 'Missing product_id or delivered_quantity',
|
||||
'item': item
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
# Deduct from parent inventory
|
||||
await self._transfer_inventory_from_parent(
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=delivered_quantity
|
||||
)
|
||||
|
||||
# Add to child inventory
|
||||
await self._transfer_inventory_to_child(
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=delivered_quantity
|
||||
)
|
||||
|
||||
transfer_results.append({
|
||||
'product_id': product_id,
|
||||
'quantity': delivered_quantity,
|
||||
'status': 'completed'
|
||||
})
|
||||
|
||||
logger.info(
|
||||
"Inventory transferred successfully",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=delivered_quantity
|
||||
)
|
||||
|
||||
except Exception as item_error:
|
||||
logger.error(
|
||||
"Failed to transfer inventory for item",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
error=str(item_error)
|
||||
)
|
||||
errors.append({
|
||||
'product_id': product_id,
|
||||
'quantity': delivered_quantity,
|
||||
'error': str(item_error)
|
||||
})
|
||||
|
||||
# Acknowledge message after processing
|
||||
await message.ack()
|
||||
|
||||
logger.info(
|
||||
"Internal transfer processed",
|
||||
shipment_id=shipment_id,
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
child_tenant_id=child_tenant_id,
|
||||
successful_transfers=len(transfer_results),
|
||||
failed_transfers=len(errors)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error processing internal transfer event", error=str(e), exc_info=True)
|
||||
# Nack with requeue=True to retry on transient errors
|
||||
await message.nack(requeue=True)
|
||||
|
||||
async def _transfer_inventory_from_parent(
|
||||
self,
|
||||
parent_tenant_id: str,
|
||||
product_id: str,
|
||||
quantity: float
|
||||
):
|
||||
"""
|
||||
Deduct inventory from parent tenant
|
||||
"""
|
||||
try:
|
||||
# Create stock movement to reduce parent inventory
|
||||
stock_movement_data = {
|
||||
"product_id": product_id,
|
||||
"movement_type": "internal_transfer_out",
|
||||
"quantity": -float(quantity), # Negative for outflow
|
||||
"reference_type": "internal_transfer",
|
||||
"reference_id": f"transfer_{parent_tenant_id}_to_{product_id}", # Would have actual transfer ID
|
||||
"source_tenant_id": parent_tenant_id,
|
||||
"destination_tenant_id": None, # Will be set when we know the child
|
||||
"notes": f"Internal transfer to child tenant"
|
||||
}
|
||||
|
||||
# Call inventory service to process the movement
|
||||
await self.internal_transfer_service.inventory_client.create_stock_movement(
|
||||
tenant_id=parent_tenant_id,
|
||||
movement_data=stock_movement_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Inventory deducted from parent tenant",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=quantity
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error deducting inventory from parent",
|
||||
parent_tenant_id=parent_tenant_id,
|
||||
product_id=product_id,
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
async def _transfer_inventory_to_child(
|
||||
self,
|
||||
child_tenant_id: str,
|
||||
product_id: str,
|
||||
quantity: float
|
||||
):
|
||||
"""
|
||||
Add inventory to child tenant
|
||||
"""
|
||||
try:
|
||||
# Create stock movement to increase child inventory
|
||||
stock_movement_data = {
|
||||
"product_id": product_id,
|
||||
"movement_type": "internal_transfer_in",
|
||||
"quantity": float(quantity), # Positive for inflow
|
||||
"reference_type": "internal_transfer",
|
||||
"reference_id": f"transfer_from_parent_{product_id}_to_{child_tenant_id}", # Would have actual transfer ID
|
||||
"source_tenant_id": None, # Will be set when we know the parent
|
||||
"destination_tenant_id": child_tenant_id,
|
||||
"notes": f"Internal transfer from parent tenant"
|
||||
}
|
||||
|
||||
# Call inventory service to process the movement
|
||||
await self.internal_transfer_service.inventory_client.create_stock_movement(
|
||||
tenant_id=child_tenant_id,
|
||||
movement_data=stock_movement_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Inventory added to child tenant",
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
quantity=quantity
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error adding inventory to child",
|
||||
child_tenant_id=child_tenant_id,
|
||||
product_id=product_id,
|
||||
error=str(e)
|
||||
)
|
||||
raise
|
||||
|
||||
async def stop_consuming(self):
|
||||
"""
|
||||
Stop consuming inventory transfer events
|
||||
"""
|
||||
logger.info("Stopping inventory transfer event consumer")
|
||||
self.is_running = False
|
||||
# In a real implementation, we would close the RabbitMQ connection
|
||||
logger.info("Inventory transfer event consumer stopped")
|
||||
|
||||
async def health_check(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Health check for the consumer
|
||||
"""
|
||||
return {
|
||||
"consumer": "inventory_transfer_event_consumer",
|
||||
"status": "running" if self.is_running else "stopped",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
484
services/inventory/app/services/internal_transfer_service.py
Normal file
484
services/inventory/app/services/internal_transfer_service.py
Normal file
@@ -0,0 +1,484 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user