# Handle order management and checkout process # # 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, :invoice ] before_action :set_event, only: [ :new, :create ] # 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 event_path(@event.slug, @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 event_path(@event.slug, @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) redirect_to checkout_order_path(@order) else redirect_to event_order_new_path(@event.slug, @event.id) 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) end # Display all user orders def index @orders = current_user.orders.includes(:event, tickets: :ticket_type) .where(status: [ "paid", "completed" ]) .order(created_at: :desc) .page(params[:page]) end # Display order summary def show @tickets = @order.tickets.includes(:ticket_type) end # Display payment page for an order # # Display a summary of all tickets in the order and permit user # to proceed to payment via Stripe def checkout # Handle expired orders if @order.expired? @order.expire_if_overdue! return redirect_to event_path(@order.event.slug, @order.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? # For free orders, automatically mark as paid and redirect to success if @order.free? @order.mark_as_paid! session.delete(:pending_cart) session.delete(:ticket_names) session.delete(:draft_order_id) return redirect_to order_path(@order), notice: "Vos billets gratuits ont été confirmés !" end # 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 event_path(@order.event.slug, @order.event), alert: "Cette commande ne peut plus être payée" return end # For POST requests, increment the payment attempt counter if request.post? @order.increment_payment_attempt! end redirect_to checkout_order_path(@order) end # Display invoice for an order def invoice unless @order.status == "paid" || @order.status == "completed" redirect_to order_path(@order), alert: "La facture n'est disponible qu'après le paiement de la commande" return end @tickets = @order.tickets.includes(:ticket_type) # Get the Stripe invoice if it exists begin @stripe_invoice_id = @order.create_stripe_invoice! @stripe_invoice_pdf_url = @order.stripe_invoice_pdf_url if @stripe_invoice_id rescue => e Rails.logger.error "Failed to retrieve or create Stripe invoice for order #{@order.id}: #{e.message}" @stripe_invoice_id = nil @stripe_invoice_pdf_url = nil end 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 # Email confirmation is handled by the order model's mark_as_paid! method # to avoid duplicate emails # 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 end private def set_order @order = current_user.orders.includes(:tickets, :event).find(params[:id]) 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 ]) 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 # No service fee added to customer; deducted from promoter payout Stripe::Checkout::Session.create( 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, metadata: { order_id: @order.id, user_id: current_user.id } ) end end