"""Order notification service (stub interface). Emits the two notifications the ordering spec requires when an order is submitted: * a confirmation to the customer, and * an internal notification to configured recipients. Real email delivery is intentionally **stubbed** behind this interface: no SMTP provider is configured in dev/alpha, so notifications are logged and returned as structured results. To go live, implement :func:`_deliver_email` (TODO) — no caller changes needed. """ from __future__ import annotations import logging from dataclasses import dataclass from sqlalchemy import select from sqlalchemy.orm import Session from app.models.client_access import ClientAccount, ClientUser from app.models.ordering import NotificationSetting, Order logger = logging.getLogger("data_entry_app.ordering") @dataclass class NotificationResult: channel: str # "customer" | "internal" recipients: list[str] subject: str delivered: bool detail: str def get_or_create_settings(db: Session, tenant_id: str) -> NotificationSetting: settings = db.scalar( select(NotificationSetting).where(NotificationSetting.tenant_id == tenant_id) ) if settings is None: settings = NotificationSetting(tenant_id=tenant_id) db.add(settings) db.flush() return settings def _internal_recipients(settings: NotificationSetting) -> list[str]: if not settings.internal_recipients: return [] return [part.strip() for part in settings.internal_recipients.split(",") if part.strip()] def _customer_recipients(db: Session, order: Order) -> list[str]: users = db.scalars( select(ClientUser).where( ClientUser.client_account_id == order.client_account_id, ClientUser.status == "active", ) ).all() return [user.email for user in users if user.email] def _deliver_email(*, to: list[str], subject: str, body: str, from_email: str | None) -> bool: """Deliver an email. Stubbed — logs instead of sending. TODO (go-live): integrate the chosen provider (SMTP / SendGrid / SES) using credentials from environment variables. Return True only on confirmed delivery. """ logger.info("ordering.email.stub to=%s subject=%s", to, subject) return False # stub: nothing actually sent def send_order_submitted_notifications(db: Session, order: Order) -> list[NotificationResult]: """Send customer confirmation + internal notification for a submitted order.""" settings = get_or_create_settings(db, order.tenant_id) customer = db.scalar(select(ClientAccount).where(ClientAccount.id == order.client_account_id)) customer_name = customer.name if customer else "Customer" order_ref = order.order_number or f"#{order.id}" results: list[NotificationResult] = [] if settings.send_customer_confirmation: to = _customer_recipients(db, order) subject = f"Order {order_ref} received" body = ( f"Thank you — we have received order {order_ref} for {customer_name}.\n" f"Lines: {len(order.lines)}. Subtotal (ex GST): {order.subtotal_ex_gst:.2f}.\n" "Our team will review and confirm shortly." ) delivered = _deliver_email(to=to, subject=subject, body=body, from_email=settings.from_email) if to else False results.append( NotificationResult( channel="customer", recipients=to, subject=subject, delivered=delivered, detail="stubbed" if not delivered else "sent", ) ) internal = _internal_recipients(settings) subject = f"New order {order_ref} from {customer_name}" body = ( f"New order {order_ref} submitted by {order.created_by_name or 'a customer user'}.\n" f"Customer: {customer_name}. Lines: {len(order.lines)}. " f"Subtotal (ex GST): {order.subtotal_ex_gst:.2f}." ) delivered = _deliver_email(to=internal, subject=subject, body=body, from_email=settings.from_email) if internal else False results.append( NotificationResult( channel="internal", recipients=internal, subject=subject, delivered=delivered, detail="no recipients configured" if not internal else ("stubbed" if not delivered else "sent"), ) ) return results