Add POI feature and imporve the overall backend implementation
This commit is contained in:
6
services/inventory/app/consumers/__init__.py
Normal file
6
services/inventory/app/consumers/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""
|
||||
Event consumers for inventory service
|
||||
"""
|
||||
from .delivery_event_consumer import DeliveryEventConsumer
|
||||
|
||||
__all__ = ["DeliveryEventConsumer"]
|
||||
241
services/inventory/app/consumers/delivery_event_consumer.py
Normal file
241
services/inventory/app/consumers/delivery_event_consumer.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
Delivery Event Consumer
|
||||
|
||||
Listens for delivery.received events from procurement service
|
||||
and automatically updates inventory stock levels.
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Any
|
||||
from decimal import Decimal
|
||||
import structlog
|
||||
|
||||
from shared.messaging.rabbitmq import RabbitMQClient
|
||||
from app.core.database import database_manager
|
||||
from app.repositories.stock_repository import StockRepository
|
||||
from app.repositories.stock_movement_repository import StockMovementRepository
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
|
||||
class DeliveryEventConsumer:
|
||||
"""
|
||||
Consumes delivery.received events and updates inventory stock.
|
||||
|
||||
When a delivery is recorded in procurement service:
|
||||
1. Listens for delivery.received event
|
||||
2. Creates stock entries for each delivered item
|
||||
3. Updates stock levels (quantity_available)
|
||||
4. Records batch numbers and expiry dates
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize delivery event consumer"""
|
||||
self.service_name = "inventory"
|
||||
|
||||
async def consume_delivery_received_events(
|
||||
self,
|
||||
rabbitmq_client: RabbitMQClient
|
||||
):
|
||||
"""
|
||||
Start consuming delivery.received events from RabbitMQ
|
||||
|
||||
Args:
|
||||
rabbitmq_client: RabbitMQ client instance
|
||||
"""
|
||||
async def process_message(message):
|
||||
"""Process a single delivery.received event message"""
|
||||
try:
|
||||
async with message.process():
|
||||
# Parse event data
|
||||
event_data = json.loads(message.body.decode())
|
||||
logger.info(
|
||||
"Received delivery.received event",
|
||||
event_id=event_data.get('event_id'),
|
||||
delivery_id=event_data.get('data', {}).get('delivery_id')
|
||||
)
|
||||
|
||||
# Process the delivery and update stock
|
||||
success = await self.process_delivery_stock_update(event_data)
|
||||
|
||||
if success:
|
||||
logger.info(
|
||||
"Successfully processed delivery stock update",
|
||||
delivery_id=event_data.get('data', {}).get('delivery_id')
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
"Failed to process delivery stock update",
|
||||
delivery_id=event_data.get('data', {}).get('delivery_id')
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error processing delivery.received event",
|
||||
error=str(e),
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Start consuming events
|
||||
await rabbitmq_client.consume_events(
|
||||
exchange_name="procurement.events",
|
||||
queue_name="inventory.delivery.received",
|
||||
routing_key="delivery.received",
|
||||
callback=process_message
|
||||
)
|
||||
|
||||
logger.info("Started consuming delivery.received events")
|
||||
|
||||
async def process_delivery_stock_update(self, event_data: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Process delivery event and update stock levels.
|
||||
|
||||
Args:
|
||||
event_data: Full event payload from RabbitMQ
|
||||
|
||||
Returns:
|
||||
bool: True if stock updated successfully
|
||||
"""
|
||||
try:
|
||||
data = event_data.get('data', {})
|
||||
|
||||
# Extract delivery information
|
||||
tenant_id = uuid.UUID(data.get('tenant_id'))
|
||||
delivery_id = uuid.UUID(data.get('delivery_id'))
|
||||
po_id = uuid.UUID(data.get('po_id'))
|
||||
items = data.get('items', [])
|
||||
received_by = data.get('received_by')
|
||||
received_at = data.get('received_at')
|
||||
|
||||
if not items:
|
||||
logger.warning(
|
||||
"No items in delivery event, skipping stock update",
|
||||
delivery_id=str(delivery_id)
|
||||
)
|
||||
return False
|
||||
|
||||
# Process each item
|
||||
async with database_manager.get_session() as session:
|
||||
stock_repo = StockRepository(session)
|
||||
movement_repo = StockMovementRepository(session)
|
||||
|
||||
for item in items:
|
||||
try:
|
||||
# inventory_product_id is the same as ingredient_id
|
||||
# The ingredients table serves as a unified catalog for both raw materials and products
|
||||
ingredient_id = uuid.UUID(item.get('inventory_product_id'))
|
||||
accepted_quantity = Decimal(str(item.get('accepted_quantity', 0)))
|
||||
|
||||
# Only process if quantity was accepted
|
||||
if accepted_quantity <= 0:
|
||||
logger.debug(
|
||||
"Skipping item with zero accepted quantity",
|
||||
ingredient_id=str(ingredient_id)
|
||||
)
|
||||
continue
|
||||
|
||||
# Create a new stock batch entry for this delivery
|
||||
# The Stock model uses batch tracking - each delivery creates a new batch entry
|
||||
stock_data = {
|
||||
'tenant_id': tenant_id,
|
||||
'ingredient_id': ingredient_id,
|
||||
'batch_number': item.get('batch_lot_number'),
|
||||
'lot_number': item.get('batch_lot_number'), # Use same as batch_number
|
||||
'supplier_batch_ref': item.get('batch_lot_number'),
|
||||
|
||||
# Quantities
|
||||
'current_quantity': float(accepted_quantity),
|
||||
'reserved_quantity': 0.0,
|
||||
'available_quantity': float(accepted_quantity),
|
||||
|
||||
# Dates
|
||||
'received_date': datetime.fromisoformat(received_at.replace('Z', '+00:00')) if received_at else datetime.now(timezone.utc),
|
||||
'expiration_date': datetime.fromisoformat(item.get('expiry_date').replace('Z', '+00:00')) if item.get('expiry_date') else None,
|
||||
|
||||
# Cost (TODO: Get actual unit cost from delivery item if available)
|
||||
'unit_cost': Decimal('0'),
|
||||
'total_cost': Decimal('0'),
|
||||
|
||||
# Production stage - default to raw ingredient for deliveries
|
||||
'production_stage': 'raw_ingredient',
|
||||
|
||||
# Status
|
||||
'is_available': True,
|
||||
'quality_status': 'GOOD'
|
||||
}
|
||||
|
||||
from app.schemas.inventory import StockCreate
|
||||
stock_create = StockCreate(**stock_data)
|
||||
stock = await stock_repo.create_stock_entry(stock_create, tenant_id)
|
||||
|
||||
logger.info(
|
||||
"Created new stock batch from delivery",
|
||||
ingredient_id=str(ingredient_id),
|
||||
stock_id=str(stock.id),
|
||||
batch_number=item.get('batch_lot_number'),
|
||||
quantity=float(accepted_quantity),
|
||||
delivery_id=str(delivery_id)
|
||||
)
|
||||
|
||||
# Create stock movement record for audit trail
|
||||
from app.models.inventory import StockMovementType
|
||||
from app.schemas.inventory import StockMovementCreate
|
||||
|
||||
movement_data = StockMovementCreate(
|
||||
ingredient_id=ingredient_id,
|
||||
stock_id=stock.id,
|
||||
movement_type=StockMovementType.PURCHASE,
|
||||
quantity=float(accepted_quantity),
|
||||
unit_cost=Decimal('0'), # TODO: Get from delivery item
|
||||
reference_number=f"DEL-{delivery_id}",
|
||||
reason_code='delivery',
|
||||
notes=f"Delivery received from PO {po_id}. Batch: {item.get('batch_lot_number', 'N/A')}",
|
||||
movement_date=datetime.fromisoformat(received_at.replace('Z', '+00:00')) if received_at else datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
movement = await movement_repo.create_movement(
|
||||
movement_data=movement_data,
|
||||
tenant_id=tenant_id,
|
||||
created_by=uuid.UUID(received_by) if received_by else None,
|
||||
quantity_before=0.0, # New batch starts at 0
|
||||
quantity_after=float(accepted_quantity)
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Created stock movement for delivery",
|
||||
movement_id=str(movement.id),
|
||||
ingredient_id=str(ingredient_id),
|
||||
quantity=float(accepted_quantity),
|
||||
batch=item.get('batch_lot_number')
|
||||
)
|
||||
|
||||
except Exception as item_error:
|
||||
logger.error(
|
||||
"Error processing delivery item",
|
||||
error=str(item_error),
|
||||
item=item,
|
||||
exc_info=True
|
||||
)
|
||||
# Continue processing other items even if one fails
|
||||
continue
|
||||
|
||||
# Commit all changes
|
||||
await session.commit()
|
||||
|
||||
logger.info(
|
||||
"Successfully processed delivery stock update",
|
||||
delivery_id=str(delivery_id),
|
||||
items_processed=len(items)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Error in delivery stock update",
|
||||
error=str(e),
|
||||
delivery_id=data.get('delivery_id'),
|
||||
exc_info=True
|
||||
)
|
||||
return False
|
||||
Reference in New Issue
Block a user