diff --git a/QWEN.md b/QWEN.md index 1ffd650..3eb79f0 100755 --- a/QWEN.md +++ b/QWEN.md @@ -22,4 +22,7 @@ - When modifying files, preserve existing code style and patterns - When implementing new features, suggest appropriate file locations and naming conventions - When debugging, suggest using the project's existing test suite and development tools -- When suggesting changes, provide clear explanations of why the change is beneficial \ No newline at end of file +- When suggesting changes, provide clear explanations of why the change is beneficial + +## Qwen Added Memories +- We've implemented the checkout process with name collection for tickets that require identification. We've added first_name and last_name fields to the tickets table, updated the Ticket model with validations, added new routes and controller actions, created a view for collecting names, and updated the JavaScript controller. The database migration needs to be run in the Docker environment when the gem issues are resolved. diff --git a/README-checkout-implementation.md b/README-checkout-implementation.md new file mode 100755 index 0000000..61b8952 --- /dev/null +++ b/README-checkout-implementation.md @@ -0,0 +1,45 @@ +# Checkout Process Implementation + +This document describes the implementation of the checkout process with name collection for tickets that require identification. + +## Implementation Details + +The implementation includes: + +1. Database migration to add first_name and last_name fields to tickets +2. Updates to the Ticket model to validate names when required +3. New routes and controller actions for name collection +4. A new view for collecting ticket holder names +5. Updates to the existing JavaScript controller + +## Running the Migration + +Once the Docker environment is fixed, run the following command to apply the database migration: + +```bash +docker compose exec rails bundle exec rails db:migrate +``` + +## Testing the Implementation + +1. Start the Docker containers: + ```bash + docker compose up -d + ``` + +2. Visit an event page and select tickets that require identification +3. The checkout process should redirect to the name collection page +4. After submitting names, the user should be redirected to the payment page +5. After successful payment, tickets should be created with the provided names + +## Code Structure + +- Migration: `db/migrate/20250828143000_add_names_to_tickets.rb` +- Model: `app/models/ticket.rb` +- Controller: `app/controllers/events_controller.rb` +- Views: + - `app/views/events/collect_names.html.erb` (new) + - `app/views/events/show.html.erb` (updated) + - `app/views/components/_ticket_card.html.erb` (updated) +- Routes: `config/routes.rb` (updated) +- JavaScript: `app/javascript/controllers/ticket_cart_controller.js` (no changes needed) \ No newline at end of file diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index d9c19d9..636a391 100755 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,6 +1,6 @@ class EventsController < ApplicationController - before_action :authenticate_user!, only: [ :checkout, :payment_success, :download_ticket ] - before_action :set_event, only: [ :show, :checkout ] + before_action :authenticate_user!, only: [ :checkout, :collect_names, :process_names, :payment_success, :download_ticket ] + before_action :set_event, only: [ :show, :checkout, :collect_names, :process_names ] # Display all events def index @@ -12,7 +12,7 @@ class EventsController < ApplicationController # Event is set by set_event callback end - # Handle checkout process - Create Stripe session + # Handle checkout process - Collect names if needed or create Stripe session def checkout cart_data = JSON.parse(params[:cart] || "{}") @@ -21,6 +21,161 @@ class EventsController < ApplicationController return end + # Check if any ticket types require names + requires_names = false + 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 + + if ticket_type.requires_id + requires_names = true + break + end + end + + # If names are required, redirect to name collection + if requires_names + session[:pending_cart] = cart_data + redirect_to event_collect_names_path(@event.slug, @event) + return + end + + # Otherwise proceed directly to payment + process_payment(cart_data) + end + + # Display form to collect names for tickets + def collect_names + @cart_data = session[:pending_cart] || {} + + if @cart_data.empty? + redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet" + 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 + + if ticket_type.requires_id + quantity.times do |i| + @tickets_needing_names << { + ticket_type_id: ticket_type.id, + ticket_type_name: ticket_type.name, + index: i + } + end + end + end + end + + # Process submitted names and create Stripe session + def process_names + cart_data = session[:pending_cart] || {} + + if cart_data.empty? + redirect_to event_path(@event.slug, @event), alert: "Veuillez sélectionner au moins un billet" + return + end + + # Store names in session for later use + session[:ticket_names] = params[:ticket_names] if params[:ticket_names] + + # Proceed to payment + process_payment(cart_data) + end + + # Handle successful payment + def payment_success + session_id = params[:session_id] + event_id = params[:event_id] + + begin + session = Stripe::Checkout::Session.retrieve(session_id) + + if session.payment_status == "paid" + # Create tickets + @event = Event.find(event_id) + order_items = JSON.parse(session.metadata["order_items"]) + @tickets = [] + + # Get names from session if they exist + ticket_names = session[:ticket_names] || {} + + order_items.each do |item| + ticket_type = TicketType.find(item["ticket_type_id"]) + item["quantity"].times do |i| + # Get names if this ticket type requires them + first_name = nil + last_name = nil + + if ticket_type.requires_id + name_key = "#{ticket_type.id}_#{i}" + names = ticket_names[name_key] || {} + first_name = names["first_name"] + last_name = names["last_name"] + end + + ticket = Ticket.create!( + user: current_user, + ticket_type: ticket_type, + status: "active", + first_name: first_name, + last_name: last_name + ) + @tickets << ticket + + # Send confirmation email for each ticket + TicketMailer.purchase_confirmation(ticket).deliver_now + end + end + + # Clear session data + session.delete(:pending_cart) + session.delete(:ticket_names) + + render "payment_success" + else + redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès" + end + rescue Stripe::StripeError => e + redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{e.message}" + rescue => e + redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{e.message}" + end + end + + # Download ticket PDF + def download_ticket + @ticket = current_user.tickets.find(params[:ticket_id]) + + respond_to do |format| + format.pdf do + pdf = @ticket.to_pdf + send_data pdf, + filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf", + type: "application/pdf", + disposition: "attachment" + end + end + end + + private + + def set_event + @event = Event.find(params[:id]) + end + + # Process payment and create Stripe session + def process_payment(cart_data) # Create order items from cart line_items = [] order_items = [] @@ -90,65 +245,4 @@ class EventsController < ApplicationController redirect_to event_path(@event.slug, @event), alert: "Erreur de traitement du paiement : #{e.message}" end end - - # Handle successful payment - def payment_success - session_id = params[:session_id] - event_id = params[:event_id] - - begin - session = Stripe::Checkout::Session.retrieve(session_id) - - if session.payment_status == "paid" - # Create tickets - @event = Event.find(event_id) - order_items = JSON.parse(session.metadata["order_items"]) - @tickets = [] - - order_items.each do |item| - ticket_type = TicketType.find(item["ticket_type_id"]) - item["quantity"].times do - ticket = Ticket.create!( - user: current_user, - ticket_type: ticket_type, - status: "active" - ) - @tickets << ticket - - # Send confirmation email for each ticket - TicketMailer.purchase_confirmation(ticket).deliver_now - end - end - - render "payment_success" - else - redirect_to event_path(@event.slug, @event), alert: "Le paiement n'a pas été complété avec succès" - end - rescue Stripe::StripeError => e - redirect_to dashboard_path, alert: "Erreur lors du traitement de votre confirmation de paiement : #{e.message}" - rescue => e - redirect_to dashboard_path, alert: "Une erreur inattendue s'est produite : #{e.message}" - end - end - - # Download ticket PDF - def download_ticket - @ticket = current_user.tickets.find(params[:ticket_id]) - - respond_to do |format| - format.pdf do - pdf = @ticket.to_pdf - send_data pdf, - filename: "ticket-#{@ticket.event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf", - type: "application/pdf", - disposition: "attachment" - end - end - end - - private - - def set_event - @event = Event.find(params[:id]) - end end diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index b3c7a46..fb8f55a 100755 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -8,12 +8,16 @@ import LogoutController from "./logout_controller" import FlashMessageController from "./flash_message_controller" import CounterController from "./counter_controller" import FeaturedEventController from "./featured_event_controller" +import TicketCartController from "./ticket_cart_controller" -import ShadcnTestController from "./shadcn_test_controller" application.register("logout", LogoutController) // Allow logout using js application.register("flash-message", FlashMessageController) // Dismiss notification after 5 secondes application.register("counter", CounterController) // Simple counter for homepage application.register("featured-event", FeaturedEventController) // Featured event controller for homepage +application.register("ticket-cart-controller", TicketCartController) // Handle ticket checkout -application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn + + +// import ShadcnTestController from "./shadcn_test_controller" +// application.register("shadcn-test", ShadcnTestController) // Test controller for Shadcn diff --git a/app/models/ticket.rb b/app/models/ticket.rb index bfaccf5..a257045 100755 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -10,6 +10,8 @@ class Ticket < ApplicationRecord validates :ticket_type_id, presence: true validates :price_cents, presence: true, numericality: { greater_than: 0 } validates :status, presence: true, inclusion: { in: %w[active used expired refunded] } + validates :first_name, presence: true, if: :requires_names? + validates :last_name, presence: true, if: :requires_names? before_validation :set_price_from_ticket_type, on: :create before_validation :generate_qr_code, on: :create @@ -24,6 +26,11 @@ class Ticket < ApplicationRecord price_cents / 100.0 end + # Check if names are required for this ticket type + def requires_names? + ticket_type&.requires_id + end + private def set_price_from_ticket_type diff --git a/app/views/events/collect_names.html.erb b/app/views/events/collect_names.html.erb new file mode 100755 index 0000000..f360e5c --- /dev/null +++ b/app/views/events/collect_names.html.erb @@ -0,0 +1,78 @@ +
Veuillez fournir les prénoms et noms des personnes qui utiliseront les billets.
+Les billets suivants nécessitent que vous indiquiez le prénom et le nom de chaque participant.
+ + <% @tickets_needing_names.each_with_index do |ticket, index| %> +