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

396 lines
14 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 import RabbitMQClient
from app.services.email_service import EmailService
from app.services.whatsapp_service import WhatsAppService
logger = structlog.get_logger()
class POEventConsumer:
"""
Consumes purchase order events from RabbitMQ and sends notifications
Sends both email and WhatsApp notifications to suppliers
"""
def __init__(self, email_service: EmailService, whatsapp_service: WhatsAppService = None):
self.email_service = email_service
self.whatsapp_service = whatsapp_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
email_success = await self.send_po_approved_email(event_data)
# Send WhatsApp notification if service is available
whatsapp_success = False
if self.whatsapp_service:
whatsapp_success = await self.send_po_approved_whatsapp(event_data)
if email_success:
logger.info(
"PO approved email sent successfully",
po_id=event_data.get('data', {}).get('po_id'),
whatsapp_sent=whatsapp_success
)
else:
logger.error(
"Failed to send PO approved email",
po_id=event_data.get('data', {}).get('po_id'),
whatsapp_sent=whatsapp_success
)
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
async def _get_tenant_settings(self, tenant_id: str) -> Dict[str, Any]:
"""Fetch tenant settings from tenant service"""
try:
from shared.clients.tenant_client import TenantServiceClient
from shared.config.base import get_settings
config = get_settings()
tenant_client = TenantServiceClient(config)
# Get tenant details
tenant = await tenant_client.get_tenant(tenant_id)
if not tenant:
logger.warning("Could not fetch tenant details", tenant_id=tenant_id)
return {}
return {
'name': tenant.get('business_name') or tenant.get('name', 'Your Bakery'),
'email': tenant.get('email', 'info@yourbakery.com'),
'phone': tenant.get('phone', '+34 XXX XXX XXX'),
'address': tenant.get('address', 'Your Bakery Address'),
'contact_person': tenant.get('contact_person', 'Bakery Manager')
}
except Exception as e:
logger.error("Failed to fetch tenant settings", tenant_id=tenant_id, error=str(e))
return {}
async 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,
}
# Fetch tenant settings (bakery info)
tenant_id = data.get('tenant_id')
tenant_settings = {}
if tenant_id:
tenant_settings = await self._get_tenant_settings(tenant_id)
# Add bakery info from tenant settings with fallbacks
context.update({
'bakery_name': tenant_settings.get('name', 'Your Bakery Name'),
'bakery_email': tenant_settings.get('email', 'orders@yourbakery.com'),
'bakery_phone': tenant_settings.get('phone', '+34 XXX XXX XXX'),
'bakery_address': tenant_settings.get('address', 'Your Bakery Address'),
'delivery_address': data.get('delivery_address') or tenant_settings.get('address', 'Bakery Delivery Address'),
'contact_person': data.get('contact_person') or tenant_settings.get('contact_person', 'Bakery Manager'),
'contact_phone': data.get('contact_phone') or tenant_settings.get('phone', '+34 XXX XXX XXX'),
# Payment & Delivery Terms - From PO data with fallbacks
'payment_terms': data.get('payment_terms', 'Net 30 days'),
'delivery_instructions': data.get('delivery_instructions', 'Please deliver during business hours'),
'notes': data.get('notes'),
})
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
async def send_po_approved_whatsapp(self, event_data: Dict[str, Any]) -> bool:
"""
Send PO approved WhatsApp notification to supplier
This sends a WhatsApp Business template message notifying the supplier
of a new purchase order. The template must be pre-approved in Meta Business Suite.
Args:
event_data: Full event payload from RabbitMQ
Returns:
bool: True if WhatsApp message sent successfully
"""
try:
# Extract data from event
data = event_data.get('data', {})
# Check for supplier phone number
supplier_phone = data.get('supplier_phone')
if not supplier_phone:
logger.debug(
"No supplier phone in event, skipping WhatsApp notification",
po_id=data.get('po_id')
)
return False
# Extract tenant ID for tracking
tenant_id = data.get('tenant_id')
# Prepare template parameters
# Template: "Hola {{1}}, has recibido una nueva orden de compra {{2}} por un total de {{3}}."
# Parameters: supplier_name, po_number, total_amount
template_params = [
data.get('supplier_name', 'Estimado proveedor'),
data.get('po_number', 'N/A'),
f"{data.get('total_amount', 0):.2f}"
]
# Send WhatsApp template message
# The template must be named 'po_notification' and approved in Meta Business Suite
success = await self.whatsapp_service.send_message(
to_phone=supplier_phone,
message="", # Not used for template messages
template_name="po_notification", # Must match template name in Meta
template_params=template_params,
tenant_id=tenant_id
)
if success:
logger.info(
"PO approved WhatsApp sent successfully",
po_id=data.get('po_id'),
supplier_phone=supplier_phone,
template="po_notification"
)
else:
logger.warning(
"Failed to send PO approved WhatsApp",
po_id=data.get('po_id'),
supplier_phone=supplier_phone
)
return success
except Exception as e:
logger.error(
"Error sending PO approved WhatsApp",
error=str(e),
po_id=data.get('po_id'),
exc_info=True
)
return False