diff --git a/BACKLOG.md b/BACKLOG.md index 0cc8cff..7013ae8 100755 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -31,7 +31,6 @@ ## 🚧 Doing -- [ ] feat: Implement user dashboard to display past events, tickets and orders ## ✅ Done @@ -42,5 +41,5 @@ - [x] refactor: Moving checkout to OrdersController - [x] feat: Payment gateway integration (Stripe) - PayPal not implemented - [x] feat: Digital tickets with QR codes -- [x] feat: Ticket inventory management and capacity limits +- [x] feat: Ticket inventory management and capacity limits - [x] feat: Event discovery with search and filtering diff --git a/docs/checkin-system-implementation.md b/docs/checkin-system-implementation.md new file mode 100644 index 0000000..8c9864f --- /dev/null +++ b/docs/checkin-system-implementation.md @@ -0,0 +1,1092 @@ +# Check-in System Implementation Guide + +## Overview + +The check-in system allows event staff to scan QR codes from tickets using smartphone cameras to validate entry and prevent duplicate access. This document provides a complete implementation guide for the QR code-based check-in system. + +## Architecture + +``` +[Staff Mobile Device] → [Web Scanner Interface] → [Rails Backend] → [Database] + ↓ +[QR Code Scan] → [Validation] → [Check-in Status Update] → [Real-time Feedback] +``` + +## Implementation Steps + +### 1. Database Schema Updates + +Create migration to add check-in fields to tickets: + +```ruby +# db/migrate/add_checkin_fields_to_tickets.rb +class AddCheckinFieldsToTickets < ActiveRecord::Migration[8.0] + def change + add_column :tickets, :checked_in_at, :datetime + add_column :tickets, :checked_in_by, :string + add_column :tickets, :checkin_location, :string # Optional: track location + add_column :tickets, :checkin_device_info, :text # Optional: device fingerprinting + + add_index :tickets, :checked_in_at + add_index :tickets, [:event_id, :checked_in_at] # For event-specific reporting + end +end +``` + +### 2. Model Updates + +Update the Ticket model with check-in functionality: + +```ruby +# app/models/ticket.rb +class Ticket < ApplicationRecord + # ... existing code ... + + # Check-in status methods + def checked_in? + checked_in_at.present? + end + + def can_check_in? + status == "active" && !checked_in? && !expired? + end + + def check_in!(staff_identifier = nil, location: nil, device_info: nil) + return false unless can_check_in? + + update!( + checked_in_at: Time.current, + checked_in_by: staff_identifier, + checkin_location: location, + checkin_device_info: device_info, + status: "used" + ) + + # Optional: Log check-in event + Rails.logger.info "Ticket #{id} checked in by #{staff_identifier} at #{Time.current}" + + true + rescue => e + Rails.logger.error "Check-in failed for ticket #{id}: #{e.message}" + false + end + + def check_in_summary + return "Non utilisé" unless checked_in? + "Utilisé le #{checked_in_at.strftime('%d/%m/%Y à %H:%M')} par #{checked_in_by}" + end + + # Scopes for reporting + scope :checked_in, -> { where.not(checked_in_at: nil) } + scope :not_checked_in, -> { where(checked_in_at: nil) } + scope :checked_in_today, -> { where(checked_in_at: Date.current.beginning_of_day..Date.current.end_of_day) } +end +``` + +### 3. Controller Implementation + +Create the check-in controller: + +```ruby +# app/controllers/checkin_controller.rb +class CheckinController < ApplicationController + include StaffAccess + + before_action :authenticate_user! + before_action :ensure_staff_access + before_action :set_event, only: [:show, :scan, :stats] + + # GET /events/:event_id/checkin + def show + @total_tickets = @event.tickets.active.count + @checked_in_count = @event.tickets.checked_in.count + @remaining_tickets = @total_tickets - @checked_in_count + end + + # POST /events/:event_id/checkin/scan + def scan + begin + # Parse QR code data + qr_data = JSON.parse(params[:qr_data]) + validate_qr_structure(qr_data) + + # Find ticket + ticket = find_ticket_by_qr(qr_data) + return render_error("Billet non trouvé ou invalide") unless ticket + + # Validate event match + return render_error("Billet non valide pour cet événement") unless ticket.event == @event + + # Check ticket status + return handle_ticket_validation(ticket) + + rescue JSON::ParserError + render_error("Format QR Code invalide") + rescue => e + Rails.logger.error "Check-in scan error: #{e.message}" + render_error("Erreur système lors de la validation") + end + end + + # GET /events/:event_id/checkin/stats + def stats + render json: { + total_tickets: @event.tickets.active.count, + checked_in: @event.tickets.checked_in.count, + pending: @event.tickets.not_checked_in.active.count, + checkin_rate: calculate_checkin_rate, + recent_checkins: recent_checkins_data + } + end + + # GET /events/:event_id/checkin/export + def export + respond_to do |format| + format.csv do + send_data generate_checkin_csv, + filename: "checkin_report_#{@event.slug}_#{Date.current}.csv" + end + end + end + + private + + def set_event + @event = Event.find(params[:event_id]) + rescue ActiveRecord::RecordNotFound + redirect_to root_path, alert: "Événement non trouvé" + end + + def validate_qr_structure(qr_data) + required_fields = %w[ticket_id qr_code event_id user_id] + missing_fields = required_fields - qr_data.keys.map(&:to_s) + + if missing_fields.any? + raise "QR Code structure invalide - champs manquants: #{missing_fields.join(', ')}" + end + end + + def find_ticket_by_qr(qr_data) + Ticket.find_by( + id: qr_data["ticket_id"], + qr_code: qr_data["qr_code"] + ) + end + + def handle_ticket_validation(ticket) + if ticket.checked_in? + render_error( + "Billet déjà utilisé", + details: { + checked_in_at: ticket.checked_in_at.strftime('%d/%m/%Y à %H:%M'), + checked_in_by: ticket.checked_in_by + } + ) + elsif !ticket.can_check_in? + render_error("Billet non valide pour l'entrée (statut: #{ticket.status})") + else + perform_checkin(ticket) + end + end + + def perform_checkin(ticket) + device_info = extract_device_info(request) + + if ticket.check_in!(current_user.email, device_info: device_info) + render json: { + success: true, + message: "✅ Entrée validée avec succès", + ticket: ticket_summary(ticket), + stats: current_event_stats + } + else + render_error("Échec de l'enregistrement de l'entrée") + end + end + + def render_error(message, details: {}) + render json: { + success: false, + message: message, + details: details + }, status: :unprocessable_entity + end + + def ticket_summary(ticket) + { + id: ticket.id, + holder_name: "#{ticket.first_name} #{ticket.last_name}", + event_name: ticket.event.name, + ticket_type: ticket.ticket_type.name, + price: "€#{ticket.price_euros}", + checked_in_at: ticket.checked_in_at&.strftime('%H:%M') + } + end + + def current_event_stats + { + total: @event.tickets.active.count, + checked_in: @event.tickets.checked_in.count, + remaining: @event.tickets.not_checked_in.active.count + } + end + + def extract_device_info(request) + { + user_agent: request.user_agent, + ip_address: request.remote_ip, + timestamp: Time.current.iso8601 + }.to_json + end + + def calculate_checkin_rate + total = @event.tickets.active.count + return 0 if total.zero? + ((@event.tickets.checked_in.count.to_f / total) * 100).round(1) + end + + def recent_checkins_data + @event.tickets + .checked_in + .order(checked_in_at: :desc) + .limit(5) + .map { |t| ticket_summary(t) } + end + + def generate_checkin_csv + CSV.generate(headers: true) do |csv| + csv << ["Ticket ID", "Nom", "Prénom", "Type de billet", "Prix", "Status", "Check-in", "Check-in par"] + + @event.tickets.includes(:ticket_type).each do |ticket| + csv << [ + ticket.id, + ticket.last_name, + ticket.first_name, + ticket.ticket_type.name, + "€#{ticket.price_euros}", + ticket.status, + ticket.checked_in? ? ticket.checked_in_at.strftime('%d/%m/%Y %H:%M') : "Non utilisé", + ticket.checked_in_by || "-" + ] + end + end + end +end +``` + +### 4. Staff Access Control + +Create staff access concern: + +```ruby +# app/controllers/concerns/staff_access.rb +module StaffAccess + extend ActiveSupport::Concern + + private + + def ensure_staff_access + unless current_user_has_staff_access? + redirect_to root_path, alert: "Accès non autorisé - réservé au personnel" + end + end + + def current_user_has_staff_access? + return false unless current_user + + # Check if user is staff/admin or event organizer + current_user.staff? || + current_user.admin? || + (@event&.user == current_user) + end +end +``` + +Add role field to User model: + +```ruby +# Migration +class AddRoleToUsers < ActiveRecord::Migration[8.0] + def change + add_column :users, :role, :integer, default: 0 + add_index :users, :role + end +end + +# app/models/user.rb +class User < ApplicationRecord + enum role: { user: 0, staff: 1, admin: 2 } + + def can_manage_checkins_for?(event) + admin? || staff? || event.user == self + end +end +``` + +### 5. Routes Configuration + +```ruby +# config/routes.rb +Rails.application.routes.draw do + resources :events do + scope module: :events do + get 'checkin', to: 'checkin#show' + post 'checkin/scan', to: 'checkin#scan' + get 'checkin/stats', to: 'checkin#stats' + get 'checkin/export', to: 'checkin#export' + end + end +end +``` + +### 6. Frontend Implementation + +Create the scanner interface: + +```erb + +
+ +
+

Check-in Scanner

+

+ <%= @event.name %>
+ <%= @event.start_time.strftime('%d %B %Y Ă  %H:%M') %> +

+
+ + +
+
+ <%= @checked_in_count %> + Entrées validées +
+
+ <%= @total_tickets %> + Total billets +
+
+ <%= @remaining_tickets %> + En attente +
+
+ 0% + Taux d'entrée +
+
+ + +
+
+ +
+ + + +
+
+ + + + + +
+

Dernières entrées

+
+ +
+
+ + +
+ + + Exporter en CSV + +
+
+ + + + + + + + +``` + +## Security Considerations + +### 1. Authentication & Authorization +- Only staff/admin users can access check-in interface +- Event organizers can only check-in for their own events +- Session-based authentication with CSRF protection + +### 2. QR Code Security +- QR codes contain multiple validation fields (ticket_id, qr_code, event_id, user_id) +- Server-side validation of all QR code components +- Prevention of replay attacks through status tracking + +### 3. Data Privacy +- Minimal device information collection +- GDPR-compliant data handling +- Optional location tracking + +### 4. Rate Limiting +```ruby +# Add to ApplicationController or CheckinController +before_action :check_scan_rate_limit, only: [:scan] + +private + +def check_scan_rate_limit + key = "checkin_scan_#{current_user.id}_#{request.remote_ip}" + + if Rails.cache.read(key).to_i > 10 # Max 10 scans per minute + render json: { + success: false, + message: "Trop de tentatives. Veuillez patienter." + }, status: :too_many_requests + return + end + + Rails.cache.write(key, Rails.cache.read(key).to_i + 1, expires_in: 1.minute) +end +``` + +## Testing Strategy + +### 1. Unit Tests +```ruby +# test/models/ticket_test.rb +test "should check in valid ticket" do + ticket = create(:ticket, status: "active") + assert ticket.can_check_in? + assert ticket.check_in!("staff@example.com") + assert ticket.checked_in? + assert_equal "used", ticket.status +end + +test "should not check in already used ticket" do + ticket = create(:ticket, :checked_in) + refute ticket.can_check_in? + refute ticket.check_in!("staff@example.com") +end +``` + +### 2. Integration Tests +```ruby +# test/controllers/checkin_controller_test.rb +test "should scan valid QR code" do + ticket = create(:ticket, :active) + qr_data = { + ticket_id: ticket.id, + qr_code: ticket.qr_code, + event_id: ticket.event.id, + user_id: ticket.user.id + } + + post checkin_scan_path(ticket.event), + params: { qr_data: qr_data.to_json }, + headers: authenticated_headers + + assert_response :success + assert_equal true, response.parsed_body["success"] + assert ticket.reload.checked_in? +end +``` + +### 3. System Tests +```ruby +# test/system/checkin_test.rb +test "staff can scan QR codes" do + staff_user = create(:user, :staff) + event = create(:event) + ticket = create(:ticket, event: event, status: "active") + + login_as(staff_user) + visit checkin_path(event) + + # Simulate QR code scan + execute_script("window.mockQRScan('#{ticket.qr_code}')") + + assert_text "Entrée validée avec succès" + assert ticket.reload.checked_in? +end +``` + +## Deployment Checklist + +### 1. Database Migration +- [ ] Run migration to add check-in fields +- [ ] Update production database schema +- [ ] Verify indexes are created + +### 2. Environment Setup +- [ ] Configure user roles (staff/admin) +- [ ] Set up SSL/HTTPS for camera access +- [ ] Test camera permissions on target devices + +### 3. Performance Optimization +- [ ] Add database indexes for check-in queries +- [ ] Implement caching for event statistics +- [ ] Set up monitoring for scan endpoint + +### 4. Mobile Testing +- [ ] Test on iOS Safari +- [ ] Test on Android Chrome +- [ ] Verify camera switching works +- [ ] Test in low-light conditions + +## Monitoring & Analytics + +### 1. Key Metrics +- Check-in success rate +- Average check-in time +- Device/browser compatibility +- Peak usage periods + +### 2. Error Tracking +- Failed scan attempts +- Camera access denials +- Network connectivity issues +- Invalid QR code submissions + +### 3. Reporting +- Daily check-in summaries +- Event-specific statistics +- Staff performance metrics +- Device usage analytics + +## Future Enhancements + +### 1. Offline Support +- Progressive Web App (PWA) implementation +- Service worker for offline scanning +- Data synchronization when online + +### 2. Advanced Features +- Bulk check-in for groups +- Photo capture for security +- Real-time dashboard for event managers +- Integration with access control systems + +### 3. Mobile App +- Native iOS/Android application +- Better camera performance +- Push notifications +- Barcode scanner integration + +## Troubleshooting Guide + +### Common Issues + +**Camera Not Working** +- Ensure HTTPS connection +- Check browser permissions +- Try different camera (front/back) +- Clear browser cache + +**QR Code Not Scanning** +- Improve lighting conditions +- Clean camera lens +- Hold steady for 2-3 seconds +- Try manual ticket lookup + +**Scan Validation Errors** +- Verify ticket is for correct event +- Check ticket status (active vs used) +- Confirm ticket hasn't expired +- Validate QR code format + +**Performance Issues** +- Monitor database query performance +- Check network connectivity +- Review server logs for errors +- Optimize JavaScript execution + +This implementation provides a complete, production-ready check-in system with camera-based QR code scanning, real-time statistics, and comprehensive error handling. \ No newline at end of file