diff --git a/CHANGELOG.md b/CHANGELOG.md index 82f0d885e..dc99b9a54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,17 @@ how this file is formatted and how the process works. ## [Unreleased] ### Added - added `currency_code` to the list of supported attributes for booking webhook and for `API::Controllers::Params::Booking` object +- add `BackgroundWorker` and related refactor on queue processing. +- add `Workers::CalendarSynchronisation` to deal with updating the calendar of availabilities. ### Changed - determine Roomorama API environment according to value in environment variable. -- fixed SafeAccessHash#get method to return nil for unexisting keys +- fixed `SafeAccessHash#get` method to return `nil` for nonexistent keys +- new structure for the `config/suppliers.yml` file. +- `Workers::Synchronisation` is now `Workers::PropertySynchronisation`. + +### Removed +- `update_calendar` method removed from `Roomorama::Property`. ## [0.4.4] - 2016-07-07 ### Added diff --git a/apps/web/templates/external_errors/events/_missing_basic_data.html.erb b/apps/web/templates/external_errors/events/_missing_basic_data.html.erb index 159e89d62..18b657f1a 100644 --- a/apps/web/templates/external_errors/events/_missing_basic_data.html.erb +++ b/apps/web/templates/external_errors/events/_missing_basic_data.html.erb @@ -1,9 +1,9 @@
<%= render partial: "external_errors/events/timestamp", locals: { event: event } %> -

Missing Basic Property Data

-

Concierge detected that basic information required for property publishing - was missing, and therefore the property was not sent to Roomorama's API.

+

Missing Basic Data

+

Concierge detected that basic information required for the synchronisation process + was missing, and therefore the request was not sent to Roomorama's API.

The error message when parsing the data for the property was:

@@ -11,7 +11,7 @@ <%= event[:error_message] %>
-

The formatted data for this property was:

+

The formatted data for this request was:

<% content_type = "application/json" diff --git a/apps/web/templates/external_errors/events/_sync_process.html.erb b/apps/web/templates/external_errors/events/_sync_process.html.erb index f7bfb943b..d074b582e 100644 --- a/apps/web/templates/external_errors/events/_sync_process.html.erb +++ b/apps/web/templates/external_errors/events/_sync_process.html.erb @@ -10,7 +10,14 @@ else description = "of ID #{host.id}, no longer in the database" end + + if event[:worker] == "metadata" + type = "property metadata" + else + type = "availabilities" + end %> -

The synchronisation process started for property <%= event[:identifier] %>, + +

The <%= type %> synchronisation process started for property <%= event[:identifier] %>, from host <%= description %>.

diff --git a/apps/workers/calendar_synchronisation.rb b/apps/workers/calendar_synchronisation.rb new file mode 100644 index 000000000..2b6391ab4 --- /dev/null +++ b/apps/workers/calendar_synchronisation.rb @@ -0,0 +1,159 @@ +module Workers + + # +Workers::CalendarSynchronisation+ + # + # Organizes the updating of the availabilities calendar calendar at Roomorama. + # It is able to validate the calendar for required data presence and perform API + # calls to Roomorama. + # + # Usage + # + # host = Host.last + # sync = Workers::CalendarSynchronisation.new(host) + # + # # supplier API is called, availabilities/rates data is fetched + # sync.start(property.identifier) do + # remote_calendar = fetch_calendar(property) + # calendar = Roomorama::Calendar.new(property.identifier) + # remote_calendar[:dates].each do |data| + # entry = Roomorama::Calendar::Entry.new( + # date: data[:date], + # available: data[:available], + # nightly_rate: data[:nightly_price] + # ) + # calendar.add(entry) + # end + # end + # + # sync.finish! # => synchronisation process record is stored on Concierge + class CalendarSynchronisation + + # the worker type to be stored in the sync processes table for later analysis. + WORKER_TYPE = "availabilities" + + attr_reader :host, :sync_record, :processed, :counters + + AvailabilityCounters = Struct.new(:available, :unavailable) + + # host - an instance of +Host+. + def initialize(host) + @host = host + @sync_record = init_sync_record(host) + @processed = 0 + @counters = AvailabilityCounters.new(0, 0) + end + + # starts the process of fetching the calendar of availabilities for + # a given property. + # + # +identifier+ - the property identifier for which the calendar is being processed. + # + # A block should be given to this method and it should return an instance of +Result+. + # When fetching and parsing the availabilities calendar from the supplier is successful, + # the +Result+ instance should wrap a +Roomorama::Calendar+ instance. If the returned + # result is not successful, an external error is persisted to the database and the method + # terminates with no API calls being performed to Roomorama. + def start(identifier) + initialize_context(identifier) + result = yield(self) + @processed += 1 + + if result.success? + calendar = result.value + process(calendar) + else + announce_failure(result) + false + end + end + + # terminates the calendar synchronisation process. This method saves a +SyncProcess+ + # record on the database for later analysis, including the number of properties + # processed, as well as the number of available/unavailable records created. + def finish! + database = Concierge::OptionalDatabaseAccess.new(SyncProcessRepository) + + sync_record.stats[:properties_processed] = processed + sync_record.stats[:available_records] = counters.available + sync_record.stats[:unavailable_records] = counters.unavailable + + sync_record.finished_at = Time.now + + database.create(sync_record) + true + end + + private + + def process(calendar) + calendar.validate! + update_counters(calendar) + operation = Roomorama::Client::Operations.update_calendar(calendar) + + run_operation(operation) + rescue Roomorama::Error => err + missing_data(err.message, calendar.to_h) + announce_failure(Result.error(:missing_data)) + false + end + + def update_counters(calendar) + data = calendar.to_h + mapping = data[:availabilities].to_s.chars.group_by(&:to_s) + + counters.available += Array(mapping["1"]).size + counters.unavailable += Array(mapping["0"]).size + end + + def initialize_context(identifier) + Concierge.context = Concierge::Context.new(type: "batch") + + sync_process = Concierge::Context::SyncProcess.new( + worker: WORKER_TYPE, + host_id: host.id, + identifier: identifier + ) + + Concierge.context.augment(sync_process) + end + + def run_operation(operation) + # enable context tracking when performing API calls to Roomorama so that + # any errors during the request can be logged. + Concierge.context.enable! + + result = Workers::OperationRunner.new(host).perform(operation, operation.calendar) + announce_failure(result) unless result.success? + end + + def missing_data(message, attributes) + missing_data = Concierge::Context::MissingBasicData.new( + error_message: message, + attributes: attributes + ) + + Concierge.context.augment(missing_data) + end + + def announce_failure(result) + Concierge::Announcer.trigger(Concierge::Errors::EXTERNAL_ERROR, { + operation: "sync", + supplier: SupplierRepository.find(host.supplier_id).name, + code: result.error.code, + context: Concierge.context.to_h, + happened_at: Time.now, + }) + end + + def init_sync_record(host) + SyncProcess.new( + type: WORKER_TYPE, + host_id: host.id, + started_at: Time.now, + successful: true, + stats: {} + ) + end + + end +end diff --git a/apps/workers/operation_runner.rb b/apps/workers/operation_runner.rb index 3b790cf3c..355e635b6 100644 --- a/apps/workers/operation_runner.rb +++ b/apps/workers/operation_runner.rb @@ -43,9 +43,7 @@ def initialize(host) # Extra arguments depend on the specific implementation of the runner. # Check particular classes for more information. def perform(operation, *args) - runner_for(operation).perform(*args).tap do |result| - update_next_run if result.success? - end + runner_for(operation).perform(*args) end private @@ -58,6 +56,8 @@ def runner_for(operation) Workers::OperationRunner::Diff.new(host, operation, roomorama_client) when Roomorama::Client::Operations::Disable Workers::OperationRunner::Disable.new(host, operation, roomorama_client) + when Roomorama::Client::Operations::UpdateCalendar + Workers::OperationRunner::UpdateCalendar.new(operation, roomorama_client) else raise InvalidOperationError.new(operation) end @@ -68,8 +68,7 @@ def roomorama_client end # if the result of performing the operation was successful, we update - # the timestamp when the next synchronisation for the given host - # should happen. + # the timestamp when the next execution of the worker should happen. def update_next_run one_day = 24 * 60 * 60 # TODO make this a dynamic value host.next_run_at = Time.now + one_day diff --git a/apps/workers/operation_runner/update_calendar.rb b/apps/workers/operation_runner/update_calendar.rb new file mode 100644 index 000000000..a2ec3d60e --- /dev/null +++ b/apps/workers/operation_runner/update_calendar.rb @@ -0,0 +1,29 @@ +class Workers::OperationRunner + + # +Workers::OperationRunner::UpdateCalendar+ + # + # This class encapsulates the operation of updating the availabilities calendar + # of a property on Roomorama. + class UpdateCalendar + + attr_reader :operation, :roomorama_client + + # operation - a +Roomorama::Client::Operations::Diff+ instance + # client - a +Roomorama::Client+ instance properly configured + def initialize(operation, client) + @operation = operation + @roomorama_client = client + end + + # calendar - a +Roomorama::Calendar+ instance representing the changes to + # be applied to a property's availability calendar. + # + # Returns a +Result+ that, when successful, wraps the given calendar instance. + def perform(calendar) + result = roomorama_client.perform(operation) + return result unless result.success? + + Result.new(calendar) + end + end +end diff --git a/apps/workers/processor.rb b/apps/workers/processor.rb index e375528a8..099b6cece 100644 --- a/apps/workers/processor.rb +++ b/apps/workers/processor.rb @@ -57,8 +57,8 @@ def process! return message unless message.success? element = Concierge::SafeAccessHash.new(message.value) - if element[:operation] == "sync" - perform_sync(element[:data]) + if element[:operation] == "background_worker" + run_worker(element[:data]) else raise UnknownOperationError.new(element[:operation]) end @@ -66,14 +66,18 @@ def process! private - def perform_sync(args) - timing_out("sync", args) do - host = HostRepository.find(args[:host_id]) - supplier = SupplierRepository.find(host.supplier_id) - broadcast = ["sync", ".", supplier.name].join + def run_worker(args) + worker = BackgroundWorkerRepository.find(args[:background_worker_id]) - Concierge::Announcer.trigger(broadcast, host) - Result.new(true) + running(worker) do + timing_out(worker.type, args) do + host = HostRepository.find(worker.host_id) + supplier = SupplierRepository.find(host.supplier_id) + broadcast = [worker.type, ".", supplier.name].join + + Concierge::Announcer.trigger(broadcast, host) + Result.new(true) + end end end @@ -89,10 +93,37 @@ def timing_out(operation, params) Result.error(:timeout) end + # coordinates the +BackgroundWorker+ instance status and timestamps by changing + # the worker status to +running+, yielding the block (which is supposed to do + # the worker's job), and ensuring that the worker's status is set back to +idle+ + # at the end of the process, as well as properly updating the +next_run_at+ column + # according to the specified worker +interval+. + def running(worker) + worker_started(worker) + yield + + ensure + # reload the worker instance to make sure to account for any possible + # changes in the process + worker_completed(BackgroundWorkerRepository.find(worker.id)) + end + def message @message = json_decode(payload) end + def worker_started(worker) + worker.status = "running" + BackgroundWorkerRepository.update(worker) + end + + def worker_completed(worker) + worker.status = "idle" + worker.next_run_at = Time.now + worker.interval + + BackgroundWorkerRepository.update(worker) + end + # NOTE this time out should be shorter than the +VisibilityTimeout+ configured # on the SQS queue to be used by Concierge. def processing_timeout diff --git a/apps/workers/synchronisation.rb b/apps/workers/property_synchronisation.rb similarity index 89% rename from apps/workers/synchronisation.rb rename to apps/workers/property_synchronisation.rb index d9d03b63f..c9591104e 100644 --- a/apps/workers/synchronisation.rb +++ b/apps/workers/property_synchronisation.rb @@ -1,6 +1,6 @@ module Workers - # +Workers::Synchronisation+ + # +Workers::PropertySynchronisation+ # # Organizes the synchronisation process for a given host from supplier. # It is able to route processed property to the adequate operation @@ -8,10 +8,14 @@ module Workers # that were published before but are no longer contained in the list # of properties for that host. # + # Handles exclusively creation/updates of property details. For changes + # on the availabilities calendar of a property, check the +Workers::CalendarSynchronisation+ + # class. + # # Usage # # host = Host.last - # sync = Workers::Synchronisation.new(host) + # sync = Workers::PropertySynchronisation.new(host) # # # supplier API is called, raw data is fetched # properties = fetch_properties @@ -25,7 +29,11 @@ module Workers # end # # sync.finish! # => non-processed properties are deleted at the end of the process. - class Synchronisation + class PropertySynchronisation + + # the kind of background worker that identifies property metadata synchronisation. + # Calendar availabilities is tackled by +Workers::CalendarSynchronisation+ + WORKER_TYPE = "metadata" PropertyCounters = Struct.new(:created, :updated, :deleted) @@ -137,6 +145,7 @@ def initialize_context(identifier) Concierge.context = Concierge::Context.new(type: "batch") sync_process = Concierge::Context::SyncProcess.new( + worker: WORKER_TYPE, host_id: host.id, identifier: identifier ) @@ -168,10 +177,10 @@ def purge_properties def save_sync_process database = Concierge::OptionalDatabaseAccess.new(SyncProcessRepository) - sync_record.properties_created = counters.created - sync_record.properties_updated = counters.updated - sync_record.properties_deleted = counters.deleted - sync_record.finished_at = Time.now + sync_record.stats[:properties_created] = counters.created + sync_record.stats[:properties_updated] = counters.updated + sync_record.stats[:properties_deleted] = counters.deleted + sync_record.finished_at = Time.now database.create(sync_record) end @@ -223,9 +232,11 @@ def update_counters(operation) def init_sync_record(host) SyncProcess.new( + type: WORKER_TYPE, host_id: host.id, started_at: Time.now, - successful: true + successful: true, + stats: {} ) end diff --git a/apps/workers/queue/element.rb b/apps/workers/queue/element.rb index ad78ffb46..8ddd87b27 100644 --- a/apps/workers/queue/element.rb +++ b/apps/workers/queue/element.rb @@ -32,7 +32,7 @@ def initialize(operation) # defines the list of possible queue operations that can be wrapped in # +Element+ instances. - SUPPORTED_OPERATIONS = %w(sync) + SUPPORTED_OPERATIONS = %w(background_worker) attr_reader :operation, :data diff --git a/apps/workers/scheduler.rb b/apps/workers/scheduler.rb index ce1cf0ad3..78520b418 100644 --- a/apps/workers/scheduler.rb +++ b/apps/workers/scheduler.rb @@ -2,7 +2,7 @@ module Workers # +Workers::Scheduler+ # - # This class is responsible for checking which hosts should be synchronised + # This class is responsible for checking which background workers should be run # (i.e., the +next_run_at+ column is either +nil+ or in a time in the past), # and enqueue the correspondent message to the queue to start the # synchronisation process. @@ -25,10 +25,10 @@ def initialize(logger: default_logger) # queries the +hosts+ table to identify which host should be synchronised, # enqueueing messages if necessary. def trigger_pending! - HostRepository.pending_synchronisation.each do |host| - log_event(host) - update_timestamp(host) - enqueue(host) + BackgroundWorkerRepository.pending.each do |background_worker| + log_event(background_worker) + mark_running(background_worker) + enqueue(background_worker) end end @@ -38,28 +38,27 @@ def default_logger ::Logger.new(LOG_PATH) end - # updates the timestamp for next synchronisation to avoid enqueueing the same - # hosts multiple times unnecessarily. If the synchronisation is performed - # successfully by the worker, this timestamp is advanced again. - def update_timestamp(host) - three_hours_from_now = Time.now + 3 * 60 * 60 - host.next_run_at = three_hours_from_now - - HostRepository.update(host) + # updates the background worker status to +running+ so that it will not + # be enqueued again while it performs its task. + def mark_running(worker) + worker.status = "running" + BackgroundWorkerRepository.update(worker) end - def enqueue(host) + def enqueue(worker) element = Workers::Queue::Element.new( - operation: "sync", - data: { host_id: host.id } + operation: "background_worker", + data: { background_worker_id: worker.id } ) queue.add(element) end - def log_event(host) + def log_event(worker) + host = HostRepository.find(worker.host_id) + message = "action=%s host.username=%s host.identifier=%s" % - ["sync", host.username, host.identifier] + [worker.type, host.username, host.identifier] logger.info(message) end diff --git a/apps/workers/suppliers/atleisure.rb b/apps/workers/suppliers/atleisure.rb index 1bec65bcb..53f84b406 100644 --- a/apps/workers/suppliers/atleisure.rb +++ b/apps/workers/suppliers/atleisure.rb @@ -10,7 +10,7 @@ class AtLeisure def initialize(host) @host = host - @synchronisation = Workers::Synchronisation.new(host) + @synchronisation = Workers::PropertySynchronisation.new(host) end def perform @@ -99,6 +99,6 @@ def announce_error(message, result) end # listen supplier worker -Concierge::Announcer.on("sync.AtLeisure") do |host| +Concierge::Announcer.on("metadata.AtLeisure") do |host| Workers::Suppliers::AtLeisure.new(host).perform end diff --git a/config/mapping.rb b/config/mapping.rb index 96c6c45a4..d4c8e4468 100644 --- a/config/mapping.rb +++ b/config/mapping.rb @@ -52,7 +52,6 @@ attribute :identifier, String attribute :username, String attribute :access_token, String - attribute :next_run_at, Time attribute :created_at, Time attribute :updated_at, Time end @@ -73,14 +72,27 @@ entity SyncProcess repository SyncProcessRepository - attribute :id, Integer - attribute :host_id, Integer - attribute :started_at, Time - attribute :finished_at, Time - attribute :successful, Boolean - attribute :properties_created, Integer - attribute :properties_updated, Integer - attribute :properties_deleted, Integer - attribute :created_at, Time - attribute :updated_at, Time + attribute :id, Integer + attribute :host_id, Integer + attribute :started_at, Time + attribute :finished_at, Time + attribute :successful, Boolean + attribute :stats, Concierge::PGJSON + attribute :type, String + attribute :created_at, Time + attribute :updated_at, Time +end + +collection :background_workers do + entity BackgroundWorker + repository BackgroundWorkerRepository + + attribute :id, Integer + attribute :host_id, Integer + attribute :next_run_at, Time + attribute :interval, Integer + attribute :type, String + attribute :status, String + attribute :created_at, Time + attribute :updated_at, Time end diff --git a/config/suppliers.yml b/config/suppliers.yml index a4bcbf1a9..16e5cbdbf 100644 --- a/config/suppliers.yml +++ b/config/suppliers.yml @@ -7,4 +7,14 @@ # Only suppliers for which the full integration flow is completed should be # added here (properties synchronisation as well as webhooks.) -- AtLeisure \ No newline at end of file +AtLeisure: + workers: + metadata: + every: "1d" + availabilities: + every: "5h" + +WayToStay: + workers: + metadata: + every: "1d" diff --git a/db/migrations/20160708081828_create_background_worker.rb b/db/migrations/20160708081828_create_background_worker.rb new file mode 100644 index 000000000..d7818dfc5 --- /dev/null +++ b/db/migrations/20160708081828_create_background_worker.rb @@ -0,0 +1,20 @@ +Hanami::Model.migration do + change do + create_table :background_workers do + primary_key :id + + foreign_key :host_id, :hosts, null: false + + column :next_run_at, Time + column :interval, Integer, null: false + column :type, String, null: false + column :status, String, null: false + + column :created_at, Time, null: false + column :updated_at, Time, null: false + + # only one worker type per host should exist + index [:host_id, :type], unique: true + end + end +end diff --git a/db/migrations/20160711091004_remove_null_inconsistencies.rb b/db/migrations/20160711091004_remove_null_inconsistencies.rb new file mode 100644 index 000000000..23e2210ea --- /dev/null +++ b/db/migrations/20160711091004_remove_null_inconsistencies.rb @@ -0,0 +1,39 @@ +# alters the foreignkey delcarations to drop the +on_delete: set_null+ option +# since that contradicts the +null: false+ option passed to those columns. + +Hanami::Model.migration do + up do + alter_table :hosts do + drop_foreign_key :supplier_id + add_foreign_key :supplier_id, :suppliers, null: false + end + + alter_table :properties do + drop_foreign_key :host_id + add_foreign_key :host_id, :hosts, null: false + end + + alter_table :sync_processes do + drop_foreign_key :host_id + add_foreign_key :host_id, :hosts, null: false + end + end + + down do + alter_table :hosts do + drop_foreign_key :supplier_id + add_foreign_key :supplier_id, :suppliers, on_delete: :set_null, null: false + end + + alter_table :properties do + drop_foreign_key :host_id + add_foreign_key :host_id, :hosts, on_delete: :set_null, null: false + end + + alter_table :sync_processes do + drop_foreign_key :host_id + add_foreign_key :host_id, :hosts, on_delete: :set_null, null: false + end + end + +end diff --git a/db/migrations/20160711095043_drop_nex_run_at_from_hosts.rb b/db/migrations/20160711095043_drop_nex_run_at_from_hosts.rb new file mode 100644 index 000000000..16f76f4de --- /dev/null +++ b/db/migrations/20160711095043_drop_nex_run_at_from_hosts.rb @@ -0,0 +1,13 @@ +Hanami::Model.migration do + up do + alter_table :hosts do + drop_column :next_run_at + end + end + + down do + alter_table :hosts do + add_column :next_run_at, Time + end + end +end diff --git a/db/migrations/20160713021515_add_stats_to_sync_process.rb b/db/migrations/20160713021515_add_stats_to_sync_process.rb new file mode 100644 index 000000000..97ea6997e --- /dev/null +++ b/db/migrations/20160713021515_add_stats_to_sync_process.rb @@ -0,0 +1,23 @@ +Hanami::Model.migration do + up do + alter_table :sync_processes do + add_column :stats, JSON, null: false, default: Sequel.pg_json({}) + add_column :type, String, null: false + + drop_column :properties_created + drop_column :properties_updated + drop_column :properties_deleted + end + end + + down do + alter_table :sync_processes do + drop_column :stats + drop_column :type + + add_column :properties_created, Integer, null: false + add_column :properties_updated, Integer, null: false + add_column :properties_deleted, Integer, null: false + end + end +end diff --git a/lib/concierge/context/sync_process.rb b/lib/concierge/context/sync_process.rb index 96d6c73c6..f6a7c99b9 100644 --- a/lib/concierge/context/sync_process.rb +++ b/lib/concierge/context/sync_process.rb @@ -15,9 +15,10 @@ class SyncProcess CONTEXT_TYPE = "sync_process" - attr_reader :host_id, :identifier, :timestamp + attr_reader :worker, :host_id, :type, :identifier, :timestamp - def initialize(host_id:, identifier:) + def initialize(worker:, host_id:, identifier:) + @worker = worker @host_id = host_id @identifier = identifier @timestamp = Time.now @@ -26,6 +27,7 @@ def initialize(host_id:, identifier:) def to_h { type: CONTEXT_TYPE, + worker: worker, timestamp: timestamp, host_id: host_id, identifier: identifier diff --git a/lib/concierge/entities/background_worker.rb b/lib/concierge/entities/background_worker.rb new file mode 100644 index 000000000..c45aec3b5 --- /dev/null +++ b/lib/concierge/entities/background_worker.rb @@ -0,0 +1,41 @@ +# +BackgroundWorker+ +# +# This entity represents a type of background, synchronising worker associated +# with a given host. Hosts can have multiple workers related to synchronising +# property metadata as well as the calendar of availabilities. However, for some +# suppliers, calendar information is provided alongside property metadata, making +# it redundant to have separate workers for each. This class makes it easy to +# associate differents kinds of background workers as supplier integrations see fit. +# +# Attributes +# +# +id+ - the ID of the the record on the database, automatically generated. +# +host_id+ - a foreign key to the +hosts+ table, indicating which host +# the background worker is associated with. +# +next_run_at+ - a timestamp that indicates when the background worker should be +# invoked next. +# +interval+ - how often (in seconds), the background worker should be invoked. +# +type+ - the type of synchronisation performed by the background worker. +# +status+ - the status of the worker, which indicate its activity at a given moment. +class BackgroundWorker + include Hanami::Entity + + # possible values for the +type+ column of a background worker: + # + # +metadata+ - where processing of property metadata is fetched, parsed and + # synchronised with Roomorama. Includes property images. + # +availabilities+ - processing of the calendar of availabilities for a property. + # Indicates availabilities and prices. + TYPES = %w(metadata availabilities) + + # possible statuses a worker can be in: + # + # +idle+ - the background worker is not being run, and the +next_run_at+ column + # stores a timestamp in the future. + # +running+ - the background worker is currently running and therefore should not + # be rescheduled. + STATUSES = %w(idle running) + + attributes :id, :host_id, :next_run_at, :interval, :type, :status, + :created_at, :updated_at +end diff --git a/lib/concierge/entities/host.rb b/lib/concierge/entities/host.rb index 26fbeb2c5..a66d39cdd 100644 --- a/lib/concierge/entities/host.rb +++ b/lib/concierge/entities/host.rb @@ -12,11 +12,9 @@ # * +username+ - the username of the user account on Roomorama. Present only # for identification purposes. # * +access_token+ - the API access token used to identify the host on Roomorama's API. -# * +next_run_at+ - a timestamp indicating when the properties belonging to this -# host should be synchronised next. class Host include Hanami::Entity attributes :id, :supplier_id, :identifier, :username, :access_token, - :next_run_at, :created_at, :updated_at + :created_at, :updated_at end diff --git a/lib/concierge/entities/sync_process.rb b/lib/concierge/entities/sync_process.rb index 8d106323b..62d721a58 100644 --- a/lib/concierge/entities/sync_process.rb +++ b/lib/concierge/entities/sync_process.rb @@ -5,6 +5,11 @@ # metadata associated with the synchronisation process, allowing later # analysis about how a given supplier behaves. # +# A synchronisation process can be related to property metadata publishing +# to Roomorama as well as keeping the availabilities calendar up to date +# (see +BackgroundWorker+). The +stats+ field collects statistics specific +# for these kinds of events. +# # Attributes # # +id+ - the ID of the the record on the database, automatically generated. @@ -14,15 +19,13 @@ # kicked in. # +finished_at+ - a timestamp that indicates the time when the synchronisation process # was completed. -# +properties_created+ - the number of properties published on Roomorama in a -# given synchronisation -# +properties_updated+ - the number of properties updated on Roomorama in a -# given synchronisation -# +properties_deleted+ - the number of properties deleted on Roomorama in a -# given synchronisation +# +type+ - the type of synchronisation process that was executed. +# +stats+ - a JSON field containing statistics about the synchronisation +# process. Differs depending on the +type+ of sync process. class SyncProcess include Hanami::Entity attributes :id, :host_id, :started_at, :finished_at, :properties_created, - :successful, :properties_updated, :properties_deleted, :created_at, :updated_at + :successful, :properties_updated, :properties_deleted, :type, :stats, + :created_at, :updated_at end diff --git a/lib/concierge/flows/external_error_creation.rb b/lib/concierge/flows/external_error_creation.rb index 5afa8f8f4..367581c2c 100644 --- a/lib/concierge/flows/external_error_creation.rb +++ b/lib/concierge/flows/external_error_creation.rb @@ -1,6 +1,6 @@ module Concierge::Flows - # +UseCases::ExtennalErrorCreation+ + # +Concierge::Flows::ExternalErrorCreation+ # # This use case class wraps the creation of an +ExternalError+ record, performing # attribute validations prior to triggering the database call. diff --git a/lib/concierge/flows/host_creation.rb b/lib/concierge/flows/host_creation.rb new file mode 100644 index 000000000..c7e2fc448 --- /dev/null +++ b/lib/concierge/flows/host_creation.rb @@ -0,0 +1,167 @@ +module Concierge::Flows + + # +Concierge::Flows::BackgroundWorkerCreation+ + # + # This class encapsulates the flow of creating a background worker for a given + # supplier, validating the parameters and interpreting human-readable interval + # times, such as "1d" for one day, and generating the resulting number of seconds + # to be stored in the database. + class BackgroundWorkerCreation + include Hanami::Validations + + INTERVAL_FORMAT = /^\s*(?\d+)\s*(?[smhd])\s*$/ + + attribute :host_id, presence: true + attribute :interval, presence: true, format: INTERVAL_FORMAT + attribute :type, presence: true, inclusion: BackgroundWorker::TYPES + attribute :status, presence: true, inclusion: BackgroundWorker::STATUSES + + def perform + if valid? + worker = find_existing || build_new + + # set the values passed to make sure that changes in the parameters update + # existing records. + worker.interval = interpret_interval(interval) + worker.type = type + worker.status = status + + worker = BackgroundWorkerRepository.persist(worker) + Result.new(worker) + else + Result.error(:invalid_parameters) + end + end + + private + + def find_existing + workers = BackgroundWorkerRepository.for_host(host).to_a + workers.find { |worker| worker.type == type } + end + + def build_new + BackgroundWorker.new(host_id: host_id) + end + + def interpret_interval(interval) + match = INTERVAL_FORMAT.match(interval) + absolute = match[:amount].to_i + + case match[:unit] + when "s" + absolute + when "m" + absolute * 60 + when "h" + absolute * 60 * 60 + when "d" + absolute * 60 * 60 * 24 + end + end + + def host + @host ||= HostRepository.find(host_id) + end + + def attributes + to_h + end + end + + # +Concierge::Flows::HostCreation+ + # + # This class encapsulates the creation of host, a including associated + # background workers. Hosts belong to a supplier and other related + # attributes. + class HostCreation + include Hanami::Validations + + attribute :supplier, presence: true + attribute :identifier, presence: true + attribute :username, presence: true + attribute :access_token, presence: true + + attr_reader :config_path + + # overrides the initializer definition by +Hanami::Validations+ to read the + # required +config_path+ option that indicates the path to the +suppliers.yml+ + # file to read the workers definition. + def initialize(attributes) + @config_path = attributes.delete(:config_path) + super + end + + # creates database records for the host, as well as associated workers. + # Parses the +config/suppliers.yml+ file to read the workers definition + # for the supplier the host belongs to. + def perform + if valid? + transaction do + host = create_host + workers_definition = find_workers_definition + + return workers_definition unless workers_definition.success? + + workers_definition.value.each do |type, data| + result = BackgroundWorkerCreation.new( + host_id: host.id, + interval: data.to_h["every"], + type: type.to_s, + status: "idle" + ).perform + + return result unless result.success? + end + + Result.new(host) + end + else + Result.error(:invalid_parameters) + end + end + + private + + def create_host + existing = HostRepository.from_supplier(supplier).identified_by(identifier).first + host = existing || Host.new(supplier_id: supplier.id, identifier: identifier) + + host.username = username + host.access_token = access_token + + HostRepository.persist(host) + end + + def find_workers_definition + definition = nil + + suppliers_config.find do |name, workers| + if name == supplier.name + definition = workers + end + end + + if definition + Result.new(definition["workers"].to_h) + else + Result.error(:no_workers_definition) + end + end + + def suppliers_config + @config ||= YAML.load_file(config_path) + end + + def transaction + result = nil + HostRepository.transaction { result = yield } + result + end + + def attributes + to_h + end + end + +end diff --git a/lib/concierge/repositories/background_worker_repository.rb b/lib/concierge/repositories/background_worker_repository.rb new file mode 100644 index 000000000..c52273238 --- /dev/null +++ b/lib/concierge/repositories/background_worker_repository.rb @@ -0,0 +1,35 @@ +# +BackgroundWorkerRepository+ +# +# Persistence and query methods for the +background_workers+ table. +class BackgroundWorkerRepository + include Hanami::Repository + + # returns the total number of properties stored in the database. + def self.count + query.count + end + + # returns a collection of background workers associated with the given +Host+ + # instance. + def self.for_host(host) + query do + where(host_id: host.id) + end + end + + # queries for idle background workers + def self.idle + query do + where(status: "idle") + end + end + + # queries for all background workers that are due for execution - that is, + # those whose +next_run_at+ column is +null+ or holds a timestamp in the past. + def self.pending + query do + where { next_run_at < Time.now }.or(next_run_at: nil).order(:next_run_at) + end.idle + end + +end diff --git a/lib/concierge/repositories/host_repository.rb b/lib/concierge/repositories/host_repository.rb index 1048ba89b..b2a274ac7 100644 --- a/lib/concierge/repositories/host_repository.rb +++ b/lib/concierge/repositories/host_repository.rb @@ -9,12 +9,18 @@ def self.count query.count end - # queries which hosts need to be synchronised at the current moment, by checking - # the +next_run_at+ column. If that column is +NULL+, it is also considered to - # be ready for synchronisation. - def self.pending_synchronisation + # queries for all hosts that belong to a given +supplier+ + def self.from_supplier(supplier) query do - where { next_run_at < Time.now }.or(next_run_at: nil).order(:next_run_at) + where(supplier_id: supplier.id) + end + end + + # queries for a host with the given +identifier+, which should be unique + # per supplier. + def self.identified_by(identifier) + query do + where(identifier: identifier) end end end diff --git a/lib/concierge/roomorama/calendar.rb b/lib/concierge/roomorama/calendar.rb new file mode 100644 index 000000000..26ca4984a --- /dev/null +++ b/lib/concierge/roomorama/calendar.rb @@ -0,0 +1,177 @@ +require_relative "error" + +module Roomorama + + # +Roomorama::Calendar+ + # + # This class represents the availabilities calendar for a given property from + # a supplier. It is able to store availabilities, rates and check-in/check-out rules + # for a range of dates. + # + # Usage + # + # calendar = Roomorama::Calendar.new(property_identifier) + # entry = Roomorama::Calendar::Entry.new( + # date: "2016-05-22", + # available: true, + # nightly_rate: 100, + # weekly_rate: 500, + # monthly_rate: 1000, + # checkin_allowed: true, + # checkout_allowed: false + # ) + # + # calendar.add(entry) + # + # Note that +weekly_rate+, +monthly_rate+, +checkin_allowed+ and +checkout_allowed+ + # are optional fields in +Roomorama::Calendar::Entry+. If the supplier API does not + # provide them, defaults should *not* be assumed and the fields can be left blank. + class Calendar + class Entry + include Hanami::Validations + + DATE_FORMAT = /\d\d\d\d-\d\d-\d\d/ + + attribute :date, presence: true, type: Date, format: DATE_FORMAT + attribute :available, presence: true, type: Boolean + attribute :nightly_rate, presence: true + attribute :weekly_rate + attribute :monthly_rate + attribute :checkin_allowed + attribute :checkout_allowed + + def initialize(attributes) + initialize_checkin_rules(attributes) + super + end + + private + + # if specific values for +checkin_allowed+ and +checkout_allowed+ are not + # given, this sets these attributes to +true+. + def initialize_checkin_rules(attributes) + [:checkin_allowed, :checkout_allowed].each do |name| + unless attributes.has_key?(name) + attributes[name] = true + end + end + end + end + + # +Roomorama::Calendar::ValidationError+ + # + # Raised when a calendar fails to meet expected parameter requirements. + class ValidationError < Roomorama::Error + def initialize(message) + super("Calendar validation error: #{message}") + end + end + + attr_reader :property_identifier, :entries + + # identifier - the identifier property identifier to which the calendar refers to. + def initialize(identifier) + @entries = [] + @property_identifier = identifier + end + + # includes a new calendar entry in the calendar instance. +entry+ is expected + # to be a +Roomorama::Calendar::Entry+ instance. + def add(entry) + entries << entry + end + + # validates if all entries passed to this calendar instance via +add+ are valid. + # In case one of them is not, this method will raise a +Roomorama::Calendar::ValidationError+ + # error. + def validate! + entries.all?(&:valid?) || (raise ValidationError.new("One of the entries miss required parameters.")) + end + + def to_h + parsed = parse_entries + + { + identifier: property_identifier, + start_date: entries.select(&:valid?).min_by(&:date).date.to_s, + availabilities: parsed.availabilities, + nightly_rates: parsed.rates.nightly, + weekly_rates: parsed.rates.weekly, + monthly_rates: parsed.rates.monthly, + checkin_allowed: parsed.checkin_rules, + checkout_allowed: parsed.checkout_rules + } + end + + private + + Rates = Struct.new(:nightly, :weekly, :monthly) + ParsedEntries = Struct.new(:availabilities, :checkin_rules, :checkout_rules, :rates) + + # parses the collection of +entries+ given on the lifecycle of this instance, + # and builds a +Roomorama::Calendar::ParsedEntries+ instance, containing data + # after parsing. + def parse_entries + sorted_entries = entries.select(&:valid?).sort_by(&:date) + start_date = sorted_entries.first.date + end_date = sorted_entries.last.date + + rates = Rates.new([], [], []) + parsed_entries = ParsedEntries.new("", "", "", rates) + + # index all entries by date, to make the lookup for a given date faster. + # Index once, and then all lookups can be performed in constant time, + # as opposed to scanning the list of entries on every iteration. + # + # Implementation of what Rails provide as the +index_by+ method. + index = {}.tap do |i| + entries.each do |entry| + i[entry.date] = entry + end + end + + (start_date..end_date).each do |date| + entry = index[date] || default_entry(date) + + parsed_entries.availabilities << boolean_to_string(entry.available) + parsed_entries.rates.nightly << entry.nightly_rate + parsed_entries.rates.weekly << entry.weekly_rate + parsed_entries.rates.monthly << entry.monthly_rate + parsed_entries.checkin_rules << boolean_to_string(entry.checkin_allowed) + parsed_entries.checkout_rules << boolean_to_string(entry.checkout_allowed) + end + + parsed_entries + end + + # builds a placeholder calendar entry to be used when there are gaps + # in the entries provided to this instance. The date is assumed to be + # available, and the price is the average of all prices given to this + # class. Checking in and out is also assumed to be possible. + def default_entry(date) + average_rate = entries.map(&:nightly_rate).reduce(:+) / entries.size + + Roomorama::Calendar::Entry.new( + date: date.to_s, + available: true, + nightly_rate: average_rate, + checkin_allowed: true, + checkout_allowed: true + ) + end + + def boolean_to_string(bool) + bool ? "1" : "0" + end + + def validate_entry!(entry) + if !entry.is_a?(Roomorama::Calendar::Entry) + message = "expected calendar entry to be a Roomorama::Calendar::Entry instance, but instead is #{entry.class}" + raise ValidationError.new(message) + elsif !entry.valid? + raise ValidationError.new("Calendar parameters are invalid: #{entry.errors}") + end + end + end + +end diff --git a/lib/concierge/roomorama/client/operations.rb b/lib/concierge/roomorama/client/operations.rb index 32b566c0f..aa7a71d2c 100644 --- a/lib/concierge/roomorama/client/operations.rb +++ b/lib/concierge/roomorama/client/operations.rb @@ -22,6 +22,12 @@ def self.disable(identifiers) Disable.new(identifiers) end + # Performs an +update_calendar+ operation, making changes in bulk to + # a property's availabilities calendar. + def self.update_calendar(calendar) + UpdateCalendar.new(calendar) + end + end end diff --git a/lib/concierge/roomorama/client/operations/diff.rb b/lib/concierge/roomorama/client/operations/diff.rb index b55f74d96..acc4ecead 100644 --- a/lib/concierge/roomorama/client/operations/diff.rb +++ b/lib/concierge/roomorama/client/operations/diff.rb @@ -11,7 +11,7 @@ class Roomorama::Client::Operations # Usage # # diff = Romorama::Diff.new("property_identifier") - # operation = Roomorama::Client::Operations::diff.new(diff) + # operation = Roomorama::Client::Operations::Diff.new(diff) # roomorama_client.perform(operation) class Diff @@ -24,7 +24,7 @@ class Diff # # On initialization the +validate!+ method of the diff is called - therefore, # an operation cannot be built unless the property given is conformant to the - # basica validations performed on that class. + # basic validations performed on that class. def initialize(diff) @property_diff = diff property_diff.validate! diff --git a/lib/concierge/roomorama/client/operations/publish.rb b/lib/concierge/roomorama/client/operations/publish.rb index 08cf3f29c..b09925773 100644 --- a/lib/concierge/roomorama/client/operations/publish.rb +++ b/lib/concierge/roomorama/client/operations/publish.rb @@ -21,14 +21,10 @@ class Publish # # On initialization the +validate!+ method of the property is called - therefore, # an operation cannot be built unless the property given is conformant to the - # basica validations performed on that class. + # basic validations performed on that class. def initialize(property) @property = property property.validate! - - # when publishing properties, an initial set of availabilities - # must be provided - property.require_calendar! end def endpoint diff --git a/lib/concierge/roomorama/client/operations/update_calendar.rb b/lib/concierge/roomorama/client/operations/update_calendar.rb new file mode 100644 index 000000000..d3df5de51 --- /dev/null +++ b/lib/concierge/roomorama/client/operations/update_calendar.rb @@ -0,0 +1,40 @@ +class Roomorama::Client::Operations + + # +Roomorama::Client::Operations::UpdateCalendar+ + # + # This class wraps the API call on Roomorama's publish API to update + # the calendar in bulk. It receives a +Roomorama::Calendar+ instance + # as input and generates an operation that can be used by + # +Roomorama::Client+. + # + # Usage + # + # calendar = Romorama::Calendar.new("property_identifier") + # operation = Roomorama::Client::Operations::UpdateCalendar.new(calendar) + # roomorama_client.perform(operation) + class UpdateCalendar + + # the Roomorama API endpoint for the +update_calendar+ call + ENDPOINT = "/v1.0/host/update_calendar" + + attr_reader :calendar + + # calendar - a +Roomorama::Calendar+ object + def initialize(calendar) + @calendar = calendar + end + + def endpoint + ENDPOINT + end + + def request_method + :put + end + + def request_data + calendar.to_h + end + + end +end diff --git a/lib/concierge/roomorama/diff.rb b/lib/concierge/roomorama/diff.rb index 5e2db5c41..6de541551 100644 --- a/lib/concierge/roomorama/diff.rb +++ b/lib/concierge/roomorama/diff.rb @@ -72,10 +72,6 @@ def delete_unit(identifier) unit_changes.deleted << identifier end - def update_calendar(dates) - calendar.merge!(dates.dup) - end - # makes sure that a non-empty property identifier is passed. def validate! if identifier.to_s.empty? @@ -103,10 +99,6 @@ def empty? to_h.tap { |changes| changes.delete(:identifier) }.empty? end - def calendar - @calendar ||= {} - end - def to_h data = { identifier: identifier, @@ -170,10 +162,6 @@ def to_h data[:images] = mapped_image_changes end - unless calendar.empty? - data[:availabilities] = map_availabilities(self) - end - mapped_unit_changes = map_changes(unit_changes) unless mapped_unit_changes.empty? data[:units] = mapped_unit_changes diff --git a/lib/concierge/roomorama/diff/unit.rb b/lib/concierge/roomorama/diff/unit.rb index 8400efecf..d480be5b1 100644 --- a/lib/concierge/roomorama/diff/unit.rb +++ b/lib/concierge/roomorama/diff/unit.rb @@ -71,14 +71,6 @@ def delete_image(identifier) image_changes.deleted << identifier end - def update_calendar(dates) - calendar.merge!(dates.dup) - end - - def calendar - @calendar ||= {} - end - def to_h # map unit attribute changes data = { @@ -115,11 +107,6 @@ def to_h data[:images] = mapped_image_changes end - # no need for the +availabilities+ field if there is no calendar changes. - unless calendar.empty? - data[:availabilities] = map_availabilities(self) - end - scrub(data, erased) end end diff --git a/lib/concierge/roomorama/mappers.rb b/lib/concierge/roomorama/mappers.rb index bde964f6b..b65630616 100644 --- a/lib/concierge/roomorama/mappers.rb +++ b/lib/concierge/roomorama/mappers.rb @@ -9,37 +9,6 @@ def map_images(place) place.images.map(&:to_h) end - # check the publish API documentation for a description of the format - # expected by the availabilities field. - def map_availabilities(place) - # availabilities are not required when loading a property from the database. - return if place.calendar.empty? - - sorted_dates = place.calendar.keys.map { |date| Date.parse(date) }.sort - min_date = sorted_dates.min - max_date = sorted_dates.max - - data = "" - (min_date..max_date).each do |date| - availability = place.calendar[date.to_s] - - if availability == true - data << "1" - elsif availability == false - data << "0" - else - # if the date is not specified, assume it to be available - # (real-time checking would involve an API call to the supplier) - data << "1" - end - end - - { - start_date: min_date.to_s, - data: data - } - end - # maps changes according to the format expected by the Roomorama Diff API. # Same format for both properties and units. def map_changes(changeset) diff --git a/lib/concierge/roomorama/property.rb b/lib/concierge/roomorama/property.rb index aaff2399d..7001bf504 100644 --- a/lib/concierge/roomorama/property.rb +++ b/lib/concierge/roomorama/property.rb @@ -4,8 +4,7 @@ module Roomorama # # This class is responsible for wrapping the properties of an entry in the `rooms` # table in Roomorama. It includes attribute accessors for all parameters accepted - # by Roomorama's API, as well as convenience methods to set property images - # and update the availabilities calendar. + # by Roomorama's API, as well as convenience methods to set property images. # # Usage # @@ -16,8 +15,6 @@ module Roomorama # image = Roomorama::Image.new("img134") # image.url = "https://www.example.org/image.png" # property.add_image(image) - # - # property.update_calendar("2016-05-22" => true, "2016-05-23" => true") class Property include Roomorama::Mappers @@ -119,10 +116,6 @@ def add_unit(unit) units << unit end - def update_calendar(dates) - calendar.merge!(dates.dup) - end - # validates that all required fields for a unit are present. A unit needs: # # * a non-empty identifier @@ -151,31 +144,10 @@ def validate! end end - # enforces that the property have a non-empty availabilities calendar - # (necessary when publishing a property). - # - # Raises a +Roomorama::Property::ValidationError+ in case the calendar - # is empty, or +true+ otherwise. - # - # If the property has any units, calendar is required for each of them - # as well. - def require_calendar! - if calendar.empty? - raise ValidationError.new("no availabilities") - else - units.each(&:require_calendar!) - true - end - end - def images @images ||= [] end - def calendar - @calendar ||= {} - end - def units @units ||= [] end @@ -241,7 +213,6 @@ def to_h } data[:images] = map_images(self) - data[:availabilities] = map_availabilities(self) data[:units] = map_units(self) scrub(data) diff --git a/lib/concierge/roomorama/unit.rb b/lib/concierge/roomorama/unit.rb index 647b19bbb..4f6a23fce 100644 --- a/lib/concierge/roomorama/unit.rb +++ b/lib/concierge/roomorama/unit.rb @@ -4,8 +4,7 @@ module Roomorama # # This class is responsible for wrapping the units of an entry in the `units` # table in Roomorama. It includes attribute accessors for all parameters accepted - # by Roomorama's API, as well as convenience methods to set unit images - # and update the availabilities calendar. + # by Roomorama's API, as well as convenience methods to set unit images. # # Usage # @@ -15,8 +14,6 @@ module Roomorama # image = Roomorama::Image.new("img134") # image.url = "https://www.example.org/image.png" # unit.add_image(image) - # - # unit.update_calendar("2016-05-22" => true, "2016-05-23" => true") class Unit include Roomorama::Mappers @@ -94,32 +91,14 @@ def validate! end end - # ensures there is a non-empty availabilities calendar. Raises an exception - # (+Roomorama::Unit::ValidationError+) in case the calendar is empty. - def require_calendar! - if calendar.empty? - raise ValidationError.new("no availabilities") - else - true - end - end - def add_image(image) images << image end - def update_calendar(dates) - calendar.merge!(dates.dup) - end - def images @images ||= [] end - def calendar - @calendar ||= {} - end - def to_h data = { identifier: identifier, @@ -148,7 +127,6 @@ def to_h } data[:images] = map_images(self) - data[:availabilities] = map_availabilities(self) scrub(data) end diff --git a/lib/concierge/suppliers/atleisure/mapper.rb b/lib/concierge/suppliers/atleisure/mapper.rb index 80c4f3454..81e8615a1 100644 --- a/lib/concierge/suppliers/atleisure/mapper.rb +++ b/lib/concierge/suppliers/atleisure/mapper.rb @@ -196,18 +196,13 @@ def set_price_and_availabilities periods = meta_data['AvailabilityPeriodV1'].map { |period| AvailabilityPeriod.new(period) } actual_periods = periods.select(&:valid?) - min_price = actual_periods.map(&:daily_price).min - dates_size = actual_periods.map {|period| period.dates.size } + min_price = actual_periods.map(&:daily_price).min + dates_size = actual_periods.map { |period| period.dates.size } + property.minimum_stay = dates_size.min property.nightly_rate = min_price property.weekly_rate = min_price * 7 property.monthly_rate = min_price * 30 - - actual_periods.each do |period| - period.dates.each do |date| - property.update_calendar(date.to_s => true) - end - end end def code_for(item) diff --git a/lib/tasks/hosts.rake b/lib/tasks/hosts.rake new file mode 100644 index 000000000..67a176414 --- /dev/null +++ b/lib/tasks/hosts.rake @@ -0,0 +1,22 @@ +require "yaml" + +namespace :hosts do + desc "Updates background worker definitions for all hosts" + task update_worker_definitions: :environment do + path = Hanami.root.join("config", "suppliers.yml").to_s + + SupplierRepository.all.each do |supplier| + HostRepository.from_supplier(supplier).each do |host| + Concierge::Flows::HostCreation.new( + supplier: supplier, + identifier: host.identifier, + username: host.username, + access_token: host.access_token, + config_path: path + ).perform + end + end + + puts "Done. All hosts updated." + end +end diff --git a/lib/tasks/suppliers.rake b/lib/tasks/suppliers.rake index 86052e3af..989e08d06 100644 --- a/lib/tasks/suppliers.rake +++ b/lib/tasks/suppliers.rake @@ -4,10 +4,10 @@ namespace :suppliers do desc "Loads the suppliers.yml file into the database" task load: :environment do path = Hanami.root.join("config", "suppliers.yml").to_s - names = YAML.load_file(path) || [] # if the file is empty, +load_file+ returns +false+ + configuration = YAML.load_file(path) || {} # if the file is empty, +load_file+ returns +false+ total = 0 - names.map do |name| + configuration.keys.map do |name| existing = SupplierRepository.named(name) unless existing diff --git a/spec/fixtures/roomorama/invalid_start_date.json b/spec/fixtures/roomorama/invalid_start_date.json new file mode 100644 index 000000000..66f7ec7b1 --- /dev/null +++ b/spec/fixtures/roomorama/invalid_start_date.json @@ -0,0 +1 @@ +{"status":"error","errors":{"start_date":"not a valid date"}} diff --git a/spec/fixtures/suppliers_configuration/2d_interval.yml b/spec/fixtures/suppliers_configuration/2d_interval.yml new file mode 100644 index 000000000..8b8c8cd25 --- /dev/null +++ b/spec/fixtures/suppliers_configuration/2d_interval.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + metadata: + every: "2d" + availabilities: + every: "2h" diff --git a/spec/fixtures/suppliers_configuration/invalid_interval.yml b/spec/fixtures/suppliers_configuration/invalid_interval.yml new file mode 100644 index 000000000..b3a81f6bf --- /dev/null +++ b/spec/fixtures/suppliers_configuration/invalid_interval.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + metadata: + every: "invalid" + availabilities: + every: "2h" diff --git a/spec/fixtures/suppliers_configuration/invalid_worker_type.yml b/spec/fixtures/suppliers_configuration/invalid_worker_type.yml new file mode 100644 index 000000000..5e0c7b54d --- /dev/null +++ b/spec/fixtures/suppliers_configuration/invalid_worker_type.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + invalid: + every: "1d" + availabilities: + every: "2h" diff --git a/spec/fixtures/suppliers_configuration/minutes_interval.yml b/spec/fixtures/suppliers_configuration/minutes_interval.yml new file mode 100644 index 000000000..bfbeaa4c0 --- /dev/null +++ b/spec/fixtures/suppliers_configuration/minutes_interval.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + metadata: + every: "1d" + availabilities: + every: "10m" diff --git a/spec/fixtures/suppliers_configuration/no_metadata_interval.yml b/spec/fixtures/suppliers_configuration/no_metadata_interval.yml new file mode 100644 index 000000000..61f736370 --- /dev/null +++ b/spec/fixtures/suppliers_configuration/no_metadata_interval.yml @@ -0,0 +1,5 @@ +Supplier X: + workers: + metadata: + availabilities: + every: "2h" diff --git a/spec/fixtures/suppliers_configuration/no_supplier_x.yml b/spec/fixtures/suppliers_configuration/no_supplier_x.yml new file mode 100644 index 000000000..279e034eb --- /dev/null +++ b/spec/fixtures/suppliers_configuration/no_supplier_x.yml @@ -0,0 +1,6 @@ +Supplier Y: + workers: + metadata: + every: "1d" + availabilities: + every: "2h" diff --git a/spec/fixtures/suppliers_configuration/seconds_interval.yml b/spec/fixtures/suppliers_configuration/seconds_interval.yml new file mode 100644 index 000000000..77984b549 --- /dev/null +++ b/spec/fixtures/suppliers_configuration/seconds_interval.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + metadata: + every: "1d" + availabilities: + every: "10s" diff --git a/spec/fixtures/suppliers_configuration/suppliers.yml b/spec/fixtures/suppliers_configuration/suppliers.yml new file mode 100644 index 000000000..8ad8a7d66 --- /dev/null +++ b/spec/fixtures/suppliers_configuration/suppliers.yml @@ -0,0 +1,6 @@ +Supplier X: + workers: + metadata: + every: "1d" + availabilities: + every: "2h" diff --git a/spec/lib/concierge/context/sync_process_spec.rb b/spec/lib/concierge/context/sync_process_spec.rb index 5bb79b8dc..ba4e9304d 100644 --- a/spec/lib/concierge/context/sync_process_spec.rb +++ b/spec/lib/concierge/context/sync_process_spec.rb @@ -3,6 +3,7 @@ RSpec.describe Concierge::Context::SyncProcess do let(:params) { { + worker: "metadata", host_id: 2, identifier: "prop1" } @@ -16,6 +17,7 @@ expect(subject.to_h).to eq({ type: "sync_process", + worker: "metadata", timestamp: Time.now, host_id: 2, identifier: "prop1" diff --git a/spec/lib/concierge/flows/host_creation_spec.rb b/spec/lib/concierge/flows/host_creation_spec.rb new file mode 100644 index 000000000..b6e1bb97e --- /dev/null +++ b/spec/lib/concierge/flows/host_creation_spec.rb @@ -0,0 +1,152 @@ +require "spec_helper" + +RSpec.describe Concierge::Flows::HostCreation do + include Support::Factories + + let(:supplier) { create_supplier(name: "Supplier X") } + + let(:parameters) { + { + supplier: supplier, + identifier: "host1", + username: "roomorama-user", + access_token: "a1b2c3", + } + } + + def config_suppliers(file) + parameters[:config_path] = Hanami.root.join("spec", "fixtures", "suppliers_configuration", file).to_s + end + + before do + config_suppliers "suppliers.yml" + end + + subject { described_class.new(parameters) } + + describe "#perform" do + it "returns an unsuccessful if any required parameter is missing" do + [nil, ""].each do |invalid_value| + [:supplier, :identifier, :username, :access_token].each do |attribute| + parameters[attribute] = invalid_value + + result = subject.perform + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :invalid_parameters + end + end + end + + it "returns an unsuccessful result without a workers definition" do + config_suppliers "no_supplier_x.yml" + + result = subject.perform + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :no_workers_definition + end + + it "returns an unsuccessful result if parameters for the workers are missing" do + config_suppliers "no_metadata_interval.yml" + + result = subject.perform + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :invalid_parameters + end + + it "returns an unsuccessful result if the worker type is unknown" do + config_suppliers "invalid_worker_type.yml" + + result = subject.perform + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :invalid_parameters + end + + it "returns an unsuccessful result if the interval specified is not recognised" do + config_suppliers "invalid_interval.yml" + + result = subject.perform + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :invalid_parameters + end + + it "creates the host and associated workers" do + expect { + expect { + expect(subject.perform).to be_success + }.to change { HostRepository.count }.by(1) + }.to change { BackgroundWorkerRepository.count }.by(2) + + host = HostRepository.last + workers = BackgroundWorkerRepository.for_host(host).to_a + + expect(host.identifier).to eq "host1" + expect(host.username).to eq "roomorama-user" + expect(host.access_token).to eq "a1b2c3" + + expect(workers.size).to eq 2 + + worker = workers.first + expect(worker.type).to eq "metadata" + expect(worker.next_run_at).to be_nil + expect(worker.interval).to eq 24 * 60 * 60 # one day + expect(worker.status).to eq "idle" + + worker = workers.last + expect(worker.type).to eq "availabilities" + expect(worker.next_run_at).to be_nil + expect(worker.interval).to eq 2 * 60 * 60 # two hours + expect(worker.status).to eq "idle" + end + + it "updates changed data on consecutive runs" do + subject.perform + + host = HostRepository.last + workers = BackgroundWorkerRepository.for_host(host).to_a + metadata_worker = workers.find { |w| w.type == "metadata" } + + expect(metadata_worker.interval).to eq 24 * 60 * 60 + + # updates metadata worker interval to every 2 days + config_suppliers "2d_interval.yml" + + expect { + described_class.new(parameters).perform + }.not_to change { HostRepository.count } + + metadata_worker = BackgroundWorkerRepository.find(metadata_worker.id) + expect(metadata_worker.interval).to eq 2 * 24 * 60 * 60 # 2 days + end + + context "interval parsing" do + it "understands seconds notation" do + config_suppliers "seconds_interval.yml" + subject.perform + + host = HostRepository.last + worker = BackgroundWorkerRepository. + for_host(host). + find { |w| w.type == "availabilities" } + + expect(worker.interval).to eq 10 + end + + it "understands minutes notation" do + config_suppliers "minutes_interval.yml" + subject.perform + + host = HostRepository.last + worker = BackgroundWorkerRepository. + for_host(host). + find { |w| w.type == "availabilities" } + + expect(worker.interval).to eq 10 * 60 + end + end + end +end diff --git a/spec/lib/concierge/repositories/background_worker_repository_spec.rb b/spec/lib/concierge/repositories/background_worker_repository_spec.rb new file mode 100644 index 000000000..82b3f681d --- /dev/null +++ b/spec/lib/concierge/repositories/background_worker_repository_spec.rb @@ -0,0 +1,51 @@ +require "spec_helper" + +RSpec.describe BackgroundWorkerRepository do + include Support::Factories + + describe ".count" do + it "is zero when the underlying database has no records" do + expect(described_class.count).to eq 0 + end + + it "returns the number of records in the database" do + 2.times { create_background_worker } + expect(described_class.count).to eq 2 + end + end + + describe ".for_host" do + it "returns an empty collection when there are no workers" do + expect(described_class.for_host(create_host).to_a).to eq [] + end + + it "returns workers associated with the given host only" do + host = create_host + host_worker = create_background_worker(host_id: host.id) + other_host_worker = create_background_worker + + expect(described_class.for_host(host).to_a).to eq [host_worker] + end + end + + describe ".pending" do + let(:one_hour) { 60 * 60 } + let(:now) { Time.now } + + it "returns an empty collection if all workers are to be run in the future" do + worker = create_background_worker(next_run_at: now + one_hour) + expect(described_class.pending.to_a).to eq [] + end + + it "returns only workers with null timestamps or timestamps in the past which are not already running" do + new_worker = create_background_worker(next_run_at: nil) + future_worker = create_background_worker(next_run_at: now + one_hour) + pending_worker = create_background_worker(next_run_at: now - one_hour) + + new_running_worker = create_background_worker(next_run_at: nil, status: "running") + pending_running_worker = create_background_worker(next_run_at: now - one_hour, status: "running") + + expect(described_class.pending.to_a).to eq [pending_worker, new_worker] + end + end +end diff --git a/spec/lib/concierge/repositories/host_repository_spec.rb b/spec/lib/concierge/repositories/host_repository_spec.rb index baca106a4..057562ce1 100644 --- a/spec/lib/concierge/repositories/host_repository_spec.rb +++ b/spec/lib/concierge/repositories/host_repository_spec.rb @@ -14,13 +14,29 @@ end end - describe ".pending_synchronisation" do - let!(:pending_host) { create_host(next_run_at: Time.now - 10) } - let!(:new_host) { create_host(next_run_at: nil) } - let!(:just_synchronised_host) { create_host(next_run_at: Time.now + 60 * 60) } + describe ".from_supplier" do + let(:supplier) { create_supplier } - it "filters hosts where the next synchronisation time is nil or is in the past" do - expect(described_class.pending_synchronisation.to_a).to eq [pending_host, new_host] + it "returns an empty collection if there are no hosts for a given supplier" do + expect(described_class.from_supplier(supplier).to_a).to eq [] + end + + it "returns only hosts that belong to the given supplier" do + host_from_supplier = create_host(supplier_id: supplier.id) + host_from_other_supplier = create_host + + expect(described_class.from_supplier(supplier).to_a).to eq [host_from_supplier] + end + end + + describe ".identified_by" do + it "returns an empty collection when no hosts match the given identifier" do + expect(described_class.identified_by("identifier").to_a).to eq [] + end + + it "returns only hosts that match the given identifier" do + host = create_host(identifier: "host1") + expect(described_class.identified_by("host1").to_a).to eq [host] end end end diff --git a/spec/lib/concierge/roomorama/calendar_spec.rb b/spec/lib/concierge/roomorama/calendar_spec.rb new file mode 100644 index 000000000..fa0823c09 --- /dev/null +++ b/spec/lib/concierge/roomorama/calendar_spec.rb @@ -0,0 +1,100 @@ +require "spec_helper" + +RSpec.describe Roomorama::Calendar do + let(:entry_attributes) { + { + date: "2016-05-22", + available: false, + nightly_rate: 100 + } + } + + let(:entry) { Roomorama::Calendar::Entry.new(entry_attributes) } + let(:property_identifier) { "prop1" } + + subject { described_class.new(property_identifier) } + + describe "#add" do + it "adds the entry to the list of calendar entries" do + subject.add(entry) + expect(subject.entries).to eq [entry] + end + end + + describe "#validate!" do + it "raises an error in case there are invalid entries in the calendar" do + subject.add(entry) + subject.add(create_entry(nightly_rate: nil)) + + expect { + subject.validate! + }.to raise_error(Roomorama::Calendar::ValidationError) + end + + it "is valid if all entries are valid" do + subject.add(entry) + subject.add(create_entry(date: "2016-05-20")) + + expect(subject.validate!).to eq true + end + end + + describe "#to_h" do + it "creates a representation of all entries given" do + subject.add(create_entry(date: "2016-05-23", available: false, nightly_rate: 120)) + subject.add(create_entry(date: "2016-05-22", available: true, nightly_rate: 100)) + subject.add(create_entry(date: "2016-05-24", available: true, nightly_rate: 150)) + + expect(subject.to_h).to eq({ + identifier: "prop1", + start_date: "2016-05-22", + availabilities: "101", + nightly_rates: [100, 120, 150], + weekly_rates: [nil, nil, nil], + monthly_rates: [nil, nil, nil], + checkin_allowed: "111", + checkout_allowed: "111" + }) + end + + it "overwrites given weekly/monthly rates as well as check-in/check-out rules" do + subject.add(create_entry(date: "2016-05-22", available: true, nightly_rate: 100, weekly_rate: 500, checkin_allowed: false)) + subject.add(create_entry(date: "2016-05-23", available: false, nightly_rate: 120, monthly_rate: 1000, checkin_allowed: true)) + subject.add(create_entry(date: "2016-05-24", available: true, nightly_rate: 150, checkout_allowed: false)) + + expect(subject.to_h).to eq({ + identifier: "prop1", + start_date: "2016-05-22", + availabilities: "101", + nightly_rates: [100, 120, 150], + weekly_rates: [500, nil, nil], + monthly_rates: [nil, 1000, nil], + checkin_allowed: "011", + checkout_allowed: "110" + }) + end + + it "fills in gaps in the given dates with approximations" do + subject.add(create_entry(date: "2016-05-22", available: true, nightly_rate: 100)) + subject.add(create_entry(date: "2016-05-25", available: false, nightly_rate: 120)) + subject.add(create_entry(date: "2016-05-27", available: false, nightly_rate: 150)) + + average = (100 + 120 + 150) / 3 + + expect(subject.to_h).to eq({ + identifier: "prop1", + start_date: "2016-05-22", + availabilities: "111010", + nightly_rates: [100, average, average, 120, average, 150], + weekly_rates: [nil, nil, nil, nil, nil, nil], + monthly_rates: [nil, nil, nil, nil, nil, nil], + checkin_allowed: "111111", + checkout_allowed: "111111" + }) + end + end + + def create_entry(overrides = {}) + Roomorama::Calendar::Entry.new(entry_attributes.merge(overrides)) + end +end diff --git a/spec/lib/concierge/roomorama/client/operations/publish_spec.rb b/spec/lib/concierge/roomorama/client/operations/publish_spec.rb index aa37a313b..7a355c348 100644 --- a/spec/lib/concierge/roomorama/client/operations/publish_spec.rb +++ b/spec/lib/concierge/roomorama/client/operations/publish_spec.rb @@ -21,13 +21,6 @@ image.url = "https://www.example.org/image2.png" image.caption = "Barbecue Pit" property.add_image(image) - - property.update_calendar({ - "2016-05-22" => true, - "2016-05-20" => false, - "2016-05-28" => true, - "2016-05-21" => true - }) end describe "#initialize" do diff --git a/spec/lib/concierge/roomorama/client/operations/update_calendar_spec.rb b/spec/lib/concierge/roomorama/client/operations/update_calendar_spec.rb new file mode 100644 index 000000000..4fca8d7a6 --- /dev/null +++ b/spec/lib/concierge/roomorama/client/operations/update_calendar_spec.rb @@ -0,0 +1,26 @@ +require "spec_helper" + +RSpec.describe Roomorama::Client::Operations::UpdateCalendar do + let(:calendar) { Roomorama::Calendar.new("JPN123") } + + subject { described_class.new(calendar) } + + describe "#endpoint" do + it "knows the endpoint where a property can be published" do + expect(subject.endpoint).to eq "/v1.0/host/update_calendar" + end + end + + describe "#method" do + it "knows the request method to be used when publishing" do + expect(subject.request_method).to eq :put + end + end + + describe "#request_data" do + it "calls the +to_h+ method of the underlying property" do + expect(calendar).to receive(:to_h) + subject.request_data + end + end +end diff --git a/spec/lib/concierge/roomorama/client_spec.rb b/spec/lib/concierge/roomorama/client_spec.rb index fc79ef564..f650b6f95 100644 --- a/spec/lib/concierge/roomorama/client_spec.rb +++ b/spec/lib/concierge/roomorama/client_spec.rb @@ -22,13 +22,6 @@ image.url = "https://www.example.org/image2.png" image.caption = "Barbecue Pit" property.add_image(image) - - property.update_calendar({ - "2016-05-22" => true, - "2016-05-20" => false, - "2016-05-28" => true, - "2016-05-21" => true - }) end subject { described_class.new(access_token) } @@ -98,12 +91,7 @@ url: "https://www.example.org/image2.png", caption: "Barbecue Pit" } - ], - - availabilities: { - start_date: "2016-05-20", - data: "011111111" - } + ] } headers = { diff --git a/spec/lib/concierge/roomorama/diff/unit_spec.rb b/spec/lib/concierge/roomorama/diff/unit_spec.rb index 53b400a76..31d807a9e 100644 --- a/spec/lib/concierge/roomorama/diff/unit_spec.rb +++ b/spec/lib/concierge/roomorama/diff/unit_spec.rb @@ -81,15 +81,6 @@ end end - describe "#update_calendar" do - it "updates its calendar with the data given" do - calendar = { "2016-05-22" => true, "2016-05-25" => true } - expect(subject.update_calendar(calendar)).to be - - expect(subject.calendar).to eq({ "2016-05-22" => true, "2016-05-25" => true }) - end - end - describe "#validate!" do before do subject.identifier = "UNIT1" @@ -140,14 +131,6 @@ subject.change_image(image_diff) subject.delete_image("IMGDEL") - - subject.update_calendar({ - "2016-06-20" => true, - "2016-06-22" => true, - "2016-06-25" => true, - "2016-06-28" => false, - "2016-06-21" => true, - }) end it "changed attributes" do @@ -170,11 +153,6 @@ } ], delete: ["IMGDEL"] - }, - - availabilities: { - start_date: "2016-06-20", - data: "111111110" } }) end @@ -191,10 +169,5 @@ expect(subject.to_h).not_to have_key :images end - - it "does not include the `availabilities` field if there are no calendar changes" do - allow(subject).to receive(:calendar) { {} } - expect(subject.to_h).not_to have_key :availabilities - end end end diff --git a/spec/lib/concierge/roomorama/diff_spec.rb b/spec/lib/concierge/roomorama/diff_spec.rb index baaeff9f0..b1c566e08 100644 --- a/spec/lib/concierge/roomorama/diff_spec.rb +++ b/spec/lib/concierge/roomorama/diff_spec.rb @@ -88,8 +88,6 @@ image = Roomorama::Image.new("IMG1") image.url = "https://www.example.org/units/image1.png" unit.add_image(image) - - unit.update_calendar("2016-05-22" => true) end it "adds a unit to the list of units of that property" do @@ -125,15 +123,6 @@ end end - describe "#update_calendar" do - it "updates its calendar with the data given" do - calendar = { "2016-05-22" => true, "2016-05-25" => true } - expect(subject.update_calendar(calendar)).to be - - expect(subject.calendar).to eq({ "2016-05-22" => true, "2016-05-25" => true }) - end - end - describe "#empty?" do it "is empty if no changes to meta attributes were applied" do expect(subject).to be_empty @@ -221,13 +210,6 @@ image_diff.caption = "Barbecue Pit (renovated)" subject.change_image(image_diff) - subject.update_calendar({ - "2016-05-22" => true, - "2016-05-20" => false, - "2016-05-28" => true, - "2016-05-21" => true - }) - unit = Roomorama::Unit.new("UNIT1") unit.title = "Unit 1" unit.nightly_rate = 200 @@ -243,8 +225,6 @@ image.caption = "Bedroom 2" unit.add_image(image) - unit.update_calendar("2016-05-22" => true, "2016-05-28" => true) - subject.add_unit(unit) unit = Roomorama::Diff::Unit.new("UNIT2") @@ -259,8 +239,6 @@ image_diff.caption = "Improved Living Room" unit.change_image(image_diff) - unit.update_calendar("2016-05-22" => true, "2016-05-28" => true) - subject.change_unit(unit) end @@ -288,11 +266,6 @@ ] }, - availabilities: { - start_date: "2016-05-20", - data: "011111111" - }, - units: { create: [ { @@ -312,12 +285,7 @@ url: "https://www.example.org/unit1/image2.png", caption: "Bedroom 2" } - ], - - availabilities: { - start_date: "2016-05-22", - data: "1111111" - } + ] } ], update: [ @@ -339,11 +307,6 @@ caption: "Improved Living Room" } ] - }, - - availabilities: { - start_date: "2016-05-22", - data: "1111111" } } ] diff --git a/spec/lib/concierge/roomorama/property_spec.rb b/spec/lib/concierge/roomorama/property_spec.rb index f75cfa51c..658a61596 100644 --- a/spec/lib/concierge/roomorama/property_spec.rb +++ b/spec/lib/concierge/roomorama/property_spec.rb @@ -189,8 +189,6 @@ image = Roomorama::Image.new("IMG1") image.url = "https://www.example.org/units/image1.png" unit.add_image(image) - - unit.update_calendar("2016-05-22" => true) end it "adds a unit to the list of units of that property" do @@ -206,15 +204,6 @@ end end - describe "#update_calendar" do - it "updates its calendar with the data given" do - calendar = { "2016-05-22" => true, "2016-05-25" => true } - expect(subject.update_calendar(calendar)).to be - - expect(subject.calendar).to eq({ "2016-05-22" => true, "2016-05-25" => true }) - end - end - describe "#validate!" do before do # populate some data so that the object is valid @@ -234,16 +223,7 @@ image.url = "https://wwww.example.org/unit1/image2.png" unit.add_image(image) - unit.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) subject.add_unit(unit) - - subject.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) end it "is invalid if the identifier is not given" do @@ -279,57 +259,6 @@ it "is valid if all required parameters are present" do expect(subject.validate!).to be end - - it "is valid if there are no availabilities for the property" do - allow(subject).to receive(:calendar) { {} } - expect(subject.validate!).to be - end - end - - describe "#require_calendar!" do - before do - image = Roomorama::Image.new("IMG1") - image.url = "https://wwww.example.org/image1.png" - subject.add_image(image) - - subject.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) - - unit = Roomorama::Unit.new("unit1") - unit.title = "Unit 1" - - image = Roomorama::Image.new("UNIT1IMG1") - image.url = "https://wwww.example.org/unit1/image1.png" - unit.add_image(image) - - unit.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) - - subject.add_unit(unit) - end - - it "is valid if the property has a non-empty availabilities calendar" do - expect(subject.require_calendar!).to be - end - - it "is invalid if there are no availabilities" do - allow(subject).to receive(:calendar) { {} } - - expect { - subject.require_calendar! - }.to raise_error Roomorama::Property::ValidationError - end - - it "is invalid if one of the units has an empty availabilities calendar" do - allow(subject.units.first).to receive(:calendar) { {} } - expect { - subject.require_calendar! - }.to raise_error Roomorama::Unit::ValidationError - end end describe "#to_h" do @@ -350,13 +279,6 @@ image.caption = "Barbecue Pit" subject.add_image(image) - subject.update_calendar({ - "2016-05-22" => true, - "2016-05-20" => false, - "2016-05-28" => true, - "2016-05-21" => true - }) - unit = Roomorama::Unit.new("UNIT1") unit.title = "Unit 1" unit.nightly_rate = 200 @@ -372,8 +294,6 @@ image.caption = "Bedroom 2" unit.add_image(image) - unit.update_calendar("2016-05-22" => true, "2016-05-28" => true) - subject.add_unit(unit) unit = Roomorama::Unit.new("UNIT2") @@ -389,8 +309,6 @@ image.url = "https://www.example.org/unit2/image2.png" unit.add_image(image) - unit.update_calendar("2016-05-22" => true, "2016-05-28" => true) - subject.add_unit(unit) end @@ -415,12 +333,7 @@ url: "https://www.example.org/image2.png", caption: "Barbecue Pit" } - ], - - availabilities: { - start_date: "2016-05-20", - data: "011111111" - } + ] } } @@ -443,12 +356,7 @@ url: "https://www.example.org/unit1/image2.png", caption: "Bedroom 2" } - ], - - availabilities: { - start_date: "2016-05-22", - data: "1111111" - } + ] }, { identifier: "UNIT2", @@ -465,12 +373,7 @@ identifier: "UNIT2-IMG2", url: "https://www.example.org/unit2/image2.png" } - ], - - availabilities: { - start_date: "2016-05-22", - data: "1111111" - } + ] } ] } diff --git a/spec/lib/concierge/roomorama/unit_spec.rb b/spec/lib/concierge/roomorama/unit_spec.rb index 77b92564d..5b3242994 100644 --- a/spec/lib/concierge/roomorama/unit_spec.rb +++ b/spec/lib/concierge/roomorama/unit_spec.rb @@ -98,15 +98,6 @@ end end - describe "#update_calendar" do - it "updates its calendar with the data given" do - calendar = { "2016-05-22" => true, "2016-05-25" => true } - expect(subject.update_calendar(calendar)).to be - - expect(subject.calendar).to eq({ "2016-05-22" => true, "2016-05-25" => true }) - end - end - describe "#validate!" do before do subject.identifier = "UNIT1" @@ -118,11 +109,6 @@ image = Roomorama::Image.new("IMG2") image.url = "https://wwww.example.org/image2.png" subject.add_image(image) - - subject.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) end it "is invalid if the identifier is not present" do @@ -150,31 +136,6 @@ it "is valid if all required parameters are present" do expect(subject.validate!).to be end - - it "valid if there are no availabilities for the unit" do - allow(subject).to receive(:calendar) { {} } - expect(subject.validate!).to be - end - end - - describe "#require_calendar!" do - before do - subject.update_calendar({ - "2016-05-22" => true, - "2015-05-28" => true - }) - end - - it "is valid in case there is a non-empty availabilities calendar" do - expect(subject.require_calendar!).to be - end - - it "is invalid in case the availabilities calendar is empty" do - allow(subject).to receive(:calendar) { {} } - expect { - subject.require_calendar! - }.to raise_error Roomorama::Unit::ValidationError - end end describe "#to_h" do @@ -192,14 +153,6 @@ image = Roomorama::Image.new("image2") image.url = "https://www.example.org/image2.png" subject.add_image(image) - - subject.update_calendar({ - "2016-06-20" => true, - "2016-06-22" => true, - "2016-06-25" => true, - "2016-06-28" => false, - "2016-06-21" => true, - }) end it "serializes all attributes" do @@ -220,12 +173,7 @@ identifier: "image2", url: "https://www.example.org/image2.png" } - ], - - availabilities: { - start_date: "2016-06-20", - data: "111111110" - } + ] }) end end diff --git a/spec/lib/concierge/suppliers/atleisure/mapper_spec.rb b/spec/lib/concierge/suppliers/atleisure/mapper_spec.rb index ec895bde3..05aec036d 100644 --- a/spec/lib/concierge/suppliers/atleisure/mapper_spec.rb +++ b/spec/lib/concierge/suppliers/atleisure/mapper_spec.rb @@ -58,20 +58,6 @@ end end - context 'calendar' do - let(:on_request_date) { '2017-12-04' } - let(:missed_date) { '2017-10-04' } - let(:available_date) { '2017-12-08' } - - it 'has dates of valid periods' do - property = subject.prepare(property_data).value - - expect(property.calendar[available_date]).to eq true - expect(property.calendar[missed_date]).to be_nil - expect(property.calendar[on_request_date]).to be_nil - end - end - context 'rates' do let(:on_request_period) { { @@ -170,4 +156,4 @@ expect(property.validate!).to be true end end -end \ No newline at end of file +end diff --git a/spec/support/factories.rb b/spec/support/factories.rb index 1a23f574f..243bc96a1 100644 --- a/spec/support/factories.rb +++ b/spec/support/factories.rb @@ -8,11 +8,9 @@ module Support module Factories def create_sync_process(overrides = {}) attributes = { + type: "metadata", started_at: Time.now - 10 * 60, # 10 minutes ago - finished_at: Time.now, - properties_created: 1, - properties_updated: 1, - properties_deleted: 1 + finished_at: Time.now }.merge(overrides) process = SyncProcess.new(attributes) @@ -75,6 +73,18 @@ def create_cache_entry(overrides = {}) entry = Concierge::Cache::Entry.new(attributes) Concierge::Cache::EntryRepository.create(entry) end + + def create_background_worker(overrides = {}) + attributes = { + host_id: create_host.id, + interval: 100, + type: "metadata", + status: "idle" + }.merge(overrides) + + worker = BackgroundWorker.new(attributes) + BackgroundWorkerRepository.create(worker) + end end end diff --git a/spec/workers/calendar_synchronisation_spec.rb b/spec/workers/calendar_synchronisation_spec.rb new file mode 100644 index 000000000..240d1a179 --- /dev/null +++ b/spec/workers/calendar_synchronisation_spec.rb @@ -0,0 +1,135 @@ +require "spec_helper" + +RSpec.describe Workers::CalendarSynchronisation do + include Support::Factories + include Support::HTTPStubbing + include Support::Fixtures + + let(:host) { create_host } + let(:calendar) { + Roomorama::Calendar.new("prop1").tap do |calendar| + entry = Roomorama::Calendar::Entry.new( + date: "2016-04-22", + available: true, + nightly_rate: 100, + weekly_rate: 500 + ) + calendar.add(entry) + + entry = Roomorama::Calendar::Entry.new( + date: "2016-04-23", + available: false, + nightly_rate: 100 + ) + calendar.add(entry) + end + } + + subject { described_class.new(host) } + + describe "#start" do + it "processes the calendar if there are no errors" do + operation = nil + expect(subject).to receive(:run_operation) { |op| operation = op } + + subject.start("prop1") { Result.new(calendar) } + expect(operation).to be_a Roomorama::Client::Operations::UpdateCalendar + expect(operation.calendar).to eq calendar + end + + context "error handling" do + it "announces an error if the calendar returned does not pass validations" do + calendar.entries.first.date = nil + subject.start("prop1") { Result.new(calendar) } + + error = ExternalErrorRepository.last + expect(error.operation).to eq "sync" + expect(error.supplier).to eq "Supplier A" + expect(error.code).to eq "missing_data" + + context = error.context + expect(context[:version]).to eq Concierge::VERSION + expect(context[:host]).to eq Socket.gethostname + expect(context[:type]).to eq "batch" + expect(context[:events].first["type"]).to eq "sync_process" + expect(context[:events].first["worker"]).to eq "availabilities" + expect(context[:events].first["identifier"]).to eq "prop1" + expect(context[:events].first["host_id"]).to eq host.id + expect(context[:events].last["error_message"]).to eq "Calendar validation error: One of the entries miss required parameters." + expect(context[:events].last["attributes"].keys).to eq calendar.to_h.keys.map(&:to_s) + end + + it "announces an error if the calendar to be processed with the supplier" do + subject.start("prop1") { Result.error(:http_status_404) } + + error = ExternalErrorRepository.last + expect(error.operation).to eq "sync" + expect(error.supplier).to eq "Supplier A" + expect(error.code).to eq "http_status_404" + + context = error.context + expect(context[:version]).to eq Concierge::VERSION + expect(context[:type]).to eq "batch" + expect(context[:events].size).to eq 1 + expect(context[:events].first["type"]).to eq "sync_process" + expect(context[:events].first["worker"]).to eq "availabilities" + expect(context[:events].first["host_id"]).to eq host.id + expect(context[:events].first["identifier"]).to eq "prop1" + end + + it "announces the error if the calendar update with Roomorama fails" do + stub_call(:put, "https://api.roomorama.com/v1.0/host/update_calendar") { + [422, {}, read_fixture("roomorama/invalid_start_date.json")] + } + + expect { + subject.start("prop1") { Result.new(calendar) } + }.to change { ExternalErrorRepository.count }.by(1) + + error = ExternalErrorRepository.last + expect(error.operation).to eq "sync" + expect(error.supplier).to eq "Supplier A" + expect(error.code).to eq "http_status_422" + expect(error.context[:type]).to eq "batch" + types = error.context[:events].map { |h| h["type"] } + expect(types).to eq ["sync_process", "network_request", "network_response"] + end + + it "is successful if the API call succeeds" do + stub_call(:put, "https://api.roomorama.com/v1.0/host/update_calendar") { + [202, {}, [""]] + } + + expect { + subject.start("prop1") { Result.new(calendar) } + }.not_to change { ExternalErrorRepository.count } + end + end + end + + describe "#finish!" do + before do + stub_call(:put, "https://api.roomorama.com/v1.0/host/update_calendar") { [202, {}, [""]] } + end + + it "creates a sync_process record when there is an error with the synchronisation" do + subject.start("prop1") { Result.new(calendar) } + subject.start("prop2") { Result.error(:http_status_500) } + + expect { + subject.finish! + }.to change { SyncProcessRepository.count }.by(1) + + sync = SyncProcessRepository.last + expect(sync).to be_a SyncProcess + expect(sync.type).to eq "availabilities" + expect(sync.host_id).to eq host.id + expect(sync.successful).to eq true + expect(sync.started_at).not_to be_nil + expect(sync.finished_at).not_to be_nil + expect(sync.stats[:properties_processed]).to eq 2 + expect(sync.stats[:available_records]).to eq 1 + expect(sync.stats[:unavailable_records]).to eq 1 + end + end +end diff --git a/spec/workers/operation_runner/diff_spec.rb b/spec/workers/operation_runner/diff_spec.rb index dc431b8bf..81d30c916 100644 --- a/spec/workers/operation_runner/diff_spec.rb +++ b/spec/workers/operation_runner/diff_spec.rb @@ -25,15 +25,6 @@ image.url = "https://www.example.org/img2" image.caption = "Swimming Pool" property.add_image(image) - - property.update_calendar({ - "2016-05-24" => true, - "2016-05-23" => true, - "2016-05-26" => false, - "2016-05-28" => false, - "2016-05-21" => true, - "2016-05-29" => true, - }) end } @@ -54,7 +45,7 @@ property = Property.new( identifier: roomorama_property.identifier, host_id: host.id, - data: roomorama_property.to_h.tap { |h| h.delete(:availabilities) } + data: roomorama_property.to_h ) PropertyRepository.create(property) @@ -109,7 +100,7 @@ expect(property).to be_a Property expect(property.identifier).to eq "prop1" expect(property.host_id).to eq host.id - data = roomorama_property.to_h.tap { |h| h.delete(:availabilities) } + data = roomorama_property.to_h expect(property.data.to_h.keys).to eq data.keys.map(&:to_s) expect(property.data[:title]).to eq "New Title for property" end diff --git a/spec/workers/operation_runner/publish_spec.rb b/spec/workers/operation_runner/publish_spec.rb index a042601e9..560da37c2 100644 --- a/spec/workers/operation_runner/publish_spec.rb +++ b/spec/workers/operation_runner/publish_spec.rb @@ -25,15 +25,6 @@ image.url = "https://www.example.org/img2" image.caption = "Swimming Pool" property.add_image(image) - - property.update_calendar({ - "2016-05-24" => true, - "2016-05-23" => true, - "2016-05-26" => false, - "2016-05-28" => false, - "2016-05-21" => true, - "2016-05-29" => true, - }) end } @@ -87,7 +78,7 @@ expect(property).to be_a Property expect(property.identifier).to eq "prop1" expect(property.host_id).to eq host.id - data = roomorama_property.to_h.tap { |h| h.delete(:availabilities) } + data = roomorama_property.to_h expect(property.data.to_h.keys).to eq data.keys.map(&:to_s) end end diff --git a/spec/workers/operation_runner/update_calendar_spec.rb b/spec/workers/operation_runner/update_calendar_spec.rb new file mode 100644 index 000000000..54720c5f5 --- /dev/null +++ b/spec/workers/operation_runner/update_calendar_spec.rb @@ -0,0 +1,74 @@ +require "spec_helper" + +RSpec.describe Workers::OperationRunner::UpdateCalendar do + include Support::Factories + include Support::HTTPStubbing + include Support::Fixtures + + let(:host) { create_host } + let(:endpoint) { "https://api.staging.roomorama.com/v1.0/host/update_calendar" } + let(:calendar) { + Roomorama::Calendar.new("prop1").tap do |calendar| + entry = Roomorama::Calendar::Entry.new( + date: "2016-04-22", + available: true, + nightly_rate: 100, + weekly_rate: 500 + ) + calendar.add(entry) + + entry = Roomorama::Calendar::Entry.new( + date: "2016-04-22", + available: false, + nightly_rate: 100 + ) + calendar.add(entry) + end + } + + let(:operation) { Roomorama::Client::Operations.update_calendar(calendar) } + let(:roomorama_client) { Roomorama::Client.new(host.access_token) } + + subject { described_class.new(operation, roomorama_client) } + + describe "#perform" do + it "returns the underlying network problem, if any" do + stub_call(:put, endpoint) { raise Faraday::TimeoutError } + result = subject.perform(calendar) + + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :connection_timeout + end + + it "saves the context information" do + stub_call(:put, endpoint) { raise Faraday::TimeoutError } + + expect { + subject.perform(calendar) + }.to change { Concierge.context.events.size } + + event = Concierge.context.events.last + expect(event).to be_a Concierge::Context::NetworkFailure + end + + it "is unsuccessful if the API call fails" do + stub_call(:put, endpoint) { [422, {}, read_fixture("roomorama/invalid_start_date.json")] } + result = nil + + result = subject.perform(calendar) + + expect(result).to be_a Result + expect(result).not_to be_success + expect(result.error.code).to eq :http_status_422 + end + + it "returns the persisted property in a Result if successful" do + stub_call(:put, endpoint) { [202, {}, [""]] } + result = subject.perform(calendar) + + expect(result).to be_a Result + expect(result).to be_success + end + end +end diff --git a/spec/workers/operation_runner_spec.rb b/spec/workers/operation_runner_spec.rb index 4ad325be3..12bc10659 100644 --- a/spec/workers/operation_runner_spec.rb +++ b/spec/workers/operation_runner_spec.rb @@ -8,7 +8,7 @@ describe "#perform" do it "performs publish calls" do - roomorama_property = double(validate!: true, require_calendar!: true) + roomorama_property = double(validate!: true) operation = Roomorama::Client::Operations.publish(roomorama_property) expect_any_instance_of(Workers::OperationRunner::Publish).to receive(:perform).with(roomorama_property) { Result.new(true) } @@ -32,18 +32,6 @@ subject.perform(operation) end - it "updates host next synchrnonisation time when successful" do - allow(subject).to receive(:runner_for) { double(perform: Result.new(true)) } - expect(host.next_run_at).to be_nil - - identifiers = ["prop1"] - operation = Roomorama::Client::Operations.disable(identifiers) - subject.perform(operation) - - updated = HostRepository.find(host.id) - expect(updated.next_run_at > Time.now).to eq true - end - it "raises an error if the operation is not recognised" do operation = nil expect { diff --git a/spec/workers/processor_spec.rb b/spec/workers/processor_spec.rb index 318b34cac..71316ccee 100644 --- a/spec/workers/processor_spec.rb +++ b/spec/workers/processor_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Workers::Processor do include Support::Factories - let(:payload) { { operation: "sync", data: { host_id: 2 } } } + let(:payload) { { operation: "background_worker", data: { background_worker_id: 2 } } } let(:json) { payload.to_json } subject { described_class.new(json) } @@ -26,34 +26,42 @@ }.to raise_error Workers::Processor::UnknownOperationError end - it "triggers the associated supplier synchronisation mechanism on sync operations" do + it "triggers the associated supplier synchronisation mechanism on background worker operations" do invoked = false - Concierge::Announcer.on("sync.AcmeTest") do |host| + supplier = create_supplier(name: "AcmeTest") + host = create_host(username: "acme-host", supplier_id: supplier.id) + worker = create_background_worker(host_id: host.id, type: "metadata", interval: 10) + payload[:data][:background_worker_id] = worker.id + + Concierge::Announcer.on("metadata.AcmeTest") do |host| expect(host.username).to eq "acme-host" expect(SupplierRepository.find(host.supplier_id).name).to eq "AcmeTest" invoked = true - end - supplier = create_supplier(name: "AcmeTest") - host = create_host(username: "acme-host", supplier_id: supplier.id) - payload[:data][:host_id] = host.id + expect(BackgroundWorkerRepository.find(worker.id).status).to eq "running" + end result = subject.process! expect(result).to be_a Result expect(result).to be_success expect(invoked).to eq true + + reloaded_worker = BackgroundWorkerRepository.find(worker.id) + expect(reloaded_worker.next_run_at - Time.now).to be_within(1).of(worker.interval) + expect(reloaded_worker.status).to eq "idle" end it "times out and fails if the operation takes too long" do supplier = create_supplier(name: "AcmeTest") host = create_host(username: "acme-host", supplier_id: supplier.id) - payload[:data][:host_id] = host.id + worker = create_background_worker(host_id: host.id, type: "availabilities") + payload[:data][:background_worker_id] = worker.id # simulates a timeout of 0.5s and a synchronisation process that takes # one minute, thus timing out. allow(subject).to receive(:processing_timeout) { 0.5 } - Concierge::Announcer.on("sync.AcmeTest") { sleep 1 } + Concierge::Announcer.on("availabilities.AcmeTest") { sleep 1 } result = subject.process! expect(result).to be_a Result diff --git a/spec/workers/synchronisation_spec.rb b/spec/workers/property_synchronisation_spec.rb similarity index 94% rename from spec/workers/synchronisation_spec.rb rename to spec/workers/property_synchronisation_spec.rb index 9b4920835..3b8d55030 100644 --- a/spec/workers/synchronisation_spec.rb +++ b/spec/workers/property_synchronisation_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -RSpec.describe Workers::Synchronisation do +RSpec.describe Workers::PropertySynchronisation do include Support::Factories include Support::HTTPStubbing include Support::Fixtures @@ -24,15 +24,6 @@ image.url = "https://www.example.org/img2" image.caption = "Swimming Pool" property.add_image(image) - - property.update_calendar({ - "2016-05-24" => true, - "2016-05-23" => true, - "2016-05-26" => false, - "2016-05-28" => false, - "2016-05-21" => true, - "2016-05-29" => true, - }) end } @@ -235,13 +226,14 @@ sync = SyncProcessRepository.last expect(sync).to be_a SyncProcess + expect(sync.type).to eq "metadata" expect(sync.host_id).to eq host.id expect(sync.successful).to eq false expect(sync.started_at).not_to be_nil expect(sync.finished_at).not_to be_nil - expect(sync.properties_created).to eq 2 - expect(sync.properties_updated).to eq 0 - expect(sync.properties_deleted).to eq 0 + expect(sync.stats[:properties_created]).to eq 2 + expect(sync.stats[:properties_updated]).to eq 0 + expect(sync.stats[:properties_deleted]).to eq 0 end it "registers updates and deletions when successful" do @@ -279,9 +271,9 @@ expect(sync.successful).to eq true expect(sync.started_at).not_to be_nil expect(sync.finished_at).not_to be_nil - expect(sync.properties_created).to eq 1 - expect(sync.properties_updated).to eq 1 - expect(sync.properties_deleted).to eq 1 + expect(sync.stats[:properties_created]).to eq 1 + expect(sync.stats[:properties_updated]).to eq 1 + expect(sync.stats[:properties_deleted]).to eq 1 end end end diff --git a/spec/workers/queue/element_spec.rb b/spec/workers/queue/element_spec.rb index 10162283c..ca9da08f4 100644 --- a/spec/workers/queue/element_spec.rb +++ b/spec/workers/queue/element_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" RSpec.describe Workers::Queue::Element do - let(:operation) { "sync" } + let(:operation) { "background_worker" } let(:data) { { key: "value" } } subject { described_class.new(operation: operation, data: data) } diff --git a/spec/workers/queue_spec.rb b/spec/workers/queue_spec.rb index 7cd55a83b..363251108 100644 --- a/spec/workers/queue_spec.rb +++ b/spec/workers/queue_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Workers::Queue do let(:credentials) { Concierge::Credentials.for("sqs") } - let(:element) { Workers::Queue::Element.new(operation: "sync", data: { key: "value" }) } + let(:element) { Workers::Queue::Element.new(operation: "background_worker", data: { key: "value" }) } subject { described_class.new(credentials) } describe "#add" do @@ -22,7 +22,7 @@ expect(sqs).to receive(:send_message).with({ queue_url: "https://www.example.org/concierge-queue", - message_body: { operation: "sync", data: { key: "value" } }.to_json + message_body: { operation: "background_worker", data: { key: "value" } }.to_json }) subject.add(element) diff --git a/spec/workers/router_spec.rb b/spec/workers/router_spec.rb index 995321655..d16fde52c 100644 --- a/spec/workers/router_spec.rb +++ b/spec/workers/router_spec.rb @@ -22,15 +22,6 @@ image.url = "https://www.example.org/img2" image.caption = "Swimming Pool" property.add_image(image) - - property.update_calendar({ - "2016-05-24" => true, - "2016-05-23" => true, - "2016-05-26" => false, - "2016-05-28" => false, - "2016-05-21" => true, - "2016-05-29" => true, - }) end } diff --git a/spec/workers/scheduler_spec.rb b/spec/workers/scheduler_spec.rb index 43ad31a9f..cf3c321c6 100644 --- a/spec/workers/scheduler_spec.rb +++ b/spec/workers/scheduler_spec.rb @@ -3,10 +3,6 @@ RSpec.describe Workers::Scheduler do include Support::Factories - let!(:new_host) { create_host(username: "new_host", identifier: "host1", next_run_at: nil) } - let!(:pending_host) { create_host(username: "pending_host", identifier: "host2", next_run_at: Time.now - 10) } - let!(:future_host) { create_host(username: "future_host", identifier: "host3", next_run_at: Time.now + 60 * 60) } - class LoggerStub attr_reader :messages @@ -23,32 +19,32 @@ def info(message) subject { described_class.new(logger: logger) } describe "#trigger_pending!" do - it "does nothing in case there is no host to be synchronised" do - HostRepository.all.each do |host| - host.next_run_at = Time.now + 60*60 - HostRepository.update(host) - end - + it "does nothing in case there is no background worker to be run" do expect(subject).not_to receive(:enqueue) subject.trigger_pending! expect(logger.messages).to eq [] end - it "triggers only new hosts and hosts that are pending" do + it "triggers only new workers and hosts that are pending" do + host = create_host(username: "host1", identifier: "host1") + new_worker = create_background_worker(host_id: host.id, type: "availabilities", next_run_at: nil) + pending_worker = create_background_worker(host_id: host.id, type: "metadata", next_run_at: Time.now - 60 * 60) + future_worker = create_background_worker(host_id: create_host.id, type: "metadata", next_run_at: Time.now + 60 * 60) + expect(subject).to receive(:enqueue).twice subject.trigger_pending! expect(logger.messages).to eq [ - "action=sync host.username=pending_host host.identifier=host2", - "action=sync host.username=new_host host.identifier=host1", + "action=metadata host.username=host1 host.identifier=host1", + "action=availabilities host.username=host1 host.identifier=host1", ] - new_reloaded = HostRepository.find(new_host.id) - pending_reloaded = HostRepository.find(pending_host.id) + new_reloaded = BackgroundWorkerRepository.find(new_worker.id) + pending_reloaded = BackgroundWorkerRepository.find(pending_worker.id) - expect(new_reloaded.next_run_at > Time.now).to eq true - expect(pending_reloaded.next_run_at > Time.now).to eq true + expect(new_reloaded.status).to eq "running" + expect(pending_reloaded.status).to eq "running" end end end