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>
498 lines
29 KiB
Python
498 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Seed loyalty email templates.
|
|
|
|
Idempotent: safe to run repeatedly — upserts by (code, language).
|
|
|
|
Run: python scripts/seed/seed_email_templates_loyalty.py
|
|
"""
|
|
|
|
import contextlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add project root to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
|
|
for _mod in [
|
|
"app.modules.billing.models",
|
|
"app.modules.inventory.models",
|
|
"app.modules.cart.models",
|
|
"app.modules.messaging.models",
|
|
"app.modules.loyalty.models",
|
|
"app.modules.catalog.models",
|
|
"app.modules.customers.models",
|
|
"app.modules.orders.models",
|
|
"app.modules.marketplace.models",
|
|
"app.modules.cms.models",
|
|
]:
|
|
with contextlib.suppress(ImportError):
|
|
__import__(_mod)
|
|
|
|
from app.core.database import get_db
|
|
from app.modules.messaging.models import EmailCategory, EmailTemplate
|
|
|
|
# =============================================================================
|
|
# SHARED HTML STRUCTURE
|
|
# =============================================================================
|
|
|
|
_HEAD = """<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
</head>"""
|
|
|
|
|
|
def _wrap(gradient_from, gradient_to, title, body_html):
|
|
"""Wrap email body in the standard template chrome."""
|
|
return f"""{_HEAD}
|
|
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<div style="background: linear-gradient(135deg, {gradient_from} 0%, {gradient_to} 100%); padding: 30px; border-radius: 10px 10px 0 0;">
|
|
<h1 style="color: white; margin: 0; font-size: 24px;">{title}</h1>
|
|
</div>
|
|
<div style="background: #f9fafb; padding: 30px; border-radius: 0 0 10px 10px;">
|
|
{body_html}
|
|
</div>
|
|
<div style="text-align: center; padding: 20px; color: #9ca3af; font-size: 12px;">
|
|
<p>Powered by RewardFlow</p>
|
|
</div>
|
|
</body>
|
|
</html>"""
|
|
|
|
|
|
# =============================================================================
|
|
# LOYALTY TEMPLATES
|
|
# =============================================================================
|
|
|
|
# ── Enrollment ──────────────────────────────────────────────────────────────
|
|
|
|
_ENROLLMENT_BODY_EN = """
|
|
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
|
<p>Welcome to <strong>{{ program_name }}</strong>! Your loyalty card is ready.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6366f1;">
|
|
<p style="margin: 5px 0;"><strong>Card Number:</strong> {{ card_number }}</p>
|
|
<p style="margin: 5px 0;"><strong>Store:</strong> {{ store_name }}</p>
|
|
</div>
|
|
<p>Start earning rewards on every visit!</p>
|
|
<p>Best regards,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_ENROLLMENT_BODY_FR = """
|
|
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
|
<p>Bienvenue dans <strong>{{ program_name }}</strong> ! Votre carte de fidélité est prête.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6366f1;">
|
|
<p style="margin: 5px 0;"><strong>Numéro de carte :</strong> {{ card_number }}</p>
|
|
<p style="margin: 5px 0;"><strong>Point de vente :</strong> {{ store_name }}</p>
|
|
</div>
|
|
<p>Commencez à gagner des récompenses à chaque visite !</p>
|
|
<p>Cordialement,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_ENROLLMENT_BODY_DE = """
|
|
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
|
<p>Willkommen bei <strong>{{ program_name }}</strong>! Ihre Treuekarte ist bereit.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6366f1;">
|
|
<p style="margin: 5px 0;"><strong>Kartennummer:</strong> {{ card_number }}</p>
|
|
<p style="margin: 5px 0;"><strong>Filiale:</strong> {{ store_name }}</p>
|
|
</div>
|
|
<p>Sammeln Sie ab sofort Prämien bei jedem Besuch!</p>
|
|
<p>Mit freundlichen Grüßen,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_ENROLLMENT_BODY_LB = """
|
|
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
|
<p>Wëllkomm bei <strong>{{ program_name }}</strong>! Är Treiekaart ass prett.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #6366f1;">
|
|
<p style="margin: 5px 0;"><strong>Kaartenummer:</strong> {{ card_number }}</p>
|
|
<p style="margin: 5px 0;"><strong>Geschäft:</strong> {{ store_name }}</p>
|
|
</div>
|
|
<p>Fänkt un Belounungen ze sammelen bei all Besuch!</p>
|
|
<p>Léif Gréiss,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
# ── Welcome Bonus ───────────────────────────────────────────────────────────
|
|
|
|
_WELCOME_BONUS_BODY_EN = """
|
|
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
|
<p>Great news! You've received <strong>{{ points }} bonus points</strong> as a welcome gift from <strong>{{ program_name }}</strong>.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center;">
|
|
<p style="font-size: 36px; font-weight: bold; color: #6366f1; margin: 0;">+{{ points }}</p>
|
|
<p style="color: #6b7280; margin: 5px 0;">bonus points</p>
|
|
</div>
|
|
<p>These points are already in your balance and ready to use toward rewards.</p>
|
|
<p>Best regards,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_WELCOME_BONUS_BODY_FR = """
|
|
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
|
<p>Bonne nouvelle ! Vous avez reçu <strong>{{ points }} points bonus</strong> en cadeau de bienvenue de <strong>{{ program_name }}</strong>.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center;">
|
|
<p style="font-size: 36px; font-weight: bold; color: #6366f1; margin: 0;">+{{ points }}</p>
|
|
<p style="color: #6b7280; margin: 5px 0;">points bonus</p>
|
|
</div>
|
|
<p>Ces points sont déjà sur votre solde et utilisables pour des récompenses.</p>
|
|
<p>Cordialement,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_WELCOME_BONUS_BODY_DE = """
|
|
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
|
<p>Tolle Neuigkeiten! Sie haben <strong>{{ points }} Bonuspunkte</strong> als Willkommensgeschenk von <strong>{{ program_name }}</strong> erhalten.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center;">
|
|
<p style="font-size: 36px; font-weight: bold; color: #6366f1; margin: 0;">+{{ points }}</p>
|
|
<p style="color: #6b7280; margin: 5px 0;">Bonuspunkte</p>
|
|
</div>
|
|
<p>Diese Punkte sind bereits auf Ihrem Konto und können für Prämien eingesetzt werden.</p>
|
|
<p>Mit freundlichen Grüßen,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_WELCOME_BONUS_BODY_LB = """
|
|
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
|
<p>Gutt Noriichten! Dir hutt <strong>{{ points }} Bonuspunkten</strong> als Wëllkommsgeschenk vu <strong>{{ program_name }}</strong> kritt.</p>
|
|
<div style="background: white; border-radius: 8px; padding: 20px; margin: 20px 0; text-align: center;">
|
|
<p style="font-size: 36px; font-weight: bold; color: #6366f1; margin: 0;">+{{ points }}</p>
|
|
<p style="color: #6b7280; margin: 5px 0;">Bonuspunkten</p>
|
|
</div>
|
|
<p>Dës Punkten sinn schonn op Ärem Konto a kënne fir Beloununge benotzt ginn.</p>
|
|
<p>Léif Gréiss,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
# ── Points Expiring ─────────────────────────────────────────────────────────
|
|
|
|
_POINTS_EXPIRING_BODY_EN = """
|
|
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
|
<p>This is a friendly reminder that <strong>{{ points }} points</strong> in your <strong>{{ program_name }}</strong> account will expire in <strong>{{ days_remaining }} days</strong> (on {{ expiration_date }}).</p>
|
|
<div style="background: #fef3c7; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #f59e0b;">
|
|
<p style="margin: 0; font-weight: bold; color: #92400e;">⏳ {{ points }} points expiring on {{ expiration_date }}</p>
|
|
</div>
|
|
<p>Visit us before then to use your points toward a reward!</p>
|
|
<p>Best regards,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRING_BODY_FR = """
|
|
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
|
<p>Un petit rappel : <strong>{{ points }} points</strong> de votre compte <strong>{{ program_name }}</strong> expireront dans <strong>{{ days_remaining }} jours</strong> (le {{ expiration_date }}).</p>
|
|
<div style="background: #fef3c7; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #f59e0b;">
|
|
<p style="margin: 0; font-weight: bold; color: #92400e;">⏳ {{ points }} points expirent le {{ expiration_date }}</p>
|
|
</div>
|
|
<p>Rendez-nous visite avant cette date pour utiliser vos points !</p>
|
|
<p>Cordialement,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRING_BODY_DE = """
|
|
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
|
<p>Eine freundliche Erinnerung: <strong>{{ points }} Punkte</strong> auf Ihrem <strong>{{ program_name }}</strong>-Konto verfallen in <strong>{{ days_remaining }} Tagen</strong> (am {{ expiration_date }}).</p>
|
|
<div style="background: #fef3c7; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #f59e0b;">
|
|
<p style="margin: 0; font-weight: bold; color: #92400e;">⏳ {{ points }} Punkte verfallen am {{ expiration_date }}</p>
|
|
</div>
|
|
<p>Besuchen Sie uns vorher, um Ihre Punkte einzulösen!</p>
|
|
<p>Mit freundlichen Grüßen,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRING_BODY_LB = """
|
|
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
|
<p>Eng kleng Erënnerung: <strong>{{ points }} Punkten</strong> op Ärem <strong>{{ program_name }}</strong>-Konto verfalen an <strong>{{ days_remaining }} Deeg</strong> (den {{ expiration_date }}).</p>
|
|
<div style="background: #fef3c7; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #f59e0b;">
|
|
<p style="margin: 0; font-weight: bold; color: #92400e;">⏳ {{ points }} Punkten verfalen den {{ expiration_date }}</p>
|
|
</div>
|
|
<p>Besicht eis virun deem Datum fir Är Punkten anzeléisen!</p>
|
|
<p>Léif Gréiss,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
# ── Points Expired ──────────────────────────────────────────────────────────
|
|
|
|
_POINTS_EXPIRED_BODY_EN = """
|
|
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
|
<p>Unfortunately, <strong>{{ expired_points }} points</strong> in your <strong>{{ program_name }}</strong> account have expired.</p>
|
|
<div style="background: #fee2e2; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #ef4444;">
|
|
<p style="margin: 0; font-weight: bold; color: #991b1b;">{{ expired_points }} points expired</p>
|
|
</div>
|
|
<p>Don't worry — you can keep earning points on your next visit!</p>
|
|
<p>Best regards,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRED_BODY_FR = """
|
|
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
|
<p>Malheureusement, <strong>{{ expired_points }} points</strong> de votre compte <strong>{{ program_name }}</strong> ont expiré.</p>
|
|
<div style="background: #fee2e2; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #ef4444;">
|
|
<p style="margin: 0; font-weight: bold; color: #991b1b;">{{ expired_points }} points expirés</p>
|
|
</div>
|
|
<p>Pas d'inquiétude — vous pouvez continuer à gagner des points lors de votre prochaine visite !</p>
|
|
<p>Cordialement,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRED_BODY_DE = """
|
|
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
|
<p>Leider sind <strong>{{ expired_points }} Punkte</strong> auf Ihrem <strong>{{ program_name }}</strong>-Konto verfallen.</p>
|
|
<div style="background: #fee2e2; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #ef4444;">
|
|
<p style="margin: 0; font-weight: bold; color: #991b1b;">{{ expired_points }} Punkte verfallen</p>
|
|
</div>
|
|
<p>Keine Sorge — Sie können bei Ihrem nächsten Besuch weiter Punkte sammeln!</p>
|
|
<p>Mit freundlichen Grüßen,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_POINTS_EXPIRED_BODY_LB = """
|
|
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
|
<p>Leider sinn <strong>{{ expired_points }} Punkten</strong> op Ärem <strong>{{ program_name }}</strong>-Konto ofgelaf.</p>
|
|
<div style="background: #fee2e2; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #ef4444;">
|
|
<p style="margin: 0; font-weight: bold; color: #991b1b;">{{ expired_points }} Punkten ofgelaf</p>
|
|
</div>
|
|
<p>Keng Suergen — Dir kënnt bei Ärem nächste Besuch weider Punkten sammelen!</p>
|
|
<p>Léif Gréiss,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
# ── Reward Ready ────────────────────────────────────────────────────────────
|
|
|
|
_REWARD_READY_BODY_EN = """
|
|
<p style="font-size: 16px;">Hi {{ customer_name }},</p>
|
|
<p>Congratulations! You've earned a reward at <strong>{{ program_name }}</strong>! 🎉</p>
|
|
<div style="background: #ecfdf5; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981; text-align: center;">
|
|
<p style="font-size: 20px; font-weight: bold; color: #065f46; margin: 0;">🎁 {{ reward_name }}</p>
|
|
</div>
|
|
<p>Visit <strong>{{ store_name }}</strong> to redeem your reward. Just show your loyalty card!</p>
|
|
<p>Best regards,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_REWARD_READY_BODY_FR = """
|
|
<p style="font-size: 16px;">Bonjour {{ customer_name }},</p>
|
|
<p>Félicitations ! Vous avez gagné une récompense chez <strong>{{ program_name }}</strong> ! 🎉</p>
|
|
<div style="background: #ecfdf5; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981; text-align: center;">
|
|
<p style="font-size: 20px; font-weight: bold; color: #065f46; margin: 0;">🎁 {{ reward_name }}</p>
|
|
</div>
|
|
<p>Rendez-vous chez <strong>{{ store_name }}</strong> pour récupérer votre récompense. Montrez simplement votre carte de fidélité !</p>
|
|
<p>Cordialement,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_REWARD_READY_BODY_DE = """
|
|
<p style="font-size: 16px;">Hallo {{ customer_name }},</p>
|
|
<p>Herzlichen Glückwunsch! Sie haben eine Prämie bei <strong>{{ program_name }}</strong> verdient! 🎉</p>
|
|
<div style="background: #ecfdf5; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981; text-align: center;">
|
|
<p style="font-size: 20px; font-weight: bold; color: #065f46; margin: 0;">🎁 {{ reward_name }}</p>
|
|
</div>
|
|
<p>Besuchen Sie <strong>{{ store_name }}</strong> um Ihre Prämie einzulösen. Zeigen Sie einfach Ihre Treuekarte!</p>
|
|
<p>Mit freundlichen Grüßen,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
_REWARD_READY_BODY_LB = """
|
|
<p style="font-size: 16px;">Moien {{ customer_name }},</p>
|
|
<p>Felicitatiounen! Dir hutt eng Belounung bei <strong>{{ program_name }}</strong> verdéngt! 🎉</p>
|
|
<div style="background: #ecfdf5; border-radius: 8px; padding: 20px; margin: 20px 0; border-left: 4px solid #10b981; text-align: center;">
|
|
<p style="font-size: 20px; font-weight: bold; color: #065f46; margin: 0;">🎁 {{ reward_name }}</p>
|
|
</div>
|
|
<p>Besicht <strong>{{ store_name }}</strong> fir Är Belounung ofzehuelen. Weist einfach Är Treiekaart!</p>
|
|
<p>Léif Gréiss,<br><strong>{{ store_name }}</strong></p>"""
|
|
|
|
|
|
# =============================================================================
|
|
# BUILD TEMPLATE LIST
|
|
# =============================================================================
|
|
|
|
def _make_templates():
|
|
"""Build the full list of loyalty email templates."""
|
|
templates = []
|
|
|
|
_defs = [
|
|
{
|
|
"code": "loyalty_enrollment",
|
|
"name": {
|
|
"en": "Loyalty Enrollment",
|
|
"fr": "Inscription fidélité",
|
|
"de": "Treue-Anmeldung",
|
|
"lb": "Treie-Umeldung",
|
|
},
|
|
"description": "Sent when a customer enrolls in a loyalty program",
|
|
"category": EmailCategory.SYSTEM.value,
|
|
"variables": ["customer_name", "program_name", "card_number", "store_name"],
|
|
"subject": {
|
|
"en": "Welcome to {{ program_name }}!",
|
|
"fr": "Bienvenue chez {{ program_name }} !",
|
|
"de": "Willkommen bei {{ program_name }}!",
|
|
"lb": "Wëllkomm bei {{ program_name }}!",
|
|
},
|
|
"body_html": {
|
|
"en": _wrap("#6366f1", "#8b5cf6", "Welcome!", _ENROLLMENT_BODY_EN),
|
|
"fr": _wrap("#6366f1", "#8b5cf6", "Bienvenue !", _ENROLLMENT_BODY_FR),
|
|
"de": _wrap("#6366f1", "#8b5cf6", "Willkommen!", _ENROLLMENT_BODY_DE),
|
|
"lb": _wrap("#6366f1", "#8b5cf6", "Wëllkomm!", _ENROLLMENT_BODY_LB),
|
|
},
|
|
"body_text": {
|
|
"en": "Hi {{ customer_name }},\n\nWelcome to {{ program_name }}! Your loyalty card number is {{ card_number }}.\n\nStart earning rewards on every visit!\n\nBest regards,\n{{ store_name }}",
|
|
"fr": "Bonjour {{ customer_name }},\n\nBienvenue chez {{ program_name }} ! Votre numéro de carte est {{ card_number }}.\n\nCommencez à gagner des récompenses !\n\nCordialement,\n{{ store_name }}",
|
|
"de": "Hallo {{ customer_name }},\n\nWillkommen bei {{ program_name }}! Ihre Kartennummer ist {{ card_number }}.\n\nSammeln Sie ab sofort Prämien!\n\nMit freundlichen Grüßen,\n{{ store_name }}",
|
|
"lb": "Moien {{ customer_name }},\n\nWëllkomm bei {{ program_name }}! Är Kaartenummer ass {{ card_number }}.\n\nFänkt un Belounungen ze sammelen!\n\nLéif Gréiss,\n{{ store_name }}",
|
|
},
|
|
},
|
|
{
|
|
"code": "loyalty_welcome_bonus",
|
|
"name": {
|
|
"en": "Loyalty Welcome Bonus",
|
|
"fr": "Bonus de bienvenue fidélité",
|
|
"de": "Treue-Willkommensbonus",
|
|
"lb": "Treie-Wëllkommsbonus",
|
|
},
|
|
"description": "Sent when a customer receives welcome bonus points",
|
|
"category": EmailCategory.SYSTEM.value,
|
|
"variables": ["customer_name", "program_name", "points", "store_name"],
|
|
"subject": {
|
|
"en": "You earned {{ points }} bonus points!",
|
|
"fr": "Vous avez gagné {{ points }} points bonus !",
|
|
"de": "Sie haben {{ points }} Bonuspunkte erhalten!",
|
|
"lb": "Dir hutt {{ points }} Bonuspunkten kritt!",
|
|
},
|
|
"body_html": {
|
|
"en": _wrap("#6366f1", "#8b5cf6", "Bonus Points!", _WELCOME_BONUS_BODY_EN),
|
|
"fr": _wrap("#6366f1", "#8b5cf6", "Points Bonus !", _WELCOME_BONUS_BODY_FR),
|
|
"de": _wrap("#6366f1", "#8b5cf6", "Bonuspunkte!", _WELCOME_BONUS_BODY_DE),
|
|
"lb": _wrap("#6366f1", "#8b5cf6", "Bonuspunkten!", _WELCOME_BONUS_BODY_LB),
|
|
},
|
|
"body_text": {
|
|
"en": "Hi {{ customer_name }},\n\nYou've received {{ points }} bonus points as a welcome gift from {{ program_name }}.\n\nThese points are already in your balance.\n\nBest regards,\n{{ store_name }}",
|
|
"fr": "Bonjour {{ customer_name }},\n\nVous avez reçu {{ points }} points bonus de {{ program_name }}.\n\nCes points sont déjà sur votre solde.\n\nCordialement,\n{{ store_name }}",
|
|
"de": "Hallo {{ customer_name }},\n\nSie haben {{ points }} Bonuspunkte von {{ program_name }} erhalten.\n\nDiese Punkte sind bereits auf Ihrem Konto.\n\nMit freundlichen Grüßen,\n{{ store_name }}",
|
|
"lb": "Moien {{ customer_name }},\n\nDir hutt {{ points }} Bonuspunkten vu {{ program_name }} kritt.\n\nDës Punkten sinn schonn op Ärem Konto.\n\nLéif Gréiss,\n{{ store_name }}",
|
|
},
|
|
},
|
|
{
|
|
"code": "loyalty_points_expiring",
|
|
"name": {
|
|
"en": "Points Expiring Warning",
|
|
"fr": "Avertissement expiration points",
|
|
"de": "Punkteverfall-Warnung",
|
|
"lb": "Punkten-Oflaaf-Warnung",
|
|
},
|
|
"description": "Sent 14 days before loyalty points expire",
|
|
"category": EmailCategory.SYSTEM.value,
|
|
"variables": ["customer_name", "program_name", "points", "days_remaining", "expiration_date", "store_name"],
|
|
"subject": {
|
|
"en": "Your {{ points }} points expire in {{ days_remaining }} days",
|
|
"fr": "Vos {{ points }} points expirent dans {{ days_remaining }} jours",
|
|
"de": "Ihre {{ points }} Punkte verfallen in {{ days_remaining }} Tagen",
|
|
"lb": "Är {{ points }} Punkten verfalen an {{ days_remaining }} Deeg",
|
|
},
|
|
"body_html": {
|
|
"en": _wrap("#f59e0b", "#d97706", "Points Expiring Soon", _POINTS_EXPIRING_BODY_EN),
|
|
"fr": _wrap("#f59e0b", "#d97706", "Points bientôt expirés", _POINTS_EXPIRING_BODY_FR),
|
|
"de": _wrap("#f59e0b", "#d97706", "Punkte verfallen bald", _POINTS_EXPIRING_BODY_DE),
|
|
"lb": _wrap("#f59e0b", "#d97706", "Punkten verfalen geschwënn", _POINTS_EXPIRING_BODY_LB),
|
|
},
|
|
"body_text": {
|
|
"en": "Hi {{ customer_name }},\n\n{{ points }} points in your {{ program_name }} account will expire in {{ days_remaining }} days (on {{ expiration_date }}).\n\nVisit us to use your points!\n\nBest regards,\n{{ store_name }}",
|
|
"fr": "Bonjour {{ customer_name }},\n\n{{ points }} points de votre compte {{ program_name }} expireront dans {{ days_remaining }} jours (le {{ expiration_date }}).\n\nRendez-nous visite !\n\nCordialement,\n{{ store_name }}",
|
|
"de": "Hallo {{ customer_name }},\n\n{{ points }} Punkte auf Ihrem {{ program_name }}-Konto verfallen in {{ days_remaining }} Tagen (am {{ expiration_date }}).\n\nBesuchen Sie uns!\n\nMit freundlichen Grüßen,\n{{ store_name }}",
|
|
"lb": "Moien {{ customer_name }},\n\n{{ points }} Punkten op Ärem {{ program_name }}-Konto verfalen an {{ days_remaining }} Deeg (den {{ expiration_date }}).\n\nBesicht eis!\n\nLéif Gréiss,\n{{ store_name }}",
|
|
},
|
|
},
|
|
{
|
|
"code": "loyalty_points_expired",
|
|
"name": {
|
|
"en": "Points Expired",
|
|
"fr": "Points expirés",
|
|
"de": "Punkte verfallen",
|
|
"lb": "Punkten ofgelaf",
|
|
},
|
|
"description": "Sent when loyalty points have expired",
|
|
"category": EmailCategory.SYSTEM.value,
|
|
"variables": ["customer_name", "program_name", "expired_points", "store_name"],
|
|
"subject": {
|
|
"en": "{{ expired_points }} points have expired",
|
|
"fr": "{{ expired_points }} points ont expiré",
|
|
"de": "{{ expired_points }} Punkte sind verfallen",
|
|
"lb": "{{ expired_points }} Punkten sinn ofgelaf",
|
|
},
|
|
"body_html": {
|
|
"en": _wrap("#ef4444", "#dc2626", "Points Expired", _POINTS_EXPIRED_BODY_EN),
|
|
"fr": _wrap("#ef4444", "#dc2626", "Points expirés", _POINTS_EXPIRED_BODY_FR),
|
|
"de": _wrap("#ef4444", "#dc2626", "Punkte verfallen", _POINTS_EXPIRED_BODY_DE),
|
|
"lb": _wrap("#ef4444", "#dc2626", "Punkten ofgelaf", _POINTS_EXPIRED_BODY_LB),
|
|
},
|
|
"body_text": {
|
|
"en": "Hi {{ customer_name }},\n\n{{ expired_points }} points in your {{ program_name }} account have expired.\n\nKeep earning on your next visit!\n\nBest regards,\n{{ store_name }}",
|
|
"fr": "Bonjour {{ customer_name }},\n\n{{ expired_points }} points de votre compte {{ program_name }} ont expiré.\n\nContinuez à gagner des points !\n\nCordialement,\n{{ store_name }}",
|
|
"de": "Hallo {{ customer_name }},\n\n{{ expired_points }} Punkte auf Ihrem {{ program_name }}-Konto sind verfallen.\n\nSammeln Sie weiter!\n\nMit freundlichen Grüßen,\n{{ store_name }}",
|
|
"lb": "Moien {{ customer_name }},\n\n{{ expired_points }} Punkten op Ärem {{ program_name }}-Konto sinn ofgelaf.\n\nSammelt weider!\n\nLéif Gréiss,\n{{ store_name }}",
|
|
},
|
|
},
|
|
{
|
|
"code": "loyalty_reward_ready",
|
|
"name": {
|
|
"en": "Reward Ready",
|
|
"fr": "Récompense disponible",
|
|
"de": "Prämie bereit",
|
|
"lb": "Belounung prett",
|
|
},
|
|
"description": "Sent when a customer earns enough stamps for a reward",
|
|
"category": EmailCategory.MARKETING.value,
|
|
"variables": ["customer_name", "program_name", "reward_name", "store_name"],
|
|
"subject": {
|
|
"en": "You've earned a reward at {{ program_name }}! 🎉",
|
|
"fr": "Vous avez gagné une récompense chez {{ program_name }} ! 🎉",
|
|
"de": "Sie haben eine Prämie bei {{ program_name }} verdient! 🎉",
|
|
"lb": "Dir hutt eng Belounung bei {{ program_name }} verdéngt! 🎉",
|
|
},
|
|
"body_html": {
|
|
"en": _wrap("#10b981", "#059669", "Reward Earned! 🎉", _REWARD_READY_BODY_EN),
|
|
"fr": _wrap("#10b981", "#059669", "Récompense gagnée ! 🎉", _REWARD_READY_BODY_FR),
|
|
"de": _wrap("#10b981", "#059669", "Prämie verdient! 🎉", _REWARD_READY_BODY_DE),
|
|
"lb": _wrap("#10b981", "#059669", "Belounung verdéngt! 🎉", _REWARD_READY_BODY_LB),
|
|
},
|
|
"body_text": {
|
|
"en": "Hi {{ customer_name }},\n\nCongratulations! You've earned a reward: {{ reward_name }}\n\nVisit {{ store_name }} to redeem it!\n\nBest regards,\n{{ store_name }}",
|
|
"fr": "Bonjour {{ customer_name }},\n\nFélicitations ! Vous avez gagné : {{ reward_name }}\n\nRendez-vous chez {{ store_name }} !\n\nCordialement,\n{{ store_name }}",
|
|
"de": "Hallo {{ customer_name }},\n\nHerzlichen Glückwunsch! Ihre Prämie: {{ reward_name }}\n\nBesuchen Sie {{ store_name }}!\n\nMit freundlichen Grüßen,\n{{ store_name }}",
|
|
"lb": "Moien {{ customer_name }},\n\nFelicitatiounen! Är Belounung: {{ reward_name }}\n\nBesicht {{ store_name }}!\n\nLéif Gréiss,\n{{ store_name }}",
|
|
},
|
|
},
|
|
]
|
|
|
|
for defn in _defs:
|
|
for lang in ("en", "fr", "de", "lb"):
|
|
templates.append({
|
|
"code": defn["code"],
|
|
"language": lang,
|
|
"name": defn["name"][lang],
|
|
"description": defn["description"],
|
|
"category": defn["category"],
|
|
"variables": json.dumps(defn["variables"]),
|
|
"required_variables": json.dumps(defn["variables"]),
|
|
"subject": defn["subject"][lang],
|
|
"body_html": defn["body_html"][lang],
|
|
"body_text": defn["body_text"][lang],
|
|
"is_platform_only": False,
|
|
})
|
|
|
|
return templates
|
|
|
|
|
|
TEMPLATES = _make_templates()
|
|
|
|
|
|
# =============================================================================
|
|
# SEED FUNCTION
|
|
# =============================================================================
|
|
|
|
|
|
def seed_templates():
|
|
"""Seed loyalty email templates into database (idempotent)."""
|
|
db = next(get_db())
|
|
|
|
try:
|
|
created = 0
|
|
updated = 0
|
|
|
|
for template_data in TEMPLATES:
|
|
existing = (
|
|
db.query(EmailTemplate)
|
|
.filter(
|
|
EmailTemplate.code == template_data["code"],
|
|
EmailTemplate.language == template_data["language"],
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if existing:
|
|
for key, value in template_data.items():
|
|
setattr(existing, key, value)
|
|
updated += 1
|
|
print(f" Updated: {template_data['code']} ({template_data['language']})")
|
|
else:
|
|
template = EmailTemplate(**template_data)
|
|
db.add(template)
|
|
created += 1
|
|
print(f" Created: {template_data['code']} ({template_data['language']})")
|
|
|
|
db.commit()
|
|
print(f"\nLoyalty templates — Created: {created}, Updated: {updated}")
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
print(f"Error: {e}")
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
seed_templates()
|