Files
bakery-ia/services/notification/app/consumers/po_event_consumer.py

279 lines
9.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Purchase Order Event Consumer
Listens for PO events and sends email notifications to suppliers
"""
import json
import structlog
from pathlib import Path
from typing import Dict, Any
from jinja2 import Environment, FileSystemLoader
from datetime import datetime
from shared.messaging.rabbitmq import RabbitMQClient
from app.services.email_service import EmailService
logger = structlog.get_logger()
class POEventConsumer:
"""
Consumes purchase order events from RabbitMQ and sends notifications
"""
def __init__(self, email_service: EmailService):
self.email_service = email_service
# Setup Jinja2 template environment
template_dir = Path(__file__).parent.parent / 'templates'
self.jinja_env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=True
)
async def consume_po_approved_event(
self,
rabbitmq_client: RabbitMQClient
):
"""
Start consuming PO approved events from RabbitMQ
"""
async def process_message(message):
"""Process a single PO approved event message"""
try:
async with message.process():
# Parse event data
event_data = json.loads(message.body.decode())
logger.info(
"Received PO approved event",
event_id=event_data.get('event_id'),
po_id=event_data.get('data', {}).get('po_id')
)
# Send notification email
success = await self.send_po_approved_email(event_data)
if success:
logger.info(
"PO approved email sent successfully",
po_id=event_data.get('data', {}).get('po_id')
)
else:
logger.error(
"Failed to send PO approved email",
po_id=event_data.get('data', {}).get('po_id')
)
except Exception as e:
logger.error(
"Error processing PO approved event",
error=str(e),
exc_info=True
)
# Start consuming events
await rabbitmq_client.consume_events(
exchange_name="procurement.events",
queue_name="notification.po.approved",
routing_key="po.approved",
callback=process_message
)
logger.info("Started consuming PO approved events")
async def send_po_approved_email(self, event_data: Dict[str, Any]) -> bool:
"""
Send PO approved email to supplier
Args:
event_data: Full event payload from RabbitMQ
Returns:
bool: True if email sent successfully
"""
try:
# Extract data from event
data = event_data.get('data', {})
# Required fields
supplier_email = data.get('supplier_email')
if not supplier_email:
logger.warning(
"No supplier email in event, skipping notification",
po_id=data.get('po_id')
)
return False
# Prepare template context
context = self._prepare_email_context(data)
# Render HTML email from template
template = self.jinja_env.get_template('po_approved_email.html')
html_content = template.render(**context)
# Prepare plain text version (fallback)
text_content = self._generate_text_email(context)
# Send email
subject = f"New Purchase Order #{data.get('po_number', 'N/A')}"
success = await self.email_service.send_email(
to_email=supplier_email,
subject=subject,
text_content=text_content,
html_content=html_content,
from_name=context.get('bakery_name', 'Bakery Management System'),
reply_to=context.get('bakery_email')
)
return success
except Exception as e:
logger.error(
"Error sending PO approved email",
error=str(e),
po_id=data.get('po_id'),
exc_info=True
)
return False
def _prepare_email_context(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""
Prepare context data for email template
Args:
data: Event data from RabbitMQ
Returns:
Dict with all template variables
"""
# Extract items and format them
items = data.get('items', [])
formatted_items = []
for item in items:
formatted_items.append({
'product_name': item.get('product_name', 'N/A'),
'ordered_quantity': f"{item.get('ordered_quantity', 0):.2f}",
'unit_of_measure': item.get('unit_of_measure', 'units'),
'unit_price': f"{item.get('unit_price', 0):.2f}",
'line_total': f"{item.get('line_total', 0):.2f}"
})
# Determine currency symbol
currency = data.get('currency', 'EUR')
currency_symbol = '' if currency == 'EUR' else '$'
# Format dates
order_date = self._format_datetime(data.get('approved_at'))
required_delivery_date = self._format_date(data.get('required_delivery_date'))
# Build context
context = {
# PO Details
'po_number': data.get('po_number', 'N/A'),
'order_date': order_date,
'required_delivery_date': required_delivery_date or 'To be confirmed',
'total_amount': f"{data.get('total_amount', 0):.2f}",
'currency': currency,
'currency_symbol': currency_symbol,
# Supplier Info
'supplier_name': data.get('supplier_name', 'Valued Supplier'),
# Items
'items': formatted_items,
# Bakery Info (these should come from tenant settings, defaulting for now)
'bakery_name': 'Your Bakery Name', # TODO: Fetch from tenant settings
'bakery_email': 'orders@yourbakery.com', # TODO: Fetch from tenant settings
'bakery_phone': '+34 XXX XXX XXX', # TODO: Fetch from tenant settings
'bakery_address': 'Your Bakery Address', # TODO: Fetch from tenant settings
'delivery_address': 'Bakery Delivery Address', # TODO: Fetch from PO/tenant
'contact_person': 'Bakery Manager', # TODO: Fetch from tenant settings
'contact_phone': '+34 XXX XXX XXX', # TODO: Fetch from tenant settings
# Payment & Delivery Terms
'payment_terms': 'Net 30 days', # TODO: Fetch from supplier/tenant settings
'delivery_instructions': 'Please deliver to main entrance between 7-9 AM', # TODO: Fetch from PO
'notes': None, # TODO: Extract from PO notes if available
}
return context
def _generate_text_email(self, context: Dict[str, Any]) -> str:
"""
Generate plain text version of email
Args:
context: Template context
Returns:
Plain text email content
"""
items_text = "\n".join([
f" - {item['product_name']}: {item['ordered_quantity']} {item['unit_of_measure']} "
f"× {context['currency_symbol']}{item['unit_price']} = {context['currency_symbol']}{item['line_total']}"
for item in context['items']
])
text = f"""
New Purchase Order #{context['po_number']}
Dear {context['supplier_name']},
We would like to place the following purchase order:
ORDER DETAILS:
- PO Number: {context['po_number']}
- Order Date: {context['order_date']}
- Required Delivery: {context['required_delivery_date']}
- Delivery Address: {context['delivery_address']}
ORDER ITEMS:
{items_text}
TOTAL AMOUNT: {context['currency_symbol']}{context['total_amount']} {context['currency']}
PAYMENT & DELIVERY:
- Payment Terms: {context['payment_terms']}
- Delivery Instructions: {context['delivery_instructions']}
- Contact Person: {context['contact_person']}
- Phone: {context['contact_phone']}
Please confirm receipt of this order by replying to this email.
Thank you for your continued partnership.
Best regards,
{context['bakery_name']}
{context['bakery_address']}
Phone: {context['bakery_phone']}
Email: {context['bakery_email']}
---
This is an automated email from your Bakery Management System.
"""
return text.strip()
def _format_datetime(self, iso_datetime: str) -> str:
"""Format ISO datetime string to readable format"""
if not iso_datetime:
return 'N/A'
try:
dt = datetime.fromisoformat(iso_datetime.replace('Z', '+00:00'))
return dt.strftime('%B %d, %Y at %H:%M')
except Exception:
return iso_datetime
def _format_date(self, iso_date: str) -> str:
"""Format ISO date string to readable format"""
if not iso_date:
return None
try:
if 'T' in iso_date:
dt = datetime.fromisoformat(iso_date.replace('Z', '+00:00'))
else:
dt = datetime.fromisoformat(iso_date)
return dt.strftime('%B %d, %Y')
except Exception:
return iso_date