Files
orion/app/modules/loyalty/tasks/notifications.py
Samir Boulahtit 52b78ce346
Some checks failed
CI / ruff (push) Successful in 13s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled
feat(loyalty): Phase 2A — transactional email notifications
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>
2026-04-11 19:11:56 +02:00

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()