require "prawn" require "prawn/qrcode" require "rqrcode" # PDF ticket generator service using Prawn # # Generates PDF tickets with QR codes for event entry validation # Includes event details, venue information, and unique QR code for each ticket class TicketPdfGenerator # Suppress Prawn's internationalization warning for built-in fonts Prawn::Fonts::AFM.hide_m17n_warning = true attr_reader :ticket def initialize(ticket) @ticket = ticket end def generate Prawn::Document.new(page_size: [400, 650], margin: 0) do |pdf| # Main container with modern gradient background create_background(pdf) # Header section with brand and visual hierarchy create_header(pdf) # Event information card create_event_card(pdf) # Ticket holder information create_holder_section(pdf) # QR Code section with modern styling create_qr_section(pdf) # Footer with security elements create_footer(pdf) end.render end private def create_background(pdf) # Gradient background effect pdf.fill_color "F8FAFC" pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, pdf.bounds.height # Top decorative band pdf.fill_color "6366F1" pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 120 # Subtle gradient effect pdf.fill_color "8B5CF6" pdf.fill_rectangle [0, pdf.bounds.height], pdf.bounds.width, 80 end def create_header(pdf) pdf.move_cursor_to(pdf.bounds.height - 30) # ApéroNight logo/brand pdf.fill_color "FFFFFF" pdf.font "Helvetica", style: :bold, size: 32 pdf.text "AperoNight", align: :center pdf.move_down 8 pdf.font "Helvetica", size: 12 pdf.fill_color "E2E8F0" pdf.text "EVENEMENT TICKET", align: :center, character_spacing: 2 end def create_event_card(pdf) pdf.move_cursor_to(480) # Main event card with shadow effect card_y = pdf.cursor # Shadow effect pdf.fill_color "E2E8F0" pdf.rounded_rectangle [22, card_y - 2], 356, 152, 15 pdf.fill # Main card pdf.fill_color "FFFFFF" pdf.stroke_color "E5E7EB" pdf.line_width 1 pdf.rounded_rectangle [20, card_y], 360, 150, 15 pdf.fill_and_stroke # Event name with accent pdf.bounding_box([40, card_y - 20], width: 320, height: 110) do pdf.fill_color "1F2937" pdf.font "Helvetica", style: :bold, size: 20 pdf.text ticket.event.name, align: :center pdf.move_down 15 # Event details grid create_event_details_grid(pdf) end end def create_event_details_grid(pdf) details = [ { label: "DATE", value: ticket.event.start_time.strftime("%d %B %Y"), icon: "[CAL]" }, { label: "HEURE", value: ticket.event.start_time.strftime("%H:%M"), icon: "[TIME]" }, { label: "LIEU", value: ticket.event.venue_name, icon: "[LOC]" }, { label: "TYPE", value: ticket.ticket_type.name, icon: "[TICK]" } ] pdf.font "Helvetica", size: 10 details.each_slice(2).with_index do |row, row_index| y_offset = row_index * 35 row.each_with_index do |detail, col_index| x_offset = col_index * 160 pdf.bounding_box([x_offset, pdf.cursor - y_offset], width: 150, height: 30) do # Icon and label pdf.fill_color "6B7280" pdf.font "Helvetica", style: :bold, size: 8 pdf.text "#{detail[:icon]} #{detail[:label]}", character_spacing: 1 pdf.move_down 3 # Value pdf.fill_color "1F2937" pdf.font "Helvetica", style: :bold, size: 11 pdf.text detail[:value] end end end end def create_holder_section(pdf) pdf.move_cursor_to(280) # Ticket holder section pdf.bounding_box([20, pdf.cursor], width: 360, height: 60) do # Section header with accent line pdf.fill_color "6366F1" pdf.fill_rectangle [0, pdf.cursor], 60, 2 pdf.move_down 10 pdf.fill_color "1F2937" pdf.font "Helvetica", style: :bold, size: 12 pdf.text "DETENTEUR DU BILLET", character_spacing: 1 pdf.move_down 8 # Holder name with elegant styling pdf.font "Helvetica", style: :bold, size: 18 pdf.fill_color "374151" pdf.text "#{ticket.first_name.upcase} #{ticket.last_name.upcase}" pdf.move_down 5 # Price badge create_price_badge(pdf) end end def create_price_badge(pdf) price_text = "€#{sprintf('%.2f', ticket.price_euros)}" # Price badge background pdf.fill_color "10B981" pdf.rounded_rectangle [0, pdf.cursor], 80, 25, 12 pdf.fill # Price text pdf.fill_color "FFFFFF" pdf.font "Helvetica", style: :bold, size: 12 pdf.text_box price_text, at: [0, pdf.cursor], width: 80, height: 25, align: :center, valign: :center end def create_qr_section(pdf) pdf.move_cursor_to(190) # QR Code section with modern card design pdf.bounding_box([20, pdf.cursor], width: 360, height: 140) do # QR background card pdf.fill_color "F1F5F9" pdf.stroke_color "E2E8F0" pdf.rounded_rectangle [0, pdf.cursor], 360, 130, 15 pdf.fill_and_stroke # QR Code title pdf.move_down 15 pdf.fill_color "475569" pdf.font "Helvetica", style: :bold, size: 12 pdf.text "CODE D'ENTREE", align: :center, character_spacing: 2 pdf.move_down 10 # Generate and place QR code generate_qr_code(pdf) pdf.move_down 10 # QR code ID pdf.font "Helvetica", size: 8 pdf.fill_color "64748B" pdf.text "ID: #{ticket.qr_code[0..11]}...", align: :center, character_spacing: 0.5 end end def generate_qr_code(pdf) # Ensure all required data is present before generating QR code if ticket.qr_code.blank? raise "Ticket QR code is missing" end # Build QR code data with safe association loading qr_code_data = build_qr_code_data(ticket) # Validate QR code data before creating QR code if qr_code_data.blank? || qr_code_data == "{}" Rails.logger.error "QR code data is empty: ticket_id=#{ticket.id}, qr_code=#{ticket.qr_code}, event_id=#{ticket.ticket_type&.event_id}, user_id=#{ticket.order&.user_id}" raise "QR code data is empty or invalid" end # Ensure qr_code_data is a proper string for QR code generation unless qr_code_data.is_a?(String) && qr_code_data.length > 2 Rails.logger.error "QR code data is not a valid string: #{qr_code_data.inspect} (class: #{qr_code_data.class})" raise "QR code data must be a valid string" end # Create QR code with white background pdf.bounding_box([130, pdf.cursor], width: 100, height: 100) do pdf.fill_color "FFFFFF" pdf.rounded_rectangle [0, pdf.cursor], 100, 100, 8 pdf.fill # Generate QR code pdf.print_qr_code(qr_code_data, extent: 85, align: :center) end end def create_footer(pdf) pdf.move_cursor_to(40) # Security notice pdf.font "Helvetica", size: 8 pdf.fill_color "6B7280" pdf.text "[!] Ce billet est valable pour une seule entree", align: :center pdf.text "Presentez ce billet a l'entree de l'evenement", align: :center pdf.move_down 8 # Generation timestamp with modern styling pdf.font "Helvetica", size: 7 pdf.fill_color "9CA3AF" timestamp = "Genere le #{Time.current.strftime('%d/%m/%Y a %H:%M')}" pdf.text timestamp, align: :center, character_spacing: 0.3 end private def build_qr_code_data(ticket) # Try multiple approaches to get valid QR code data begin # Primary approach: full JSON with all data data = { ticket_id: ticket.id, qr_code: ticket.qr_code, event_id: ticket.ticket_type&.event_id, user_id: ticket.order&.user_id }.compact # Ensure we have the minimum required data if data[:ticket_id] && data[:qr_code] return data.to_json end rescue StandardError => e Rails.logger.warn "Failed to build complex QR data: #{e.message}" end # Fallback approach: just use the ticket's QR code string begin return ticket.qr_code.to_s if ticket.qr_code.present? rescue StandardError => e Rails.logger.warn "Failed to use ticket QR code: #{e.message}" end # Final fallback: simple ticket identifier "TICKET-#{ticket.id}" end end