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
- Rate Limiting: Add rate limiting to checkout endpoints
- CSRF Protection: Already implemented ✅
- Input Validation: Validate all cart data thoroughly
- Audit Logging: Log all payment attempts and outcomes
- 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
- Add database row locking to prevent overselling during concurrent purchases
- Implement Stripe webhooks for reliable payment processing
- Use background jobs for ticket creation to prevent timeouts
- Add duplicate prevention with stripe_session_id tracking
Security & Reliability
- Enhanced error recovery with fallback ticket creation
- Comprehensive logging for debugging and monitoring
- Database schema improvements for better payment tracking
Key Files to Modify
app/controllers/events_controller.rb- Add inventory lockingapp/controllers/webhooks_controller.rb- New webhook handlerapp/jobs/create_tickets_job.rb- Background ticket creation- Migration for
stripe_session_idfield
These enhancements will make your checkout system robust for high-traffic scenarios and edge cases.