Files
aperonight/app/models/event.rb
kbe 8103629370 fix: add missing total_earnings_cents method and payout method to Event model
- Add alias total_earnings_cents for total_gross_cents for template compatibility
- Add payout method to get the latest payout for an event
- Fixes NoMethodError in promoter events earnings preview template

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 12:01:59 +02:00

311 lines
10 KiB
Ruby
Executable File

# 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
enum :payout_status, {
pending_request: 0,
requested: 1,
processing: 2,
completed: 3,
failed: 4
}, default: :pending_request
# === Relations ===
belongs_to :user
has_many :ticket_types
has_many :tickets, through: :ticket_types
has_many :orders
has_many :earnings, dependent: :destroy
has_many :payouts, dependent: :destroy
# === Callbacks ===
before_validation :geocode_address, if: :should_geocode_address?
# === Validations ===
# 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) }
# Scope for events eligible for payout
scope :eligible_for_payout, -> { where("end_time <= ?", Time.current).joins(:earnings).group("events.id").having("SUM(earnings.amount_cents) > 0") }
# === Instance Methods ===
# Payout methods
def total_gross_cents
tickets.active.sum(:price_cents)
end
# Alias for template compatibility
alias_method :total_earnings_cents, :total_gross_cents
def total_fees_cents
earnings.pending.sum(:fee_cents)
end
def net_earnings_cents
total_gross_cents - total_fees_cents
end
def can_request_payout?(user = self.user)
event_ended? && (net_earnings_cents > 0) && user.is_professionnal? && payouts.pending.empty?
end
# Get the latest payout for this event
def payout
payouts.order(created_at: :desc).first
end
# 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
# Check if booking is allowed during the event
# This is a simple attribute reader that defaults to false if nil
def allow_booking_during_event?
!!allow_booking_during_event
end
# Duplicate an event with all its ticket types
def duplicate(clone_ticket_types: true)
# Duplicate the event
new_event = self.dup
new_event.name = "Copie de #{name}"
new_event.slug = "#{slug}-copy-#{Time.current.to_i}"
new_event.state = :draft
new_event.created_at = Time.current
new_event.updated_at = Time.current
Event.transaction do
if new_event.save
# Duplicate all ticket types if requested
if clone_ticket_types
ticket_types.each do |ticket_type|
new_ticket_type = ticket_type.dup
new_ticket_type.event = new_event
new_ticket_type.save!
end
end
new_event
else
nil
end
end
rescue
nil
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