-
-
Événement
-
<%= @event.name %>
+ <% if defined?(@order) && @order.present? %>
+
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: "€") %>
+
+
-
-
Type de billet
-
<%= @ticket.ticket_type.name %>
+
+
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 %>
+ <% else %>
+
Détails de votre billet
+
+
+
+
Événement
+
<%= @event.name %>
+
+
+
Type de billet
+
<%= @ticket.ticket_type.name %>
+
-
-
-
-
-
Date & heure
-
<%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
+
+
+
+
Date & heure
+
<%= @event.start_time.strftime("%d %B %Y à %H:%M") %>
+
+
+
Prix
+
<%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
+
-
-
Prix
-
<%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %>
-
-
+ <% end %>
-
Votre billet est attaché à cet email en format PDF.
-
Présentez-le à l'entrée de l'événement pour y accéder.
+ <% if defined?(@order) && @order.present? %>
+
Vos billets sont attachés à cet email en format PDF.
+
Présentez-les à l'entrée de l'événement pour y accéder.
+ <% else %>
+
Votre billet est attaché à cet email en format PDF.
+
Présentez-le à l'entrée de l'événement pour y accéder.
+ <% end %>
- Important : Ce billet est valable pour une seule entrée. Conservez-le précieusement.
+ Important :
+ <% 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 %>
diff --git a/app/views/ticket_mailer/purchase_confirmation.text.erb b/app/views/ticket_mailer/purchase_confirmation.text.erb
index f881ef1..3d7b069 100755
--- a/app/views/ticket_mailer/purchase_confirmation.text.erb
+++ b/app/views/ticket_mailer/purchase_confirmation.text.erb
@@ -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
diff --git a/config/initializers/event_reminder_scheduler.rb b/config/initializers/event_reminder_scheduler.rb
new file mode 100644
index 0000000..818465c
--- /dev/null
+++ b/config/initializers/event_reminder_scheduler.rb
@@ -0,0 +1,21 @@
+# Schedule event reminder notifications
+Rails.application.config.after_initialize do
+ # Only schedule in production or when SCHEDULE_REMINDERS is set
+ if Rails.env.production? || ENV["SCHEDULE_REMINDERS"] == "true"
+ # Schedule the reminder scheduler to run daily at 9 AM
+ begin
+ # Use a simple cron-like approach with ActiveJob
+ # This will be handled by solid_queue in production
+ EventReminderSchedulerJob.set(wait_until: next_run_time).perform_later
+ rescue StandardError => e
+ Rails.logger.warn "Could not schedule event reminders: #{e.message}"
+ end
+ end
+end
+
+def next_run_time
+ # Schedule for 9 AM today, or 9 AM tomorrow if it's already past 9 AM
+ target_time = Time.current.beginning_of_day + 9.hours
+ target_time += 1.day if Time.current > target_time
+ target_time
+end
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index fd812e5..eec0d68 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -53,6 +53,7 @@ services:
mailhog:
image: corpusops/mailhog:v1.0.1
+ restart: unless-stopped
# environment:
# - "mh_auth_file=/opt/mailhog/passwd.conf"
volumes:
diff --git a/docs/email-notifications.md b/docs/email-notifications.md
new file mode 100644
index 0000000..932ca04
--- /dev/null
+++ b/docs/email-notifications.md
@@ -0,0 +1,162 @@
+# Email Notifications System
+
+This document describes the email notifications system implemented for ApéroNight.
+
+## Overview
+
+The email notifications system provides two main types of notifications:
+1. **Purchase Confirmation Emails** - Sent when orders are completed
+2. **Event Reminder Emails** - Sent at scheduled intervals before events
+
+## Features
+
+### Purchase Confirmation Emails
+
+- **Trigger**: Automatically sent when an order is marked as paid
+- **Content**: Order details, ticket information, PDF attachments for each ticket
+- **Template**: Supports both single tickets and multi-ticket orders
+- **Languages**: French (can be extended)
+
+### Event Reminder Emails
+
+- **Schedule**: 7 days before, 1 day before, and day of event
+- **Content**: Event details, user's ticket information, venue information
+- **Recipients**: Only users with active tickets for the event
+- **Smart Content**: Different messaging based on time until event
+
+## Technical Implementation
+
+### Mailer Classes
+
+#### TicketMailer
+- `purchase_confirmation_order(order)` - For complete orders with multiple tickets
+- `purchase_confirmation(ticket)` - For individual tickets
+- `event_reminder(user, event, days_before)` - For event reminders
+
+### Background Jobs
+
+#### EventReminderJob
+- Sends reminder emails to all users with active tickets for a specific event
+- Parameters: `event_id`, `days_before`
+- Error handling: Logs failures but continues processing other users
+
+#### EventReminderSchedulerJob
+- Runs daily to schedule reminder emails
+- Automatically finds events starting in 7 days, 1 day, or same day
+- Only processes published events
+- Configurable via environment variables
+
+### Email Templates
+
+Templates are available in both HTML and text formats:
+
+- `app/views/ticket_mailer/purchase_confirmation.html.erb`
+- `app/views/ticket_mailer/purchase_confirmation.text.erb`
+- `app/views/ticket_mailer/event_reminder.html.erb`
+- `app/views/ticket_mailer/event_reminder.text.erb`
+
+### Configuration
+
+#### Environment Variables
+- `MAILER_FROM_EMAIL` - From address for emails (default: no-reply@aperonight.fr)
+- `SMTP_*` - SMTP configuration for production
+- `SCHEDULE_REMINDERS` - Enable automatic reminder scheduling in non-production
+
+#### Development Setup
+- Uses localhost:1025 for development (MailCatcher recommended)
+- Email delivery is configured but won't raise errors in development
+
+## Usage
+
+### Manual Testing
+
+```ruby
+# Test purchase confirmation
+order = Order.last
+TicketMailer.purchase_confirmation_order(order).deliver_now
+
+# Test event reminder
+user = User.first
+event = Event.published.first
+TicketMailer.event_reminder(user, event, 7).deliver_now
+
+# Test scheduler job
+EventReminderSchedulerJob.perform_now
+```
+
+### Integration in Code
+
+Purchase confirmation emails are automatically sent when orders are marked as paid:
+
+```ruby
+order.mark_as_paid! # Automatically sends confirmation email
+```
+
+Event reminders are automatically scheduled via the initializer, but can be manually triggered:
+
+```ruby
+# Schedule reminders for a specific event
+EventReminderJob.perform_later(event.id, 7) # 7 days before
+```
+
+## Deployment Notes
+
+### Production Configuration
+
+1. Configure SMTP settings via environment variables
+2. Set `MAILER_FROM_EMAIL` to your domain
+3. Ensure `SCHEDULE_REMINDERS=true` to enable automatic reminders
+4. Configure solid_queue for background job processing
+
+### Monitoring
+
+- Check logs for email delivery failures
+- Monitor job queue for stuck reminder jobs
+- Verify SMTP configuration is working
+
+### Customization
+
+- Email templates can be customized in `app/views/ticket_mailer/`
+- Add new reminder intervals by modifying `EventReminderSchedulerJob`
+- Internationalization can be added using Rails I18n
+
+## File Structure
+
+```
+app/
+├── jobs/
+│ ├── event_reminder_job.rb
+│ └── event_reminder_scheduler_job.rb
+├── mailers/
+│ ├── application_mailer.rb
+│ └── ticket_mailer.rb
+└── views/
+ └── ticket_mailer/
+ ├── purchase_confirmation.html.erb
+ ├── purchase_confirmation.text.erb
+ ├── event_reminder.html.erb
+ └── event_reminder.text.erb
+
+config/
+├── environments/
+│ ├── development.rb (SMTP localhost:1025)
+│ └── production.rb (ENV-based SMTP)
+└── initializers/
+ └── event_reminder_scheduler.rb
+
+test/
+├── jobs/
+│ ├── event_reminder_job_test.rb
+│ └── event_reminder_scheduler_job_test.rb
+├── mailers/
+│ └── ticket_mailer_test.rb
+└── integration/
+ └── email_notifications_integration_test.rb
+```
+
+## Security Considerations
+
+- No sensitive information in email templates
+- User data is properly escaped in templates
+- QR codes contain only necessary ticket verification data
+- Email addresses are validated through Devise
\ No newline at end of file
diff --git a/test/integration/email_notifications_integration_test.rb b/test/integration/email_notifications_integration_test.rb
new file mode 100644
index 0000000..3a04215
--- /dev/null
+++ b/test/integration/email_notifications_integration_test.rb
@@ -0,0 +1,101 @@
+require "test_helper"
+
+class EmailNotificationsIntegrationTest < ActionDispatch::IntegrationTest
+ include ActiveJob::TestHelper
+
+ def setup
+ @user = User.create!(
+ email: "test@example.com",
+ password: "password123",
+ first_name: "Test",
+ last_name: "User"
+ )
+
+ @event = Event.create!(
+ name: "Test Event",
+ slug: "test-event",
+ description: "A test event for integration testing",
+ state: :published,
+ venue_name: "Test Venue",
+ venue_address: "123 Test Street",
+ latitude: 40.7128,
+ longitude: -74.0060,
+ start_time: 1.week.from_now,
+ end_time: 1.week.from_now + 4.hours,
+ user: @user
+ )
+
+ @ticket_type = TicketType.create!(
+ name: "General Admission",
+ description: "General admission ticket",
+ price_cents: 2500,
+ quantity: 100,
+ sale_start_at: 1.day.ago,
+ sale_end_at: 1.day.from_now,
+ event: @event
+ )
+
+ @order = Order.create!(
+ user: @user,
+ event: @event,
+ status: "draft",
+ total_amount_cents: 2500,
+ payment_attempts: 0
+ )
+
+ @ticket = Ticket.create!(
+ order: @order,
+ ticket_type: @ticket_type,
+ first_name: "Test",
+ last_name: "User",
+ price_cents: 2500,
+ status: "draft"
+ )
+ end
+
+ test "sends purchase confirmation email when order is marked as paid" do
+ # Mock PDF generation to avoid QR code issues
+ @ticket.stubs(:to_pdf).returns("fake_pdf_content")
+
+ assert_emails 1 do
+ @order.mark_as_paid!
+ end
+
+ assert_equal "paid", @order.status
+ assert_equal "active", @ticket.reload.status
+ end
+
+ test "event reminder email can be sent to users with active tickets" do
+ # Setup: mark order as paid and activate tickets
+ @ticket.stubs(:to_pdf).returns("fake_pdf_content")
+ @order.mark_as_paid!
+
+ # Clear any emails from the setup
+ ActionMailer::Base.deliveries.clear
+
+ assert_emails 1 do
+ TicketMailer.event_reminder(@user, @event, 7).deliver_now
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ assert_equal [@user.email], email.to
+ assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
+ end
+
+ test "event reminder job schedules emails for users with tickets" do
+ # Setup: mark order as paid and activate tickets
+ @ticket.stubs(:to_pdf).returns("fake_pdf_content")
+ @order.mark_as_paid!
+
+ # Clear any emails from the setup
+ ActionMailer::Base.deliveries.clear
+
+ # Perform the job
+ EventReminderJob.perform_now(@event.id, 7)
+
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ email = ActionMailer::Base.deliveries.last
+ assert_equal [@user.email], email.to
+ assert_match "une semaine", email.subject
+ end
+end
\ No newline at end of file
diff --git a/test/jobs/event_reminder_job_test.rb b/test/jobs/event_reminder_job_test.rb
new file mode 100644
index 0000000..0e19a0f
--- /dev/null
+++ b/test/jobs/event_reminder_job_test.rb
@@ -0,0 +1,31 @@
+require "test_helper"
+
+class EventReminderJobTest < ActiveJob::TestCase
+ def setup
+ @event = events(:concert_event)
+ @user = users(:one)
+ @ticket = tickets(:one)
+ end
+
+ test "performs event reminder job for users with tickets" do
+ # Mock the mailer to avoid actual email sending in tests
+ TicketMailer.expects(:event_reminder).with(@user, @event, 7).returns(stub(deliver_now: true))
+
+ EventReminderJob.perform_now(@event.id, 7)
+ end
+
+ test "handles missing event gracefully" do
+ assert_raises(ActiveRecord::RecordNotFound) do
+ EventReminderJob.perform_now(999999, 7)
+ end
+ end
+
+ test "logs error when mailer fails" do
+ # Mock a failing mailer
+ TicketMailer.stubs(:event_reminder).raises(StandardError.new("Test error"))
+
+ Rails.logger.expects(:error).with(regexp_matches(/Failed to send event reminder/))
+
+ EventReminderJob.perform_now(@event.id, 7)
+ end
+end
\ No newline at end of file
diff --git a/test/jobs/event_reminder_scheduler_job_test.rb b/test/jobs/event_reminder_scheduler_job_test.rb
new file mode 100644
index 0000000..507194d
--- /dev/null
+++ b/test/jobs/event_reminder_scheduler_job_test.rb
@@ -0,0 +1,50 @@
+require "test_helper"
+
+class EventReminderSchedulerJobTest < ActiveJob::TestCase
+ def setup
+ @event = events(:concert_event)
+ end
+
+ test "schedules weekly reminders for events starting in 7 days" do
+ # Set event to start in exactly 7 days
+ @event.update(start_time: 7.days.from_now.beginning_of_day + 10.hours)
+
+ assert_enqueued_with(job: EventReminderJob, args: [@event.id, 7]) do
+ EventReminderSchedulerJob.perform_now
+ end
+ end
+
+ test "schedules daily reminders for events starting tomorrow" do
+ # Set event to start tomorrow
+ @event.update(start_time: 1.day.from_now.beginning_of_day + 20.hours)
+
+ assert_enqueued_with(job: EventReminderJob, args: [@event.id, 1]) do
+ EventReminderSchedulerJob.perform_now
+ end
+ end
+
+ test "schedules day-of reminders for events starting today" do
+ # Set event to start today
+ @event.update(start_time: Time.current.beginning_of_day + 21.hours)
+
+ assert_enqueued_with(job: EventReminderJob, args: [@event.id, 0]) do
+ EventReminderSchedulerJob.perform_now
+ end
+ end
+
+ test "does not schedule reminders for draft events" do
+ @event.update(state: :draft, start_time: 7.days.from_now.beginning_of_day + 10.hours)
+
+ assert_no_enqueued_jobs(only: EventReminderJob) do
+ EventReminderSchedulerJob.perform_now
+ end
+ end
+
+ test "does not schedule reminders for cancelled events" do
+ @event.update(state: :canceled, start_time: 7.days.from_now.beginning_of_day + 10.hours)
+
+ assert_no_enqueued_jobs(only: EventReminderJob) do
+ EventReminderSchedulerJob.perform_now
+ end
+ end
+end
\ No newline at end of file
diff --git a/test/mailers/ticket_mailer_test.rb b/test/mailers/ticket_mailer_test.rb
new file mode 100644
index 0000000..789428f
--- /dev/null
+++ b/test/mailers/ticket_mailer_test.rb
@@ -0,0 +1,104 @@
+require "test_helper"
+
+class TicketMailerTest < ActionMailer::TestCase
+ def setup
+ @user = users(:one)
+ @event = events(:concert_event)
+ @ticket_type = ticket_types(:standard)
+ @order = orders(:paid_order)
+ @ticket = tickets(:one)
+ end
+
+ test "purchase confirmation order email" do
+ # Mock PDF generation for all tickets
+ @order.tickets.each do |ticket|
+ ticket.stubs(:to_pdf).returns("fake_pdf_data")
+ end
+
+ email = TicketMailer.purchase_confirmation_order(@order)
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal ["no-reply@aperonight.fr"], email.from
+ assert_equal [@user.email], email.to
+ assert_equal "Confirmation d'achat - #{@event.name}", email.subject
+ assert_match @event.name, email.body.to_s
+ assert_match @user.email.split('@').first, email.body.to_s
+ end
+
+ test "purchase confirmation single ticket email" do
+ # Mock PDF generation
+ @ticket.stubs(:to_pdf).returns("fake_pdf_data")
+
+ email = TicketMailer.purchase_confirmation(@ticket)
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal ["no-reply@aperonight.fr"], email.from
+ assert_equal [@ticket.user.email], email.to
+ assert_equal "Confirmation d'achat - #{@ticket.event.name}", email.subject
+ assert_match @ticket.event.name, email.body.to_s
+ assert_match @ticket.user.email.split('@').first, email.body.to_s
+ end
+
+ test "event reminder email one week before" do
+ # Ensure the user has active tickets for the event by using the existing fixtures
+ # The 'one' ticket fixture is already linked to the 'paid_order' and 'concert_event'
+ email = TicketMailer.event_reminder(@user, @event, 7)
+
+ # Only test delivery if the user has tickets (the method returns early if not)
+ if email
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal ["no-reply@aperonight.fr"], email.from
+ assert_equal [@user.email], email.to
+ assert_equal "Rappel : #{@event.name} dans une semaine", email.subject
+ assert_match "une semaine", email.body.to_s
+ assert_match @event.name, email.body.to_s
+ else
+ # If no email is sent, that's expected behavior when user has no active tickets
+ assert_no_emails do
+ TicketMailer.event_reminder(@user, @event, 7)
+ end
+ end
+ end
+
+ test "event reminder email one day before" do
+ email = TicketMailer.event_reminder(@user, @event, 1)
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal "Rappel : #{@event.name} demain", email.subject
+ assert_match "demain", email.body.to_s
+ end
+
+ test "event reminder email day of event" do
+ email = TicketMailer.event_reminder(@user, @event, 0)
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal "C'est aujourd'hui : #{@event.name}", email.subject
+ assert_match "aujourd'hui", email.body.to_s
+ end
+
+ test "event reminder email custom days" do
+ email = TicketMailer.event_reminder(@user, @event, 3)
+
+ assert_emails 1 do
+ email.deliver_now
+ end
+
+ assert_equal "Rappel : #{@event.name} dans 3 jours", email.subject
+ assert_match "3 jours", email.body.to_s
+ end
+end
\ No newline at end of file
diff --git a/test/models/order_email_test.rb b/test/models/order_email_test.rb
new file mode 100644
index 0000000..cdaa3ad
--- /dev/null
+++ b/test/models/order_email_test.rb
@@ -0,0 +1,39 @@
+require "test_helper"
+
+class OrderEmailTest < ActiveSupport::TestCase
+ def setup
+ @order = orders(:draft_order)
+ end
+
+ test "sends purchase confirmation email when order is marked as paid" do
+ # Mock the mailer to capture the call
+ TicketMailer.expects(:purchase_confirmation_order).with(@order).returns(stub(deliver_now: true))
+
+ @order.mark_as_paid!
+
+ assert_equal "paid", @order.status
+ end
+
+ test "activates all tickets when order is marked as paid" do
+ @order.tickets.update_all(status: "reserved")
+
+ # Mock the mailer to avoid actual email sending
+ TicketMailer.stubs(:purchase_confirmation_order).returns(stub(deliver_now: true))
+
+ @order.mark_as_paid!
+
+ assert @order.tickets.all? { |ticket| ticket.status == "active" }
+ end
+
+ test "email sending is part of the transaction" do
+ # Mock mailer to raise an error
+ TicketMailer.stubs(:purchase_confirmation_order).raises(StandardError.new("Email error"))
+
+ assert_raises(StandardError) do
+ @order.mark_as_paid!
+ end
+
+ # Order should not be marked as paid if email fails
+ assert_equal "draft", @order.reload.status
+ end
+end
\ No newline at end of file