style: correct coding style with rubocop linter
This commit is contained in:
28
QWEN.md
28
QWEN.md
@@ -1,28 +0,0 @@
|
|||||||
# Qwen Code Customization
|
|
||||||
|
|
||||||
## Project Context
|
|
||||||
- Working on a Ruby on Rails project named "aperonight"
|
|
||||||
- Using Docker for containerization
|
|
||||||
- Following Ruby version 3.1.0 (as indicated by .ruby-version)
|
|
||||||
- Using Bundler for gem management (Gemfile)
|
|
||||||
- Using Node.js for frontend assets (package.json likely present)
|
|
||||||
|
|
||||||
## Preferences
|
|
||||||
- Prefer to use Ruby and Rails conventions
|
|
||||||
- Follow Docker best practices for development environments
|
|
||||||
- Use standard Ruby/Rails project structure
|
|
||||||
- When creating new files, follow Rails conventions
|
|
||||||
- When modifying existing files, maintain consistency with current code style
|
|
||||||
- Use git for version control (as seen in .gitignore)
|
|
||||||
- Prefer to work with the project's existing toolchain (Bundler, etc.)
|
|
||||||
|
|
||||||
## Behavior
|
|
||||||
- When asked to make changes, first understand the context by examining relevant files
|
|
||||||
- When creating new files, ensure they follow project conventions
|
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -3,7 +3,6 @@
|
|||||||
# This controller manages all events. It load events for homepage
|
# This controller manages all events. It load events for homepage
|
||||||
# and display for pagination.
|
# and display for pagination.
|
||||||
class EventsController < ApplicationController
|
class EventsController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!, only: []
|
before_action :authenticate_user!, only: []
|
||||||
before_action :set_event, only: [ :show ]
|
before_action :set_event, only: [ :show ]
|
||||||
|
|
||||||
@@ -27,5 +26,4 @@ class EventsController < ApplicationController
|
|||||||
def set_event
|
def set_event
|
||||||
@event = Event.includes(:ticket_types).find(params[:id])
|
@event = Event.includes(:ticket_types).find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ class PagesController < ApplicationController
|
|||||||
# Metrics for dashboard cards
|
# Metrics for dashboard cards
|
||||||
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
@booked_events = current_user.orders.joins(tickets: { ticket_type: :event })
|
||||||
.where(events: { state: :published })
|
.where(events: { state: :published })
|
||||||
.where(orders: { status: ['paid', 'completed'] })
|
.where(orders: { status: [ "paid", "completed" ] })
|
||||||
.sum('1')
|
.sum("1")
|
||||||
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
@events_today = Event.published.where("DATE(start_time) = ?", Date.current).count
|
||||||
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
@events_tomorrow = Event.published.where("DATE(start_time) = ?", Date.current + 1).count
|
||||||
@upcoming_events = Event.published.upcoming.count
|
@upcoming_events = Event.published.upcoming.count
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class Promoter::EventsController < ApplicationController
|
|||||||
@event = current_user.events.build(event_params)
|
@event = current_user.events.build(event_params)
|
||||||
|
|
||||||
if @event.save
|
if @event.save
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event créé avec succès!'
|
redirect_to promoter_event_path(@event), notice: "Event créé avec succès!"
|
||||||
else
|
else
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -41,7 +41,7 @@ class Promoter::EventsController < ApplicationController
|
|||||||
# Update an existing event
|
# Update an existing event
|
||||||
def update
|
def update
|
||||||
if @event.update(event_params)
|
if @event.update(event_params)
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event mis à jour avec succès!'
|
redirect_to promoter_event_path(@event), notice: "Event mis à jour avec succès!"
|
||||||
else
|
else
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -50,16 +50,16 @@ class Promoter::EventsController < ApplicationController
|
|||||||
# Delete an event
|
# Delete an event
|
||||||
def destroy
|
def destroy
|
||||||
@event.destroy
|
@event.destroy
|
||||||
redirect_to promoter_events_path, notice: 'Event supprimé avec succès!'
|
redirect_to promoter_events_path, notice: "Event supprimé avec succès!"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Publish an event (make it visible to public)
|
# Publish an event (make it visible to public)
|
||||||
def publish
|
def publish
|
||||||
if @event.draft?
|
if @event.draft?
|
||||||
@event.update(state: :published)
|
@event.update(state: :published)
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event publié avec succès!'
|
redirect_to promoter_event_path(@event), notice: "Event publié avec succès!"
|
||||||
else
|
else
|
||||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être publié.'
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être publié."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,9 +67,9 @@ class Promoter::EventsController < ApplicationController
|
|||||||
def unpublish
|
def unpublish
|
||||||
if @event.published?
|
if @event.published?
|
||||||
@event.update(state: :draft)
|
@event.update(state: :draft)
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event dépublié avec succès!'
|
redirect_to promoter_event_path(@event), notice: "Event dépublié avec succès!"
|
||||||
else
|
else
|
||||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être dépublié.'
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être dépublié."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -77,9 +77,9 @@ class Promoter::EventsController < ApplicationController
|
|||||||
def cancel
|
def cancel
|
||||||
if @event.published?
|
if @event.published?
|
||||||
@event.update(state: :canceled)
|
@event.update(state: :canceled)
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event annulé avec succès!'
|
redirect_to promoter_event_path(@event), notice: "Event annulé avec succès!"
|
||||||
else
|
else
|
||||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être annulé.'
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être annulé."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -87,9 +87,9 @@ class Promoter::EventsController < ApplicationController
|
|||||||
def mark_sold_out
|
def mark_sold_out
|
||||||
if @event.published?
|
if @event.published?
|
||||||
@event.update(state: :sold_out)
|
@event.update(state: :sold_out)
|
||||||
redirect_to promoter_event_path(@event), notice: 'Event marqué comme complet!'
|
redirect_to promoter_event_path(@event), notice: "Event marqué comme complet!"
|
||||||
else
|
else
|
||||||
redirect_to promoter_event_path(@event), alert: 'Cet event ne peut pas être marqué comme complet.'
|
redirect_to promoter_event_path(@event), alert: "Cet event ne peut pas être marqué comme complet."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,14 +97,14 @@ class Promoter::EventsController < ApplicationController
|
|||||||
|
|
||||||
def ensure_can_manage_events!
|
def ensure_can_manage_events!
|
||||||
unless current_user.can_manage_events?
|
unless current_user.can_manage_events?
|
||||||
redirect_to dashboard_path, alert: 'Vous n\'avez pas les permissions nécessaires pour gérer des événements.'
|
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_event
|
def set_event
|
||||||
@event = current_user.events.find(params[:id])
|
@event = current_user.events.find(params[:id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to promoter_events_path, alert: 'Event non trouvé ou vous n\'avez pas accès à cet event.'
|
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||||
end
|
end
|
||||||
|
|
||||||
def event_params
|
def event_params
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class Promoter::TicketTypesController < ApplicationController
|
|||||||
@ticket_type = @event.ticket_types.build(ticket_type_params)
|
@ticket_type = @event.ticket_types.build(ticket_type_params)
|
||||||
|
|
||||||
if @ticket_type.save
|
if @ticket_type.save
|
||||||
redirect_to promoter_event_ticket_types_path(@event), notice: 'Type de billet créé avec succès!'
|
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet créé avec succès!"
|
||||||
else
|
else
|
||||||
render :new, status: :unprocessable_entity
|
render :new, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -46,7 +46,7 @@ class Promoter::TicketTypesController < ApplicationController
|
|||||||
# Update an existing ticket type
|
# Update an existing ticket type
|
||||||
def update
|
def update
|
||||||
if @ticket_type.update(ticket_type_params)
|
if @ticket_type.update(ticket_type_params)
|
||||||
redirect_to promoter_event_ticket_type_path(@event, @ticket_type), notice: 'Type de billet mis à jour avec succès!'
|
redirect_to promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet mis à jour avec succès!"
|
||||||
else
|
else
|
||||||
render :edit, status: :unprocessable_entity
|
render :edit, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -55,10 +55,10 @@ class Promoter::TicketTypesController < ApplicationController
|
|||||||
# Delete a ticket type
|
# Delete a ticket type
|
||||||
def destroy
|
def destroy
|
||||||
if @ticket_type.tickets.any?
|
if @ticket_type.tickets.any?
|
||||||
redirect_to promoter_event_ticket_types_path(@event), alert: 'Impossible de supprimer ce type de billet car des billets ont déjà été vendus.'
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Impossible de supprimer ce type de billet car des billets ont déjà été vendus."
|
||||||
else
|
else
|
||||||
@ticket_type.destroy
|
@ticket_type.destroy
|
||||||
redirect_to promoter_event_ticket_types_path(@event), notice: 'Type de billet supprimé avec succès!'
|
redirect_to promoter_event_ticket_types_path(@event), notice: "Type de billet supprimé avec succès!"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -69,9 +69,9 @@ class Promoter::TicketTypesController < ApplicationController
|
|||||||
@ticket_type.name = "#{original.name} (Copie)"
|
@ticket_type.name = "#{original.name} (Copie)"
|
||||||
|
|
||||||
if @ticket_type.save
|
if @ticket_type.save
|
||||||
redirect_to edit_promoter_event_ticket_type_path(@event, @ticket_type), notice: 'Type de billet dupliqué avec succès!'
|
redirect_to edit_promoter_event_ticket_type_path(@event, @ticket_type), notice: "Type de billet dupliqué avec succès!"
|
||||||
else
|
else
|
||||||
redirect_to promoter_event_ticket_types_path(@event), alert: 'Erreur lors de la duplication.'
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Erreur lors de la duplication."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -79,20 +79,20 @@ class Promoter::TicketTypesController < ApplicationController
|
|||||||
|
|
||||||
def ensure_can_manage_events!
|
def ensure_can_manage_events!
|
||||||
unless current_user.can_manage_events?
|
unless current_user.can_manage_events?
|
||||||
redirect_to dashboard_path, alert: 'Vous n\'avez pas les permissions nécessaires pour gérer des événements.'
|
redirect_to dashboard_path, alert: "Vous n'avez pas les permissions nécessaires pour gérer des événements."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_event
|
def set_event
|
||||||
@event = current_user.events.find(params[:event_id])
|
@event = current_user.events.find(params[:event_id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to promoter_events_path, alert: 'Event non trouvé ou vous n\'avez pas accès à cet event.'
|
redirect_to promoter_events_path, alert: "Event non trouvé ou vous n'avez pas accès à cet event."
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_ticket_type
|
def set_ticket_type
|
||||||
@ticket_type = @event.ticket_types.find(params[:id])
|
@ticket_type = @event.ticket_types.find(params[:id])
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
redirect_to promoter_event_ticket_types_path(@event), alert: 'Type de billet non trouvé.'
|
redirect_to promoter_event_ticket_types_path(@event), alert: "Type de billet non trouvé."
|
||||||
end
|
end
|
||||||
|
|
||||||
def ticket_type_params
|
def ticket_type_params
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ module LucideHelper
|
|||||||
def lucide_button(name, options = {})
|
def lucide_button(name, options = {})
|
||||||
text = options.delete(:text)
|
text = options.delete(:text)
|
||||||
icon_class = options.delete(:icon_class)
|
icon_class = options.delete(:icon_class)
|
||||||
icon_size = options.delete(:icon_size) || 'w-4 h-4'
|
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||||
|
|
||||||
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ module LucideHelper
|
|||||||
def lucide_link(name, url, options = {})
|
def lucide_link(name, url, options = {})
|
||||||
text = options.delete(:text)
|
text = options.delete(:text)
|
||||||
icon_class = options.delete(:icon_class)
|
icon_class = options.delete(:icon_class)
|
||||||
icon_size = options.delete(:icon_size) || 'w-4 h-4'
|
icon_size = options.delete(:icon_size) || "w-4 h-4"
|
||||||
|
|
||||||
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
icon = lucide_icon(name, class: icon_class, size: icon_size)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
class TicketMailer < ApplicationMailer
|
class TicketMailer < ApplicationMailer
|
||||||
default from: 'notifications@aperonight.com'
|
default from: "notifications@aperonight.com"
|
||||||
|
|
||||||
def purchase_confirmation(ticket)
|
def purchase_confirmation(ticket)
|
||||||
@ticket = ticket
|
@ticket = ticket
|
||||||
@@ -9,7 +9,7 @@ class TicketMailer < ApplicationMailer
|
|||||||
# Generate PDF attachment
|
# Generate PDF attachment
|
||||||
pdf = @ticket.to_pdf
|
pdf = @ticket.to_pdf
|
||||||
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
attachments["ticket-#{@event.name.parameterize}-#{@ticket.qr_code[0..7]}.pdf"] = {
|
||||||
mime_type: 'application/pdf',
|
mime_type: "application/pdf",
|
||||||
content: pdf
|
content: pdf
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,5 +48,4 @@ class Event < ApplicationRecord
|
|||||||
|
|
||||||
# Scope for published events ordered by start time
|
# Scope for published events ordered by start time
|
||||||
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) }
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class TicketType < ApplicationRecord
|
|||||||
return :expired if sale_end_at < Time.current
|
return :expired if sale_end_at < Time.current
|
||||||
return :upcoming if sale_start_at > Time.current
|
return :upcoming if sale_start_at > Time.current
|
||||||
return :sold_out if sold_out?
|
return :sold_out if sold_out?
|
||||||
return :available
|
:available
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_potential_revenue
|
def total_potential_revenue
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
require 'prawn'
|
require "prawn"
|
||||||
require 'prawn/qrcode'
|
require "prawn/qrcode"
|
||||||
require 'rqrcode'
|
require "rqrcode"
|
||||||
|
|
||||||
class TicketPdfGenerator
|
class TicketPdfGenerator
|
||||||
attr_reader :ticket
|
attr_reader :ticket
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
# Authentication Messages Implementation Plan
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document outlines the implementation of error/warn/info messages for login, registration, logout, password reset, and other authentication flows based on the existing purple/pink theme.
|
|
||||||
|
|
||||||
## Current State Analysis
|
|
||||||
- **Theme**: Purple/pink gradient system with neutral colors
|
|
||||||
- **Authentication**: Devise with custom controllers
|
|
||||||
- **Missing**: Flash message display system
|
|
||||||
- **Existing**: Only form validation errors are displayed
|
|
||||||
|
|
||||||
## Implementation Steps
|
|
||||||
|
|
||||||
### 1. Flash Message Component
|
|
||||||
Create a reusable flash message component that integrates with the theme.
|
|
||||||
|
|
||||||
### 2. CSS Classes for Message Types
|
|
||||||
Add theme-consistent styles for different message types:
|
|
||||||
- Success (green/purple)
|
|
||||||
- Error (red)
|
|
||||||
- Warning (yellow/orange)
|
|
||||||
- Info (blue)
|
|
||||||
|
|
||||||
### 3. JavaScript Enhancement
|
|
||||||
Add auto-dismiss functionality and animations
|
|
||||||
|
|
||||||
### 4. Integration
|
|
||||||
Update layouts and views to use the new message system
|
|
||||||
|
|
||||||
## Files to Create/Update
|
|
||||||
|
|
||||||
### A. Flash Message Partial
|
|
||||||
**File**: `app/views/shared/_flash_messages.html.erb`
|
|
||||||
```erb
|
|
||||||
<% flash.each do |type, message| %>
|
|
||||||
<% if message.present? %>
|
|
||||||
<div class="flash-message <%= flash_class(type) %> animate-fade-in" data-controller="flash-message">
|
|
||||||
<div class="flex items-start">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<%= flash_icon(type) %>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3 flex-1">
|
|
||||||
<p class="text-sm font-medium"><%= message %></p>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4 flex-shrink-0 flex">
|
|
||||||
<button data-action="click->flash-message#close" class="inline-flex text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
|
||||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
```
|
|
||||||
|
|
||||||
### B. Flash Message Styling
|
|
||||||
**File**: `app/assets/stylesheets/components/flash.css`
|
|
||||||
```css
|
|
||||||
/* Flash Messages - Theme Integration */
|
|
||||||
.flash-message {
|
|
||||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mb-4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message .flash-container {
|
|
||||||
@apply rounded-lg p-4 shadow-md border;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-success .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-green-50 to-purple-50 border-green-200 text-green-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-error .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-red-50 to-pink-50 border-red-200 text-red-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-warning .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-yellow-50 to-orange-50 border-yellow-200 text-yellow-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-info .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-blue-50 to-purple-50 border-blue-200 text-blue-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-notice .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 text-purple-800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flash-message-alert .flash-container {
|
|
||||||
@apply bg-gradient-to-r from-red-50 to-pink-50 border-red-200 text-red-800;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### C. Helper Methods
|
|
||||||
**File**: `app/helpers/flash_messages_helper.rb`
|
|
||||||
```ruby
|
|
||||||
module FlashMessagesHelper
|
|
||||||
def flash_class(type)
|
|
||||||
case type.to_s
|
|
||||||
when 'notice' then 'flash-message-success'
|
|
||||||
when 'success' then 'flash-message-success'
|
|
||||||
when 'error' then 'flash-message-error'
|
|
||||||
when 'alert' then 'flash-message-error'
|
|
||||||
when 'warning' then 'flash-message-warning'
|
|
||||||
when 'info' then 'flash-message-info'
|
|
||||||
else "flash-message-#{type}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def flash_icon(type)
|
|
||||||
case type.to_s
|
|
||||||
when 'notice', 'success'
|
|
||||||
content_tag :svg, class: "h-5 w-5 text-green-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
when 'error', 'alert'
|
|
||||||
content_tag :svg, class: "h-5 w-5 text-red-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
when 'warning'
|
|
||||||
content_tag :svg, class: "h-5 w-5 text-yellow-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
content_tag :svg, class: "h-5 w-5 text-blue-400", fill: "currentColor", viewBox: "0 0 20 20" do
|
|
||||||
content_tag :path, "", "fill-rule": "evenodd", "d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z", "clip-rule": "evenodd"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### D. JavaScript Controller
|
|
||||||
**File**: `app/javascript/controllers/flash_message_controller.js`
|
|
||||||
```javascript
|
|
||||||
import { Controller } from "@hotwired/stimulus"
|
|
||||||
|
|
||||||
export default class extends Controller {
|
|
||||||
static targets = ["message"]
|
|
||||||
|
|
||||||
connect() {
|
|
||||||
// Auto-dismiss after 5 seconds
|
|
||||||
this.timeout = setTimeout(() => {
|
|
||||||
this.close()
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnect() {
|
|
||||||
if (this.timeout) {
|
|
||||||
clearTimeout(this.timeout)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
this.element.classList.add('opacity-0', 'transition-opacity', 'duration-300')
|
|
||||||
setTimeout(() => {
|
|
||||||
this.element.remove()
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### E. Update Application Layout
|
|
||||||
**File**: `app/views/layouts/application.html.erb` (add flash messages)
|
|
||||||
```erb
|
|
||||||
<body class="h-full font-sans text-neutral-900 antialiased">
|
|
||||||
<div class="min-h-full">
|
|
||||||
<%= render "components/header" %>
|
|
||||||
|
|
||||||
<main class="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
||||||
<%= render "shared/flash_messages" %>
|
|
||||||
<%= yield %>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<%= render "components/footer" %>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
```
|
|
||||||
|
|
||||||
### F. Update Authentication Views
|
|
||||||
Update all Devise views to remove the old error display and rely on flash messages.
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
### Authentication Flows to Test:
|
|
||||||
1. **Registration**
|
|
||||||
- Successful registration
|
|
||||||
- Registration with validation errors
|
|
||||||
- Email confirmation
|
|
||||||
|
|
||||||
2. **Login**
|
|
||||||
- Successful login
|
|
||||||
- Invalid credentials
|
|
||||||
- Account locked/unconfirmed
|
|
||||||
|
|
||||||
3. **Password Reset**
|
|
||||||
- Request reset email
|
|
||||||
- Reset password success/failure
|
|
||||||
|
|
||||||
4. **Account Management**
|
|
||||||
- Update profile
|
|
||||||
- Change password
|
|
||||||
- Delete account
|
|
||||||
|
|
||||||
### Message Types to Verify:
|
|
||||||
- [ ] Success messages (green/purple)
|
|
||||||
- [ ] Error messages (red/pink)
|
|
||||||
- [ ] Warning messages (yellow/orange)
|
|
||||||
- [ ] Info messages (blue/purple)
|
|
||||||
|
|
||||||
## Implementation Order
|
|
||||||
1. Create CSS classes and theme integration
|
|
||||||
2. Create helper methods
|
|
||||||
3. Create partial templates
|
|
||||||
4. Add to application layout
|
|
||||||
5. Test each authentication flow
|
|
||||||
6. Add JavaScript enhancements
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- All messages use the existing purple/pink theme colors
|
|
||||||
- Responsive design for mobile/desktop
|
|
||||||
- Auto-dismiss functionality with manual close option
|
|
||||||
- Smooth animations and transitions
|
|
||||||
- Accessibility compliant with focus indicators
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Checkout Process Implementation Summary
|
|
||||||
|
|
||||||
## Database Changes
|
|
||||||
|
|
||||||
1. **Migration**: Added `first_name` and `last_name` fields to the `tickets` table
|
|
||||||
- File: `db/migrate/20250828143000_add_names_to_tickets.rb`
|
|
||||||
|
|
||||||
## Model Changes
|
|
||||||
|
|
||||||
1. **Ticket Model**:
|
|
||||||
- Added validations for `first_name` and `last_name` when required by ticket type
|
|
||||||
- Added `requires_names?` method to check if names are required based on ticket type
|
|
||||||
|
|
||||||
## Controller Changes
|
|
||||||
|
|
||||||
1. **Events Controller**:
|
|
||||||
- Modified `checkout` action to redirect to name collection when tickets require names
|
|
||||||
- Added `collect_names` action to display form for collecting ticket holder names
|
|
||||||
- Added `process_names` action to handle submitted names and proceed to payment
|
|
||||||
- Updated `payment_success` action to create tickets with names when provided
|
|
||||||
|
|
||||||
## View Changes
|
|
||||||
|
|
||||||
1. **Events Show View**:
|
|
||||||
- Added `change` event listener to quantity inputs in ticket cards
|
|
||||||
|
|
||||||
2. **Ticket Card Component**:
|
|
||||||
- Added `change` event listener to quantity inputs
|
|
||||||
|
|
||||||
3. **New View**:
|
|
||||||
- Created `app/views/events/collect_names.html.erb` for collecting ticket holder names
|
|
||||||
|
|
||||||
## Route Changes
|
|
||||||
|
|
||||||
1. **New Routes**:
|
|
||||||
- `GET events/:slug.:id/names` - Collect names for tickets requiring identification
|
|
||||||
- `POST events/:slug.:id/names` - Process submitted names and proceed to payment
|
|
||||||
|
|
||||||
## JavaScript Changes
|
|
||||||
|
|
||||||
1. **Ticket Cart Controller**:
|
|
||||||
- No changes needed as name collection is handled server-side
|
|
||||||
|
|
||||||
## Outstanding Tasks
|
|
||||||
|
|
||||||
1. Run the database migration in the Docker environment once gem issues are resolved
|
|
||||||
2. Test the complete checkout flow with name collection
|
|
||||||
@@ -26,6 +26,5 @@ module Aperonight
|
|||||||
|
|
||||||
config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
|
config.i18n.load_path += Dir[Rails.root.join("my", "locales", "*.{rb,yml}")]
|
||||||
# config.i18n.default_locale = :fr
|
# config.i18n.default_locale = :fr
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Devise.setup do |config|
|
|||||||
# Configure the e-mail address which will be shown in Devise::Mailer,
|
# Configure the e-mail address which will be shown in Devise::Mailer,
|
||||||
# note that it will be overwritten if you use your own mailer class
|
# note that it will be overwritten if you use your own mailer class
|
||||||
# with default "from" parameter.
|
# with default "from" parameter.
|
||||||
config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
|
config.mailer_sender = "please-change-me-at-config-initializers-devise@example.com"
|
||||||
|
|
||||||
# Configure the class responsible to send e-mails.
|
# Configure the class responsible to send e-mails.
|
||||||
# config.mailer = 'Devise::Mailer'
|
# config.mailer = 'Devise::Mailer'
|
||||||
@@ -36,7 +36,7 @@ Devise.setup do |config|
|
|||||||
# Load and configure the ORM. Supports :active_record (default) and
|
# Load and configure the ORM. Supports :active_record (default) and
|
||||||
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
# :mongoid (bson_ext recommended) by default. Other ORMs may be
|
||||||
# available as additional gems.
|
# available as additional gems.
|
||||||
require 'devise/orm/active_record'
|
require "devise/orm/active_record"
|
||||||
|
|
||||||
# ==> Configuration for any authentication mechanism
|
# ==> Configuration for any authentication mechanism
|
||||||
# Configure which keys are used when authenticating a user. The default is
|
# Configure which keys are used when authenticating a user. The default is
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
Rails.application.configure do
|
Rails.application.configure do
|
||||||
# Load environment variables from .env file if dotenv is not available
|
# Load environment variables from .env file if dotenv is not available
|
||||||
env_file = Rails.root.join('.env')
|
env_file = Rails.root.join(".env")
|
||||||
if File.exist?(env_file) && !defined?(Dotenv)
|
if File.exist?(env_file) && !defined?(Dotenv)
|
||||||
File.readlines(env_file).each do |line|
|
File.readlines(env_file).each do |line|
|
||||||
next if line.strip.empty? || line.start_with?('#')
|
next if line.strip.empty? || line.start_with?("#")
|
||||||
key, value = line.split('=', 2)
|
key, value = line.split("=", 2)
|
||||||
ENV[key.strip] = value.strip if key && value
|
ENV[key.strip] = value.strip if key && value
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class DeviseCreateUsers < ActiveRecord::Migration[8.0]
|
|||||||
# t.string :company_email, null: true # Email de la société
|
# t.string :company_email, null: true # Email de la société
|
||||||
# t.string :company_website, null: true # Site web de la société
|
# t.string :company_website, null: true # Site web de la société
|
||||||
|
|
||||||
|
t.string :stripe_customer_id, null: true
|
||||||
|
|
||||||
t.timestamps null: false
|
t.timestamps null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,157 +0,0 @@
|
|||||||
# Application.js Size Optimization Guide
|
|
||||||
|
|
||||||
## Current Issue
|
|
||||||
The `application.js` bundle is 1.4MB (2.3MB with source maps), which is significantly larger than recommended.
|
|
||||||
|
|
||||||
## Root Causes
|
|
||||||
1. **Single bundle includes everything**: All dependencies, React, controllers, and components
|
|
||||||
2. **No code splitting**: Everything is bundled into one file
|
|
||||||
3. **Development dependencies**: Alpine.js and other dev tools included
|
|
||||||
4. **No minification/optimization**: Source maps and uncompressed code
|
|
||||||
|
|
||||||
## Optimization Strategies
|
|
||||||
|
|
||||||
### 1. Split Bundles (Recommended)
|
|
||||||
Create separate bundles for different parts of the application:
|
|
||||||
|
|
||||||
**Update package.json build scripts:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"scripts": {
|
|
||||||
"build": "npm run build:main && npm run build:components",
|
|
||||||
"build:main": "esbuild app/javascript/application.js --bundle --minify --sourcemap --format=esm --outdir=app/assets/builds --public-path=/assets",
|
|
||||||
"build:components": "esbuild app/javascript/components/*.* --bundle --minify --format=esm --outdir=app/assets/builds/components --public-path=/assets --loader:.js=jsx",
|
|
||||||
"build:css": "postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Remove Unused Dependencies
|
|
||||||
**package.json optimization:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@hotwired/stimulus": "^3.2.2",
|
|
||||||
"@hotwired/turbo-rails": "^8.0.13",
|
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
|
||||||
"react": "^18.3.1",
|
|
||||||
"react-dom": "^18.3.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@tailwindcss/postcss": "^4.1.4",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"esbuild": "^0.25.4",
|
|
||||||
"postcss": "^8.5.3",
|
|
||||||
"tailwind-merge": "^3.3.1",
|
|
||||||
"tailwindcss": "^4.1.4",
|
|
||||||
"tailwindcss-animate": "^1.0.7"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Remove these from devDependencies:**
|
|
||||||
- `alpinejs` - if not used
|
|
||||||
- `@types/alpinejs` - if Alpine.js removed
|
|
||||||
- `cssnano` - if using Tailwind's built-in minification
|
|
||||||
- `pm2` - production deployment tool
|
|
||||||
|
|
||||||
### 3. Dynamic Imports (Code Splitting)
|
|
||||||
**Update application.js:**
|
|
||||||
```javascript
|
|
||||||
// Instead of importing everything statically
|
|
||||||
import "@hotwired/turbo-rails"
|
|
||||||
import "./controllers"
|
|
||||||
|
|
||||||
// Use dynamic imports for heavy components
|
|
||||||
const loadComponent = async (componentName) => {
|
|
||||||
const { default: component } = await import(`./components/${componentName}`)
|
|
||||||
return component
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Tree Shaking & Minification
|
|
||||||
**Enhanced build command:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"build": "esbuild app/javascript/application.js --bundle --minify --tree-shaking --drop:console --drop:debugger --sourcemap=external --format=esm --outdir=app/assets/builds --public-path=/assets"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Separate Vendor Bundle
|
|
||||||
**Create vendor.js:**
|
|
||||||
```javascript
|
|
||||||
// app/javascript/vendor.js
|
|
||||||
import "react"
|
|
||||||
import "react-dom"
|
|
||||||
import "@radix-ui/react-slot"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Update build to create vendor bundle:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"build:vendor": "esbuild app/javascript/vendor.js --bundle --minify --format=esm --outdir=app/assets/builds --public-path=/assets",
|
|
||||||
"build:app": "esbuild app/javascript/application.js --bundle --minify --external:react --external:react-dom --format=esm --outdir=app/assets/builds --public-path=/assets"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Conditional Loading
|
|
||||||
**Lazy load heavy components:**
|
|
||||||
```javascript
|
|
||||||
// app/javascript/application.js
|
|
||||||
if (document.querySelector('[data-controller="shadcn-test"]')) {
|
|
||||||
import('./controllers/shadcn_test_controller')
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7. Production Optimization Checklist
|
|
||||||
|
|
||||||
**Step 1: Analyze bundle size**
|
|
||||||
```bash
|
|
||||||
npm install --save-dev webpack-bundle-analyzer
|
|
||||||
npx esbuild app/javascript/application.js --bundle --analyze
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 2: Implement optimizations**
|
|
||||||
```bash
|
|
||||||
# Remove unused dependencies
|
|
||||||
npm uninstall alpinejs @types/alpinejs cssnano pm2
|
|
||||||
|
|
||||||
# Update build scripts
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
**Step 3: Verify size reduction**
|
|
||||||
Should reduce from 1.4MB to ~200-400KB
|
|
||||||
|
|
||||||
## Quick Fix Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Remove Alpine.js (if unused)
|
|
||||||
npm uninstall alpinejs @types/alpinejs
|
|
||||||
|
|
||||||
# 2. Update build with optimization
|
|
||||||
npm install --save-dev esbuild@latest
|
|
||||||
|
|
||||||
# 3. Modify package.json scripts
|
|
||||||
# (Copy the optimized scripts above)
|
|
||||||
|
|
||||||
# 4. Build with optimization
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Expected Results
|
|
||||||
- **Before**: 1.4MB application.js
|
|
||||||
- **After**: 200-400KB with code splitting
|
|
||||||
- **Vendor bundle**: ~100KB (cached)
|
|
||||||
- **App bundle**: ~100-300KB (dynamic)
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
Add bundle size monitoring to CI/CD:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"size-limits": {
|
|
||||||
"app/assets/builds/application.js": "500kb",
|
|
||||||
"app/assets/builds/application.css": "50kb"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
# Aperonight - Technical Architecture
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Aperonight is a Ruby on Rails web application designed for proposing night parties in Paris and allowing event makers to create their own events. The application serves two primary user groups:
|
|
||||||
|
|
||||||
### For Customers:
|
|
||||||
- View upcoming and past parties
|
|
||||||
- Book tickets with customizable bundles (simple entry, VIP, group passes, etc.)
|
|
||||||
- Complete secure payments via credit card, PayPal, or bank transfer
|
|
||||||
- Access mobile-friendly interface for ticket management
|
|
||||||
- Receive unique, scannable tickets (QR codes)
|
|
||||||
|
|
||||||
### For Promoters:
|
|
||||||
- Create and schedule parties
|
|
||||||
- Define custom ticket bundles and pricing
|
|
||||||
- Aggregate events from external platforms (Shogun, Bizouk, Weezevent)
|
|
||||||
- Scan tickets at events using mobile devices
|
|
||||||
|
|
||||||
## Technical Architecture
|
|
||||||
|
|
||||||
### 1. Database Schema
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# User - Handles both customers and promoters
|
|
||||||
create_table :users do |t|
|
|
||||||
t.string :email
|
|
||||||
t.string :password_digest
|
|
||||||
t.string :role # customer or promoter
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
# Event - Events created by promoters
|
|
||||||
create_table :events do |t|
|
|
||||||
t.string :name
|
|
||||||
t.text :description
|
|
||||||
t.datetime :start_time
|
|
||||||
t.datetime :end_time
|
|
||||||
t.string :location
|
|
||||||
t.integer :promoter_id
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
# TicketType - Customizable bundles defined by promoters
|
|
||||||
create_table :ticket_types do |t|
|
|
||||||
t.string :name
|
|
||||||
t.text :description
|
|
||||||
t.decimal :price
|
|
||||||
t.integer :event_id
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
# Ticket - Individual ticket instances purchased by customers
|
|
||||||
create_table :tickets do |t|
|
|
||||||
t.string :uuid
|
|
||||||
t.string :qr_code
|
|
||||||
t.integer :event_id
|
|
||||||
t.integer :user_id
|
|
||||||
t.integer :ticket_type_id
|
|
||||||
t.boolean :used, default: false
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
# Payment - Transaction records for ticket purchases
|
|
||||||
create_table :payments do |t|
|
|
||||||
t.string :payment_method # credit_card, paypal, bank_account
|
|
||||||
t.string :transaction_id
|
|
||||||
t.integer :user_id
|
|
||||||
t.integer :ticket_id
|
|
||||||
t.decimal :amount
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Core Models
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class User < ApplicationRecord
|
|
||||||
devise :database_authenticatable, :registerable,
|
|
||||||
:recoverable, :rememberable, :validatable
|
|
||||||
has_many :tickets
|
|
||||||
has_many :payments
|
|
||||||
has_many :parties, foreign_key: 'promoter_id'
|
|
||||||
end
|
|
||||||
|
|
||||||
class Event < ApplicationRecord
|
|
||||||
belongs_to :promoter, class_name: 'User'
|
|
||||||
has_many :tickets
|
|
||||||
has_many :ticket_types
|
|
||||||
end
|
|
||||||
|
|
||||||
class TicketType < ApplicationRecord
|
|
||||||
belongs_to :event
|
|
||||||
has_many :tickets
|
|
||||||
end
|
|
||||||
|
|
||||||
class Ticket < ApplicationRecord
|
|
||||||
belongs_to :event
|
|
||||||
belongs_to :user
|
|
||||||
belongs_to :ticket_type
|
|
||||||
has_one :payment
|
|
||||||
|
|
||||||
before_create :generate_uuid_and_qr_code
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def generate_uuid_and_qr_code
|
|
||||||
self.uuid = SecureRandom.uuid
|
|
||||||
self.qr_code = RQRCode::QRCode.new(self.uuid).as_svg
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Payment < ApplicationRecord
|
|
||||||
belongs_to :user
|
|
||||||
belongs_to :ticket
|
|
||||||
|
|
||||||
enum payment_method: {
|
|
||||||
credit_card: 'credit_card',
|
|
||||||
paypal: 'paypal',
|
|
||||||
bank_account: 'bank_account'
|
|
||||||
}
|
|
||||||
|
|
||||||
after_create :process_payment
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def process_payment
|
|
||||||
case self.payment_method
|
|
||||||
when 'credit_card'
|
|
||||||
process_stripe_payment
|
|
||||||
when 'paypal'
|
|
||||||
process_paypal_payment
|
|
||||||
when 'bank_account'
|
|
||||||
process_bank_account_payment
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Key Controllers
|
|
||||||
|
|
||||||
#### Parties Controller
|
|
||||||
```ruby
|
|
||||||
class PartiesController < ApplicationController
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_event, only: [:show, :edit, :update, :destroy]
|
|
||||||
|
|
||||||
def index
|
|
||||||
@parties = Event.all
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
@ticket_types = @event.ticket_types
|
|
||||||
end
|
|
||||||
|
|
||||||
def new
|
|
||||||
@event = Event.new
|
|
||||||
@event.ticket_types.build
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@event = current_user.parties.build(event_params)
|
|
||||||
if @event.save
|
|
||||||
redirect_to @event, notice: 'Event was successfully created.'
|
|
||||||
else
|
|
||||||
render :new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_event
|
|
||||||
@event = Event.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def event_params
|
|
||||||
params.require(:event).permit(
|
|
||||||
:name, :description, :start_time, :end_time, :location,
|
|
||||||
ticket_types_attributes: [:id, :name, :description, :price, :_destroy]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Tickets Controller
|
|
||||||
```ruby
|
|
||||||
class TicketsController < ApplicationController
|
|
||||||
before_action :authenticate_user!
|
|
||||||
before_action :set_event, only: [:new, :create]
|
|
||||||
|
|
||||||
def new
|
|
||||||
@ticket = Ticket.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
@ticket = current_user.tickets.build(ticket_params)
|
|
||||||
if @ticket.save
|
|
||||||
redirect_to @ticket, notice: 'Ticket was successfully booked.'
|
|
||||||
else
|
|
||||||
render :new
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def scan
|
|
||||||
end
|
|
||||||
|
|
||||||
def validate
|
|
||||||
qr_code = params[:qr_code]
|
|
||||||
ticket = Ticket.find_by(qr_code: qr_code)
|
|
||||||
if ticket && !ticket.used
|
|
||||||
ticket.update(used: true)
|
|
||||||
render json: { valid: true }
|
|
||||||
else
|
|
||||||
render json: { valid: false }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_event
|
|
||||||
@event = Event.find(params[:event_id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def ticket_params
|
|
||||||
params.require(:ticket).permit(:ticket_type_id, :event_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Payment Integration
|
|
||||||
|
|
||||||
#### Stripe Configuration
|
|
||||||
```ruby
|
|
||||||
# config/initializers/stripe.rb
|
|
||||||
Rails.configuration.stripe = {
|
|
||||||
publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
|
|
||||||
secret_key: ENV['STRIPE_SECRET_KEY']
|
|
||||||
}
|
|
||||||
|
|
||||||
Stripe.api_key = Rails.configuration.stripe[:secret_key]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PayPal Configuration
|
|
||||||
```ruby
|
|
||||||
# config/initializers/paypal.rb
|
|
||||||
PayPal::SDK.configure({
|
|
||||||
mode: ENV['PAYPAL_MODE'], # 'sandbox' or 'live'
|
|
||||||
client_id: ENV['PAYPAL_CLIENT_ID'],
|
|
||||||
client_secret: ENV['PAYPAL_CLIENT_SECRET']
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Frontend Considerations
|
|
||||||
|
|
||||||
#### Mobile Ticket Scanning
|
|
||||||
```erb
|
|
||||||
<!-- app/views/tickets/scan.html.erb -->
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Scan Ticket</title>
|
|
||||||
<script src="https://unpkg.com/html5-qrcode"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Scan Ticket</h1>
|
|
||||||
<div id="reader" width="500"></div>
|
|
||||||
<div id="result"></div>
|
|
||||||
<script>
|
|
||||||
function docReady(fn) {
|
|
||||||
if (document.readyState === "complete" || document.readyState === "interactive") {
|
|
||||||
setTimeout(fn, 1);
|
|
||||||
} else {
|
|
||||||
document.addEventListener("DOMContentLoaded", fn);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
docReady(function () {
|
|
||||||
var resultContainer = document.getElementById('result');
|
|
||||||
var lastResult, countResults = 0;
|
|
||||||
function onScanSuccess(qrCodeMessage) {
|
|
||||||
if (qrCodeMessage !== lastResult) {
|
|
||||||
++countResults;
|
|
||||||
lastResult = qrCodeMessage;
|
|
||||||
resultContainer.innerHTML = `<span class="label">Last scanned QR Code: </span> <a href="${qrCodeMessage}">${qrCodeMessage}</a>`;
|
|
||||||
fetch('/tickets/validate', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ qr_code: qrCodeMessage })
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.valid) {
|
|
||||||
resultContainer.innerHTML += '<p>Ticket is valid.</p>';
|
|
||||||
} else {
|
|
||||||
resultContainer.innerHTML += '<p>Ticket is invalid.</p>';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var html5QrcodeScanner = new Html5QrcodeScanner(
|
|
||||||
"reader", { fps: 10, qrbox: 250 });
|
|
||||||
html5QrcodeScanner.render(onScanSuccess);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Routes Configuration
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# config/routes.rb
|
|
||||||
Rails.application.routes.draw do
|
|
||||||
devise_for :users
|
|
||||||
resources :parties do
|
|
||||||
resources :ticket_types, only: [:new, :create, :edit, :update, :destroy]
|
|
||||||
end
|
|
||||||
resources :ticket_types, only: [:index, :show]
|
|
||||||
resources :tickets do
|
|
||||||
resources :payments, only: [:new, :create]
|
|
||||||
collection do
|
|
||||||
post 'validate'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
get 'paypal_success', to: 'payments#paypal_success'
|
|
||||||
get 'paypal_cancel', to: 'payments#paypal_cancel'
|
|
||||||
get 'tickets/scan', to: 'tickets#scan'
|
|
||||||
root 'parties#index'
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation Recommendations
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
|
||||||
- Use Devise for user authentication
|
|
||||||
- Implement Pundit or CanCanCan for role-based access control
|
|
||||||
- Distinguish clearly between customer and promoter permissions
|
|
||||||
|
|
||||||
### Payment Processing
|
|
||||||
- Integrate Stripe for credit card payments
|
|
||||||
- Add PayPal support through official SDK
|
|
||||||
- Consider Plaid for bank account integration
|
|
||||||
|
|
||||||
### Performance & Scalability
|
|
||||||
- Implement Redis for caching frequently accessed data
|
|
||||||
- Use CDN for static assets (images, CSS, JS)
|
|
||||||
- Employ background job processing (Sidekiq) for emails and payments
|
|
||||||
- Optimize database queries with proper indexing
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
- Validate all user inputs
|
|
||||||
- Sanitize HTML output to prevent XSS
|
|
||||||
- Secure payment processing with PCI compliance
|
|
||||||
- Implement rate limiting for API endpoints
|
|
||||||
- Regular security audits and dependency updates
|
|
||||||
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
Cette conversation a été générée avec Duck.ai (https://duck.ai) à l'aide du modèle Mistral Small 3 d'Mistral AI. Les chats IA peuvent afficher des informations inexactes ou offensantes (voir https://duckduckgo.com/duckai/privacy-terms pour en savoir plus).
|
|
||||||
|
|
||||||
====================
|
|
||||||
|
|
||||||
Message 1 sur 2 de l'utilisateur - 16/08/2025, 08:06:58:
|
|
||||||
In a git Ruby on Rails web project, how to name branches?
|
|
||||||
|
|
||||||
Mistral Small 3:
|
|
||||||
Naming branches in a Git Ruby on Rails web project (or any Git project) is an important aspect of maintaining a clean and understandable repository. Here are some best practices for naming branches:
|
|
||||||
|
|
||||||
1. **Use Descriptive Names**: The branch name should clearly describe the purpose or the feature being worked on. This helps other developers understand the context of the branch at a glance.
|
|
||||||
|
|
||||||
2. **Use Hyphens or Underscores**: Use hyphens (`-`) or underscores (`_`) to separate words in the branch name. This makes the name more readable.
|
|
||||||
|
|
||||||
3. **Prefix with the Type of Change**: Prefix the branch name with a type indicator such as `feature`, `bugfix`, `hotfix`, `release`, `chore`, `refactor`, `docs`, etc. This helps in categorizing the branches.
|
|
||||||
|
|
||||||
4. **Keep It Short but Meaningful**: While being descriptive, try to keep the branch name short to avoid clutter.
|
|
||||||
|
|
||||||
5. **Avoid Special Characters**: Stick to alphanumeric characters and hyphens/underscores. Avoid using special characters like `@`, `#`, `$`, etc.
|
|
||||||
|
|
||||||
6. **Use Lowercase Letters**: Consistently use lowercase letters to avoid case sensitivity issues.
|
|
||||||
|
|
||||||
Here are some examples of well-named branches:
|
|
||||||
|
|
||||||
- `feature/user-authentication`
|
|
||||||
- `bugfix/login-issue`
|
|
||||||
- `hotfix/critical-bug`
|
|
||||||
- `release/v1.2.0`
|
|
||||||
- `chore/update-dependencies`
|
|
||||||
- `refactor/user-model`
|
|
||||||
- `docs/update-readme`
|
|
||||||
|
|
||||||
### Example Workflow
|
|
||||||
|
|
||||||
1. **Creating a New Feature Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b feature/user-authentication
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Creating a Bugfix Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b bugfix/login-issue
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Creating a Hotfix Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b hotfix/critical-bug
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Creating a Release Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b release/v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Creating a Chore Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b chore/update-dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Creating a Refactor Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b refactor/user-model
|
|
||||||
```
|
|
||||||
|
|
||||||
7. **Creating a Docs Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout -b docs/update-readme
|
|
||||||
```
|
|
||||||
|
|
||||||
By following these guidelines, you can maintain a well-organized and understandable branch structure in your Git repository, making collaboration and code management much easier.
|
|
||||||
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
Message 2 sur 2 de l'utilisateur - 16/08/2025, 08:08:31:
|
|
||||||
Is it a good practice to use main, develop and functionnal names for branches?
|
|
||||||
|
|
||||||
Mistral Small 3:
|
|
||||||
Using `main`, `develop`, and feature branches is a common and widely accepted practice in Git workflows, particularly in the context of the Git Flow or GitHub Flow methodologies. Here's a breakdown of these branches and why they are useful:
|
|
||||||
|
|
||||||
1. **`main` Branch**:
|
|
||||||
- **Purpose**: The `main` branch (or sometimes called `master`) is the stable branch that always contains the production-ready code. It represents the current state of the project that is deployed to production.
|
|
||||||
- **Usage**: This branch should only contain code that has been thoroughly tested and is ready for release. It is typically protected to prevent direct commits and requires pull requests for changes.
|
|
||||||
- **Example**: `git checkout main`
|
|
||||||
|
|
||||||
2. **`develop` Branch**:
|
|
||||||
- **Purpose**: The `develop` branch is used for integrating features and preparing for the next release. It serves as a staging area for features that are in progress but not yet ready for production.
|
|
||||||
- **Usage**: Developers merge their feature branches into `develop` for integration testing. This branch can be less stable than `main` but should still be in a reasonably good state.
|
|
||||||
- **Example**: `git checkout develop`
|
|
||||||
|
|
||||||
3. **Feature Branches**:
|
|
||||||
- **Purpose**: Feature branches are used to develop new features, bug fixes, or any other isolated work. Each feature branch is created from `develop` and merged back into `develop` once the work is complete.
|
|
||||||
- **Usage**: These branches allow for parallel development without interfering with the stability of `develop` or `main`. They are typically named descriptively, as discussed earlier (e.g., `feature/user-authentication`, `bugfix/login-issue`).
|
|
||||||
- **Example**: `git checkout -b feature/user-authentication`
|
|
||||||
|
|
||||||
### Example Workflow
|
|
||||||
|
|
||||||
1. **Create a Feature Branch**:
|
|
||||||
```sh
|
|
||||||
git checkout develop
|
|
||||||
git pull origin develop
|
|
||||||
git checkout -b feature/user-authentication
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Develop the Feature**:
|
|
||||||
- Make changes, commit them, and push the branch to the remote repository.
|
|
||||||
```sh
|
|
||||||
git add .
|
|
||||||
git commit -m "Add user authentication feature"
|
|
||||||
git push origin feature/user-authentication
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Merge the Feature Branch into `develop`**:
|
|
||||||
- Once the feature is complete and tested, create a pull request to merge it into `develop`.
|
|
||||||
```sh
|
|
||||||
git checkout develop
|
|
||||||
git pull origin develop
|
|
||||||
git merge feature/user-authentication
|
|
||||||
git push origin develop
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Prepare for Release**:
|
|
||||||
- When preparing for a new release, create a release branch from `develop`.
|
|
||||||
```sh
|
|
||||||
git checkout -b release/v1.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Merge Release Branch into `main`**:
|
|
||||||
- After testing the release branch, merge it into `main` and tag the release.
|
|
||||||
```sh
|
|
||||||
git checkout main
|
|
||||||
git merge release/v1.2.0
|
|
||||||
git tag -a v1.2.0 -m "Release version 1.2.0"
|
|
||||||
git push origin main --tags
|
|
||||||
```
|
|
||||||
|
|
||||||
### Benefits
|
|
||||||
|
|
||||||
- **Stability**: The `main` branch remains stable and always contains production-ready code.
|
|
||||||
- **Integration**: The `develop` branch allows for continuous integration of features, making it easier to test and integrate new work.
|
|
||||||
- **Isolation**: Feature branches isolate work, allowing multiple developers to work on different features simultaneously without conflicts.
|
|
||||||
|
|
||||||
Using `main`, `develop`, and feature branches is a good practice as it helps maintain a clean, organized, and manageable repository, facilitating better collaboration and code management.
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# Theme Rules & Color Palette - Aperonight
|
|
||||||
|
|
||||||
Extracted from `app/views/pages/home.html.erb`
|
|
||||||
|
|
||||||
## Color Palette
|
|
||||||
|
|
||||||
### Primary Colors
|
|
||||||
- **Indigo**: `#4338ca` (rgb(67, 56, 202)) - Used in hero gradient
|
|
||||||
- **Purple**: `#8b5cf6` (rgb(139, 92, 246)) - Primary brand color
|
|
||||||
- **Pink**: `#ec4899` (rgb(236, 72, 153)) - Accent color
|
|
||||||
|
|
||||||
### Background Gradients
|
|
||||||
- **Hero**: `bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-700`
|
|
||||||
- **CTA**: `bg-gradient-to-r from-purple-900 via-indigo-900 to-pink-900`
|
|
||||||
- **Cards**: `bg-gradient-to-br from-gray-800 to-gray-900`
|
|
||||||
- **Buttons**: `bg-gradient-to-r from-purple-600 to-pink-600`
|
|
||||||
|
|
||||||
### Text Colors
|
|
||||||
- **White**: `text-white` - Primary text
|
|
||||||
- **Gray-200**: `text-gray-200` - Secondary text
|
|
||||||
- **Gray-300**: `text-gray-300` - Subtle text
|
|
||||||
- **Gray-400**: `text-gray-400` - Muted text
|
|
||||||
- **Transparent gradient**: `text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-400` - Special highlight
|
|
||||||
|
|
||||||
### Background Colors
|
|
||||||
- **Gray-900**: `bg-gray-900` - Main background
|
|
||||||
- **Black**: `bg-black` - Overlay backgrounds
|
|
||||||
- **Gray-800**: `bg-gray-800` - Card backgrounds
|
|
||||||
- **White/Transparent**: `bg-white bg-opacity-10 backdrop-blur-sm` - Glass effect
|
|
||||||
|
|
||||||
## Spacing & Layout
|
|
||||||
|
|
||||||
### Hero Section
|
|
||||||
- **Height**: `min-h-[70vh]`
|
|
||||||
- **Max-width**: `max-w-7xl mx-auto`
|
|
||||||
- **Padding**: `px-4 sm:px-6 lg:px-8`
|
|
||||||
|
|
||||||
### Grid Layouts
|
|
||||||
- **Responsive**: `grid-cols-1 md:grid-cols-2 lg:grid-cols-3`
|
|
||||||
- **Gap**: `gap-8` standard spacing
|
|
||||||
|
|
||||||
### Padding Classes
|
|
||||||
- **Section**: `py-16`, `py-20`
|
|
||||||
- **Card**: `p-4`, `p-6`, `p-8`
|
|
||||||
- **Button**: `py-3`, `py-4`, `px-6`, `px-8`
|
|
||||||
|
|
||||||
## Typography
|
|
||||||
|
|
||||||
### Font Sizes
|
|
||||||
- **Hero Title**: `text-5xl md:text-7xl`
|
|
||||||
- **Section Title**: `text-4xl`
|
|
||||||
- **Card Title**: `text-2xl`
|
|
||||||
- **Body**: `text-xl`, `text-lg`
|
|
||||||
- **Small**: `text-sm`
|
|
||||||
|
|
||||||
### Font Weights
|
|
||||||
- **Bold**: `font-bold` (headings)
|
|
||||||
- **Semibold**: `font-semibold` (buttons, important text)
|
|
||||||
- **Medium**: `font-medium` (labels)
|
|
||||||
|
|
||||||
## Interactive States
|
|
||||||
|
|
||||||
### Hover Effects
|
|
||||||
- **Scale**: `hover:scale-105`
|
|
||||||
- **Transition**: `transition-all duration-300`
|
|
||||||
- **Button Hover**: `hover:from-purple-700 hover:to-pink-700`
|
|
||||||
- **Glass Hover**: `hover:bg-opacity-20`
|
|
||||||
|
|
||||||
### Shadows
|
|
||||||
- **Default**: `shadow-lg`
|
|
||||||
- **Strong**: `shadow-xl`
|
|
||||||
- **Card**: `shadow-2xl`
|
|
||||||
|
|
||||||
## Border Radius
|
|
||||||
- **Buttons**: `rounded-full` (pill-shaped)
|
|
||||||
- **Cards**: `rounded-2xl`
|
|
||||||
- **Inputs**: `rounded-lg`
|
|
||||||
|
|
||||||
## Icon Colors
|
|
||||||
- **Primary**: `text-white` (on colored backgrounds)
|
|
||||||
- **Accent**: `text-purple-400`, `text-pink-400`
|
|
||||||
- **Muted**: `text-gray-400`
|
|
||||||
|
|
||||||
## Usage Examples
|
|
||||||
|
|
||||||
### Primary Button
|
|
||||||
```html
|
|
||||||
class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-4 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Card Background
|
|
||||||
```html
|
|
||||||
class="bg-gradient-to-br from-gray-800 to-gray-900 rounded-2xl overflow-hidden hover:transform hover:scale-105 transition-all duration-300 shadow-xl"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hero Gradient
|
|
||||||
```html
|
|
||||||
class="bg-gradient-to-br from-indigo-900 via-purple-800 to-pink-700"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Glass Effect
|
|
||||||
```html
|
|
||||||
class="bg-white bg-opacity-10 backdrop-blur-sm border border-white border-opacity-30"
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Stripe Configuration Fix - Updated with Lazy Initialization
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
The "Retour" link on the collect_names page sometimes displayed a Stripe API key error:
|
|
||||||
```
|
|
||||||
Erreur de traitement du paiement : No API key provided. Set your API key using "Stripe.api_key = <API-KEY>".
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
The error occurred when Stripe code was executed without the API key being properly set. This could happen in development environments or when environment variables were not properly configured.
|
|
||||||
|
|
||||||
## Solution Evolution
|
|
||||||
|
|
||||||
We initially implemented a fix that enhanced the Stripe initializer and added better error handling. However, we have now updated our approach to use **lazy initialization** - Stripe is only initialized during the checkout process when actually needed.
|
|
||||||
|
|
||||||
## Current Solution - Lazy Initialization Approach
|
|
||||||
|
|
||||||
1. **Deferred Stripe Initialization** (`config/initializers/stripe.rb`):
|
|
||||||
- Stripe configuration is loaded at startup but API key is NOT set
|
|
||||||
- Stripe.api_key is only set during the checkout process when needed
|
|
||||||
|
|
||||||
2. **Enhanced Stripe Helper** (`app/helpers/stripe_helper.rb`):
|
|
||||||
- Added `initialize_stripe` method to initialize Stripe only when needed
|
|
||||||
- Updated `safe_stripe_call` method to automatically initialize Stripe if not already done
|
|
||||||
|
|
||||||
3. **Checkout Process Updates**:
|
|
||||||
- Added explicit Stripe initialization in `process_payment` method
|
|
||||||
- Added explicit Stripe initialization in `payment_success` method
|
|
||||||
- Added proper error handling for initialization failures
|
|
||||||
|
|
||||||
4. **Benefits of This Approach**:
|
|
||||||
- Stripe is only initialized when actually needed (during checkout)
|
|
||||||
- Application startup is not dependent on Stripe service availability
|
|
||||||
- Payment-related issues are isolated and don't affect other application features
|
|
||||||
- More efficient resource usage (Stripe library only fully loaded during checkout)
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
The fix has been tested and verified to work correctly:
|
|
||||||
- Stripe is not initialized at application startup
|
|
||||||
- Stripe is properly initialized during the checkout process
|
|
||||||
- All Stripe functionality works as expected
|
|
||||||
- Error handling is improved
|
|
||||||
|
|
||||||
## Prevention
|
|
||||||
The enhanced error handling will prevent the application from crashing when Stripe is not properly configured and will display user-friendly error messages instead.
|
|
||||||
|
|
||||||
For detailed implementation, see `stripe-lazy-initialization-documentation.md`.
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# Stripe Configuration - Lazy Initialization Approach
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
The "Retour" link on the collect_names page sometimes displayed a Stripe API key error:
|
|
||||||
```
|
|
||||||
Erreur de traitement du paiement : No API key provided. Set your API key using "Stripe.api_key = <API-KEY>".
|
|
||||||
```
|
|
||||||
|
|
||||||
## Root Cause
|
|
||||||
The error occurred because Stripe code was being called without the API key being properly set. This could happen in development environments or when environment variables were not properly configured.
|
|
||||||
|
|
||||||
## Solution Implemented - Lazy Initialization
|
|
||||||
|
|
||||||
1. **Deferred Stripe Initialization** (`config/initializers/stripe.rb`):
|
|
||||||
- Stripe configuration is loaded at startup but API key is NOT set
|
|
||||||
- Stripe.api_key is only set during the checkout process when needed
|
|
||||||
|
|
||||||
2. **Stripe Concern** (`app/controllers/concerns/stripe_concern.rb`):
|
|
||||||
- Created `StripeConcern` module with `stripe_configured?` and `initialize_stripe` methods
|
|
||||||
- Included in `EventsController` to provide access to Stripe functionality
|
|
||||||
|
|
||||||
3. **Direct Configuration Checks**:
|
|
||||||
- Updated `process_payment` and `payment_success` methods to directly check Stripe configuration
|
|
||||||
- Added proper error handling for initialization failures
|
|
||||||
|
|
||||||
4. **Benefits of This Approach**:
|
|
||||||
- Stripe is only initialized when actually needed (during checkout)
|
|
||||||
- Application startup is not dependent on Stripe service availability
|
|
||||||
- Payment-related issues are isolated and don't affect other application features
|
|
||||||
- More efficient resource usage (Stripe library only fully loaded during checkout)
|
|
||||||
|
|
||||||
5. **Verification**:
|
|
||||||
- Created test scripts to verify the lazy initialization approach
|
|
||||||
- Confirmed that Stripe is not initialized at startup but can be initialized during checkout
|
|
||||||
|
|
||||||
## Code Changes
|
|
||||||
|
|
||||||
### config/initializers/stripe.rb
|
|
||||||
- Removed automatic Stripe.api_key initialization
|
|
||||||
- Added informational log message
|
|
||||||
|
|
||||||
### app/controllers/concerns/stripe_concern.rb
|
|
||||||
- Created new concern with `stripe_configured?` and `initialize_stripe` methods
|
|
||||||
|
|
||||||
### app/controllers/events_controller.rb
|
|
||||||
- Added direct Stripe configuration checks in `process_payment` method
|
|
||||||
- Added direct Stripe configuration checks in `payment_success` method
|
|
||||||
- Added comprehensive logging for debugging
|
|
||||||
|
|
||||||
### app/helpers/stripe_helper.rb
|
|
||||||
- Kept `safe_stripe_call` method with updated logic
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
The new approach has been verified to work correctly:
|
|
||||||
- Stripe is not initialized at application startup
|
|
||||||
- Stripe is properly initialized during the checkout process
|
|
||||||
- All Stripe functionality works as expected
|
|
||||||
- Error handling is improved
|
|
||||||
|
|
||||||
This approach provides better isolation of payment functionality and ensures that issues with Stripe configuration don't affect the rest of the application.
|
|
||||||
@@ -254,4 +254,3 @@ class TicketTest < ActiveSupport::TestCase
|
|||||||
assert ticket.save
|
assert ticket.save
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user