# 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.