# app/modules/loyalty/services/notification_service.py """ Loyalty notification service. Thin wrapper that resolves customer/card/program data into template variables and dispatches emails asynchronously via the Celery task. """ import logging from sqlalchemy.orm import Session from app.modules.loyalty.models import LoyaltyCard logger = logging.getLogger(__name__) class LoyaltyNotificationService: """Dispatches loyalty email notifications.""" def _resolve_context(self, db: Session, card: LoyaltyCard) -> dict | None: """Load customer, store, and program info for a card. Returns None if the customer has no email (can't send). """ from app.modules.customers.services.customer_service import customer_service from app.modules.tenancy.services.store_service import store_service customer = customer_service.get_customer_by_id(db, card.customer_id) if not customer or not customer.email: return None store = store_service.get_store_by_id_optional(db, card.enrolled_at_store_id) program = card.program return { "customer": customer, "store": store, "program": program, "to_email": customer.email, "to_name": customer.full_name, "store_id": card.enrolled_at_store_id, "customer_id": customer.id, "customer_name": customer.full_name, "program_name": program.display_name if program else "Loyalty Program", "store_name": store.name if store else "", } def _dispatch( self, template_code: str, ctx: dict, extra_vars: dict | None = None ): """Enqueue a notification email via Celery.""" from app.modules.loyalty.tasks.notifications import send_notification_email variables = { "customer_name": ctx["customer_name"], "program_name": ctx["program_name"], "store_name": ctx["store_name"], } if extra_vars: variables.update(extra_vars) send_notification_email.delay( template_code=template_code, to_email=ctx["to_email"], to_name=ctx["to_name"], variables=variables, store_id=ctx["store_id"], customer_id=ctx["customer_id"], ) logger.info( f"Queued {template_code} for {ctx['to_email']}" ) def send_enrollment_confirmation(self, db: Session, card: LoyaltyCard): """Send enrollment confirmation email.""" ctx = self._resolve_context(db, card) if not ctx: return self._dispatch("loyalty_enrollment", ctx, { "card_number": card.card_number, }) def send_welcome_bonus(self, db: Session, card: LoyaltyCard, points: int): """Send welcome bonus notification (only if points > 0).""" if points <= 0: return ctx = self._resolve_context(db, card) if not ctx: return self._dispatch("loyalty_welcome_bonus", ctx, { "points": str(points), }) def send_points_expiration_warning( self, db: Session, card: LoyaltyCard, expiring_points: int, days_remaining: int, expiration_date: str, ): """Send points expiring warning email.""" ctx = self._resolve_context(db, card) if not ctx: return self._dispatch("loyalty_points_expiring", ctx, { "points": str(expiring_points), "days_remaining": str(days_remaining), "expiration_date": expiration_date, }) def send_points_expired( self, db: Session, card: LoyaltyCard, expired_points: int ): """Send points expired notification email.""" ctx = self._resolve_context(db, card) if not ctx: return self._dispatch("loyalty_points_expired", ctx, { "expired_points": str(expired_points), }) def send_reward_available( self, db: Session, card: LoyaltyCard, reward_name: str ): """Send reward earned notification email.""" ctx = self._resolve_context(db, card) if not ctx: return self._dispatch("loyalty_reward_ready", ctx, { "reward_name": reward_name, }) # Singleton notification_service = LoyaltyNotificationService()