diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index 648b30d3..4bb8994d 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -44,7 +44,15 @@ class MapController < ApplicationController ) end - distance.round(1) + # Convert distance to meters for consistent storage + distance_in_meters = case current_user.safe_settings.distance_unit.to_s + when 'miles', 'mi' + distance * 1609.344 # miles to meters + else + distance * 1000 # km to meters + end + + distance_in_meters.round # Return as integer meters end def parsed_start_at diff --git a/app/models/concerns/calculateable.rb b/app/models/concerns/calculateable.rb index ef4c6eee..c612ef6c 100644 --- a/app/models/concerns/calculateable.rb +++ b/app/models/concerns/calculateable.rb @@ -69,9 +69,9 @@ module Calculateable # For Track model - convert to meters for storage (Track expects distance in meters) case user_distance_unit.to_s when 'mi' - (calculated_distance * 1609.344).round(2) # miles to meters + (calculated_distance * 1609.344).round # miles to meters else - (calculated_distance * 1000).round(2) # km to meters + (calculated_distance * 1000).round # km to meters end end diff --git a/app/models/stat.rb b/app/models/stat.rb index b763aa76..e46a65c5 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -38,7 +38,7 @@ class Stat < ApplicationRecord timespan.to_a.map.with_index(1) do |day, index| daily_points = filter_points_for_day(monthly_points, day) distance = Point.total_distance(daily_points, user.safe_settings.distance_unit) - [index, distance.round(2)] + [index, distance.round] end end diff --git a/app/models/track.rb b/app/models/track.rb index b36e320e..b785bac8 100644 --- a/app/models/track.rb +++ b/app/models/track.rb @@ -7,7 +7,7 @@ class Track < ApplicationRecord has_many :points, dependent: :nullify validates :start_at, :end_at, :original_path, presence: true - validates :distance, :avg_speed, :duration, numericality: { greater_than: 0 } + validates :distance, :avg_speed, :duration, numericality: { greater_than_or_equal_to: 0 } after_update :recalculate_path_and_distance!, if: -> { points.exists? && (saved_change_to_start_at? || saved_change_to_end_at?) } end diff --git a/app/serializers/track_serializer.rb b/app/serializers/track_serializer.rb index 7cdd1bbc..4767735f 100644 --- a/app/serializers/track_serializer.rb +++ b/app/serializers/track_serializer.rb @@ -38,7 +38,7 @@ class TrackSerializer id: id, start_at: start_at.iso8601, end_at: end_at.iso8601, - distance: distance.to_f, + distance: distance.to_i, avg_speed: avg_speed.to_f, duration: duration, elevation_gain: elevation_gain, diff --git a/app/services/tracks/create_from_points.rb b/app/services/tracks/create_from_points.rb index 8ef63b00..fdf783be 100644 --- a/app/services/tracks/create_from_points.rb +++ b/app/services/tracks/create_from_points.rb @@ -154,9 +154,9 @@ class Tracks::CreateFromPoints # Convert to meters for storage (Track model expects distance in meters) case user.safe_settings.distance_unit when 'miles', 'mi' - (distance_in_user_unit * 1609.344).round(2) # miles to meters + (distance_in_user_unit * 1609.344).round # miles to meters else - (distance_in_user_unit * 1000).round(2) # km to meters + (distance_in_user_unit * 1000).round # km to meters end end diff --git a/spec/fixtures/files/geojson/export_same_points.json b/spec/fixtures/files/geojson/export_same_points.json index 3f6845cc..9a510998 100644 --- a/spec/fixtures/files/geojson/export_same_points.json +++ b/spec/fixtures/files/geojson/export_same_points.json @@ -1 +1 @@ -{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459200,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459201,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459202,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459203,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459204,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459205,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459206,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459207,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459208,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459209,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null}}]} +{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459200,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459201,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459202,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459203,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459204,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459205,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459206,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459207,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459208,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}},{"type":"Feature","geometry":{"type":"Point","coordinates":[37.6173,55.755826]},"properties":{"battery_status":"unplugged","ping":"MyString","battery":1,"tracker_id":"MyString","topic":"MyString","altitude":1,"longitude":"37.6173","velocity":"0","trigger":"background_event","bssid":"MyString","ssid":"MyString","connection":"wifi","vertical_accuracy":1,"accuracy":1,"timestamp":1609459209,"latitude":"55.755826","mode":1,"inrids":[],"in_regions":[],"city":null,"country":null,"geodata":{},"course":null,"course_accuracy":null,"external_track_id":null,"track_id":null}}]} diff --git a/spec/jobs/tracks/create_job_spec.rb b/spec/jobs/tracks/create_job_spec.rb index 0c948a4a..cf88c8a2 100644 --- a/spec/jobs/tracks/create_job_spec.rb +++ b/spec/jobs/tracks/create_job_spec.rb @@ -54,28 +54,35 @@ RSpec.describe Tracks::CreateJob, type: :job do expect(notification_service).to have_received(:call) end - it 'logs the error' do - allow(Rails.logger).to receive(:error) - allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil)) + it 'reports the error using ExceptionReporter' do + allow(ExceptionReporter).to receive(:call) described_class.new.perform(user.id) - expect(Rails.logger).to have_received(:error).with("Failed to create tracks for user #{user.id}: #{error_message}") + expect(ExceptionReporter).to have_received(:call).with( + kind_of(StandardError), + 'Failed to create tracks for user' + ) end end context 'when user does not exist' do - it 'raises ActiveRecord::RecordNotFound' do - expect { - described_class.new.perform(999) - }.to raise_error(ActiveRecord::RecordNotFound) + it 'handles the error gracefully and creates error notification' do + allow(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound) + allow(ExceptionReporter).to receive(:call) + allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil)) + + # Should not raise an error because it's caught by the rescue block + expect { described_class.new.perform(999) }.not_to raise_error + + expect(ExceptionReporter).to have_received(:call) end end end describe 'queue' do it 'is queued on default queue' do - expect(described_class.new.queue_name).to eq('tracks') + expect(described_class.new.queue_name).to eq('default') end end end diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 90337f2f..a65f191d 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Stat, type: :model do create(:point, user:, lonlat: 'POINT(2 2)', timestamp: DateTime.new(year, 1, 1, 2)) end - before { expected_distance[0][1] = 156.88 } + before { expected_distance[0][1] = 157 } it 'returns distance by day' do expect(subject).to eq(expected_distance) diff --git a/spec/models/track_spec.rb b/spec/models/track_spec.rb index 04ab9a90..59557010 100644 --- a/spec/models/track_spec.rb +++ b/spec/models/track_spec.rb @@ -7,12 +7,14 @@ RSpec.describe Track, type: :model do end describe 'validations' do + subject { build(:track) } + it { is_expected.to validate_presence_of(:start_at) } it { is_expected.to validate_presence_of(:end_at) } it { is_expected.to validate_presence_of(:original_path) } - it { is_expected.to validate_numericality_of(:distance).is_greater_than(0) } - it { is_expected.to validate_numericality_of(:avg_speed).is_greater_than(0) } - it { is_expected.to validate_numericality_of(:duration).is_greater_than(0) } + it { is_expected.to validate_numericality_of(:distance).is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:avg_speed).is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:duration).is_greater_than_or_equal_to(0) } end describe 'Calculateable concern' do @@ -21,8 +23,8 @@ RSpec.describe Track, type: :model do let!(:points) do [ create(:point, user: user, track: track, lonlat: 'POINT(13.404954 52.520008)', timestamp: 1.hour.ago.to_i), - create(:point, user: user, track: track, lonlat: 'POINT(13.404955 52.520009)', timestamp: 30.minutes.ago.to_i), - create(:point, user: user, track: track, lonlat: 'POINT(13.404956 52.520010)', timestamp: Time.current.to_i) + create(:point, user: user, track: track, lonlat: 'POINT(13.405954 52.521008)', timestamp: 30.minutes.ago.to_i), + create(:point, user: user, track: track, lonlat: 'POINT(13.406954 52.522008)', timestamp: Time.current.to_i) ] end @@ -41,7 +43,7 @@ RSpec.describe Track, type: :model do track.calculate_distance expect(track.distance).to be > 0 - expect(track.distance).to be_a(Float) + expect(track.distance).to be_a(Integer) end it 'stores distance in meters for Track model' do @@ -50,7 +52,7 @@ RSpec.describe Track, type: :model do track.calculate_distance - expect(track.distance).to eq(1500.0) # Should be in meters + expect(track.distance).to eq(1500) # Should be in meters as integer end end diff --git a/spec/serializers/point_serializer_spec.rb b/spec/serializers/point_serializer_spec.rb index 2f2a9742..d7ae5336 100644 --- a/spec/serializers/point_serializer_spec.rb +++ b/spec/serializers/point_serializer_spec.rb @@ -33,7 +33,8 @@ RSpec.describe PointSerializer do 'geodata' => point.geodata, 'course' => point.course, 'course_accuracy' => point.course_accuracy, - 'external_track_id' => point.external_track_id + 'external_track_id' => point.external_track_id, + 'track_id' => point.track_id } end diff --git a/spec/services/stats/calculate_month_spec.rb b/spec/services/stats/calculate_month_spec.rb index 83069d08..cb9c94e1 100644 --- a/spec/services/stats/calculate_month_spec.rb +++ b/spec/services/stats/calculate_month_spec.rb @@ -61,7 +61,7 @@ RSpec.describe Stats::CalculateMonth do it 'calculates distance' do calculate_stats - expect(user.stats.last.distance).to eq(339) + expect(user.stats.last.distance).to eq(340) end context 'when there is an error' do diff --git a/spec/services/tracks/create_from_points_spec.rb b/spec/services/tracks/create_from_points_spec.rb index 711114df..df9a3352 100644 --- a/spec/services/tracks/create_from_points_spec.rb +++ b/spec/services/tracks/create_from_points_spec.rb @@ -9,8 +9,8 @@ RSpec.describe Tracks::CreateFromPoints do describe '#initialize' do it 'sets user and thresholds from user settings' do expect(service.user).to eq(user) - expect(service.distance_threshold_meters).to eq(user.safe_settings.meters_between_routes) - expect(service.time_threshold_minutes).to eq(user.safe_settings.minutes_between_routes) + expect(service.distance_threshold_meters).to eq(user.safe_settings.meters_between_routes.to_i) + expect(service.time_threshold_minutes).to eq(user.safe_settings.minutes_between_routes.to_i) end context 'with custom user settings' do @@ -233,14 +233,14 @@ RSpec.describe Tracks::CreateFromPoints do end end - describe '#calculate_distance_meters' do + describe '#calculate_distance_kilometers' do let(:point1) { build(:point, lonlat: 'POINT(-74.0060 40.7128)') } let(:point2) { build(:point, lonlat: 'POINT(-74.0070 40.7130)') } - it 'calculates distance between two points in meters' do - distance = service.send(:calculate_distance_meters, point1, point2) + it 'calculates distance between two points in kilometers' do + distance = service.send(:calculate_distance_kilometers, point1, point2) expect(distance).to be > 0 - expect(distance).to be < 200 # Should be small distance for close points + expect(distance).to be < 0.2 # Should be small distance for close points (in km) end end @@ -276,7 +276,7 @@ RSpec.describe Tracks::CreateFromPoints do it 'converts km to meters by default' do distance = service.send(:calculate_track_distance, points) - expect(distance).to eq(1500.0) # 1.5 km = 1500 meters + expect(distance).to eq(1500) # 1.5 km = 1500 meters end context 'with miles unit' do @@ -286,7 +286,7 @@ RSpec.describe Tracks::CreateFromPoints do it 'converts miles to meters' do distance = service.send(:calculate_track_distance, points) - expect(distance).to eq(2414.02) # 1.5 miles ≈ 2414 meters + expect(distance).to eq(2414) # 1.5 miles ≈ 2414 meters (rounded) end end end