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