vol1ura/Sat_9am_5km

View on GitHub
app/jobs/athletes_awarding_job.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
96%
# frozen_string_literal: true

class AthletesAwardingJob < ApplicationJob
  queue_as :default

  ALL_SECONDS = 60.times.to_a.freeze

  def perform(activity_id)
    @activity = Activity.published.find activity_id

    [true, false].each { |male| process_event_records(male:) }

    @activity.athletes.find_each do |athlete|
      award_athlete(athlete, badge_type: 'result')
    end

    @activity.volunteers.includes(:athlete).find_each do |volunteer|
      award_athlete(volunteer.athlete, badge_type: 'volunteer')
    end

    award_by_jubilee_participating_kind_badges
  end

  private

  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  def process_event_records(male:)
    best_result = @activity.results.joins(:athlete).where(athlete: { male: }).order(:position).first
    return unless best_result

    record_badge = Badge.record_kind.find_by("(info->'male')::boolean = ?", male)
    trophies = Trophy.where(badge: record_badge).where("info @@ '$.data[*].event_id == #{event_id}'")
    award_by_record_badge!(record_badge, best_result) and return if trophies.empty?

    award_best_result = false
    Trophy.transaction do
      trophies.each do |trophy|
        record_data = trophy.data.find { |d| d['event_id'] == event_id }
        unless (record_result = Result.find_by(id: record_data['result_id']))
          record_result =
            Result
              .published
              .joins(:athlete)
              .where(activity: { event_id: }, athlete: { male: })
              .order(total_time: :asc, date: :desc, position: :asc)
              .first
          trophy.update!(athlete_id: record_result.athlete_id)
          award_by_record_badge!(record_badge, record_result) and next
        end
        next if best_result.total_time > record_result.total_time || best_result.date < record_result.date

        award_best_result ||= true
        trophy.data.delete(record_data)
        if best_result.athlete_id == record_result.athlete_id
          trophy.data << { event_id: event_id, result_id: best_result.id }
          trophy.save! and next
        end
        trophy.destroy and next if trophy.data.empty?

        trophy.save!
      end
    end
    return if !award_best_result || trophies.exists?(athlete_id: best_result.athlete_id)

    award_by_record_badge!(record_badge, best_result)
  rescue StandardError => e
    Rollbar.error e, activity_id: @activity.id
  end
  # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

  def award_athlete(athlete, badge_type:)
    dataset =
      athlete
        .send(Badge::ASSOCIATION_TYPE_MAPPING[badge_type])
        .published
        .where(activity: { date: ..activity_date })

    threshold_awarding!(athlete, kind: :participating, type: badge_type, value: dataset.size)

    home_dataset = dataset.where(activity: { event: athlete.event })
    threshold_awarding!(athlete, kind: :home_participating, type: badge_type, value: home_dataset.size)

    events_count = dataset.joins(activity: :event).select('events.id').distinct.count
    threshold_awarding!(athlete, kind: :tourist, type: badge_type, value: events_count)

    if badge_type == 'result'
      rage_badge_awarding!(athlete)
      minute_bingo_awarding!(athlete)
    end
  rescue StandardError => e
    Rollbar.error e, activity_id: @activity.id, athlete_id: athlete.id
  end

  def activity_date
    @activity_date ||= @activity.date
  end

  def event_id
    @event_id ||= @activity.event_id
  end

  def rage_badge
    @rage_badge ||= Badge.rage_kind.sole
  end

  def minute_bingo_badge
    @minute_bingo_badge ||= Badge.minute_bingo_kind.sole
  end

  def award_by_record_badge!(badge, result)
    trophy = Trophy.find_or_initialize_by(badge: badge, athlete_id: result.athlete_id)
    trophy.info = { data: [] } unless trophy.data
    trophy.data.delete_if { |d| d['event_id'] == event_id }
    trophy.data << { event_id: event_id, result_id: result.id }
    trophy.save!
  end

  def rage_badge_awarding!(athlete)
    if athlete.award_by_rage_badge?
      athlete.trophies.create!(badge: rage_badge, date: activity_date) unless athlete.trophies.exists?(badge: rage_badge)
    else
      athlete.trophies.where(badge: rage_badge).destroy_all
    end
  end

  def minute_bingo_awarding!(athlete)
    return if athlete.trophies.exists?(badge: minute_bingo_badge)

    tt_sec = @activity.results.where(athlete_id: athlete.id).pick(:total_time).sec
    athlete_seconds = athlete.stats.dig('results', 'seconds') || []
    if athlete_seconds.exclude?(tt_sec)
      athlete.update!(stats: athlete.stats.deep_merge('results' => { 'seconds' => athlete_seconds.push(tt_sec).sort }))
    end
    athlete.trophies.create! badge: minute_bingo_badge, date: activity_date if (ALL_SECONDS - athlete_seconds).empty?
  end

  def threshold_awarding!(athlete, kind:, type:, value:)
    badges_dataset = Badge.dataset_of(kind:, type:).where("(info->'threshold')::integer <= ?", value)
    return unless (badge = badges_dataset.last)
    return if athlete.trophies.exists?(badge:)

    athlete.transaction do
      athlete.trophies.where(badge: badges_dataset.where.not(id: badge.id)).destroy_all
      athlete.trophies.create! badge: badge, date: activity_date
    end
  end

  def award_by_jubilee_participating_kind_badges
    return unless (badge = Badge.jubilee_participating_kind.find_by("(info->'threshold')::integer = ?", @activity.number))

    FunrunAwardingJob.perform_later @activity.id, badge.id
  end
end