diff --git a/app/controllers/shared/yearly_digests_controller.rb b/app/controllers/shared/digests_controller.rb similarity index 76% rename from app/controllers/shared/yearly_digests_controller.rb rename to app/controllers/shared/digests_controller.rb index 3f58d9fc..fe3855f9 100644 --- a/app/controllers/shared/yearly_digests_controller.rb +++ b/app/controllers/shared/digests_controller.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -class Shared::YearlyDigestsController < ApplicationController - helper YearlyDigestsHelper +class Shared::DigestsController < ApplicationController + helper Users::DigestsHelper before_action :authenticate_user!, except: [:show] before_action :authenticate_active_user!, only: [:update] def show - @digest = YearlyDigest.find_by(sharing_uuid: params[:uuid]) + @digest = Users::Digest.find_by(sharing_uuid: params[:uuid]) unless @digest&.public_accessible? return redirect_to root_path, @@ -19,18 +19,18 @@ class Shared::YearlyDigestsController < ApplicationController @distance_unit = @user.safe_settings.distance_unit || 'km' @is_public_view = true - render 'yearly_digests/public_year' + render 'users/digests/public_year' end def update @year = params[:year].to_i - @digest = current_user.yearly_digests.yearly.find_by(year: @year) + @digest = current_user.digests.yearly.find_by(year: @year) return head :not_found unless @digest if params[:enabled] == '1' @digest.enable_sharing!(expiration: params[:expiration] || '24h') - sharing_url = shared_yearly_digest_url(@digest.sharing_uuid) + sharing_url = shared_users_digest_url(@digest.sharing_uuid) render json: { success: true, diff --git a/app/controllers/yearly_digests_controller.rb b/app/controllers/users/digests_controller.rb similarity index 62% rename from app/controllers/yearly_digests_controller.rb rename to app/controllers/users/digests_controller.rb index e0112f81..a6f050b9 100644 --- a/app/controllers/yearly_digests_controller.rb +++ b/app/controllers/users/digests_controller.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -class YearlyDigestsController < ApplicationController - helper YearlyDigestsHelper +class Users::DigestsController < ApplicationController + helper Users::DigestsHelper before_action :authenticate_user! before_action :authenticate_active_user!, only: [:create] before_action :set_digest, only: [:show] def index - @digests = current_user.yearly_digests.yearly.order(year: :desc) + @digests = current_user.digests.yearly.order(year: :desc) @available_years = available_years_for_generation end @@ -20,26 +20,26 @@ class YearlyDigestsController < ApplicationController year = params[:year].to_i if valid_year?(year) - YearlyDigests::CalculatingJob.perform_later(current_user.id, year) - redirect_to yearly_digests_path, + Users::Digests::CalculatingJob.perform_later(current_user.id, year) + redirect_to users_digests_path, notice: "Year-end digest for #{year} is being generated. Check back soon!", status: :see_other else - redirect_to yearly_digests_path, alert: 'Invalid year selected', status: :see_other + redirect_to users_digests_path, alert: 'Invalid year selected', status: :see_other end end private def set_digest - @digest = current_user.yearly_digests.yearly.find_by!(year: params[:year]) + @digest = current_user.digests.yearly.find_by!(year: params[:year]) rescue ActiveRecord::RecordNotFound - redirect_to yearly_digests_path, alert: 'Digest not found' + redirect_to users_digests_path, alert: 'Digest not found' end def available_years_for_generation tracked_years = current_user.stats.select(:year).distinct.pluck(:year) - existing_digests = current_user.yearly_digests.yearly.pluck(:year) + existing_digests = current_user.digests.yearly.pluck(:year) (tracked_years - existing_digests).sort.reverse end diff --git a/app/helpers/users/digests_helper.rb b/app/helpers/users/digests_helper.rb new file mode 100644 index 00000000..a00b0abe --- /dev/null +++ b/app/helpers/users/digests_helper.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Users + module DigestsHelper + EARTH_CIRCUMFERENCE_KM = 40_075 + MOON_DISTANCE_KM = 384_400 + + def distance_with_unit(distance_meters, unit) + value = Users::Digest.convert_distance(distance_meters, unit).round + "#{number_with_delimiter(value)} #{unit}" + end + + def distance_comparison_text(distance_meters) + distance_km = distance_meters.to_f / 1000 + + if distance_km >= MOON_DISTANCE_KM + percentage = ((distance_km / MOON_DISTANCE_KM) * 100).round(1) + "That's #{percentage}% of the distance to the Moon!" + else + percentage = ((distance_km / EARTH_CIRCUMFERENCE_KM) * 100).round(1) + "That's #{percentage}% of Earth's circumference!" + end + end + + def format_time_spent(minutes) + return "#{minutes} minutes" if minutes < 60 + + hours = minutes / 60 + remaining_minutes = minutes % 60 + + if hours < 24 + "#{hours}h #{remaining_minutes}m" + else + days = hours / 24 + remaining_hours = hours % 24 + "#{days}d #{remaining_hours}h" + end + end + + def yoy_change_class(change) + return '' if change.nil? + + change.negative? ? 'negative' : 'positive' + end + + def yoy_change_text(change) + return '' if change.nil? + + prefix = change.positive? ? '+' : '' + "#{prefix}#{change}%" + end + end +end diff --git a/app/helpers/yearly_digests_helper.rb b/app/helpers/yearly_digests_helper.rb deleted file mode 100644 index 7c68beb2..00000000 --- a/app/helpers/yearly_digests_helper.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module YearlyDigestsHelper - EARTH_CIRCUMFERENCE_KM = 40_075 - MOON_DISTANCE_KM = 384_400 - - def distance_with_unit(distance_meters, unit) - value = YearlyDigest.convert_distance(distance_meters, unit).round - "#{number_with_delimiter(value)} #{unit}" - end - - def distance_comparison_text(distance_meters) - distance_km = distance_meters.to_f / 1000 - - if distance_km >= MOON_DISTANCE_KM - percentage = ((distance_km / MOON_DISTANCE_KM) * 100).round(1) - "That's #{percentage}% of the distance to the Moon!" - else - percentage = ((distance_km / EARTH_CIRCUMFERENCE_KM) * 100).round(1) - "That's #{percentage}% of Earth's circumference!" - end - end - - def format_time_spent(minutes) - return "#{minutes} minutes" if minutes < 60 - - hours = minutes / 60 - remaining_minutes = minutes % 60 - - if hours < 24 - "#{hours}h #{remaining_minutes}m" - else - days = hours / 24 - remaining_hours = hours % 24 - "#{days}d #{remaining_hours}h" - end - end - - def yoy_change_class(change) - return '' if change.nil? - - change.negative? ? 'negative' : 'positive' - end - - def yoy_change_text(change) - return '' if change.nil? - - prefix = change.positive? ? '+' : '' - "#{prefix}#{change}%" - end -end diff --git a/app/jobs/yearly_digests/calculating_job.rb b/app/jobs/users/digests/calculating_job.rb similarity index 83% rename from app/jobs/yearly_digests/calculating_job.rb rename to app/jobs/users/digests/calculating_job.rb index 798be33e..bf6f92c0 100644 --- a/app/jobs/yearly_digests/calculating_job.rb +++ b/app/jobs/users/digests/calculating_job.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -class YearlyDigests::CalculatingJob < ApplicationJob +class Users::Digests::CalculatingJob < ApplicationJob queue_as :digests def perform(user_id, year) - YearlyDigests::CalculateYear.new(user_id, year).call + Users::Digests::CalculateYear.new(user_id, year).call rescue StandardError => e create_digest_failed_notification(user_id, e) end diff --git a/app/jobs/yearly_digests/email_sending_job.rb b/app/jobs/users/digests/email_sending_job.rb similarity index 71% rename from app/jobs/yearly_digests/email_sending_job.rb rename to app/jobs/users/digests/email_sending_job.rb index 1119c4ab..03e67971 100644 --- a/app/jobs/yearly_digests/email_sending_job.rb +++ b/app/jobs/users/digests/email_sending_job.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true -class YearlyDigests::EmailSendingJob < ApplicationJob +class Users::Digests::EmailSendingJob < ApplicationJob queue_as :mailers def perform(user_id, year) user = User.find(user_id) - digest = user.yearly_digests.yearly.find_by(year: year) + digest = user.digests.yearly.find_by(year: year) return unless should_send_email?(user, digest) - YearlyDigestsMailer.with(user: user, digest: digest).year_end_digest.deliver_later + Users::DigestsMailer.with(user: user, digest: digest).year_end_digest.deliver_later digest.update!(sent_at: Time.current) rescue ActiveRecord::RecordNotFound ExceptionReporter.call( - 'YearlyDigests::EmailSendingJob', + 'Users::Digests::EmailSendingJob', "User with ID #{user_id} not found. Skipping year-end digest email." ) end diff --git a/app/jobs/yearly_digests/year_end_scheduling_job.rb b/app/jobs/users/digests/year_end_scheduling_job.rb similarity index 57% rename from app/jobs/yearly_digests/year_end_scheduling_job.rb rename to app/jobs/users/digests/year_end_scheduling_job.rb index eaeb83ab..7d673629 100644 --- a/app/jobs/yearly_digests/year_end_scheduling_job.rb +++ b/app/jobs/users/digests/year_end_scheduling_job.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true -class YearlyDigests::YearEndSchedulingJob < ApplicationJob +class Users::Digests::YearEndSchedulingJob < ApplicationJob queue_as :digests def perform year = Time.current.year - 1 # Previous year's digest - User.active_or_trial.find_each do |user| + ::User.active_or_trial.find_each do |user| # Skip if user has no data for the year next unless user.stats.where(year: year).exists? # Schedule calculation first - YearlyDigests::CalculatingJob.perform_later(user.id, year) + Users::Digests::CalculatingJob.perform_later(user.id, year) # Schedule email with delay to allow calculation to complete - YearlyDigests::EmailSendingJob.set(wait: 30.minutes).perform_later(user.id, year) + Users::Digests::EmailSendingJob.set(wait: 30.minutes).perform_later(user.id, year) end end end diff --git a/app/mailers/yearly_digests_mailer.rb b/app/mailers/users/digests_mailer.rb similarity index 79% rename from app/mailers/yearly_digests_mailer.rb rename to app/mailers/users/digests_mailer.rb index efcef1a6..0f5f1e00 100644 --- a/app/mailers/yearly_digests_mailer.rb +++ b/app/mailers/users/digests_mailer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -class YearlyDigestsMailer < ApplicationMailer - helper YearlyDigestsHelper +class Users::DigestsMailer < ApplicationMailer + helper Users::DigestsHelper def year_end_digest @user = params[:user] @@ -20,7 +20,7 @@ class YearlyDigestsMailer < ApplicationMailer private def generate_chart_attachment - image_data = YearlyDigests::ChartImageGenerator.new(@digest, distance_unit: @distance_unit).call + image_data = Users::Digests::ChartImageGenerator.new(@digest, distance_unit: @distance_unit).call filename = 'monthly_distance_chart.png' attachments.inline[filename] = { diff --git a/app/models/user.rb b/app/models/user.rb index 87081342..0b9250d8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -21,7 +21,7 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength has_many :trips, dependent: :destroy has_many :tracks, dependent: :destroy has_many :raw_data_archives, class_name: 'Points::RawDataArchive', dependent: :destroy - has_many :yearly_digests, dependent: :destroy + has_many :digests, class_name: 'Users::Digest', dependent: :destroy after_create :create_api_key after_commit :activate, on: :create, if: -> { DawarichSettings.self_hosted? } diff --git a/app/models/yearly_digest.rb b/app/models/users/digest.rb similarity index 98% rename from app/models/yearly_digest.rb rename to app/models/users/digest.rb index ce3c050a..e9adbd02 100644 --- a/app/models/yearly_digest.rb +++ b/app/models/users/digest.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class YearlyDigest < ApplicationRecord +class Users::Digest < ApplicationRecord self.table_name = 'digests' include DistanceConvertible diff --git a/app/services/users/digests/calculate_year.rb b/app/services/users/digests/calculate_year.rb new file mode 100644 index 00000000..eaf3be26 --- /dev/null +++ b/app/services/users/digests/calculate_year.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module Users + module Digests + class CalculateYear + def initialize(user_id, year) + @user = ::User.find(user_id) + @year = year.to_i + end + + def call + return nil if monthly_stats.empty? + + digest = Users::Digest.find_or_initialize_by(user: user, year: year, period_type: :yearly) + + digest.assign_attributes( + distance: total_distance, + toponyms: aggregate_toponyms, + monthly_distances: build_monthly_distances, + time_spent_by_location: calculate_time_spent, + first_time_visits: calculate_first_time_visits, + year_over_year: calculate_yoy_comparison, + all_time_stats: calculate_all_time_stats + ) + + digest.save! + digest + end + + private + + attr_reader :user, :year + + def monthly_stats + @monthly_stats ||= user.stats.where(year: year).order(:month) + end + + def total_distance + monthly_stats.sum(:distance) + end + + def aggregate_toponyms + countries = [] + cities = [] + + monthly_stats.each do |stat| + toponyms = stat.toponyms + next unless toponyms.is_a?(Array) + + toponyms.each do |toponym| + next unless toponym.is_a?(Hash) + + countries << toponym['country'] if toponym['country'].present? + + next unless toponym['cities'].is_a?(Array) + + toponym['cities'].each do |city| + cities << city['city'] if city.is_a?(Hash) && city['city'].present? + end + end + end + + { + 'countries' => countries.uniq.compact.sort, + 'cities' => cities.uniq.compact.sort + } + end + + def build_monthly_distances + result = {} + + monthly_stats.each do |stat| + result[stat.month.to_s] = stat.distance + end + + # Fill in missing months with 0 + (1..12).each do |month| + result[month.to_s] ||= 0 + end + + result + end + + def calculate_time_spent + country_time = Hash.new(0) + city_time = Hash.new(0) + + monthly_stats.each do |stat| + toponyms = stat.toponyms + next unless toponyms.is_a?(Array) + + toponyms.each do |toponym| + next unless toponym.is_a?(Hash) + + country = toponym['country'] + next unless toponym['cities'].is_a?(Array) + + toponym['cities'].each do |city| + next unless city.is_a?(Hash) + + stayed_for = city['stayed_for'].to_i + city_name = city['city'] + + country_time[country] += stayed_for if country.present? + city_time[city_name] += stayed_for if city_name.present? + end + end + end + + { + 'countries' => country_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } }, + 'cities' => city_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } } + } + end + + def calculate_first_time_visits + FirstTimeVisitsCalculator.new(user, year).call + end + + def calculate_yoy_comparison + YearOverYearCalculator.new(user, year).call + end + + def calculate_all_time_stats + { + 'total_countries' => user.countries_visited.count, + 'total_cities' => user.cities_visited.count, + 'total_distance' => user.stats.sum(:distance) + } + end + end + end +end diff --git a/app/services/users/digests/chart_image_generator.rb b/app/services/users/digests/chart_image_generator.rb new file mode 100644 index 00000000..cdb09ff9 --- /dev/null +++ b/app/services/users/digests/chart_image_generator.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Users + module Digests + class ChartImageGenerator + def initialize(digest, distance_unit: 'km') + @digest = digest + @distance_unit = distance_unit + end + + def call + html = render_chart_html + generate_image(html) + end + + private + + attr_reader :digest, :distance_unit + + def render_chart_html + ApplicationController.render( + template: 'user/digests/chart', + layout: false, + assigns: { + monthly_distances: digest.monthly_distances, + distance_unit: distance_unit + } + ) + end + + def generate_image(html) + grover = Grover.new( + html, + format: 'png', + viewport: { width: 600, height: 320 }, + wait_until: 'networkidle0' + ) + grover.to_png + end + end + end +end diff --git a/app/services/users/digests/first_time_visits_calculator.rb b/app/services/users/digests/first_time_visits_calculator.rb new file mode 100644 index 00000000..d7078b97 --- /dev/null +++ b/app/services/users/digests/first_time_visits_calculator.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Users + module Digests + class FirstTimeVisitsCalculator + def initialize(user, year) + @user = user + @year = year.to_i + end + + def call + { + 'countries' => first_time_countries, + 'cities' => first_time_cities + } + end + + private + + attr_reader :user, :year + + def previous_years_stats + @previous_years_stats ||= user.stats.where('year < ?', year) + end + + def current_year_stats + @current_year_stats ||= user.stats.where(year: year) + end + + def previous_countries + @previous_countries ||= extract_countries(previous_years_stats) + end + + def previous_cities + @previous_cities ||= extract_cities(previous_years_stats) + end + + def current_countries + @current_countries ||= extract_countries(current_year_stats) + end + + def current_cities + @current_cities ||= extract_cities(current_year_stats) + end + + def first_time_countries + (current_countries - previous_countries).sort + end + + def first_time_cities + (current_cities - previous_cities).sort + end + + def extract_countries(stats) + stats.flat_map do |stat| + toponyms = stat.toponyms + next [] unless toponyms.is_a?(Array) + + toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) } + end.uniq.compact + end + + def extract_cities(stats) + stats.flat_map do |stat| + toponyms = stat.toponyms + next [] unless toponyms.is_a?(Array) + + toponyms.flat_map do |t| + next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array) + + t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) } + end + end.uniq.compact + end + end + end +end diff --git a/app/services/users/digests/year_over_year_calculator.rb b/app/services/users/digests/year_over_year_calculator.rb new file mode 100644 index 00000000..50783eb6 --- /dev/null +++ b/app/services/users/digests/year_over_year_calculator.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Users + module Digests + class YearOverYearCalculator + def initialize(user, year) + @user = user + @year = year.to_i + end + + def call + return {} unless previous_year_stats.exists? + + { + 'previous_year' => year - 1, + 'distance_change_percent' => calculate_distance_change_percent, + 'countries_change' => calculate_countries_change, + 'cities_change' => calculate_cities_change + }.compact + end + + private + + attr_reader :user, :year + + def previous_year_stats + @previous_year_stats ||= user.stats.where(year: year - 1) + end + + def current_year_stats + @current_year_stats ||= user.stats.where(year: year) + end + + def calculate_distance_change_percent + prev_distance = previous_year_stats.sum(:distance) + return nil if prev_distance.zero? + + curr_distance = current_year_stats.sum(:distance) + ((curr_distance - prev_distance).to_f / prev_distance * 100).round + end + + def calculate_countries_change + prev_count = count_countries(previous_year_stats) + curr_count = count_countries(current_year_stats) + + curr_count - prev_count + end + + def calculate_cities_change + prev_count = count_cities(previous_year_stats) + curr_count = count_cities(current_year_stats) + + curr_count - prev_count + end + + def count_countries(stats) + stats.flat_map do |stat| + toponyms = stat.toponyms + next [] unless toponyms.is_a?(Array) + + toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) } + end.uniq.compact.count + end + + def count_cities(stats) + stats.flat_map do |stat| + toponyms = stat.toponyms + next [] unless toponyms.is_a?(Array) + + toponyms.flat_map do |t| + next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array) + + t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) } + end + end.uniq.compact.count + end + end + end +end diff --git a/app/services/yearly_digests/calculate_year.rb b/app/services/yearly_digests/calculate_year.rb deleted file mode 100644 index 3910dee5..00000000 --- a/app/services/yearly_digests/calculate_year.rb +++ /dev/null @@ -1,131 +0,0 @@ -# frozen_string_literal: true - -module YearlyDigests - class CalculateYear - def initialize(user_id, year) - @user = User.find(user_id) - @year = year.to_i - end - - def call - return nil if monthly_stats.empty? - - digest = YearlyDigest.find_or_initialize_by(user: user, year: year, period_type: :yearly) - - digest.assign_attributes( - distance: total_distance, - toponyms: aggregate_toponyms, - monthly_distances: build_monthly_distances, - time_spent_by_location: calculate_time_spent, - first_time_visits: calculate_first_time_visits, - year_over_year: calculate_yoy_comparison, - all_time_stats: calculate_all_time_stats - ) - - digest.save! - digest - end - - private - - attr_reader :user, :year - - def monthly_stats - @monthly_stats ||= user.stats.where(year: year).order(:month) - end - - def total_distance - monthly_stats.sum(:distance) - end - - def aggregate_toponyms - countries = [] - cities = [] - - monthly_stats.each do |stat| - toponyms = stat.toponyms - next unless toponyms.is_a?(Array) - - toponyms.each do |toponym| - next unless toponym.is_a?(Hash) - - countries << toponym['country'] if toponym['country'].present? - - next unless toponym['cities'].is_a?(Array) - - toponym['cities'].each do |city| - cities << city['city'] if city.is_a?(Hash) && city['city'].present? - end - end - end - - { - 'countries' => countries.uniq.compact.sort, - 'cities' => cities.uniq.compact.sort - } - end - - def build_monthly_distances - result = {} - - monthly_stats.each do |stat| - result[stat.month.to_s] = stat.distance - end - - # Fill in missing months with 0 - (1..12).each do |month| - result[month.to_s] ||= 0 - end - - result - end - - def calculate_time_spent - country_time = Hash.new(0) - city_time = Hash.new(0) - - monthly_stats.each do |stat| - toponyms = stat.toponyms - next unless toponyms.is_a?(Array) - - toponyms.each do |toponym| - next unless toponym.is_a?(Hash) - - country = toponym['country'] - next unless toponym['cities'].is_a?(Array) - - toponym['cities'].each do |city| - next unless city.is_a?(Hash) - - stayed_for = city['stayed_for'].to_i - city_name = city['city'] - - country_time[country] += stayed_for if country.present? - city_time[city_name] += stayed_for if city_name.present? - end - end - end - - { - 'countries' => country_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } }, - 'cities' => city_time.sort_by { |_, v| -v }.first(10).map { |name, minutes| { 'name' => name, 'minutes' => minutes } } - } - end - - def calculate_first_time_visits - FirstTimeVisitsCalculator.new(user, year).call - end - - def calculate_yoy_comparison - YearOverYearCalculator.new(user, year).call - end - - def calculate_all_time_stats - { - 'total_countries' => user.countries_visited.count, - 'total_cities' => user.cities_visited.count, - 'total_distance' => user.stats.sum(:distance) - } - end - end -end diff --git a/app/services/yearly_digests/chart_image_generator.rb b/app/services/yearly_digests/chart_image_generator.rb deleted file mode 100644 index 6ae28186..00000000 --- a/app/services/yearly_digests/chart_image_generator.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module YearlyDigests - class ChartImageGenerator - def initialize(digest, distance_unit: 'km') - @digest = digest - @distance_unit = distance_unit - end - - def call - html = render_chart_html - generate_image(html) - end - - private - - attr_reader :digest, :distance_unit - - def render_chart_html - ApplicationController.render( - template: 'yearly_digests/chart', - layout: false, - assigns: { - monthly_distances: digest.monthly_distances, - distance_unit: distance_unit - } - ) - end - - def generate_image(html) - grover = Grover.new( - html, - format: 'png', - viewport: { width: 600, height: 320 }, - wait_until: 'networkidle0' - ) - grover.to_png - end - end -end diff --git a/app/services/yearly_digests/first_time_visits_calculator.rb b/app/services/yearly_digests/first_time_visits_calculator.rb deleted file mode 100644 index b8a22f90..00000000 --- a/app/services/yearly_digests/first_time_visits_calculator.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -module YearlyDigests - class FirstTimeVisitsCalculator - def initialize(user, year) - @user = user - @year = year.to_i - end - - def call - { - 'countries' => first_time_countries, - 'cities' => first_time_cities - } - end - - private - - attr_reader :user, :year - - def previous_years_stats - @previous_years_stats ||= user.stats.where('year < ?', year) - end - - def current_year_stats - @current_year_stats ||= user.stats.where(year: year) - end - - def previous_countries - @previous_countries ||= extract_countries(previous_years_stats) - end - - def previous_cities - @previous_cities ||= extract_cities(previous_years_stats) - end - - def current_countries - @current_countries ||= extract_countries(current_year_stats) - end - - def current_cities - @current_cities ||= extract_cities(current_year_stats) - end - - def first_time_countries - (current_countries - previous_countries).sort - end - - def first_time_cities - (current_cities - previous_cities).sort - end - - def extract_countries(stats) - stats.flat_map do |stat| - toponyms = stat.toponyms - next [] unless toponyms.is_a?(Array) - - toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) } - end.uniq.compact - end - - def extract_cities(stats) - stats.flat_map do |stat| - toponyms = stat.toponyms - next [] unless toponyms.is_a?(Array) - - toponyms.flat_map do |t| - next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array) - - t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) } - end - end.uniq.compact - end - end -end diff --git a/app/services/yearly_digests/year_over_year_calculator.rb b/app/services/yearly_digests/year_over_year_calculator.rb deleted file mode 100644 index b583dfcf..00000000 --- a/app/services/yearly_digests/year_over_year_calculator.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module YearlyDigests - class YearOverYearCalculator - def initialize(user, year) - @user = user - @year = year.to_i - end - - def call - return {} unless previous_year_stats.exists? - - { - 'previous_year' => year - 1, - 'distance_change_percent' => calculate_distance_change_percent, - 'countries_change' => calculate_countries_change, - 'cities_change' => calculate_cities_change - }.compact - end - - private - - attr_reader :user, :year - - def previous_year_stats - @previous_year_stats ||= user.stats.where(year: year - 1) - end - - def current_year_stats - @current_year_stats ||= user.stats.where(year: year) - end - - def calculate_distance_change_percent - prev_distance = previous_year_stats.sum(:distance) - return nil if prev_distance.zero? - - curr_distance = current_year_stats.sum(:distance) - ((curr_distance - prev_distance).to_f / prev_distance * 100).round - end - - def calculate_countries_change - prev_count = count_countries(previous_year_stats) - curr_count = count_countries(current_year_stats) - - curr_count - prev_count - end - - def calculate_cities_change - prev_count = count_cities(previous_year_stats) - curr_count = count_cities(current_year_stats) - - curr_count - prev_count - end - - def count_countries(stats) - stats.flat_map do |stat| - toponyms = stat.toponyms - next [] unless toponyms.is_a?(Array) - - toponyms.filter_map { |t| t['country'] if t.is_a?(Hash) } - end.uniq.compact.count - end - - def count_cities(stats) - stats.flat_map do |stat| - toponyms = stat.toponyms - next [] unless toponyms.is_a?(Array) - - toponyms.flat_map do |t| - next [] unless t.is_a?(Hash) && t['cities'].is_a?(Array) - - t['cities'].filter_map { |c| c['city'] if c.is_a?(Hash) } - end - end.uniq.compact.count - end - end -end diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 85e794f3..1673d5e0 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -3,7 +3,7 @@