Compare commits
5 Commits
73eefdd7bd
...
d5326c7dc6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5326c7dc6 | ||
|
|
fdad3bfb7b | ||
|
|
c3f5d72a91 | ||
|
|
241256e373 | ||
|
|
7f36abbcec |
14
.env.example
14
.env.example
@@ -1,18 +1,18 @@
|
|||||||
# Application data
|
# Application data
|
||||||
RAILS_ENV=development
|
RAILS_ENV=production
|
||||||
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
SECRET_KEY_BASE=a3f5c6e7b8d9e0f1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9s0t1u2v3w4x5y6z7
|
||||||
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
DEVISE_SECRET_KEY=your_devise_secret_key_here
|
||||||
APP_NAME=Aperonight
|
APP_NAME=Aperonight
|
||||||
|
|
||||||
# Database Configuration for production and development
|
# Database Configuration for production and development
|
||||||
DB_HOST=localhost
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
DB_ROOT_PASSWORD=root
|
DB_ROOT_PASSWORD=root
|
||||||
DB_DATABASE=aperonight
|
DB_DATABASE=aperonight
|
||||||
DB_USERNAME=root
|
DB_USERNAME=root
|
||||||
DB_PASSWORD=root
|
DB_PASSWORD=root
|
||||||
|
|
||||||
# Test database
|
# Test database
|
||||||
DB_TEST_ADAPTER=sqlite3
|
|
||||||
DB_TEST_DATABASE=aperonight_test
|
DB_TEST_DATABASE=aperonight_test
|
||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
DB_TEST_USERNAME=root
|
DB_TEST_USERNAME=root
|
||||||
@@ -28,14 +28,6 @@ SMTP_PORT=1025
|
|||||||
# SMTP_DOMAIN=localhost
|
# SMTP_DOMAIN=localhost
|
||||||
SMTP_AUTHENTICATION=plain
|
SMTP_AUTHENTICATION=plain
|
||||||
SMTP_ENABLE_STARTTLS=false
|
SMTP_ENABLE_STARTTLS=false
|
||||||
|
|
||||||
# Production SMTP Configuration (set these in .env.production)
|
|
||||||
# SMTP_ADDRESS=smtp.example.com
|
|
||||||
# SMTP_PORT=587
|
|
||||||
# SMTP_USERNAME=your_smtp_username
|
|
||||||
# SMTP_PASSWORD=your_smtp_password
|
|
||||||
# SMTP_AUTHENTICATION=plain
|
|
||||||
# SMTP_DOMAIN=example.com
|
|
||||||
# SMTP_STARTTLS=true
|
# SMTP_STARTTLS=true
|
||||||
|
|
||||||
# Application variables
|
# Application variables
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
### Medium Priority
|
### Medium Priority
|
||||||
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
- [ ] feat: Promoter system with event creation, ticket types creation and metrics display
|
||||||
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
- [ ] feat: Multiple ticket types (early bird, VIP, general admission)
|
||||||
- [ ] feat: Email notifications (purchase confirmations, event reminders)
|
|
||||||
- [ ] feat: Refund management system
|
- [ ] feat: Refund management system
|
||||||
- [ ] feat: Real-time sales analytics dashboard
|
- [ ] feat: Real-time sales analytics dashboard
|
||||||
- [ ] feat: Guest checkout without account creation
|
- [ ] feat: Guest checkout without account creation
|
||||||
@@ -43,3 +42,4 @@
|
|||||||
- [x] feat: Digital tickets with QR codes
|
- [x] feat: Digital tickets with QR codes
|
||||||
- [x] feat: Ticket inventory management and capacity limits
|
- [x] feat: Ticket inventory management and capacity limits
|
||||||
- [x] feat: Event discovery with search and filtering
|
- [x] feat: Event discovery with search and filtering
|
||||||
|
- [x] feat: Email notifications (purchase confirmations, event reminders)
|
||||||
|
|||||||
@@ -188,15 +188,8 @@ class OrdersController < ApplicationController
|
|||||||
# Don't fail the payment process due to job scheduling issues
|
# Don't fail the payment process due to job scheduling issues
|
||||||
end
|
end
|
||||||
|
|
||||||
# Send confirmation emails
|
# Email confirmation is handled by the order model's mark_as_paid! method
|
||||||
@order.tickets.each do |ticket|
|
# to avoid duplicate emails
|
||||||
begin
|
|
||||||
TicketMailer.purchase_confirmation(ticket).deliver_now
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}"
|
|
||||||
# Don't fail the entire payment process due to email/PDF generation issues
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Clear session data
|
# Clear session data
|
||||||
session.delete(:pending_cart)
|
session.delete(:pending_cart)
|
||||||
|
|||||||
19
app/jobs/event_reminder_job.rb
Normal file
19
app/jobs/event_reminder_job.rb
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
class EventReminderJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(event_id, days_before)
|
||||||
|
event = Event.find(event_id)
|
||||||
|
|
||||||
|
# Find all users with active tickets for this event
|
||||||
|
users_with_tickets = User.joins(orders: { tickets: :ticket_type })
|
||||||
|
.where(ticket_types: { event: event })
|
||||||
|
.where(tickets: { status: "active" })
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
users_with_tickets.find_each do |user|
|
||||||
|
TicketMailer.event_reminder(user, event, days_before).deliver_now
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Failed to send event reminder to user #{user.id} for event #{event.id}: #{e.message}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
44
app/jobs/event_reminder_scheduler_job.rb
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
class EventReminderSchedulerJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform
|
||||||
|
schedule_weekly_reminders
|
||||||
|
schedule_daily_reminders
|
||||||
|
schedule_day_of_reminders
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def schedule_weekly_reminders
|
||||||
|
# Find events starting in exactly 7 days
|
||||||
|
target_date = 7.days.from_now.beginning_of_day
|
||||||
|
events = Event.published
|
||||||
|
.where(start_time: target_date..(target_date + 1.day))
|
||||||
|
|
||||||
|
events.find_each do |event|
|
||||||
|
EventReminderJob.perform_later(event.id, 7)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_daily_reminders
|
||||||
|
# Find events starting in exactly 1 day (tomorrow)
|
||||||
|
target_date = 1.day.from_now.beginning_of_day
|
||||||
|
events = Event.published
|
||||||
|
.where(start_time: target_date..(target_date + 1.day))
|
||||||
|
|
||||||
|
events.find_each do |event|
|
||||||
|
EventReminderJob.perform_later(event.id, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def schedule_day_of_reminders
|
||||||
|
# Find events starting today
|
||||||
|
target_date = Time.current.beginning_of_day
|
||||||
|
events = Event.published
|
||||||
|
.where(start_time: target_date..(target_date + 1.day))
|
||||||
|
|
||||||
|
events.find_each do |event|
|
||||||
|
EventReminderJob.perform_later(event.id, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class ApplicationMailer < ActionMailer::Base
|
class ApplicationMailer < ActionMailer::Base
|
||||||
default from: "from@example.com"
|
default from: ENV.fetch("MAILER_FROM_EMAIL", "no-reply@aperonight.fr")
|
||||||
layout "mailer"
|
layout "mailer"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,5 +1,30 @@
|
|||||||
class TicketMailer < ApplicationMailer
|
class TicketMailer < ApplicationMailer
|
||||||
default from: "notifications@aperonight.com"
|
def purchase_confirmation_order(order)
|
||||||
|
@order = order
|
||||||
|
@user = order.user
|
||||||
|
@event = order.event
|
||||||
|
@tickets = order.tickets
|
||||||
|
|
||||||
|
# Generate PDF attachments for all tickets
|
||||||
|
@tickets.each do |ticket|
|
||||||
|
begin
|
||||||
|
pdf = ticket.to_pdf
|
||||||
|
attachments["ticket-#{@event.name.parameterize}-#{ticket.qr_code[0..7]}.pdf"] = {
|
||||||
|
mime_type: "application/pdf",
|
||||||
|
content: pdf
|
||||||
|
}
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Failed to generate PDF for ticket #{ticket.id}: #{e.message}"
|
||||||
|
# Continue without PDF attachment rather than failing the entire email
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: @user.email,
|
||||||
|
subject: "Confirmation d'achat - #{@event.name}",
|
||||||
|
template_name: "purchase_confirmation"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def purchase_confirmation(ticket)
|
def purchase_confirmation(ticket)
|
||||||
@ticket = ticket
|
@ticket = ticket
|
||||||
@@ -7,15 +32,49 @@ class TicketMailer < ApplicationMailer
|
|||||||
@event = ticket.event
|
@event = ticket.event
|
||||||
|
|
||||||
# Generate PDF attachment
|
# Generate PDF attachment
|
||||||
|
begin
|
||||||
pdf = @ticket.to_pdf
|
pdf = @ticket.to_pdf
|
||||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||||
mime_type: "application/pdf",
|
mime_type: "application/pdf",
|
||||||
content: pdf
|
content: pdf
|
||||||
}
|
}
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Failed to generate PDF for ticket #{@ticket.id}: #{e.message}"
|
||||||
|
# Continue without PDF attachment rather than failing the entire email
|
||||||
|
end
|
||||||
|
|
||||||
mail(
|
mail(
|
||||||
to: @user.email,
|
to: @user.email,
|
||||||
subject: "Confirmation d'achat - #{@event.name}"
|
subject: "Confirmation d'achat - #{@event.name}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def event_reminder(user, event, days_before)
|
||||||
|
@user = user
|
||||||
|
@event = event
|
||||||
|
@days_before = days_before
|
||||||
|
|
||||||
|
# Get user's tickets for this event
|
||||||
|
@tickets = Ticket.joins(:order, :ticket_type)
|
||||||
|
.where(orders: { user: @user }, ticket_types: { event: @event }, status: "active")
|
||||||
|
|
||||||
|
return if @tickets.empty?
|
||||||
|
|
||||||
|
subject = case days_before
|
||||||
|
when 7
|
||||||
|
"Rappel : #{@event.name} dans une semaine"
|
||||||
|
when 1
|
||||||
|
"Rappel : #{@event.name} demain"
|
||||||
|
when 0
|
||||||
|
"C'est aujourd'hui : #{@event.name}"
|
||||||
|
else
|
||||||
|
"Rappel : #{@event.name} dans #{days_before} jours"
|
||||||
|
end
|
||||||
|
|
||||||
|
mail(
|
||||||
|
to: @user.email,
|
||||||
|
subject: subject,
|
||||||
|
template_name: "event_reminder"
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ class Order < ApplicationRecord
|
|||||||
update!(status: "paid")
|
update!(status: "paid")
|
||||||
tickets.update_all(status: "active")
|
tickets.update_all(status: "active")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Send purchase confirmation email outside the transaction
|
||||||
|
# so that payment completion isn't affected by email failures
|
||||||
|
begin
|
||||||
|
TicketMailer.purchase_confirmation_order(self).deliver_now
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.error "Failed to send purchase confirmation email for order #{id}: #{e.message}"
|
||||||
|
Rails.logger.error e.backtrace.join("\n")
|
||||||
|
# Don't re-raise the error - payment should still succeed
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Calculate total from tickets
|
# Calculate total from tickets
|
||||||
|
|||||||
@@ -74,18 +74,21 @@ class TicketPdfGenerator
|
|||||||
raise "Ticket QR code is missing"
|
raise "Ticket QR code is missing"
|
||||||
end
|
end
|
||||||
|
|
||||||
qr_code_data = {
|
# Build QR code data with safe association loading
|
||||||
ticket_id: ticket.id,
|
qr_code_data = build_qr_code_data(ticket)
|
||||||
qr_code: ticket.qr_code,
|
|
||||||
event_id: ticket.event&.id,
|
|
||||||
user_id: ticket.user&.id
|
|
||||||
}.compact.to_json
|
|
||||||
|
|
||||||
# Validate QR code data before creating QR code
|
# Validate QR code data before creating QR code
|
||||||
if qr_code_data.blank? || qr_code_data == "{}"
|
if qr_code_data.blank? || qr_code_data == "{}"
|
||||||
|
Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}"
|
||||||
raise "QR code data is empty or invalid"
|
raise "QR code data is empty or invalid"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Ensure qr_code_data is a proper string for QR code generation
|
||||||
|
unless qr_code_data.is_a?(String) && qr_code_data.length > 2
|
||||||
|
Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})"
|
||||||
|
raise "QR code data must be a valid string"
|
||||||
|
end
|
||||||
|
|
||||||
qrcode = RQRCode::QRCode.new(qr_code_data)
|
qrcode = RQRCode::QRCode.new(qr_code_data)
|
||||||
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
pdf.print_qr_code(qrcode, extent: 120, align: :center)
|
||||||
|
|
||||||
@@ -110,4 +113,36 @@ class TicketPdfGenerator
|
|||||||
pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center
|
pdf.text "Generated on #{Time.current.strftime('%B %d, %Y at %I:%M %p')}", align: :center
|
||||||
end.render
|
end.render
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def build_qr_code_data(ticket)
|
||||||
|
# Try multiple approaches to get valid QR code data
|
||||||
|
begin
|
||||||
|
# Primary approach: full JSON with all data
|
||||||
|
data = {
|
||||||
|
ticket_id: ticket.id,
|
||||||
|
qr_code: ticket.qr_code,
|
||||||
|
event_id: ticket.ticket_type&.event_id,
|
||||||
|
user_id: ticket.order&.user_id
|
||||||
|
}.compact
|
||||||
|
|
||||||
|
# Ensure we have the minimum required data
|
||||||
|
if data[:ticket_id] && data[:qr_code]
|
||||||
|
return data.to_json
|
||||||
|
end
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn "Failed to build complex QR data: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Fallback approach: just use the ticket's QR code string
|
||||||
|
begin
|
||||||
|
return ticket.qr_code.to_s if ticket.qr_code.present?
|
||||||
|
rescue StandardError => e
|
||||||
|
Rails.logger.warn "Failed to use ticket QR code: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# Final fallback: simple ticket identifier
|
||||||
|
"TICKET-#{ticket.id}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
85
app/views/ticket_mailer/event_reminder.html.erb
Normal file
85
app/views/ticket_mailer/event_reminder.html.erb
Normal 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;">© <%= Time.current.year %> ApéroNight. Tous droits réservés.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
41
app/views/ticket_mailer/event_reminder.text.erb
Normal file
41
app/views/ticket_mailer/event_reminder.text.erb
Normal 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.
|
||||||
@@ -8,10 +8,56 @@
|
|||||||
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
<h2 style="color: #212529; margin-top: 0;">Bonjour <%= @user.email.split('@').first %>,</h2>
|
||||||
|
|
||||||
<p style="color: #495057; line-height: 1.6;">
|
<p style="color: #495057; line-height: 1.6;">
|
||||||
|
<% 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>.
|
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement <strong><%= @event.name %></strong>.
|
||||||
|
<% end %>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
<div style="background-color: #f8f9fa; border-radius: 6px; padding: 20px; margin: 25px 0;">
|
||||||
|
<% 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>
|
||||||
|
|
||||||
|
<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>
|
<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 style="display: flex; justify-content: space-between; margin-bottom: 15px;">
|
||||||
@@ -35,16 +81,27 @@
|
|||||||
<p style="margin: 5px 0 0; font-weight: bold; color: #212529;"><%= number_to_currency(@ticket.price_cents / 100.0, unit: "€") %></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>
|
</div>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="text-align: center; margin: 30px 0;">
|
<div style="text-align: center; margin: 30px 0;">
|
||||||
|
<% 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;">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>
|
<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>
|
||||||
|
|
||||||
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
<div style="background-color: #fff3cd; border-radius: 6px; padding: 15px; border-left: 4px solid #ffc107;">
|
||||||
<p style="margin: 0; color: #856404; font-size: 14px;">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
Bonjour <%= @user.email.split('@').first %>,
|
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 %>".
|
Merci pour votre achat ! Nous avons le plaisir de vous confirmer votre billet pour l'événement "<%= @event.name %>".
|
||||||
|
|
||||||
DÉTAILS DE VOTRE BILLET
|
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.
|
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.
|
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
|
Si vous avez des questions, contactez-nous à support@aperonight.com
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,6 @@ module Aperonight
|
|||||||
# config.eager_load_paths << Rails.root.join("extras")
|
# config.eager_load_paths << Rails.root.join("extras")
|
||||||
|
|
||||||
config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
|
config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
|
||||||
config.i18n.default_locale = :fr
|
# config.i18n.default_locale = :fr
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
23
config/initializers/disable_mailer_annotations.rb
Normal file
23
config/initializers/disable_mailer_annotations.rb
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Disable view annotations for mailer templates to prevent HTML comments
|
||||||
|
# from breaking email formatting in development mode
|
||||||
|
if Rails.env.development?
|
||||||
|
Rails.application.configure do
|
||||||
|
# Override the annotation setting for ActionMailer specifically
|
||||||
|
config.to_prepare do
|
||||||
|
ActionMailer::Base.prepend(Module.new do
|
||||||
|
def mail(headers = {}, &block)
|
||||||
|
# Temporarily disable view annotations during email rendering
|
||||||
|
original_setting = ActionView::Base.annotate_rendered_view_with_filenames
|
||||||
|
ActionView::Base.annotate_rendered_view_with_filenames = false
|
||||||
|
|
||||||
|
result = super(headers, &block)
|
||||||
|
|
||||||
|
# Restore original setting
|
||||||
|
ActionView::Base.annotate_rendered_view_with_filenames = original_setting
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
21
config/initializers/event_reminder_scheduler.rb
Normal file
21
config/initializers/event_reminder_scheduler.rb
Normal file
@@ -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
|
||||||
@@ -53,6 +53,7 @@ services:
|
|||||||
|
|
||||||
mailhog:
|
mailhog:
|
||||||
image: corpusops/mailhog:v1.0.1
|
image: corpusops/mailhog:v1.0.1
|
||||||
|
restart: unless-stopped
|
||||||
# environment:
|
# environment:
|
||||||
# - "mh_auth_file=/opt/mailhog/passwd.conf"
|
# - "mh_auth_file=/opt/mailhog/passwd.conf"
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
162
docs/email-notifications.md
Normal file
162
docs/email-notifications.md
Normal file
@@ -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
|
||||||
101
test/integration/email_notifications_integration_test.rb
Normal file
101
test/integration/email_notifications_integration_test.rb
Normal file
@@ -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
|
||||||
31
test/jobs/event_reminder_job_test.rb
Normal file
31
test/jobs/event_reminder_job_test.rb
Normal file
@@ -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
|
||||||
50
test/jobs/event_reminder_scheduler_job_test.rb
Normal file
50
test/jobs/event_reminder_scheduler_job_test.rb
Normal file
@@ -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
|
||||||
104
test/mailers/ticket_mailer_test.rb
Normal file
104
test/mailers/ticket_mailer_test.rb
Normal file
@@ -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
|
||||||
38
test/models/order_email_test.rb
Normal file
38
test/models/order_email_test.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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 failure does not prevent order completion" do
|
||||||
|
# Mock mailer to raise an error
|
||||||
|
TicketMailer.stubs(:purchase_confirmation_order).raises(StandardError.new("Email error"))
|
||||||
|
|
||||||
|
# Should not raise error - email failure is logged but doesn't fail the payment
|
||||||
|
@order.mark_as_paid!
|
||||||
|
|
||||||
|
# Order should still be marked as paid even if email fails
|
||||||
|
assert_equal "paid", @order.reload.status
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user