Files
bakery-ia/services/inventory/app/consumers/inventory_transfer_consumer.py

256 lines
9.4 KiB
Python
Raw Normal View History

2025-11-30 09:12:40 +01:00
"""
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
2025-12-05 20:07:01 +01:00
from shared.messaging import RabbitMQClient
2025-11-30 09:12:40 +01:00
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()
}