120 lines
4.3 KiB
Python
120 lines
4.3 KiB
Python
|
|
"""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
|