# Event model representing nightlife events and events # Manages event details, location data, and publication state require "net/http" require "json" class Event < ApplicationRecord # Define states for Event lifecycle management # draft: Initial state when Event is being created # published: Event is visible to public and can be discovered # canceled: Event has been canceled by organizer # sold_out: Event has reached capacity and tickets are no longer available enum :state, { draft: 0, published: 1, canceled: 2, sold_out: 3 }, default: :draft # === Relations === belongs_to :user has_many :ticket_types has_many :tickets, through: :ticket_types has_many :orders # === Callbacks === before_validation :geocode_address, if: :should_geocode_address? # Validations for Event attributes # Basic information validates :name, presence: true, length: { minimum: 3, maximum: 100 } validates :slug, presence: true, length: { minimum: 3, maximum: 100 } validates :description, presence: true, length: { minimum: 10, maximum: 2000 } validates :state, presence: true, inclusion: { in: states.keys } validates :image, length: { maximum: 500 } # URL or path to image # Venue information validates :venue_name, presence: true, length: { maximum: 100 } validates :venue_address, presence: true, length: { maximum: 200 } # Geographic coordinates for map display validates :latitude, presence: true, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 } validates :longitude, presence: true, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 } # Scopes for querying events with common filters scope :featured, -> { where(featured: true) } # Get featured events for homepage scope :published, -> { where(state: :published) } # Get publicly visible events scope :search_by_name, ->(query) { where("name ILIKE ?", "%#{query}%") } # Search by name (case-insensitive) # Scope for published events ordered by start time scope :upcoming, -> { published.where("start_time >= ?", Time.current).order(start_time: :asc) } # === Instance Methods === # Check if coordinates were successfully geocoded or are fallback coordinates def geocoding_successful? coordinates_look_valid? end # Get a user-friendly status message about geocoding def geocoding_status_message return nil if geocoding_successful? "Les coordonnées exactes n'ont pas pu être déterminées automatiquement. Une localisation approximative a été utilisée." end # Check if ticket booking is currently allowed for this event def booking_allowed? return false unless published? return false if sold_out? return false if canceled? # Check if event has started and if booking during event is disabled if event_started? && !allow_booking_during_event? return false end true end # Check if the event has already started def event_started? return false if start_time.blank? Time.current >= start_time end # Check if the event has ended def event_ended? return false if end_time.blank? Time.current >= end_time end private # Determine if we should perform server-side geocoding def should_geocode_address? # Don't geocode if address is blank return false if venue_address.blank? # Don't geocode if we already have valid coordinates (likely from frontend) return false if coordinates_look_valid? # Only geocode if address changed and we don't have coordinates venue_address_changed? end # Check if the current coordinates look like they were set by frontend geocoding def coordinates_look_valid? return false if latitude.blank? || longitude.blank? lat_f = latitude.to_f lng_f = longitude.to_f # Basic sanity checks for coordinate ranges return false if lat_f < -90 || lat_f > 90 return false if lng_f < -180 || lng_f > 180 # Check if coordinates are not the default fallback coordinates fallback_lat = 46.603354 fallback_lng = 1.888334 # Check if coordinates are not exactly 0,0 (common invalid default) return false if lat_f == 0.0 && lng_f == 0.0 # Coordinates are valid if they're not exactly the fallback coordinates !(lat_f == fallback_lat && lng_f == fallback_lng) end # Automatically geocode address to get latitude and longitude # This only runs when no valid coordinates are provided (fallback for non-JS users) def geocode_address Rails.logger.info "Running server-side geocoding for '#{venue_address}' (no frontend coordinates provided)" # Store original coordinates in case we need to fall back original_lat = latitude original_lng = longitude begin # Use OpenStreetMap Nominatim API for geocoding encoded_address = URI.encode_www_form_component(venue_address.strip) uri = URI("https://nominatim.openstreetmap.org/search?q=#{encoded_address}&format=json&limit=1&addressdetails=1") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true request = Net::HTTP::Get.new(uri) request['User-Agent'] = 'AperoNight Event Platform/1.0 (https://aperonight.com)' request['Accept'] = 'application/json' response = http.request(request) if response.code == "200" data = JSON.parse(response.body) if data.any? result = data.first self.latitude = result["lat"].to_f.round(6) self.longitude = result["lon"].to_f.round(6) Rails.logger.info "Server-side geocoded '#{venue_address}' to coordinates: #{latitude}, #{longitude}" return end end # If we reach here, geocoding failed handle_geocoding_failure(original_lat, original_lng) rescue => e Rails.logger.error "Server-side geocoding failed for '#{venue_address}': #{e.message}" handle_geocoding_failure(original_lat, original_lng) end end # Handle geocoding failure with fallback strategies def handle_geocoding_failure(original_lat, original_lng) # Strategy 1: Keep existing coordinates if this is an update if original_lat.present? && original_lng.present? self.latitude = original_lat self.longitude = original_lng Rails.logger.warn "Geocoding failed for '#{venue_address}', keeping existing coordinates: #{latitude}, #{longitude}" return end # Strategy 2: Try to extract country/city and use approximate coordinates fallback_coordinates = get_fallback_coordinates_from_address if fallback_coordinates self.latitude = fallback_coordinates[:lat] self.longitude = fallback_coordinates[:lng] Rails.logger.warn "Using fallback coordinates for '#{venue_address}': #{latitude}, #{longitude}" return end # Strategy 3: Use default coordinates (center of France) as last resort # This ensures the event can still be created self.latitude = 46.603354 # Center of France self.longitude = 1.888334 Rails.logger.warn "Using default coordinates for '#{venue_address}' due to geocoding failure: #{latitude}, #{longitude}" end # Extract country/city from address and return approximate coordinates def get_fallback_coordinates_from_address address_lower = venue_address.downcase # Common French cities with approximate coordinates french_cities = { "paris" => { lat: 48.8566, lng: 2.3522 }, "lyon" => { lat: 45.7640, lng: 4.8357 }, "marseille" => { lat: 43.2965, lng: 5.3698 }, "toulouse" => { lat: 43.6047, lng: 1.4442 }, "nice" => { lat: 43.7102, lng: 7.2620 }, "nantes" => { lat: 47.2184, lng: -1.5536 }, "montpellier" => { lat: 43.6110, lng: 3.8767 }, "strasbourg" => { lat: 48.5734, lng: 7.7521 }, "bordeaux" => { lat: 44.8378, lng: -0.5792 }, "lille" => { lat: 50.6292, lng: 3.0573 } } # Check if any known city is mentioned in the address french_cities.each do |city, coords| if address_lower.include?(city) return coords end end # Check for common country indicators if address_lower.include?("france") || address_lower.include?("french") return { lat: 46.603354, lng: 1.888334 } # Center of France end nil end end