Some checks failed
Add async email notifications for 5 loyalty lifecycle events, using the existing messaging module infrastructure (EmailService, EmailLog, store template overrides). - New seed script: scripts/seed/seed_email_templates_loyalty.py Seeds 5 templates × 4 locales (en/fr/de/lb) = 20 rows. Idempotent. Renamed existing script to seed_email_templates_core.py. - Celery task: loyalty.send_notification_email — async dispatch with 3 retries and 60s backoff. Opens own DB session. - Notification service: LoyaltyNotificationService with 5 methods that resolve customer/card/program into template variables and enqueue via Celery (never blocks request handlers). - Enrollment: sends loyalty_enrollment + loyalty_welcome_bonus (if bonus > 0) after card creation commit. - Stamps: sends loyalty_reward_ready when stamp target reached. - Expiration task: sends loyalty_points_expiring 14 days before expiry (tracked via new last_expiration_warning_at column to prevent dupes), and loyalty_points_expired after points are zeroed. - Migration loyalty_005: adds last_expiration_warning_at to cards. - 8 new unit tests for notification service dispatch. - Fix: rate limiter autouse fixture in integration tests to prevent state bleed between tests. Templates: loyalty_enrollment, loyalty_welcome_bonus, loyalty_points_expiring, loyalty_points_expired, loyalty_reward_ready. All support store-level overrides via the existing email template UI. Birthday + re-engagement emails deferred to future marketing module (cross-platform: OMS, loyalty, hosting). 342 tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
73 lines
2.2 KiB
Python
73 lines
2.2 KiB
Python
# app/modules/loyalty/tasks/notifications.py
|
|
"""
|
|
Async email notification dispatch for loyalty events.
|
|
|
|
All loyalty notification emails are sent asynchronously via this Celery
|
|
task to avoid blocking request handlers. The task opens its own DB
|
|
session and calls EmailService.send_template() which handles language
|
|
resolution, store overrides, Jinja2 rendering, and EmailLog creation.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from celery import shared_task
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@shared_task(
|
|
name="loyalty.send_notification_email",
|
|
bind=True,
|
|
max_retries=3,
|
|
default_retry_delay=60,
|
|
)
|
|
def send_notification_email(
|
|
self,
|
|
template_code: str,
|
|
to_email: str,
|
|
to_name: str | None = None,
|
|
variables: dict | None = None,
|
|
store_id: int | None = None,
|
|
customer_id: int | None = None,
|
|
language: str | None = None,
|
|
):
|
|
"""
|
|
Send a loyalty notification email asynchronously.
|
|
|
|
Args:
|
|
template_code: Email template code (e.g. 'loyalty_enrollment')
|
|
to_email: Recipient email address
|
|
to_name: Recipient display name
|
|
variables: Template variables dict
|
|
store_id: Store ID for branding and template overrides
|
|
customer_id: Customer ID for language resolution
|
|
language: Explicit language override (otherwise auto-resolved)
|
|
"""
|
|
from app.core.database import SessionLocal
|
|
from app.modules.messaging.services.email_service import EmailService
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
email_service = EmailService(db)
|
|
email_log = email_service.send_template(
|
|
template_code=template_code,
|
|
to_email=to_email,
|
|
to_name=to_name,
|
|
language=language,
|
|
variables=variables or {},
|
|
store_id=store_id,
|
|
customer_id=customer_id,
|
|
)
|
|
logger.info(
|
|
f"Loyalty notification sent: {template_code} to {to_email} "
|
|
f"(log_id={email_log.id if email_log else 'none'})"
|
|
)
|
|
except Exception as exc:
|
|
logger.error(
|
|
f"Loyalty notification failed: {template_code} to {to_email}: {exc}"
|
|
)
|
|
db.rollback()
|
|
raise self.retry(exc=exc)
|
|
finally:
|
|
db.close()
|