feat: Implement comprehensive email notifications system

This commit implements a complete email notifications system for purchase
confirmations and event reminders as requested in the medium priority
backlog tasks.

## Features Added

### Purchase Confirmation Emails
- Automatically sent when orders are marked as paid
- Supports both single tickets and multi-ticket orders
- Includes PDF ticket attachments
- Professional HTML and text templates in French

### Event Reminder Emails
- Automated reminders sent 7 days, 1 day, and day of events
- Only sent to users with active tickets
- Smart messaging based on time until event
- Venue details and ticket information included

### Background Jobs
- EventReminderJob: Sends reminders to all users for a specific event
- EventReminderSchedulerJob: Daily scheduler to queue reminder jobs
- Proper error handling and logging

### Email Templates
- Responsive HTML templates with ApéroNight branding
- Text fallbacks for better email client compatibility
- Dynamic content based on number of tickets and time until event

### Configuration & Testing
- Environment-based SMTP configuration for production
- Development setup with MailCatcher support
- Comprehensive test suite with mocking for PDF generation
- Integration tests for end-to-end functionality
- Documentation with usage examples

## Technical Implementation
- Enhanced TicketMailer with new notification methods
- Background job scheduling via Rails initializer
- Order model integration for automatic purchase confirmations
- Proper associations handling for user/ticket relationships
- Configurable via environment variables

## Files Added/Modified
- Enhanced app/mailers/ticket_mailer.rb with order support
- Added app/jobs/event_reminder_*.rb for background processing
- Updated email templates in app/views/ticket_mailer/
- Added automatic scheduling in config/initializers/
- Comprehensive test coverage in test/ directory
- Complete documentation in docs/email-notifications.md

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
kbe
2025-09-06 13:25:02 +02:00
parent 73eefdd7bd
commit 7f36abbcec
17 changed files with 854 additions and 26 deletions

View File

@@ -0,0 +1,85 @@
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; border-radius: 8px;">
<div style="text-align: center; padding: 20px 0; border-bottom: 1px solid #e9ecef;">
<h1 style="color: #4c1d95; margin: 0; font-size: 28px;">ApéroNight</h1>
<p style="color: #6c757d; margin: 10px 0 0;">Rappel d'événement</p>
</div>
<div style="background-color: white; border-radius: 8px; padding: 30px; margin: 20px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h2 style="color: #212529; margin-top: 0;">Salut <%= @user.email.split('@').first %> ! 🎉</h2>
<p style="color: #495057; line-height: 1.6; font-size: 18px;">
<% case @days_before %>
<% when 7 %>
Plus qu'une semaine avant <strong><%= @event.name %></strong> !
<% when 1 %>
C'est demain ! <strong><%= @event.name %></strong> a lieu demain.
<% when 0 %>
C'est aujourd'hui ! <strong><%= @event.name %></strong> a lieu aujourd'hui.
<% else %>
Plus que <%= @days_before %> jours avant <strong><%= @event.name %></strong> !
<% end %>
</p>
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de l'événement</h3>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">📅 Date & heure</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529; font-size: 16px;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
</div>
</div>
<div style="margin-bottom: 15px;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">📍 Lieu</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.venue_name %></p>
<p style="margin: 5px 0 0; color: #495057;"><%= @event.venue_address %></p>
</div>
</div>
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
<h4 style="color: #4c1d95; margin-top: 0; margin-bottom: 15px;">Vos billets pour cet événement :</h4>
<% @tickets.each_with_index do |ticket, index| %>
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">🎫 Billet #<%= index + 1 %></p>
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
</div>
<div style="text-align: right;">
<span style="background-color: #d4edda; color: #155724; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold;">ACTIF</span>
</div>
</div>
</div>
<% end %>
</div>
<div style="text-align: center; margin: 30px 0;">
<% if @days_before == 0 %>
<p style="color: #495057; margin-bottom: 20px; font-size: 16px;">🚨 N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.</p>
<% else %>
<p style="color: #495057; margin-bottom: 20px;">📧 Vos billets ont été envoyés par email lors de votre achat.</p>
<% end %>
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
</div>
<% if @days_before <= 1 %>
<div style="background-color: #d1ecf1; border-radius: 6px; padding: 15px; border-left: 4px solid #17a2b8; margin: 20px 0;">
<p style="margin: 0; color: #0c5460; font-size: 14px;">
<strong>💡 Conseil :</strong> Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
</p>
</div>
<% else %>
<div style="background-color: #d4edda; border-radius: 6px; padding: 15px; border-left: 4px solid #28a745;">
<p style="margin: 0; color: #155724; font-size: 14px;">
<strong>📅 Ajoutez à votre calendrier :</strong> N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
</p>
</div>
<% end %>
</div>
<div style="text-align: center; color: #6c757d; font-size: 14px; padding: 20px 0;">
<p style="margin: 0;">Des questions ? Contactez-nous à <a href="mailto:support@aperonight.com" style="color: #4c1d95; text-decoration: none;">support@aperonight.com</a></p>
<p style="margin: 10px 0 0;">&copy; <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
</div>
</div>

View File

@@ -0,0 +1,41 @@
Salut <%= @user.email.split('@').first %> !
<% case @days_before %>
<% when 7 %>
Plus qu'une semaine avant "<%= @event.name %>" !
<% when 1 %>
C'est demain ! "<%= @event.name %>" a lieu demain.
<% when 0 %>
C'est aujourd'hui ! "<%= @event.name %>" a lieu aujourd'hui.
<% else %>
Plus que <%= @days_before %> jours avant "<%= @event.name %>" !
<% end %>
DÉTAILS DE L'ÉVÉNEMENT
======================
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
Lieu : <%= @event.venue_name %>
Adresse : <%= @event.venue_address %>
VOS BILLETS POUR CET ÉVÉNEMENT :
<% @tickets.each_with_index do |ticket, index| %>
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> (ACTIF)
<% end %>
<% if @days_before == 0 %>
N'oubliez pas vos billets ! Ils ont été envoyés par email lors de votre achat.
<% else %>
Vos billets ont été envoyés par email lors de votre achat.
<% end %>
Présentez-les à l'entrée de l'événement pour y accéder.
<% if @days_before <= 1 %>
Conseil : Arrivez un peu en avance pour éviter les files d'attente à l'entrée !
<% else %>
N'oubliez pas d'ajouter cet événement à votre calendrier pour ne pas le manquer !
<% end %>
Des questions ? Contactez-nous à support@aperonight.com
© <%= Time.current.year %> ApéroNight. Tous droits réservés.

View File

@@ -8,43 +8,100 @@
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
<p style="color: #495057; line-height: 1.6;">
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
<% if defined?(@order) && @order.present? %>
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement <strong><%= @event.name %></strong>.
<% else %>
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
<% end %>
</p>
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
<% if defined?(@order) && @order.present? %>
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre commande</h3>
<div style="margin-bottom: 20px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
</div>
</div>
<div style="display: flex; justify-content: space-between;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Nombre de billets</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @tickets.count %></p>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Total</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@order.total_amount_euros, unit: "€") %></p>
</div>
</div>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Type de billet</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @ticket.ticket_type.name %></p>
<h4 style="color: #4c1d95; margin: 20px 0 15px;">Billets inclus :</h4>
<% @tickets.each_with_index do |ticket, index| %>
<div style="border: 1px solid #e9ecef; border-radius: 4px; padding: 15px; margin-bottom: 10px; background-color: white;">
<div style="display: flex; justify-content: space-between;">
<div>
<p style="margin: 0 0 5px; font-weight: bold; color: #212529;">Billet #<%= index + 1 %></p>
<p style="margin: 0; color: #6c757d; font-size: 14px;"><%= ticket.ticket_type.name %></p>
</div>
<div style="text-align: right;">
<p style="margin: 0; font-weight: bold; color: #212529;"><%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %></p>
</div>
</div>
</div>
<% end %>
<% else %>
<h3 style="color: #4c1d95; margin-top: 0; border-bottom: 1px solid #e9ecef; padding-bottom: 10px;">Détails de votre billet</h3>
<div style="display: flex; justify-content: space-between; margin-bottom: 15px;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Événement</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.name %></p>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Type de billet</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @ticket.ticket_type.name %></p>
</div>
</div>
</div>
<div style="display: flex; justify-content: space-between;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
<div style="display: flex; justify-content: space-between;">
<div>
<p style="margin: 0; color: #6c757d; font-size: 14px;">Date & heure</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= @event.start_time.strftime("%d %B %Y à %H:%M") %></p>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
</div>
</div>
<div style="text-align: right;">
<p style="margin: 0; color: #6c757d; font-size: 14px;">Prix</p>
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></p>
</div>
</div>
<% end %>
</div>
<div style="text-align: center; margin: 30px 0;">
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
<% if defined?(@order) && @order.present? %>
<p style="color: #495057; margin-bottom: 20px;">Vos billets sont attachés à cet email en format PDF.</p>
<p style="color: #495057; margin-bottom: 20px;">Présentez-les à l'entrée de l'événement pour y accéder.</p>
<% else %>
<p style="color: #495057; margin-bottom: 20px;">Votre billet est attaché à cet email en format PDF.</p>
<p style="color: #495057; margin-bottom: 20px;">Présentez-le à l'entrée de l'événement pour y accéder.</p>
<% end %>
</div>
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
<p style="margin: 0; color: #856404; font-size: 14px;">
<strong>Important :</strong> Ce billet est valable pour une seule entrée. Conservez-le précieusement.
<strong>Important :</strong>
<% if defined?(@order) && @order.present? %>
Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
<% else %>
Ce billet est valable pour une seule entrée. Conservez-le précieusement.
<% end %>
</p>
</div>
</div>

View File

@@ -1,5 +1,25 @@
Bonjour <%= @user.email.split('@').first %>,
<% if defined?(@order) && @order.present? %>
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre commande pour l'événement "<%= @event.name %>".
DÉTAILS DE VOTRE COMMANDE
=========================
Événement : <%= @event.name %>
Date & heure : <%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
Nombre de billets : <%= @tickets.count %>
Total : <%= number_to_currency(@order.total_amount_euros, unit: "€") %>
BILLETS INCLUS :
<% @tickets.each_with_index do |ticket, index| %>
- Billet #<%= index + 1 %> : <%= ticket.ticket_type.name %> - <%= number_to_currency(ticket.price_cents / 100.0, unit: "€") %>
<% end %>
Vos billets sont attachés à cet email en format PDF. Présentez-les à l'entrée de l'événement pour y accéder.
Important : Ces billets sont valables pour une seule entrée chacun. Conservez-les précieusement.
<% else %>
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
DÉTAILS DE VOTRE BILLET
@@ -13,6 +33,7 @@ Prix : <%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
Votre billet est attaché à cet email en format PDF. Présentez-le à l'entrée de l'événement pour y accéder.
Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
<% end %>
Si vous avez des questions, contactez-nous à support@aperonight.com