From 5105964b392270439b95ea68a6f41b17f59a5eb6 Mon Sep 17 00:00:00 2001 From: kbe Date: Sat, 6 Sep 2025 01:44:48 +0200 Subject: [PATCH] feat: improve seo urls? --- app/assets/stylesheets/pdf.scss | 141 ++++++++++ app/controllers/application_controller.rb | 44 ++++ .../booking/payments_controller.rb | 92 +++++++ app/controllers/events_controller.rb | 29 +- .../legacy_redirects_controller.rb | 17 ++ app/controllers/orders_controller.rb | 172 ++++-------- app/controllers/tickets_controller.rb | 247 +++++++----------- app/helpers/pdf_helper.rb | 17 ++ .../ticket_selection_controller.js | 2 +- app/views/booking/payments/success.html.erb | 61 +++++ app/views/components/_event_item.html.erb | 2 +- app/views/events/index.html.erb | 4 +- app/views/events/show.html.erb | 62 ++++- app/views/layouts/application.html.erb | 23 +- app/views/orders/checkout.html.erb | 4 +- app/views/orders/new.html.erb | 6 +- app/views/orders/show.html.erb | 6 +- app/views/pages/home.html.erb | 4 +- app/views/promoter/events/show.html.erb | 2 +- app/views/tickets/checkout.html.erb | 2 +- app/views/tickets/new.html.erb | 4 +- config/routes.rb | 128 ++++----- 22 files changed, 702 insertions(+), 367 deletions(-) create mode 100644 app/assets/stylesheets/pdf.scss create mode 100644 app/controllers/booking/payments_controller.rb create mode 100644 app/controllers/legacy_redirects_controller.rb create mode 100644 app/helpers/pdf_helper.rb create mode 100644 app/views/booking/payments/success.html.erb diff --git a/app/assets/stylesheets/pdf.scss b/app/assets/stylesheets/pdf.scss new file mode 100644 index 0000000..9d374ca --- /dev/null +++ b/app/assets/stylesheets/pdf.scss @@ -0,0 +1,141 @@ +/* PDF Styles for Ticket Generation */ + +body { + font-family: Helvetica, Arial, sans-serif; + font-size: 12px; + color: #000000; + margin: 0; + padding: 20px; + background-color: #ffffff; +} + +.ticket-container { + max-width: 350px; + margin: 0 auto; + padding: 20px; + border: 1px solid #e5e7eb; + border-radius: 10px; + background-color: #ffffff; +} + +/* Header */ +.header { + text-align: center; + margin-bottom: 10px; +} + +.header h1 { + color: #2D1B69; + font-size: 24px; + font-weight: bold; + margin: 0; +} + +/* Event name */ +.event-name { + text-align: center; + margin-bottom: 20px; +} + +.event-name h2 { + color: #000000; + font-size: 18px; + font-weight: bold; + margin: 0; +} + +/* Ticket info box */ +.ticket-info-box { + background-color: #F9FAFB; + border: 1px solid #E5E7EB; + border-radius: 10px; + padding: 15px; + margin-bottom: 20px; +} + +.info-row { + margin-bottom: 8px; +} + +.info-row:last-child { + margin-bottom: 0; +} + +.info-label { + font-weight: bold; + color: #000000; + display: inline-block; + width: 100px; +} + +.info-value { + display: inline-block; + color: #000000; +} + +/* Venue information */ +.venue-info { + margin-bottom: 20px; +} + +.venue-info h3 { + color: #374151; + font-size: 14px; + font-weight: bold; + margin: 0 0 8px 0; +} + +.venue-details { + font-size: 11px; +} + +.venue-name { + font-weight: bold; + margin-bottom: 4px; +} + +.venue-address { + color: #000000; +} + +/* QR Code */ +.qr-code-section { + text-align: center; + margin-bottom: 15px; +} + +.qr-code-section h3 { + color: #000000; + font-size: 14px; + font-weight: bold; + margin: 0 0 10px 0; +} + +.qr-code-container { + text-align: center; + margin: 0 auto 10px auto; + width: 120px; + height: 120px; +} + +.qr-code-text { + font-size: 8px; + color: #6B7280; +} + +/* Footer */ +.footer { + border-top: 1px solid #E5E7EB; + padding-top: 15px; + text-align: center; + font-size: 8px; + color: #6B7280; +} + +.footer p { + margin: 0 0 5px 0; +} + +.generated-date { + margin-top: 5px; +} \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0cbd1a8..9ba314c 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -14,4 +14,48 @@ class ApplicationController < ActionController::Base # - CSS nesting and :has() pseudo-class # allow_browser versions: :modern # allow_browser versions: { safari: 16.4, firefox: 121, ie: false } + + protected + + # Generate SEO-friendly path for an event + def seo_event_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + event_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_event_path + + # Generate SEO-friendly booking URL for an event + def seo_book_tickets_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + book_event_tickets_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_book_tickets_path + + # Generate SEO-friendly checkout URL for an event + def seo_checkout_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + event_checkout_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_checkout_path + + # Generate SEO-friendly ticket URL + def seo_ticket_path(ticket) + ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id) + end + helper_method :seo_ticket_path + + # Generate SEO-friendly ticket view URL + def seo_ticket_view_path(ticket) + view_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id) + end + helper_method :seo_ticket_view_path + + # Generate SEO-friendly ticket download URL + def seo_ticket_download_path(ticket) + download_ticket_path(event_slug: ticket.event.slug, ticket_id: ticket.id) + end + helper_method :seo_ticket_download_path end diff --git a/app/controllers/booking/payments_controller.rb b/app/controllers/booking/payments_controller.rb new file mode 100644 index 0000000..ea98127 --- /dev/null +++ b/app/controllers/booking/payments_controller.rb @@ -0,0 +1,92 @@ +# Handle payment callbacks for booking workflow +class Booking::PaymentsController < ApplicationController + before_action :authenticate_user! + + # Handle successful payment callback + def success + session_id = params[:session_id] + + # Check if Stripe is properly configured + stripe_configured = Rails.application.config.stripe[:secret_key].present? + Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}" + + unless stripe_configured + redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur." + return + end + + begin + stripe_session = Stripe::Checkout::Session.retrieve(session_id) + + if stripe_session.payment_status == "paid" + # Get order_id from session metadata + order_id = stripe_session.metadata["order_id"] + + unless order_id.present? + redirect_to dashboard_path, alert: "Informations de commande manquantes" + return + end + + # Find and update the order + @order = current_user.orders.includes(tickets: :ticket_type).find(order_id) + @order.mark_as_paid! + + # Schedule Stripe invoice generation in background + begin + StripeInvoiceGenerationJob.perform_later(@order.id) + Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}" + rescue => e + Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}" + end + + # Send confirmation emails + @order.tickets.each do |ticket| + begin + TicketMailer.purchase_confirmation(ticket).deliver_now + rescue => e + Rails.logger.error "Failed to send confirmation email for ticket #{ticket.id}: #{e.message}" + end + end + + # Clear session data + session.delete(:pending_cart) + session.delete(:ticket_names) + session.delete(:draft_order_id) + + render "payment_success" + else + redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès" + end + rescue Stripe::StripeError => e + error_message = e.message.present? ? e.message : "Erreur Stripe inconnue" + redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}" + rescue => e + error_message = e.message.present? ? e.message : "Erreur inconnue" + Rails.logger.error "Payment success error: #{e.class} - #{error_message}" + redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}" + end + end + + # Handle payment cancellation callback + def cancel + order_id = params[:order_id] || session[:draft_order_id] + + if order_id.present? + order = current_user.orders.find_by(id: order_id, status: "draft") + + if order&.can_retry_payment? + # Extract year and month from event start_time for SEO URL + year = order.event.start_time.year + month = format("%02d", order.event.start_time.month) + + redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug), + alert: "Le paiement a été annulé. Vous pouvez réessayer." + else + session.delete(:draft_order_id) + redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré." + end + else + redirect_to root_path, alert: "Le paiement a été annulé" + end + end +end \ No newline at end of file diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index ed2d660..edacc42 100755 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -27,10 +27,33 @@ class EventsController < ApplicationController private # Find and set the current event with eager-loaded associations - # + # Supports both old slug-only format and new SEO-friendly year/month/slug format # Loads event with ticket types to avoid N+1 queries - # Raises ActiveRecord::RecordNotFound if event doesn't exist def set_event - @event = Event.includes(:ticket_types).find(params[:id]) + if params[:year] && params[:month] + # New SEO-friendly format: /events/2024/07/summer-party + year = params[:year].to_i + month = params[:month].to_i + start_of_month = Date.new(year, month, 1).beginning_of_month + end_of_month = start_of_month.end_of_month + + @event = Event.includes(:ticket_types) + .where(slug: params[:slug]) + .where(start_time: start_of_month..end_of_month) + .first! + else + # Legacy format: /events/summer-party (for backward compatibility) + @event = Event.includes(:ticket_types).find_by!(slug: params[:slug]) + end + rescue ActiveRecord::RecordNotFound + redirect_to events_path, alert: "Événement non trouvé" end + + # Generate SEO-friendly path for an event + def seo_event_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + event_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_event_path end diff --git a/app/controllers/legacy_redirects_controller.rb b/app/controllers/legacy_redirects_controller.rb new file mode 100644 index 0000000..6b06944 --- /dev/null +++ b/app/controllers/legacy_redirects_controller.rb @@ -0,0 +1,17 @@ +# Handle legacy URL redirects to new SEO-friendly URLs +class LegacyRedirectsController < ApplicationController + # Redirect old event URLs to new SEO-friendly format + # OLD: /events/summer-party-2024 + # NEW: /events/2024/07/summer-party-2024 + def event_redirect + event = Event.find_by(slug: params[:slug]) + + if event + year = event.start_time.year + month = format("%02d", event.start_time.month) + redirect_to event_path(year: year, month: month, slug: event.slug), status: :moved_permanently + else + redirect_to events_path, alert: "Événement non trouvé" + end + end +end \ No newline at end of file diff --git a/app/controllers/orders_controller.rb b/app/controllers/orders_controller.rb index 0576ed7..7c48ad1 100644 --- a/app/controllers/orders_controller.rb +++ b/app/controllers/orders_controller.rb @@ -1,11 +1,11 @@ -# Handle order management and checkout process +# Handle order management and checkout process with SEO-friendly URLs # # This controller manages the order lifecycle from checkout to payment completion # Orders group multiple tickets together for better transaction management class OrdersController < ApplicationController before_action :authenticate_user! - before_action :set_order, only: [ :show, :checkout, :retry_payment, :increment_payment_attempt ] - before_action :set_event, only: [ :new, :create ] + before_action :set_event_from_seo_params, only: [:new, :create, :checkout] + before_action :set_order_from_id, only: [:show, :retry_payment, :increment_payment_attempt] # Display new order form with name collection # @@ -15,7 +15,7 @@ class OrdersController < ApplicationController @cart_data = params[:cart_data] || session[:pending_cart] || {} if @cart_data.empty? - redirect_to event_path(@event.slug, @event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" + redirect_to seo_event_path(@event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" return end @@ -47,7 +47,7 @@ class OrdersController < ApplicationController @cart_data = params[:cart_data] || session[:pending_cart] || {} if @cart_data.empty? - redirect_to event_path(@event.slug, @event), alert: "Aucun billet sélectionné" + redirect_to seo_event_path(@event), alert: "Aucun billet sélectionné" return end @@ -87,32 +87,44 @@ class OrdersController < ApplicationController if success session[:draft_order_id] = @order.id session.delete(:pending_cart) - redirect_to checkout_order_path(@order) + year = @event.start_time.year + month = format("%02d", @event.start_time.month) + redirect_to event_checkout_path(year: year, month: month, slug: @event.slug) else - redirect_to event_order_new_path(@event.slug, @event.id) + year = @event.start_time.year + month = format("%02d", @event.start_time.month) + redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug) end rescue => e error_message = e.message.present? ? e.message : "Erreur inconnue" flash[:alert] = "Une erreur est survenue: #{error_message}" - redirect_to event_order_new_path(@event.slug, @event.id) + year = @event.start_time.year + month = format("%02d", @event.start_time.month) + redirect_to book_event_tickets_path(year: year, month: month, slug: @event.slug) end # Display order summary - # - # def show @tickets = @order.tickets.includes(:ticket_type) end - # Display payment page for an order + # Display payment page for an order (SEO-friendly checkout URL) # # Display a summary of all tickets in the order and permit user # to proceed to payment via Stripe def checkout + # Find order from session or create one + @order = current_user.orders.find_by(id: session[:draft_order_id], event: @event, status: "draft") + + unless @order + redirect_to seo_event_path(@event), alert: "Aucune commande en attente trouvée" + return + end + # Handle expired orders if @order.expired? @order.expire_if_overdue! - return redirect_to event_path(@order.event.slug, @order.event), + return redirect_to seo_event_path(@event), alert: "Votre commande a expiré. Veuillez recommencer." end @@ -141,117 +153,41 @@ class OrdersController < ApplicationController # Allow users to retry payment for failed/cancelled payments def retry_payment unless @order.can_retry_payment? - redirect_to event_path(@order.event.slug, @order.event), + redirect_to seo_event_path(@order.event), alert: "Cette commande ne peut plus être payée" return end - redirect_to checkout_order_path(@order) - end - - # Handle successful payment - def payment_success - session_id = params[:session_id] - - # Check if Stripe is properly configured - stripe_configured = Rails.application.config.stripe[:secret_key].present? - Rails.logger.debug "Payment success - Stripe configured: #{stripe_configured}" - - unless stripe_configured - redirect_to root_path, alert: "Le système de paiement n'est pas correctement configuré. Veuillez contacter l'administrateur." - return - end - - begin - stripe_session = Stripe::Checkout::Session.retrieve(session_id) - - if stripe_session.payment_status == "paid" - # Get order_id from session metadata - order_id = stripe_session.metadata["order_id"] - - unless order_id.present? - redirect_to dashboard_path, alert: "Informations de commande manquantes" - return - end - - # Find and update the order - @order = current_user.orders.includes(tickets: :ticket_type).find(order_id) - @order.mark_as_paid! - - # Schedule Stripe invoice generation in background - # This creates accounting records without blocking the payment success flow - begin - StripeInvoiceGenerationJob.perform_later(@order.id) - Rails.logger.info "Scheduled Stripe invoice generation for order #{@order.id}" - rescue => e - Rails.logger.error "Failed to schedule invoice generation for order #{@order.id}: #{e.message}" - # Don't fail the payment process due to job scheduling issues - end - - # Send confirmation emails - @order.tickets.each do |ticket| - 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 - session.delete(:pending_cart) - session.delete(:ticket_names) - session.delete(:draft_order_id) - - render "payment_success" - else - redirect_to dashboard_path, alert: "Le paiement n'a pas été complété avec succès" - end - rescue Stripe::StripeError => e - error_message = e.message.present? ? e.message : "Erreur Stripe inconnue" - redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{error_message}" - rescue => e - error_message = e.message.present? ? e.message : "Erreur inconnue" - Rails.logger.error "Payment success error: #{e.class} - #{error_message}" - redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{error_message}" - end - end - - # Handle payment failure/cancellation - def payment_cancel - order_id = params[:order_id] || session[:draft_order_id] - - if order_id.present? - order = current_user.orders.find_by(id: order_id, status: "draft") - - if order&.can_retry_payment? - redirect_to checkout_order_path(order), - alert: "Le paiement a été annulé. Vous pouvez réessayer." - else - session.delete(:draft_order_id) - redirect_to root_path, alert: "Le paiement a été annulé et votre commande a expiré." - end - else - redirect_to root_path, alert: "Le paiement a été annulé" - end + year = @order.event.start_time.year + month = format("%02d", @order.event.start_time.month) + redirect_to event_checkout_path(year: year, month: month, slug: @order.event.slug) end private - def set_order - @order = current_user.orders.includes(:tickets, :event).find(params[:id]) + def set_event_from_seo_params + year = params[:year].to_i + month = params[:month].to_i + start_of_month = Date.new(year, month, 1).beginning_of_month + end_of_month = start_of_month.end_of_month + + @event = Event.includes(:ticket_types) + .where(slug: params[:slug]) + .where(start_time: start_of_month..end_of_month) + .first + + return redirect_to events_path, alert: "Événement non trouvé" unless @event + end + + def set_order_from_id + @order = current_user.orders.includes(:tickets, :event).find(params[:order_id]) + @event = @order.event rescue ActiveRecord::RecordNotFound redirect_to root_path, alert: "Commande non trouvée" end - def set_event - @event = Event.includes(:ticket_types).find(params[:id]) - rescue ActiveRecord::RecordNotFound - redirect_to events_path, alert: "Événement non trouvé" - end - def order_params - params.permit(tickets_attributes: [ :ticket_type_id, :first_name, :last_name ]) + params.permit(tickets_attributes: [:ticket_type_id, :first_name, :last_name]) end def create_stripe_session @@ -270,15 +206,23 @@ class OrdersController < ApplicationController end Stripe::Checkout::Session.create( - payment_method_types: [ "card" ], + payment_method_types: ["card"], line_items: line_items, mode: "payment", - success_url: order_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}", - cancel_url: order_payment_cancel_url, + success_url: booking_payment_success_url + "?session_id={CHECKOUT_SESSION_ID}", + cancel_url: booking_payment_cancelled_url + "?order_id=#{@order.id}", metadata: { order_id: @order.id, user_id: current_user.id } ) end -end + + # Generate SEO-friendly path for an event + def seo_event_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + event_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_event_path +end \ No newline at end of file diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index ea5c0b9..9ad8a79 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -1,95 +1,24 @@ -# Legacy tickets controller - redirects to new order system +# Tickets controller - handles ticket viewing and downloads with SEO-friendly URLs # -# This controller now primarily handles legacy redirects and backward compatibility -# Most ticket creation functionality has been moved to OrdersController +# This controller manages individual ticket display and downloads +# Uses event-slug-ticket-id format for SEO-friendly URLs class TicketsController < ApplicationController - before_action :authenticate_user!, only: [ :payment_success, :payment_cancel, :show, :ticket_view, :download_ticket ] - before_action :set_event, only: [ :checkout, :retry_payment ] - - # Redirect to order-based checkout - def checkout - # Check for draft order - if session[:draft_order_id].present? - order = current_user.orders.find_by(id: session[:draft_order_id], status: "draft") - if order.present? - redirect_to order_checkout_path(order) - return - end - end - - # No order found - @event = Event.includes(:ticket_types).find(params[:id]) - redirect_to event_path(@event.slug, @event), alert: "Aucun billet en attente de paiement" - end - - # Redirect to order-based payment success - def payment_success - redirect_to order_payment_success_path(session_id: params[:session_id]) - end - - # Redirect to order-based payment cancel - def payment_cancel - redirect_to order_payment_cancel_path - end - - # Redirect retry payment to order system - def retry_payment - @event = Event.includes(:ticket_types).find(params[:id]) - - # Look for draft order for this event - order = current_user.orders.find_by(event: @event, status: "draft") - - if order&.can_retry_payment? - redirect_to retry_payment_order_path(order) - else - redirect_to event_path(@event.slug, @event), - alert: "Aucune commande disponible pour un nouveau paiement" - end - end + before_action :authenticate_user! + before_action :set_ticket_from_seo_params, only: [:show, :view, :download, :retry_payment] # Display ticket details def show - @ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by( - tickets: { id: params[:ticket_id] }, - orders: { user_id: current_user.id } - ) @event = @ticket.event - rescue ActiveRecord::RecordNotFound - redirect_to dashboard_path, alert: "Billet non trouvé" end # Display ticket in PDF-like format - def ticket_view - @ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by( - tickets: { id: params[:ticket_id] }, - orders: { user_id: current_user.id } - ) - - if @ticket.nil? - redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet" - return - end - + def view @event = @ticket.event - rescue ActiveRecord::RecordNotFound - redirect_to dashboard_path, alert: "Billet non trouvé" end # Download PDF ticket - only accessible by ticket owner # User must be authenticated to download ticket - # TODO: change ID to an unique identifier (UUID) - def download_ticket - # Find ticket and ensure it belongs to current user - @ticket = Ticket.joins(order: :user).includes(:event, :ticket_type, order: :user).find_by( - tickets: { id: params[:ticket_id] }, - orders: { user_id: current_user.id } - ) - - if @ticket.nil? - redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet" - return - end - + def download # Generate PDF using Grover begin Rails.logger.info "Starting PDF generation for ticket ID: #{@ticket.id}" @@ -103,96 +32,96 @@ class TicketsController < ApplicationController Rails.logger.info "HTML template rendered successfully, length: #{html.length}" - # Try to load and use Grover - begin - Rails.logger.info "Attempting to load Grover gem" + # Configure Grover options for PDF generation + pdf_options = { + format: 'A4', + margin: { + top: '0.5in', + bottom: '0.5in', + left: '0.5in', + right: '0.5in' + }, + print_background: true, + display_header_footer: false, + prefer_css_page_size: true, + launch_args: ["--no-sandbox", "--disable-setuid-sandbox"] # For better compatibility + } - # Try different approaches to load grover - begin - require "bundler" - Bundler.require(:default, Rails.env) - Rails.logger.info "Bundler required gems successfully" - rescue => bundler_error - Rails.logger.warn "Bundler require failed: #{bundler_error.message}" - end + # Generate PDF + pdf = Grover.new(html, pdf_options).to_pdf + + Rails.logger.info "PDF generation completed for ticket ID: #{@ticket.id}" - # Direct path approach using bundle show - grover_gem_path = `bundle show grover`.strip - grover_path = File.join(grover_gem_path, "lib", "grover") - - if File.exist?(grover_path + ".rb") - Rails.logger.info "Loading Grover from direct path: #{grover_path}" - require grover_path - else - Rails.logger.error "Grover not found at path: #{grover_path}" - raise LoadError, "Grover gem not available at expected path" - end - - Rails.logger.info "Creating Grover instance with options" - grover = Grover.new(html, - format: "A6", - margin: { - top: "10mm", - bottom: "10mm", - left: "10mm", - right: "10mm" - }, - prefer_css_page_size: true, - emulate_media: "print", - cache: false, - launch_args: [ "--no-sandbox", "--disable-setuid-sandbox" ] # For better compatibility - ) - Rails.logger.info "Grover instance created successfully" - - pdf_content = grover.to_pdf - Rails.logger.info "PDF generated successfully, length: #{pdf_content.length}" - - # Send PDF as download - send_data pdf_content, - filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.pdf", - type: "application/pdf", - disposition: "attachment" - rescue LoadError => grover_error - Rails.logger.error "Failed to load Grover: #{grover_error.message}" - # Fallback: return HTML instead of PDF - send_data html, - filename: "ticket_#{@ticket.id}_#{@ticket.event.name.parameterize}.html", - type: "text/html", - disposition: "attachment" - end + # Send PDF as download with SEO-friendly filename + send_data pdf, + filename: "billet-#{@ticket.event.slug}-#{@ticket.id}.pdf", + type: 'application/pdf', + disposition: 'attachment' rescue => e - Rails.logger.error "Error generating ticket PDF with Grover:" - Rails.logger.error "Message: #{e.message}" - Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}" - redirect_to dashboard_path, alert: "Erreur lors de la génération du billet" + Rails.logger.error "PDF generation failed for ticket ID: #{@ticket.id} - Error: #{e.message}" + Rails.logger.error e.backtrace.join("\n") + + redirect_to view_ticket_path(event_slug: @ticket.event.slug, ticket_id: @ticket.id), + alert: "Erreur lors de la génération du PDF. Veuillez réessayer." end - rescue ActiveRecord::RecordNotFound => e - Rails.logger.error "ActiveRecord::RecordNotFound error: #{e.message}" - redirect_to dashboard_path, alert: "Billet non trouvé" - rescue => e - Rails.logger.error "Unexpected error in download_ticket action:" - Rails.logger.error "Message: #{e.message}" - Rails.logger.error "Backtrace: #{e.backtrace.join("\n")}" - redirect_to dashboard_path, alert: "Erreur lors de la génération du billet" + end + + # Redirect retry payment to order system + def retry_payment + # Look for draft order for this ticket's event + order = current_user.orders.find_by(event: @ticket.event, status: "draft") + + if order&.can_retry_payment? + year = order.event.start_time.year + month = format("%02d", order.event.start_time.month) + redirect_to event_checkout_path(year: year, month: month, slug: order.event.slug) + else + redirect_to seo_event_path(@ticket.event), + alert: "Aucune commande disponible pour un nouveau paiement" + end + end + + # Legacy redirects for backward compatibility + def payment_success + redirect_to booking_payment_success_path(session_id: params[:session_id]) + end + + def payment_cancel + redirect_to booking_payment_cancelled_path end private - def set_event - event_id = params[:id] || session[:event_id] + def set_ticket_from_seo_params + # Parse event_slug and ticket_id from the SEO-friendly format: event-slug-123 + slug_and_id = params[:event_slug_ticket_id] || "#{params[:event_slug]}-#{params[:ticket_id]}" + + # Split by last dash to separate event slug from ticket ID + parts = slug_and_id.split('-') + ticket_id = parts.pop + event_slug = parts.join('-') - Rails.logger.debug "TicketsController#set_event - params[:id]: #{params[:id].inspect}, session[:event_id]: #{session[:event_id].inspect}" + # Find ticket and ensure it belongs to current user + @ticket = Ticket.joins(order: :user) + .includes(:event, :ticket_type, order: :user) + .joins(:event) + .where( + tickets: { id: ticket_id }, + orders: { user_id: current_user.id }, + events: { slug: event_slug } + ) + .first - unless event_id - Rails.logger.error "TicketsController#set_event - No event ID found" - redirect_to events_path, alert: "Aucun événement spécifié" - return + unless @ticket + redirect_to dashboard_path, alert: "Billet non trouvé ou vous n'avez pas l'autorisation d'accéder à ce billet" end - - @event = Event.includes(:ticket_types).find(event_id) - Rails.logger.debug "TicketsController#set_event - Found event: #{@event.id} - #{@event.name}" - rescue ActiveRecord::RecordNotFound - Rails.logger.error "TicketsController#set_event - Event not found with ID: #{event_id}" - redirect_to events_path, alert: "Événement non trouvé" end -end + + # Generate SEO-friendly path for an event + def seo_event_path(event) + year = event.start_time.year + month = format("%02d", event.start_time.month) + event_path(year: year, month: month, slug: event.slug) + end + helper_method :seo_event_path +end \ No newline at end of file diff --git a/app/helpers/pdf_helper.rb b/app/helpers/pdf_helper.rb new file mode 100644 index 0000000..c03cc87 --- /dev/null +++ b/app/helpers/pdf_helper.rb @@ -0,0 +1,17 @@ +module PdfHelper + require "rqrcode" + + # Generate SVG QR code for tickets + def qr_code_tag(data) + qrcode = RQRCode::QRCode.new(data) + + # Render as SVG + raw qrcode.as_svg( + offset: 0, + color: "000", + shape_rendering: "crispEdges", + module_size: 4, + standalone: true + ) + end +end diff --git a/app/javascript/controllers/ticket_selection_controller.js b/app/javascript/controllers/ticket_selection_controller.js index 4dfd365..f503897 100644 --- a/app/javascript/controllers/ticket_selection_controller.js +++ b/app/javascript/controllers/ticket_selection_controller.js @@ -118,7 +118,7 @@ export default class extends Controller { await this.storeCartInSession(cartData); // Redirect to event-scoped orders/new page - const OrderNewUrl = `/orders/new/events/${this.eventSlugValue}.${this.eventIdValue}`; + const OrderNewUrl = `/events/${this.eventSlugValue}/orders/new`; window.location.href = OrderNewUrl; } catch (error) { console.error("Error storing cart:", error); diff --git a/app/views/booking/payments/success.html.erb b/app/views/booking/payments/success.html.erb new file mode 100644 index 0000000..4c38d52 --- /dev/null +++ b/app/views/booking/payments/success.html.erb @@ -0,0 +1,61 @@ +
+
+ +
+
+ + + +
+
+ + +
+

Paiement réussi !

+

Votre commande a été confirmée et vos billets ont été envoyés par email.

+
+ + + <% if @order&.present? %> +
+
+ Commande #<%= @order.id %> + <%= @order.created_at.strftime("%d/%m/%Y à %H:%M") %> +
+ +
+
+ Événement: + <%= @order.event.name %> +
+
+ Nombre de billets: + <%= @order.tickets.count %> +
+
+ Total: + <%= @order.total_amount_euros %>€ +
+
+
+ <% end %> + + +
+ <%= link_to dashboard_path, class: "w-full bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %> + Voir mes billets + <% end %> + + <%= link_to events_path, class: "w-full bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium py-3 px-4 rounded-lg transition-colors text-center block" do %> + Découvrir d'autres événements + <% end %> +
+ + +
+

+ Un email de confirmation a été envoyé à votre adresse email avec vos billets en pièce jointe. +

+
+
+
\ No newline at end of file diff --git a/app/views/components/_event_item.html.erb b/app/views/components/_event_item.html.erb index a3dd4a2..3b71652 100755 --- a/app/views/components/_event_item.html.erb +++ b/app/views/components/_event_item.html.erb @@ -1,4 +1,4 @@ -<%= link_to event_path(event.slug, event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %> +<%= link_to seo_event_path(event), class: "group block p-4 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-purple-300 dark:hover:border-purple-600 hover:shadow-md transition-all duration-200" do %>
<%= image_tag event.image, alt: event.name, class: "w-full h-full object-cover" if event.image.present? %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 4b480bf..fdaf7d4 100755 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -56,7 +56,7 @@ > <% if event.image.present? %>
- <%= link_to event_path(event.slug, event) do %> + <%= link_to event_path(event) do %> <%= event.name %>
- <%= link_to event_path(event.slug, event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %> + <%= link_to event_path(event), class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-lg shadow-sm text-white bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 transition-all duration-200" do %> Détails + +<% content_for :title, "#{@event.name} - #{@event.start_time.strftime('%d/%m/%Y')} | Aperonight" %> +<% content_for :description, @event.description.truncate(160) %> +<% content_for :keywords, "#{@event.name}, événement, soirée, #{@event.venue_name}, billets, réservation" %> +<% content_for :canonical_url, seo_event_path(@event) %> +<% content_for :og_image, @event.image if @event.image.present? %> + + +<% content_for :head do %> + +<% end %> + +