Add visits detection

This commit is contained in:
Eugene Burmakin 2024-08-05 21:23:08 +02:00
parent 1e207f297c
commit 5394e9dd52
34 changed files with 473 additions and 112 deletions

View file

@ -4,3 +4,4 @@ DATABASE_PASSWORD=password
DATABASE_NAME=dawarich_development
DATABASE_PORT=5432
REDIS_URL=redis://localhost:6379/1
GOOGLE_PLACES_API_KEY=''

View file

@ -9,8 +9,7 @@ gem 'chartkick'
gem 'data_migrate'
gem 'devise'
gem 'geocoder'
gem 'geokit'
gem 'geokit-rails'
gem 'google_places'
gem 'importmap-rails'
gem 'kaminari'
gem 'lograge'

View file

@ -141,7 +141,13 @@ GEM
rails (>= 3.0)
globalid (1.2.1)
activesupport (>= 6.1)
google_places (2.0.0)
httparty (>= 0.13.1)
hashdiff (1.1.0)
httparty (0.22.0)
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.5)
concurrent-ruby (~> 1.0)
importmap-rails (2.0.1)
@ -186,6 +192,8 @@ GEM
mini_mime (1.1.5)
minitest (5.24.1)
msgpack (1.7.2)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
mutex_m (0.2.0)
net-imap (0.4.12)
date
@ -429,6 +437,7 @@ DEPENDENCIES
geocoder
geokit
geokit-rails
google_places
importmap-rails
kaminari
lograge

File diff suppressed because one or more lines are too long

View file

@ -6,15 +6,17 @@ class VisitsController < ApplicationController
def index
order_by = params[:order_by] || 'asc'
status = params[:status] || 'confirmed'
visits = current_user
.visits
.where(status: :pending)
.or(current_user.visits.where(status: :confirmed))
.where(status:)
.order(started_at: order_by)
.group_by { |visit| visit.started_at.to_date }
.map { |k, v| { date: k, visits: v } }
@suggested_visits_count = current_user.visits.suggested.count
@visits = Kaminari.paginate_array(visits).page(params[:page]).per(10)
end

View file

@ -0,0 +1,32 @@
import { Controller } from "@hotwired/stimulus"
import L, { latLng } from "leaflet";
import { osmMapLayer } from "../maps/layers";
// Connects to data-controller="visit-modal-map"
export default class extends Controller {
static targets = ["container"];
connect() {
console.log("Visits maps controller connected");
this.coordinates = JSON.parse(this.element.dataset.coordinates);
this.center = JSON.parse(this.element.dataset.center);
this.radius = this.element.dataset.radius;
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 17);
osmMapLayer(this.map),
this.addMarkers();
L.circle([this.center[0], this.center[1]], {
radius: this.radius,
color: 'red',
fillColor: '#f03',
fillOpacity: 0.5
}).addTo(this.map);
}
addMarkers() {
this.coordinates.forEach((coordinate) => {
L.circleMarker([coordinate[0], coordinate[1]], { radius: 4 }).addTo(this.map);
});
}
}

View file

@ -3,9 +3,15 @@
class ReverseGeocodingJob < ApplicationJob
queue_as :reverse_geocoding
def perform(point_id)
def perform(klass, id)
return unless REVERSE_GEOCODING_ENABLED
ReverseGeocoding::FetchData.new(point_id).call
data_fetcher(klass, id).call
end
private
def data_fetcher(klass, id)
"ReverseGeocoding::#{klass.pluralize.camelize}::FetchData".constantize.new(id)
end
end

View file

@ -0,0 +1,7 @@
class VisitSuggestingJob < ApplicationJob
queue_as :default
def perform(*args)
# Do something later
end
end

View file

@ -5,4 +5,6 @@ class Area < ApplicationRecord
has_many :visits, dependent: :destroy
validates :name, :latitude, :longitude, :radius, presence: true
def center = [latitude.to_f, longitude.to_f]
end

17
app/models/place.rb Normal file
View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Place < ApplicationRecord
validates :name, :longitude, :latitude, presence: true
enum source: { manual: 0, google_places: 1 }
after_commit :async_reverse_geocode, on: :create
private
def async_reverse_geocode
return unless REVERSE_GEOCODING_ENABLED
ReverseGeocodingJob.perform_later(self.class.to_s, id)
end
end

View file

@ -33,6 +33,6 @@ class Point < ApplicationRecord
def async_reverse_geocode
return unless REVERSE_GEOCODING_ENABLED
ReverseGeocodingJob.perform_later(id)
ReverseGeocodingJob.perform_later(self.class.to_s, id)
end
end

View file

@ -7,5 +7,11 @@ class Visit < ApplicationRecord
validates :started_at, :ended_at, :duration, :name, :status, presence: true
enum status: { pending: 0, confirmed: 1, declined: 2 }
enum status: { suggested: 0, confirmed: 1, declined: 2 }
delegate :name, to: :area, prefix: true
def coordinates
points.pluck(:latitude, :longitude).map { [_1[0].to_f, _1[1].to_f] }
end
end

View file

@ -64,7 +64,7 @@ class Areas::Visits::Create
v.name = "#{area.name}, #{time_range}"
v.ended_at = Time.zone.at(visit_points.last.timestamp)
v.duration = (visit_points.last.timestamp - visit_points.first.timestamp) / 60
v.status = :pending
v.status = :suggested
end
visit.save!

View file

@ -2,6 +2,7 @@
class Jobs::Create
class InvalidJobName < StandardError; end
attr_reader :job_name, :user
def initialize(job_name, user_id)

View file

@ -0,0 +1,65 @@
# frozen_string_literal: true
class ReverseGeocoding::Places::FetchData
attr_reader :place
def initialize(place_id)
@place = Place.find(place_id)
rescue ActiveRecord::RecordNotFound => e
Rails.logger.error("Place with id #{place_id} not found: #{e.message}")
end
def call
return if place.reverse_geocoded?
if ENV['GOOGLE_PLACES_API_KEY'].blank?
Rails.logger.warn('GOOGLE_PLACES_API_KEY is not set')
return
end
data = Geocoder.search([place.latitude, place.longitude])
google_place_ids = data.map { _1.data['place_id'] }
first_place = google_places_client.spot(google_place_ids.shift)
update_place(first_place)
google_place_ids.each { |google_place_id| fetch_and_create_place(google_place_id) }
end
private
def google_places_client
@google_places_client ||= GooglePlaces::Client.new(ENV['GOOGLE_PLACES_API_KEY'])
end
def update_place(place)
place.update!(
name: place.name,
latitude: place.lat,
longitude: place.lng,
city: place.city,
country: place.country,
raw_data: place.raw_data,
source: :google_places,
reverse_geocoded_at: Time.zone.now
)
end
def fetch_and_create_place(place_id)
place_data = google_places_client.spot(place_id)
Place.create!(
name: place_data.name,
latitude: place_data.lat,
longitude: place_data.lng,
city: place_data.city,
country: place_data.country,
raw_data: place_data.raw_data,
source: :google_places
)
end
def reverse_geocoded?
place.geodata.present?
end
end

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
class ReverseGeocoding::FetchData
class ReverseGeocoding::Points::FetchData
attr_reader :point
def initialize(point_id)

View file

@ -1,77 +0,0 @@
require 'geokit'
class Visits::Detect
def initialize
a = 5.days.ago.beginning_of_month
b = 5.days.ago.end_of_month
@points = Point.order(timestamp: :asc).where(timestamp: a..b)
end
def call
# Group points by day
points_by_day = @points.group_by { |point| point_date(point) }
# Iterate through each day's points
points_by_day.each do |day, day_points|
# Sort points by timestamp
day_points.sort_by! { |point| point.timestamp }
# Call the method for each day's points
grouped_points = group_points_by_radius(day_points)
# Print the grouped points for the day
puts "Day: #{day}"
grouped_points.each_with_index do |group, index|
puts "Group #{index + 1}:"
group.each do |point|
puts point
end
end
end
end
private
# Method to convert timestamp to date
def point_date(point)
Time.zone.at(point.timestamp).to_date
end
# Method to check if two points are within a certain radius (in meters)
def within_radius?(point1, point2, radius)
loc1 = Geokit::LatLng.new(point1.latitude, point1.longitude)
loc2 = Geokit::LatLng.new(point2.latitude, point2.longitude)
loc1.distance_to(loc2, units: :kms) * 1000 <= radius
end
# Method to group points by increasing radius
def group_points_by_radius(day_points, initial_radius = 30, max_radius = 100, step = 10)
grouped = []
remaining_points = day_points.dup
while remaining_points.any?
point = remaining_points.shift
group = [point]
radius = initial_radius
while radius <= max_radius
remaining_points.each do |next_point|
group << next_point if within_radius?(point, next_point, radius)
end
if group.size > 1
remaining_points -= group
grouped << group
break
else
radius += step
end
end
end
grouped
end
end
# Execute the detection
# Visits::Detect.new.call

View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
class Visits::GroupPoints
INITIAL_RADIUS = 30 # meters
MAX_RADIUS = 100 # meters
RADIUS_STEP = 10 # meters
MIN_VISIT_DURATION = 3 * 60 # 3 minutes in seconds
attr_reader :day_points, :initial_radius, :max_radius, :step
def initialize(day_points, initial_radius = INITIAL_RADIUS, max_radius = MAX_RADIUS, step = RADIUS_STEP)
@day_points = day_points
@initial_radius = initial_radius
@max_radius = max_radius
@step = step
end
def group_points_by_radius
grouped = []
remaining_points = day_points.dup
while remaining_points.any?
point = remaining_points.shift
radius = initial_radius
while radius <= max_radius
new_group = [point]
remaining_points.each do |next_point|
break unless within_radius?(new_group.first, next_point, radius)
new_group << next_point
end
if new_group.size > 1
group_duration = new_group.last.timestamp - new_group.first.timestamp
if group_duration >= MIN_VISIT_DURATION
remaining_points -= new_group
grouped << new_group
end
break
else
radius += step
end
end
end
grouped
end
private
def within_radius?(point1, point2, radius)
point1.distance_to(point2) * 1000 <= radius
end
end

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
class Visits::Suggest
def initialize(start_at: nil, end_at: nil)
start_at ||= Date.new(2024, 7, 15).to_datetime.beginning_of_day.to_i
end_at ||= Date.new(2024, 7, 19).to_datetime.end_of_day.to_i
@points = Point.order(timestamp: :asc).where(timestamp: start_at..end_at)
end
def call
points_by_day = @points.group_by { |point| point_date(point) }
result = {}
points_by_day.each do |day, day_points|
day_points.sort_by!(&:timestamp)
grouped_points = Visits::GroupPoints.new(day_points).group_points_by_radius
day_result = prepare_day_result(grouped_points)
result[day] = day_result
end
result
end
private
def point_date(point) = Time.zone.at(point.timestamp).to_date.to_s
def calculate_radius(center_point, group)
max_distance = group.map { |point| center_point.distance_to(point) }.max
(max_distance / 10.0).ceil * 10
end
def prepare_day_result(grouped_points)
result = {}
grouped_points.each do |group|
center_point = group.first
radius = calculate_radius(center_point, group)
key = "#{center_point.latitude},#{center_point.longitude},#{radius}m,#{group.size}"
result[key] = group.count
end
result
end
end

View file

@ -3,6 +3,19 @@
<div class="flex justify-between my-5">
<h1 class="font-bold text-4xl">Visits</h1>
<div role="tablist" class="tabs tabs-boxed">
<%= link_to 'Confirmed', visits_path(status: :confirmed), role: 'tab',
class: "tab #{active_tab?(visits_path(status: :confirmed))}" %>
<%= link_to visits_path(status: :suggested), role: 'tab',
class: "tab #{active_tab?(visits_path(status: :suggested))}" do %>
Suggested
<% if @suggested_visits_count.positive? %>
<span class="badge badge-sm badge-primary mx-1"><%= @suggested_visits_count %></span>
<% end %>
<% end %>
<%= link_to 'Declined', visits_path(status: :declined), role: 'tab',
class: "tab #{active_tab?(visits_path(status: :declined))}" %>
</div>
<div class="flex items-center">
<span class="mr-2">Order by:</span>
<%= link_to 'Newest', visits_path(order_by: :desc), class: 'btn btn-xs btn-primary mx-1' %>
@ -16,7 +29,7 @@
<div class="max-w-md">
<h1 class="text-5xl font-bold">Hello there!</h1>
<p class="py-6">
Here you'll find your visits, but now there are none. Create some areas on your map and pretty soon you'll see visit suggestions on this page!
Here you'll find your <%= params[:status] %> visits, but now there are none. Create some areas on your map and pretty soon you'll see visit suggestions on this page!
</p>
</div>
</div>
@ -49,19 +62,49 @@
<div class="group relative">
<div class="flex items-center justify-between">
<div>
<div class="text-lg font-black <%= 'underline decoration-dotted' if visit.pending? %>">
<div class="text-lg font-black <%= 'underline decoration-dotted' if visit.suggested? %>">
<%= visit.area.name %>
</div>
<div>
<%= "#{visit.started_at.strftime('%H:%M')} - #{visit.ended_at.strftime('%H:%M')}" %>
</div>
</div>
<% if visit.pending? %>
<div class="opacity-0 transition-opacity duration-300 group-hover:opacity-100 flex items-center ml-4">
<%= button_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-success mr-1' %>
<%= button_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-error' %>
<div class="opacity-0 transition-opacity duration-300 group-hover:opacity-100 flex items-center ml-4">
<% if visit.suggested? %>
<%= button_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-success' %>
<%= button_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-error mx-1' %>
<% end %>
<!-- The button to open modal -->
<label for="visit_details_popup_<%= visit.id %>" class='btn btn-xs btn-info'>Map</label>
<!-- Put this part before </body> tag -->
<input type="checkbox" id="visit_details_popup_<%= visit.id %>" class="modal-toggle" />
<div class="modal" role="dialog">
<div class="modal-box w-10/12">
<h3 class="text-lg font-bold">
<%= visit.area_name %>, <%= visit.started_at.strftime('%d.%m.%Y') %>, <%= visit.started_at.strftime('%H:%M') %> - <%= visit.ended_at.strftime('%H:%M') %>
</h3>
<div class="flex justify-between my-5">
<div><a class='link'><</a> Cafe, Sterndamm 86D <a class='link'>></a>, <%= visit.points.count %></div>
<div class='flex'>
<%= button_to 'Confirm', visit_path(visit, 'visit[status]': :confirmed), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-success' %>
<%= button_to 'Decline', visit_path(visit, 'visit[status]': :declined), method: :patch, data: { turbo: false }, class: 'btn btn-xs btn-error mx-1' %>
</div>
</div>
<div class='w-full h-[25rem]'
data-controller="visit-modal-map"
data-coordinates="<%= visit.coordinates %>"
data-radius="<%= visit.area.radius %>"
data-center="<%= visit.area.center %>"
>
<div data-visit-modal-map-target="container" class="h-[25rem] w-auto h-96">
</div>
</div>
</div>
<label class="modal-backdrop" for="visit_details_popup_<%= visit.id %>">Close</label>
</div>
<% end %>
</div>
</div>
</div>
<% end %>

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
Geocoder.configure(
config = {
# geocoding service request timeout, in seconds (default 3):
# timeout: 5,
@ -13,4 +13,11 @@ Geocoder.configure(
expiration: 1.day # Defaults to `nil`
# prefix: "another_key:" # Defaults to `geocoder:`
}
)
}
if ENV['GOOGLE_PLACES_API_KEY'].present?
config[:lookup] = :google
config[:api_key] = ENV['GOOGLE_PLACES_API_KEY']
end
Geocoder.configure(config)

View file

@ -1,11 +1,16 @@
# config/schedule.yml
stat_creating_job:
cron: "0 */6 * * *"
cron: "0 */6 * * *" # every 6 hours
class: "StatCreatingJob"
queue: default
area_visits_calculation_scheduling_job:
cron: "0 * * * *" # every hour
cron: "0 0 * * *" # every day at 0:00
class: "AreaVisitsCalculationSchedulingJob"
queue: default
visit_suggesting_job:
cron: "0 1 * * *" # every day at 1:00
class: "VisitSuggestingJob"
queue: default

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CreatePlaces < ActiveRecord::Migration[7.1]
def change
create_table :places do |t|
t.string :name, null: false
t.decimal :longitude, precision: 10, scale: 6, null: false
t.decimal :latitude, precision: 10, scale: 6, null: false
t.string :city
t.string :country
t.integer :source, default: 0
t.jsonb :geodata, default: {}, null: false
t.datetime :reverse_geocoded_at
t.timestamps
end
end
end

18
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_07_21_183116) do
ActiveRecord::Schema[7.1].define(version: 2024_08_05_150111) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -53,6 +53,9 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_21_183116) do
t.index ["user_id"], name: "index_areas_on_user_id"
end
create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t|
end
create_table "exports", force: :cascade do |t|
t.string "name", null: false
t.string "url"
@ -90,6 +93,19 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_21_183116) do
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "places", force: :cascade do |t|
t.string "name", null: false
t.decimal "longitude", precision: 10, scale: 6, null: false
t.decimal "latitude", precision: 10, scale: 6, null: false
t.string "city"
t.string "country"
t.integer "source", default: 0
t.jsonb "geodata", default: {}, null: false
t.datetime "reverse_geocoded_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "points", force: :cascade do |t|
t.integer "battery_status"
t.string "ping"

View file

@ -47,6 +47,7 @@ services:
APPLICATION_HOSTS: localhost
TIME_ZONE: Europe/London
APPLICATION_PROTOCOL: http
GOOGLE_PLACES_API_KEY: ''
logging:
driver: "json-file"
options:
@ -79,6 +80,7 @@ services:
APPLICATION_HOSTS: localhost
BACKGROUND_PROCESSING_CONCURRENCY: 10
APPLICATION_PROTOCOL: http
GOOGLE_PLACES_API_KEY: ''
logging:
driver: "json-file"
options:

5
spec/factories/places.rb Normal file
View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :place do
name { "MyString" }
end
end

View file

@ -8,6 +8,6 @@ FactoryBot.define do
ended_at { Time.zone.now + 1.hour }
duration { 1.hour }
name { 'Visit' }
status { 'pending' }
status { 'suggested' }
end
end

View file

@ -4,7 +4,7 @@ require 'rails_helper'
RSpec.describe ReverseGeocodingJob, type: :job do
describe '#perform' do
subject(:perform) { described_class.new.perform(point.id) }
subject(:perform) { described_class.new.perform('Point', point.id) }
let(:point) { create(:point) }
@ -19,12 +19,12 @@ RSpec.describe ReverseGeocodingJob, type: :job do
expect { perform }.not_to(change { point.reload.city })
end
it 'does not call ReverseGeocoding::FetchData' do
allow(ReverseGeocoding::FetchData).to receive(:new).and_call_original
it 'does not call ReverseGeocoding::Points::FetchData' do
allow(ReverseGeocoding::Points::FetchData).to receive(:new).and_call_original
perform
expect(ReverseGeocoding::FetchData).not_to have_received(:new)
expect(ReverseGeocoding::Points::FetchData).not_to have_received(:new)
end
end
@ -35,11 +35,11 @@ RSpec.describe ReverseGeocodingJob, type: :job do
it 'calls Geocoder' do
allow(Geocoder).to receive(:search).and_return([stubbed_geocoder])
allow(ReverseGeocoding::FetchData).to receive(:new).and_call_original
allow(ReverseGeocoding::Points::FetchData).to receive(:new).and_call_original
perform
expect(ReverseGeocoding::FetchData).to have_received(:new).with(point.id)
expect(ReverseGeocoding::Points::FetchData).to have_received(:new).with(point.id)
end
end
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe VisitSuggestingJob, type: :job do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe Place, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -49,6 +49,13 @@ RSpec.describe Point, type: :model do
it 'enqueues ReverseGeocodeJob' do
expect { point.async_reverse_geocode }.to have_enqueued_job(ReverseGeocodingJob)
end
it 'enqueues ReverseGeocodeJob with correct arguments' do
point.save
expect { point.async_reverse_geocode }.to have_enqueued_job(ReverseGeocodingJob)
.with('Point', point.id)
end
end
end
end

View file

@ -17,11 +17,83 @@ RSpec.describe '/visits', type: :request do
expect(response).to be_successful
end
context 'with confirmed visits' do
let!(:confirmed_visits) { create_list(:visit, 3, user:, status: :confirmed) }
it 'returns confirmed visits' do
get visits_url
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).to match_array(confirmed_visits)
end
end
context 'with suggested visits' do
let!(:suggested_visits) { create_list(:visit, 3, user:, status: :suggested) }
it 'does not return suggested visits' do
get visits_url
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).not_to include(suggested_visits)
end
it 'returns suggested visits' do
get visits_url, params: { status: 'suggested' }
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).to match_array(suggested_visits)
end
end
context 'with declined visits' do
let!(:declined_visits) { create_list(:visit, 3, user:, status: :declined) }
it 'does not return declined visits' do
get visits_url
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).not_to include(declined_visits)
end
it 'returns declined visits' do
get visits_url, params: { status: 'declined' }
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).to match_array(declined_visits)
end
end
context 'with suggested visits' do
let!(:suggested_visits) { create_list(:visit, 3, user:, status: :suggested) }
it 'does not return suggested visits' do
get visits_url
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).not_to include(suggested_visits)
end
it 'returns suggested visits' do
get visits_url, params: { status: 'suggested' }
expect(@controller.instance_variable_get(:@visits).map do |v|
v[:visits]
end.flatten).to match_array(suggested_visits)
end
end
end
describe 'PATCH /update' do
context 'with valid parameters' do
let(:visit) { create(:visit, user:, status: :pending) }
let(:visit) { create(:visit, user:, status: :suggested) }
it 'confirms the requested visit' do
patch visit_url(visit), params: { visit: { status: :confirmed } }

View file

@ -15,7 +15,7 @@ RSpec.describe Jobs::Create do
described_class.new(job_name, user.id).call
points.each do |point|
expect(ReverseGeocodingJob).to have_received(:perform_later).with(point.id)
expect(ReverseGeocodingJob).to have_received(:perform_later).with(point.class.to_s, point.id)
end
end
end
@ -33,7 +33,7 @@ RSpec.describe Jobs::Create do
described_class.new(job_name, user.id).call
points_without_address.each do |point|
expect(ReverseGeocodingJob).to have_received(:perform_later).with(point.id)
expect(ReverseGeocodingJob).to have_received(:perform_later).with(point.class.to_s, point.id)
end
end
end

View file

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe ReverseGeocoding::FetchData do
RSpec.describe ReverseGeocoding::Points::FetchData do
subject(:fetch_data) { described_class.new(point.id).call }
let(:point) { create(:point) }