From 0b58768a24f3d33fb690a110a7013d16bf83598b Mon Sep 17 00:00:00 2001 From: kbe Date: Thu, 28 Aug 2025 19:11:23 +0200 Subject: [PATCH] docs: More about how to process the checkout --- docs/checkout-handle.md | 322 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100755 docs/checkout-handle.md diff --git a/docs/checkout-handle.md b/docs/checkout-handle.md new file mode 100755 index 0000000..c8c78fb --- /dev/null +++ b/docs/checkout-handle.md @@ -0,0 +1,322 @@ +# 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: + +```ruby +# 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 + +```ruby +# config/routes.rb +post '/webhooks/stripe', to: 'webhooks#stripe' +``` + +### Webhooks Controller + +```ruby +# 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: + +```ruby +# 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: + +```ruby +# 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: + +```ruby +# 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: + +```ruby +# 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 + +5. Enhanced error recovery with fallback ticket creation +6. Comprehensive logging for debugging and monitoring +7. 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.