develop #3
322
docs/checkout-handle.md
Executable file
322
docs/checkout-handle.md
Executable file
@@ -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.
|
||||||
Reference in New Issue
Block a user