Files
aperonight/docs/checkout-handle.md

9.5 KiB
Executable File

Backend Checkout Handling Improvements

Based on your current Stripe integration, here are key improvements for robust checkout handling:

1. Enhanced Inventory Management with Concurrency Protection

The current implementation doesn't prevent overselling during concurrent purchases.

Add database-level concurrency protection:

# app/controllers/events_controller.rb
def checkout
  cart_data = JSON.parse(params[:cart] || "{}")

  if cart_data.empty?
    redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet"
    return
  end

  # Use transaction with row-level locking for inventory protection
  ActiveRecord::Base.transaction do
    line_items = []
    order_items = []

    cart_data.each do |ticket_type_id, item|
      # Lock the ticket type row to prevent race conditions
      ticket_type = @event.ticket_types.lock.find_by(id: ticket_type_id)
      next unless ticket_type

      quantity = item["quantity"].to_i
      next if quantity <= 0

      # Check real-time availability with locked row
      sold_count = ticket_type.tickets.count
      available = ticket_type.quantity - sold_count

      if quantity > available
        redirect_to event_path(@event.slug, @event), alert: "Plus que #{available} billets disponibles pour #{ticket_type.name}"
        return
      end

      # Create line items and order data
      line_items << {
        price_data: {
          currency: "eur",
          product_data: {
            name: "#{@event.name} - #{ticket_type.name}",
            description: ticket_type.description
          },
          unit_amount: ticket_type.price_cents
        },
        quantity: quantity
      }

      order_items << {
        ticket_type_id: ticket_type.id,
        ticket_type_name: ticket_type.name,
        quantity: quantity,
        price_cents: ticket_type.price_cents
      }
    end

    if order_items.empty?
      redirect_to event_path(@event.slug, @event), alert: "Commande invalide"
      return
    end

    # Create Stripe session only after inventory validation
    session = Stripe::Checkout::Session.create({
      payment_method_types: ["card"],
      line_items: line_items,
      mode: "payment",
      success_url: payment_success_url(event_id: @event.id, session_id: "{CHECKOUT_SESSION_ID}"),
      cancel_url: event_url(@event.slug, @event),
      customer_email: current_user.email,
      metadata: {
        event_id: @event.id,
        user_id: current_user.id,
        order_items: order_items.to_json
      }
    })

    redirect_to session.url, allow_other_host: true
  end
rescue ActiveRecord::RecordNotFound
  redirect_to event_path(@event.slug, @event), alert: "Type de billet introuvable"
rescue Stripe::StripeError => e
  redirect_to event_path(@event.slug, @event), alert: "Erreur de traitement du paiement : #{e.message}"
end

2. Webhook Handler for Reliable Payment Confirmation

Create a dedicated webhook endpoint for more reliable payment processing:

Routes Configuration

# config/routes.rb
post '/webhooks/stripe', to: 'webhooks#stripe'

Webhooks Controller

# app/controllers/webhooks_controller.rb
class WebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token
  before_action :verify_stripe_signature

  def stripe
    case @event.type
    when 'checkout.session.completed'
      handle_successful_payment(@event.data.object)
    when 'payment_intent.payment_failed'
      handle_failed_payment(@event.data.object)
    end

    head :ok
  end

  private

  def handle_successful_payment(session)
    # Process ticket creation in background job for reliability
    CreateTicketsJob.perform_later(session.id)
  end

  def handle_failed_payment(session)
    Rails.logger.error "Payment failed for session: #{session.id}"
    # Add any additional handling for failed payments
  end

  def verify_stripe_signature
    payload = request.body.read
    sig_header = request.env['HTTP_STRIPE_SIGNATURE']

    begin
      @event = Stripe::Webhook.construct_event(
        payload, sig_header, ENV['STRIPE_WEBHOOK_SECRET']
      )
    rescue JSON::ParserError, Stripe::SignatureVerificationError => e
      Rails.logger.error "Stripe webhook signature verification failed: #{e.message}"
      head :bad_request
    end
  end
end

3. Background Job for Ticket Creation

Use background jobs to prevent timeouts and improve reliability:

# app/jobs/create_tickets_job.rb
class CreateTicketsJob < ApplicationJob
  queue_as :default
  retry_on StandardError, wait: :exponentially_longer, attempts: 5

  def perform(session_id)
    session = Stripe::Checkout::Session.retrieve(session_id)
    return unless session.payment_status == 'paid'

    # Prevent duplicate processing
    return if Ticket.exists?(stripe_session_id: session_id)

    order_items = JSON.parse(session.metadata['order_items'])
    user = User.find(session.metadata['user_id'])
    event = Event.find(session.metadata['event_id'])

    ActiveRecord::Base.transaction do
      order_items.each do |item|
        ticket_type = TicketType.find(item['ticket_type_id'])

        item['quantity'].times do
          ticket = Ticket.create!(
            user: user,
            ticket_type: ticket_type,
            status: 'active',
            stripe_session_id: session_id, # Prevent duplicates
            price_cents: item['price_cents'] # Store historical price
          )

          # Send email asynchronously
          TicketMailer.purchase_confirmation(ticket).deliver_later
        end
      end
    end
  end
end

4. Enhanced Error Handling & Recovery in Payment Success

Improve the payment success handler with better error recovery:

# app/controllers/events_controller.rb - Enhanced payment_success method
def payment_success
  session_id = params[:session_id]
  event_id = params[:event_id]

  # Validate parameters
  unless session_id.present? && event_id.present?
    redirect_to dashboard_path, alert: "Paramètres de confirmation manquants"
    return
  end

  begin
    @tickets = Ticket.includes(:ticket_type, :event)
                   .where(stripe_session_id: session_id, user: current_user)

    if @tickets.any?
      # Tickets already created (webhook processed first)
      @event = @tickets.first.event
      render 'payment_success'
    else
      # Fallback: create tickets synchronously if webhook failed
      session = Stripe::Checkout::Session.retrieve(session_id)

      if session.payment_status == 'paid'
        CreateTicketsJob.perform_now(session_id)
        redirect_to payment_success_path(session_id: session_id, event_id: event_id)
      else
        redirect_to dashboard_path, alert: "Le paiement n'est pas encore confirmé"
      end
    end

  rescue Stripe::StripeError => e
    logger.error "Stripe error in payment_success: #{e.message}"
    redirect_to dashboard_path, alert: "Erreur de confirmation de paiement"
  rescue => e
    logger.error "Unexpected error in payment_success: #{e.message}"
    redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite"
  end
end

5. Database Schema Improvements

Add migration for better payment tracking:

# db/migrate/xxx_add_payment_tracking_to_tickets.rb
class AddPaymentTrackingToTickets < ActiveRecord::Migration[7.0]
  def change
    add_column :tickets, :stripe_session_id, :string
    add_column :tickets, :purchased_at, :timestamp, default: -> { 'CURRENT_TIMESTAMP' }

    add_index :tickets, :stripe_session_id, unique: true
    add_index :tickets, [:user_id, :purchased_at]
  end
end

6. Security Considerations

  1. Rate Limiting: Add rate limiting to checkout endpoints
  2. CSRF Protection: Already implemented
  3. Input Validation: Validate all cart data thoroughly
  4. Audit Logging: Log all payment attempts and outcomes
  5. PCI Compliance: Never store card data (Stripe handles this)

7. Monitoring & Observability

Add metrics tracking to monitor checkout performance:

# Add to ApplicationController or EventsController
around_action :track_checkout_metrics, only: [:checkout]

private

def track_checkout_metrics
  start_time = Time.current
  begin
    yield
    # Log successful checkout
    Rails.logger.info("Checkout completed", {
      event_id: @event&.id,
      user_id: current_user&.id,
      duration: Time.current - start_time
    })
  rescue => e
    # Log failed checkout
    Rails.logger.error("Checkout failed", {
      event_id: @event&.id,
      user_id: current_user&.id,
      error: e.message,
      duration: Time.current - start_time
    })
    raise
  end
end

Summary of Improvements

Your ticket checkout system is already well-implemented with Stripe integration! The enhancements above will make it production-ready:

Critical Improvements

  1. Add database row locking to prevent overselling during concurrent purchases
  2. Implement Stripe webhooks for reliable payment processing
  3. Use background jobs for ticket creation to prevent timeouts
  4. Add duplicate prevention with stripe_session_id tracking

Security & Reliability

  1. Enhanced error recovery with fallback ticket creation
  2. Comprehensive logging for debugging and monitoring
  3. Database schema improvements for better payment tracking

Key Files to Modify

  • app/controllers/events_controller.rb - Add inventory locking
  • app/controllers/webhooks_controller.rb - New webhook handler
  • app/jobs/create_tickets_job.rb - Background ticket creation
  • Migration for stripe_session_id field

These enhancements will make your checkout system robust for high-traffic scenarios and edge cases.