# 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_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 # # On this page user can see order summary and complete the tickets details # (first name and last name) for each ticket ordered def new @cart_data = params[:cart_data] || session[:pending_cart] || {} if @cart_data.empty? redirect_to seo_event_path(@event), alert: "Veuillez d'abord sélectionner vos billets sur la page de l'événement" return end # Build list of tickets requiring names @tickets_needing_names = [] @cart_data.each do |ticket_type_id, item| ticket_type = @event.ticket_types.find_by(id: ticket_type_id) next unless ticket_type quantity = item["quantity"].to_i next if quantity <= 0 quantity.times do |i| @tickets_needing_names << { ticket_type_id: ticket_type.id, ticket_type_name: ticket_type.name, ticket_type_price: ticket_type.price_cents, index: i } end end end # Create a new order with tickets # # Here a new order is created with associated tickets in draft state. # When user is ready they can proceed to payment via the order checkout def create @cart_data = params[:cart_data] || session[:pending_cart] || {} if @cart_data.empty? redirect_to seo_event_path(@event), alert: "Aucun billet sélectionné" return end success = false ActiveRecord::Base.transaction do @order = current_user.orders.create!(event: @event, status: "draft") order_params[:tickets_attributes]&.each do |index, ticket_attrs| next if ticket_attrs[:first_name].blank? || ticket_attrs[:last_name].blank? ticket_type = @event.ticket_types.find(ticket_attrs[:ticket_type_id]) ticket = @order.tickets.build( ticket_type: ticket_type, first_name: ticket_attrs[:first_name], last_name: ticket_attrs[:last_name], status: "draft" ) unless ticket.save flash[:alert] = "Erreur lors de la création des billets: #{ticket.errors.full_messages.join(', ')}" raise ActiveRecord::Rollback end end if @order.tickets.present? @order.calculate_total! success = true else flash[:alert] = "Aucun billet valide créé" raise ActiveRecord::Rollback end end # Handle redirects outside transaction if success session[:draft_order_id] = @order.id session.delete(:pending_cart) 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 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}" 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 (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 seo_event_path(@event), alert: "Votre commande a expiré. Veuillez recommencer." end @tickets = @order.tickets.includes(:ticket_type) @total_amount = @order.total_amount_cents @expiring_soon = @order.expiring_soon? # Create Stripe checkout session if Stripe is configured if Rails.application.config.stripe[:secret_key].present? begin @checkout_session = create_stripe_session rescue => e error_message = e.message.present? ? e.message : "Erreur Stripe inconnue" Rails.logger.error "Stripe checkout session creation failed: #{error_message}" flash[:alert] = "Erreur lors de la création de la session de paiement" end end end # Increment payment attempt - called via AJAX when user clicks pay button def increment_payment_attempt @order.increment_payment_attempt! render json: { success: true, attempts: @order.payment_attempts } end # Allow users to retry payment for failed/cancelled payments def retry_payment unless @order.can_retry_payment? redirect_to seo_event_path(@order.event), alert: "Cette commande ne peut plus être payée" return 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_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 order_params params.permit(tickets_attributes: [:ticket_type_id, :first_name, :last_name]) end def create_stripe_session line_items = @order.tickets.map do |ticket| { price_data: { currency: "eur", product_data: { name: "#{@order.event.name} - #{ticket.ticket_type.name}", description: ticket.ticket_type.description }, unit_amount: ticket.price_cents }, quantity: 1 } end Stripe::Checkout::Session.create( payment_method_types: ["card"], line_items: line_items, mode: "payment", 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 # 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