Files
dawarich/spec/services/visits/creator_spec.rb
2025-11-22 18:51:11 +01:00

329 lines
11 KiB
Ruby

# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Visits::Creator do
let(:user) { create(:user) }
subject { described_class.new(user) }
describe '#create_visits' do
let(:point1) { create(:point, user: user) }
let(:point2) { create(:point, user: user) }
let(:visit_data) do
{
start_time: 1.hour.ago.to_i,
end_time: 30.minutes.ago.to_i,
duration: 30.minutes.to_i,
center_lat: 40.7128,
center_lon: -74.0060,
radius: 50,
suggested_name: 'Test Place',
points: [point1, point2]
}
end
context 'when a confirmed visit already exists at the same location' do
let(:place) { create(:place, lonlat: 'POINT(-74.0060 40.7128)', name: 'Existing Place', latitude: 40.7128, longitude: -74.0060, user_id: nil) }
let!(:existing_visit) do
create(
:visit,
user: user,
place: place,
status: :confirmed,
started_at: 1.5.hours.ago,
ended_at: 45.minutes.ago,
duration: 45
)
end
it 'returns the existing confirmed visit instead of creating a duplicate suggested visit' do
visits = subject.create_visits([visit_data])
expect(visits.size).to eq(1)
expect(visits.first).to eq(existing_visit)
expect(visits.first.status).to eq('confirmed')
# Verify no new visits were created
expect(user.visits.reload.count).to eq(1)
end
it 'does not change points associations' do
original_visit_id = point1.visit_id
subject.create_visits([visit_data])
# Points should remain unassociated
expect(point1.reload.visit_id).to eq(original_visit_id)
expect(point2.reload.visit_id).to eq(nil)
end
end
context 'when a confirmed visit exists but at a different location' do
let(:different_place) { create(:place, lonlat: 'POINT(-73.9000 41.0000)', name: 'Different Place', latitude: 41.0000, longitude: -73.9000) }
let!(:existing_visit) do
create(
:visit,
user: user,
place: different_place,
status: :confirmed,
started_at: 1.5.hours.ago,
ended_at: 45.minutes.ago,
duration: 45
)
end
let(:place) { create(:place, lonlat: 'POINT(-74.0060 40.7128)', name: 'New Place', latitude: 40.7128, longitude: -74.0060) }
let(:place_finder) { instance_double(Visits::PlaceFinder) }
before do
allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
allow(place_finder).to receive(:find_or_create_place).and_return({ main_place: place, suggested_places: [] })
end
it 'creates a new suggested visit' do
visits = subject.create_visits([visit_data])
expect(visits.size).to eq(1)
expect(visits.first).not_to eq(existing_visit)
expect(visits.first.place).to eq(place)
expect(visits.first.status).to eq('suggested')
# Should now have two visits
expect(user.visits.reload.count).to eq(2)
end
end
context 'when matching an area' do
let!(:area) { create(:area, user: user, latitude: 40.7128, longitude: -74.0060, radius: 100) }
it 'creates a visit associated with the area' do
visits = subject.create_visits([visit_data])
expect(visits.size).to eq(1)
visit = visits.first
expect(visit.area).to eq(area)
expect(visit.place).to be_nil
expect(visit.started_at).to be_within(1.second).of(Time.zone.at(visit_data[:start_time]))
expect(visit.ended_at).to be_within(1.second).of(Time.zone.at(visit_data[:end_time]))
expect(visit.duration).to eq(30)
expect(visit.name).to eq(area.name)
expect(visit.status).to eq('suggested')
expect(point1.reload.visit_id).to eq(visit.id)
expect(point2.reload.visit_id).to eq(visit.id)
end
it 'uses area name for visit name' do
area.update(name: 'Custom Area Name')
visits = subject.create_visits([visit_data])
expect(visits.first.name).to eq('Custom Area Name')
end
it 'does not find areas too far from the visit center' do
far_area = create(:area, user: user, latitude: 41.8781, longitude: -87.6298, radius: 100) # Chicago
visits = subject.create_visits([visit_data])
expect(visits.first.area).to eq(area) # Should match the closer area
expect(visits.first.area).not_to eq(far_area)
end
end
context 'when matching a place' do
let(:place) { create(:place, name: 'Test Place') }
let(:suggested_place1) { create(:place, name: 'Suggested Place 1') }
let(:suggested_place2) { create(:place, name: 'Suggested Place 2') }
let(:place_finder) { instance_double(Visits::PlaceFinder) }
let(:place_data) do
{
main_place: place,
suggested_places: [suggested_place1, suggested_place2]
}
end
before do
allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
allow(place_finder).to receive(:find_or_create_place).and_return(place_data)
end
it 'creates a visit associated with the place' do
visits = subject.create_visits([visit_data])
expect(visits.size).to eq(1)
visit = visits.first
expect(visit.area).to be_nil
expect(visit.place).to eq(place)
expect(visit.name).to eq(place.name)
end
it 'associates suggested places with the visit' do
visits = subject.create_visits([visit_data])
visit = visits.first
# Check for place_visits associations
expect(visit.place_visits.count).to eq(2)
expect(visit.place_visits.pluck(:place_id)).to contain_exactly(
suggested_place1.id,
suggested_place2.id
)
expect(visit.suggested_places).to contain_exactly(suggested_place1, suggested_place2)
end
it 'does not create duplicate place_visit associations' do
# Create an existing association
visit = create(:visit, user: user, place: place)
create(:place_visit, visit: visit, place: suggested_place1)
allow(Visit).to receive(:create!).and_return(visit)
# Only one new association should be created
expect do
subject.create_visits([visit_data])
end.to change(PlaceVisit, :count).by(1)
expect(visit.place_visits.pluck(:place_id)).to contain_exactly(
suggested_place1.id,
suggested_place2.id
)
end
end
context 'when no area or place is found' do
let(:place_finder) { instance_double(Visits::PlaceFinder) }
before do
allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
allow(place_finder).to receive(:find_or_create_place).and_return(nil)
end
it 'uses suggested name from visit data' do
visits = subject.create_visits([visit_data])
expect(visits.first.area).to be_nil
expect(visits.first.place).to be_nil
expect(visits.first.name).to eq('Test Place')
end
it 'uses "Unknown Location" when no name is available' do
visit_data_without_name = visit_data.dup
visit_data_without_name[:suggested_name] = nil
visits = subject.create_visits([visit_data_without_name])
expect(visits.first.name).to eq('Unknown Location')
end
end
context 'when processing multiple visits' do
let(:place1) { create(:place, name: 'Place 1') }
let(:place2) { create(:place, name: 'Place 2') }
let(:place_finder) { instance_double(Visits::PlaceFinder) }
let(:visit_data2) do
{
start_time: 3.hours.ago.to_i,
end_time: 2.hours.ago.to_i,
duration: 60.minutes.to_i,
center_lat: 41.8781,
center_lon: -87.6298,
radius: 50,
suggested_name: 'Chicago Visit',
points: [create(:point, user: user), create(:point, user: user)]
}
end
before do
allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
allow(place_finder).to receive(:find_or_create_place)
.with(visit_data).and_return({ main_place: place1, suggested_places: [] })
allow(place_finder).to receive(:find_or_create_place)
.with(visit_data2).and_return({ main_place: place2, suggested_places: [] })
end
it 'creates multiple visits' do
visits = subject.create_visits([visit_data, visit_data2])
expect(visits.size).to eq(2)
expect(visits[0].place).to eq(place1)
expect(visits[0].name).to eq('Place 1')
expect(visits[1].place).to eq(place2)
expect(visits[1].name).to eq('Place 2')
end
end
context 'when transaction fails' do
let(:place_finder) { instance_double(Visits::PlaceFinder) }
before do
allow(Visits::PlaceFinder).to receive(:new).with(user).and_return(place_finder)
allow(place_finder).to receive(:find_or_create_place).and_return(nil)
allow(Visit).to receive(:create!).and_raise(ActiveRecord::RecordInvalid)
end
it 'does not update points if visit creation fails' do
expect do
subject.create_visits([visit_data])
end.to raise_error(ActiveRecord::RecordInvalid)
# Points should not be associated with any visit
expect(point1.reload.visit_id).to be_nil
expect(point2.reload.visit_id).to be_nil
end
end
end
describe '#find_matching_area' do
let(:visit_data) do
{
center_lat: 40.7128,
center_lon: -74.0060,
radius: 50
}
end
it 'finds areas within radius' do
area_within = create(:area, user: user, latitude: 40.7129, longitude: -74.0061, radius: 100)
area_outside = create(:area, user: user, latitude: 40.7500, longitude: -74.0500, radius: 100)
result = subject.send(:find_matching_area, visit_data)
expect(result).to eq(area_within)
end
it 'returns nil when no areas match' do
create(:area, user: user, latitude: 42.0, longitude: -72.0, radius: 100)
result = subject.send(:find_matching_area, visit_data)
expect(result).to be_nil
end
it 'only considers user areas' do
area_other_user = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
area_user = create(:area, user: user, latitude: 40.7128, longitude: -74.0060, radius: 100)
result = subject.send(:find_matching_area, visit_data)
expect(result).to eq(area_user)
end
end
describe '#near_area?' do
it 'returns true when point is within area radius' do
area = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
center = [40.7129, -74.0061] # Very close to area center
result = subject.send(:near_area?, center, area)
expect(result).to be true
end
it 'returns false when point is outside area radius' do
area = create(:area, latitude: 40.7128, longitude: -74.0060, radius: 100)
center = [40.7500, -74.0500] # Further away
result = subject.send(:near_area?, center, area)
expect(result).to be false
end
end
end